feat(app): 项目重命名为干杯日记并优化UI设计
- 将应用名称从"喝了么"更改为"干杯日记" - 更新所有相关文件中的品牌标识和描述信息 - 升级HaveADrink依赖包从1.0.3到1.0.4版本 - 在pages.json中新增分享个人资料页面配置 - 重构DrinkCard组件UI,增加装饰背景、酒类图标支持、饮酒感受徽章等功能 - 优化API错误处理逻辑,增加静默模式支持 - 修改白酒类别图标为 urn emoji,黄酒图标为 tea emoji - 添加分享功能按钮并优化页面返回交互体验 - 引入新的工具函数用于获取酒类图标和路径判断 - 更新主题混入、常量定义等基础配置文件 - 调整全局样式和设计令牌以匹配新品牌形象
This commit is contained in:
+431
-145
@@ -18,11 +18,12 @@
|
||||
<button class="btn-cta save-btn" :loading="saving" :disabled="saving" @click="saveToAlbum">
|
||||
<text>{{ saving ? '保存中...' : '保存到相册' }}</text>
|
||||
</button>
|
||||
<button class="btn-secondary share-btn" @click="shareToFriend">
|
||||
<button class="btn-secondary share-btn" open-type="share">
|
||||
<text>分享给好友</text>
|
||||
</button>
|
||||
<button class="btn-text close-home-btn" @click="closeToHome">
|
||||
<text>返回首页</text>
|
||||
<button class="close-home-btn" @click="closeToHome">
|
||||
<text class="close-home-icon">⌂</text>
|
||||
<text>完成,返回首页</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
@@ -34,7 +35,7 @@
|
||||
import DrinkCard from '../../components/DrinkCard.vue'
|
||||
import client from '../../common/api'
|
||||
import { getRecords } from '../../common/mock-data'
|
||||
import { getThemeColors, calcStandardCups, unitToMl } from '../../common/utils'
|
||||
import { calcStandardCups, unitToMl } from '../../common/utils'
|
||||
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../../common/constants'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
@@ -49,7 +50,8 @@ export default {
|
||||
statusBarHeight: sysInfo.statusBarHeight || 25,
|
||||
record: null,
|
||||
saving: false,
|
||||
saved: false
|
||||
saved: false,
|
||||
posterPath: '' // 预生成的海报图片路径
|
||||
}
|
||||
},
|
||||
async onLoad(options) {
|
||||
@@ -76,6 +78,10 @@ export default {
|
||||
}
|
||||
if (!this.record) this.record = this.loadLocalRecord(options.recordId) || this.fallback()
|
||||
},
|
||||
onReady() {
|
||||
// 页面渲染完成后预生成海报,供分享使用
|
||||
this.preGeneratePoster()
|
||||
},
|
||||
methods: {
|
||||
loadLocalRecord(id) {
|
||||
if (!id) return null
|
||||
@@ -134,160 +140,271 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
// ===== 海报绘制 =====
|
||||
// ===== 海报绘制(醉美夜色风格) =====
|
||||
async drawExportCard() {
|
||||
const ctx = uni.createCanvasContext('shareCanvas', this)
|
||||
const W = 750, H = 1600
|
||||
const W = 750
|
||||
const rec = this.record
|
||||
|
||||
const colors = getThemeColors(this.currentTheme)
|
||||
const WHITE = colors.textPrimary
|
||||
const SUB = colors.textSecondary
|
||||
const DIM = colors.textTertiary
|
||||
const AMBER = colors.amber
|
||||
const P = 56 // 内边距
|
||||
const maxW = W - P * 2
|
||||
const F = '-apple-system, PingFang SC, Helvetica Neue, sans-serif'
|
||||
|
||||
const cardW = W
|
||||
const cardX = 0
|
||||
const cardY = 0
|
||||
const R = 0
|
||||
const P = 48
|
||||
const maxW = cardW - 96
|
||||
const gap = 12
|
||||
const colW = (maxW - gap) / 2
|
||||
// 色彩定义
|
||||
const AMBER = '#E8A838'
|
||||
const AMBER_DIM = 'rgba(232,168,56,0.7)'
|
||||
const AMBER_FAINT = 'rgba(232,168,56,0.3)'
|
||||
const WHITE = '#FFFFFF'
|
||||
const WHITE_85 = 'rgba(255,255,255,0.85)'
|
||||
const WHITE_30 = 'rgba(255,255,255,0.3)'
|
||||
const WHITE_15 = 'rgba(255,255,255,0.15)'
|
||||
const WHITE_05 = 'rgba(255,255,255,0.05)'
|
||||
|
||||
// 计算照片区域高度
|
||||
// 动态计算卡片高度
|
||||
const drinks = rec.drinks || []
|
||||
const photoCount = (rec.photos || []).length
|
||||
let photoAreaH = 0
|
||||
if (photoCount === 0) photoAreaH = 248 // emoji
|
||||
else if (photoCount === 1) photoAreaH = 320
|
||||
const gap = 12
|
||||
const colW = (maxW - gap) / 2
|
||||
if (photoCount === 0) photoAreaH = 240
|
||||
else if (photoCount === 1) photoAreaH = 340
|
||||
else if (photoCount === 2) photoAreaH = colW
|
||||
else if (photoCount === 3) photoAreaH = 240 + gap + colW
|
||||
else if (photoCount === 4) photoAreaH = 2 * colW + gap
|
||||
else if (photoCount === 5) photoAreaH = 240 + gap + 2 * (colW + gap) - gap
|
||||
else photoAreaH = 3 * colW + 2 * gap
|
||||
|
||||
// 动态计算卡片高度
|
||||
const drinks = rec.drinks || []
|
||||
const drinkH = drinks.length * 52
|
||||
const foodH = rec.food ? 52 : 0
|
||||
const feelingH = rec.feeling ? 48 : 0
|
||||
const cardH = 48 + 56 + photoAreaH + 32 + drinkH + foodH + feelingH + 16 + 40 + 36 + 56 + 80 + 40
|
||||
const drinkH = drinks.length * 52 + (rec.food ? 52 : 0)
|
||||
const feelingH = rec.feeling ? 64 : 0
|
||||
const cardH = 56 + 50 + photoAreaH + 32 + feelingH + 32 + 60 + drinkH + 32 + 50 + 32 + 120 + 40 + 100 + 40
|
||||
|
||||
// 卡片背景渐变(填满整个画布)
|
||||
const grad = ctx.createLinearGradient(0, 0, cardW, cardY + cardH)
|
||||
grad.addColorStop(0, colors.cardGradStart)
|
||||
grad.addColorStop(1, colors.cardGradEnd)
|
||||
ctx.setFillStyle(grad)
|
||||
// === 背景渐变(深紫夜色) ===
|
||||
const bgGrad = ctx.createLinearGradient(0, 0, W * 0.3, cardH)
|
||||
bgGrad.addColorStop(0, '#1C1428')
|
||||
bgGrad.addColorStop(0.4, '#0F0A1A')
|
||||
bgGrad.addColorStop(1, '#0A0710')
|
||||
ctx.setFillStyle(bgGrad)
|
||||
ctx.fillRect(0, 0, W, cardH)
|
||||
|
||||
let y = cardY + 48
|
||||
// 装饰圆形
|
||||
ctx.setStrokeStyle('rgba(232,168,56,0.12)')
|
||||
ctx.setLineWidth(1.5)
|
||||
ctx.beginPath()
|
||||
ctx.arc(W - 40, 60, 150, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.arc(30, cardH - 300, 100, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
ctx.setFillStyle('rgba(232,168,56,0.03)')
|
||||
ctx.beginPath()
|
||||
ctx.arc(W - 80, 380, 60, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
// 顶部光晕(用线性渐变模拟)
|
||||
const glowGrad = ctx.createLinearGradient(W / 2, 0, W / 2, 280)
|
||||
glowGrad.addColorStop(0, 'rgba(232,168,56,0.06)')
|
||||
glowGrad.addColorStop(1, 'rgba(232,168,56,0)')
|
||||
ctx.setFillStyle(glowGrad)
|
||||
ctx.fillRect(0, 0, W, 280)
|
||||
|
||||
// 日期
|
||||
let y = 56
|
||||
|
||||
// === 日期(居中 + 两侧线条) ===
|
||||
ctx.font = `500 22px ${F}`
|
||||
ctx.setFillStyle(DIM)
|
||||
ctx.fillText(rec.date || '', P, y)
|
||||
y += 56
|
||||
const dateStr = rec.date || ''
|
||||
const dateW = ctx.measureText(dateStr).width
|
||||
const dateX = (W - dateW) / 2
|
||||
// 左线
|
||||
const lineGrad1 = ctx.createLinearGradient(dateX - 80, y, dateX - 10, y)
|
||||
lineGrad1.addColorStop(0, 'transparent')
|
||||
lineGrad1.addColorStop(1, 'rgba(232,168,56,0.5)')
|
||||
ctx.setStrokeStyle(lineGrad1)
|
||||
ctx.setLineWidth(1.5)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(dateX - 80, y - 6)
|
||||
ctx.lineTo(dateX - 14, y - 6)
|
||||
ctx.stroke()
|
||||
// 右线
|
||||
const lineGrad2 = ctx.createLinearGradient(dateX + dateW + 14, y, dateX + dateW + 80, y)
|
||||
lineGrad2.addColorStop(0, 'rgba(232,168,56,0.5)')
|
||||
lineGrad2.addColorStop(1, 'transparent')
|
||||
ctx.setStrokeStyle(lineGrad2)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(dateX + dateW + 14, y - 6)
|
||||
ctx.lineTo(dateX + dateW + 80, y - 6)
|
||||
ctx.stroke()
|
||||
// 日期文字
|
||||
ctx.setFillStyle(AMBER_DIM)
|
||||
ctx.fillText(dateStr, dateX, y)
|
||||
y += 50
|
||||
|
||||
// 照片区域
|
||||
// === 照片/图标区域 ===
|
||||
const photos = rec.photos || []
|
||||
if (photos.length > 0) {
|
||||
// 预加载所有照片
|
||||
const imgs = await Promise.all(photos.map(p => this.loadImg(p)))
|
||||
|
||||
if (photos.length === 1) {
|
||||
// 1张:大图
|
||||
const heroH = 320
|
||||
const heroH = 340
|
||||
ctx.save()
|
||||
this.rr(ctx, P, y, maxW, heroH, 16, 'fill')
|
||||
this.rr(ctx, P, y, maxW, heroH, 24, 'fill')
|
||||
ctx.clip()
|
||||
if (imgs[0]) {
|
||||
try { ctx.drawImage(imgs[0], P, y, maxW, heroH) } catch (e) {}
|
||||
} else {
|
||||
ctx.setFillStyle(colors.bgPlaceholder)
|
||||
ctx.fillRect(P, y, maxW, heroH)
|
||||
}
|
||||
if (imgs[0]) { try { ctx.drawImage(imgs[0], P, y, maxW, heroH) } catch (e) {} }
|
||||
else { ctx.setFillStyle(WHITE_05); ctx.fillRect(P, y, maxW, heroH) }
|
||||
ctx.restore()
|
||||
y += heroH + 32
|
||||
} else {
|
||||
// 2-6张:网格布局
|
||||
const positions = []
|
||||
|
||||
if (photos.length % 2 !== 0) {
|
||||
// 奇数张:第一张占满两列
|
||||
const heroH = 240
|
||||
positions.push({ x: P, y: y, w: maxW, h: heroH })
|
||||
positions.push({ x: P, y, w: maxW, h: heroH })
|
||||
const rowStart = y + heroH + gap
|
||||
for (let i = 1; i < photos.length; i += 2) {
|
||||
const row = Math.floor((i - 1) / 2)
|
||||
const rowY = rowStart + row * (colW + gap)
|
||||
positions.push({ x: P, y: rowY, w: colW, h: colW })
|
||||
if (i + 1 < photos.length) {
|
||||
positions.push({ x: P + colW + gap, y: rowY, w: colW, h: colW })
|
||||
}
|
||||
if (i + 1 < photos.length) positions.push({ x: P + colW + gap, y: rowY, w: colW, h: colW })
|
||||
}
|
||||
const totalRows = Math.ceil((photos.length - 1) / 2)
|
||||
y = rowStart + totalRows * (colW + gap) - gap + 32
|
||||
} else {
|
||||
// 偶数张:全部两列
|
||||
for (let i = 0; i < photos.length; i += 2) {
|
||||
const row = i / 2
|
||||
const rowY = y + row * (colW + gap)
|
||||
positions.push({ x: P, y: rowY, w: colW, h: colW })
|
||||
positions.push({ x: P + colW + gap, y: rowY, w: colW, h: colW })
|
||||
}
|
||||
const totalRows = photos.length / 2
|
||||
y = y + totalRows * (colW + gap) - gap + 32
|
||||
y = y + (photos.length / 2) * (colW + gap) - gap + 32
|
||||
}
|
||||
|
||||
// 绘制每张照片
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const pos = positions[i]
|
||||
ctx.save()
|
||||
this.rr(ctx, pos.x, pos.y, pos.w, pos.h, 12, 'fill')
|
||||
this.rr(ctx, pos.x, pos.y, pos.w, pos.h, 16, 'fill')
|
||||
ctx.clip()
|
||||
if (imgs[i]) {
|
||||
try { ctx.drawImage(imgs[i], pos.x, pos.y, pos.w, pos.h) } catch (e) {}
|
||||
} else {
|
||||
ctx.setFillStyle(colors.bgPlaceholder)
|
||||
ctx.fillRect(pos.x, pos.y, pos.w, pos.h)
|
||||
}
|
||||
if (imgs[i]) { try { ctx.drawImage(imgs[i], pos.x, pos.y, pos.w, pos.h) } catch (e) {} }
|
||||
else { ctx.setFillStyle(WHITE_05); ctx.fillRect(pos.x, pos.y, pos.w, pos.h) }
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// emoji 圆形
|
||||
const eR = 100
|
||||
const eCx = W / 2, eCy = y + eR
|
||||
ctx.setFillStyle(colors.emojiBg)
|
||||
// 图标圆形 + 光环
|
||||
const eR = 90
|
||||
const eCx = W / 2, eCy = y + eR + 20
|
||||
// 外环
|
||||
ctx.setStrokeStyle('rgba(232,168,56,0.15)')
|
||||
ctx.setLineWidth(2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(eCx, eCy, eR + 20, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
// 内圆背景(用线性渐变模拟)
|
||||
const innerGrad = ctx.createLinearGradient(eCx, eCy - eR, eCx, eCy + eR)
|
||||
innerGrad.addColorStop(0, 'rgba(232,168,56,0.15)')
|
||||
innerGrad.addColorStop(1, 'rgba(232,168,56,0.04)')
|
||||
ctx.setFillStyle(innerGrad)
|
||||
ctx.beginPath()
|
||||
ctx.arc(eCx, eCy, eR, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
// 图标
|
||||
const firstDrink = (rec.drinks || [])[0]
|
||||
const cat = firstDrink ? DRINK_CATEGORIES.find(cc => cc.id === firstDrink.category) : null
|
||||
const emoji = cat ? cat.emoji : '🥃'
|
||||
ctx.font = `90px ${F}`
|
||||
ctx.setFillStyle(WHITE)
|
||||
ctx.fillText(emoji, eCx - 45, eCy + 30)
|
||||
y += eR * 2 + 48
|
||||
if (cat && cat.icon) {
|
||||
this.drawJarIcon(ctx, eCx, eCy, 130)
|
||||
} else {
|
||||
const emoji = cat ? cat.emoji : '🥃'
|
||||
ctx.font = `80px ${F}`
|
||||
ctx.fillText(emoji, eCx - 40, eCy + 28)
|
||||
}
|
||||
y += (eR + 20) * 2 + 32
|
||||
}
|
||||
|
||||
// 酒水清单
|
||||
// === 感受徽章 ===
|
||||
if (rec.feeling) {
|
||||
const feeling = FEELINGS.find(f => f.id === rec.feeling)
|
||||
if (feeling) {
|
||||
const badgeText = `${feeling.emoji} ${feeling.name} ${feeling.desc}`
|
||||
ctx.font = `500 24px ${F}`
|
||||
const bw = ctx.measureText(badgeText).width + 60
|
||||
const bx = (W - bw) / 2
|
||||
const bh = 48
|
||||
// 胶囊背景
|
||||
ctx.setFillStyle('rgba(232,168,56,0.1)')
|
||||
this.rr(ctx, bx, y, bw, bh, bh / 2, 'fill')
|
||||
// 边框
|
||||
ctx.setStrokeStyle('rgba(232,168,56,0.25)')
|
||||
ctx.setLineWidth(1.5)
|
||||
this.rr(ctx, bx, y, bw, bh, bh / 2, 'stroke')
|
||||
// 文字
|
||||
ctx.font = `500 24px ${F}`
|
||||
ctx.setFillStyle(AMBER)
|
||||
const tw = ctx.measureText(badgeText).width
|
||||
ctx.fillText(badgeText, (W - tw) / 2, y + 32)
|
||||
y += bh + 32
|
||||
}
|
||||
}
|
||||
|
||||
// === 今夜菜单标题 ===
|
||||
// 背景卡片
|
||||
const menuY = y
|
||||
const menuH = 60 + drinkH + 20
|
||||
ctx.setFillStyle('rgba(255,255,255,0.02)')
|
||||
this.rr(ctx, P - 8, menuY, maxW + 16, menuH, 24, 'fill')
|
||||
ctx.setStrokeStyle(WHITE_05)
|
||||
ctx.setLineWidth(1.5)
|
||||
this.rr(ctx, P - 8, menuY, maxW + 16, menuH, 24, 'stroke')
|
||||
|
||||
y += 36
|
||||
// 标题 "今夜菜单"
|
||||
ctx.font = `500 22px ${F}`
|
||||
const menuTitle = '今 夜 菜 单'
|
||||
const mtw = ctx.measureText(menuTitle).width
|
||||
const mtx = (W - mtw) / 2
|
||||
// 两侧圆点
|
||||
ctx.setFillStyle('rgba(232,168,56,0.5)')
|
||||
ctx.beginPath()
|
||||
ctx.arc(mtx - 24, y - 6, 4, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.arc(mtx + mtw + 24, y - 6, 4, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.setFillStyle(AMBER_DIM)
|
||||
ctx.fillText(menuTitle, mtx, y)
|
||||
y += 40
|
||||
|
||||
// === 酒水清单 ===
|
||||
drinks.forEach(d => {
|
||||
const cat = DRINK_CATEGORIES.find(cc => cc.id === d.category)
|
||||
const emoji = cat ? cat.emoji : '🥃'
|
||||
const name = `${emoji} ${d.brand}`
|
||||
const unitName = UNIT_NAMES[d.unit] || d.unit
|
||||
const detail = `${d.amount}${unitName}`
|
||||
|
||||
let nameX = P + 12
|
||||
if (cat && cat.icon) {
|
||||
this.drawJarIcon(ctx, P + 12 + 14, y - 12, 28)
|
||||
nameX = P + 12 + 38
|
||||
} else {
|
||||
const emoji = cat ? cat.emoji : '🥃'
|
||||
ctx.font = `26px ${F}`
|
||||
ctx.fillText(emoji, P + 12, y)
|
||||
nameX = P + 12 + 36
|
||||
}
|
||||
|
||||
ctx.font = `500 28px ${F}`
|
||||
ctx.setFillStyle(WHITE)
|
||||
ctx.fillText(name, P, y)
|
||||
ctx.fillText(d.brand, nameX, y)
|
||||
const brandW = ctx.measureText(d.brand).width
|
||||
|
||||
// 虚线引导线
|
||||
const dotsStartX = nameX + brandW + 16
|
||||
const dotsEndX = W - P - 12 - ctx.measureText(detail).width - 16
|
||||
ctx.setStrokeStyle(WHITE_15)
|
||||
ctx.setLineWidth(1)
|
||||
ctx.setLineDash([4, 6])
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(dotsStartX, y - 6)
|
||||
ctx.lineTo(dotsEndX, y - 6)
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
|
||||
// 用量
|
||||
ctx.font = `bold 24px ${F}`
|
||||
ctx.setFillStyle(AMBER)
|
||||
const dw = ctx.measureText(detail).width
|
||||
ctx.fillText(detail, W - P - dw, y)
|
||||
ctx.fillText(detail, W - P - 12 - dw, y)
|
||||
|
||||
y += 52
|
||||
})
|
||||
@@ -295,75 +412,91 @@ export default {
|
||||
// 配餐
|
||||
if (rec.food) {
|
||||
const food = FOOD_CATEGORIES.find(f => f.id === rec.food.category)
|
||||
const name = `🍲 ${food ? food.name : '美食'}`
|
||||
const foodName = food ? food.name : '美食'
|
||||
ctx.font = `26px ${F}`
|
||||
ctx.fillText('🍲', P + 12, y)
|
||||
ctx.font = `500 28px ${F}`
|
||||
ctx.setFillStyle(WHITE)
|
||||
ctx.fillText(name, P, y)
|
||||
ctx.fillText(foodName, P + 48, y)
|
||||
ctx.font = `bold 24px ${F}`
|
||||
ctx.setFillStyle(AMBER)
|
||||
ctx.fillText('佐酒', W - P - 12 - ctx.measureText('佐酒').width, y)
|
||||
y += 52
|
||||
}
|
||||
|
||||
// 饮酒感受
|
||||
if (rec.feeling) {
|
||||
const feeling = FEELINGS.find(f => f.id === rec.feeling)
|
||||
if (feeling) {
|
||||
y += 8
|
||||
const feelingText = `${feeling.emoji} ${feeling.name}`
|
||||
ctx.font = `500 26px ${F}`
|
||||
ctx.setFillStyle(AMBER)
|
||||
ctx.fillText(feelingText, P, y)
|
||||
y += 40
|
||||
}
|
||||
}
|
||||
y += 32
|
||||
|
||||
// 分割线(渐变)
|
||||
y += 16
|
||||
const lineGrad = ctx.createLinearGradient(P, y, W - P, y)
|
||||
lineGrad.addColorStop(0, 'transparent')
|
||||
lineGrad.addColorStop(0.5, colors.borderSubtle)
|
||||
lineGrad.addColorStop(1, 'transparent')
|
||||
ctx.setStrokeStyle(lineGrad)
|
||||
ctx.setLineWidth(2)
|
||||
// === 分割装饰 ===
|
||||
const divY = y
|
||||
const divGrad = ctx.createLinearGradient(P, divY, W - P, divY)
|
||||
divGrad.addColorStop(0, 'transparent')
|
||||
divGrad.addColorStop(0.5, 'rgba(232,168,56,0.2)')
|
||||
divGrad.addColorStop(1, 'transparent')
|
||||
ctx.setStrokeStyle(divGrad)
|
||||
ctx.setLineWidth(1.5)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(P, divY)
|
||||
ctx.lineTo(W / 2 - 30, divY)
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(W / 2 + 30, divY)
|
||||
ctx.lineTo(W - P, divY)
|
||||
ctx.stroke()
|
||||
ctx.font = `24px ${F}`
|
||||
ctx.fillText('🥂', W / 2 - 12, divY + 8)
|
||||
y += 50
|
||||
|
||||
// === 酒言酒语 ===
|
||||
const quote = DRINK_QUOTES[Math.floor(Math.random() * DRINK_QUOTES.length)]
|
||||
// 上引号
|
||||
ctx.font = `48px Georgia, serif`
|
||||
ctx.setFillStyle(AMBER_FAINT)
|
||||
ctx.fillText('“', P + 20, y + 10)
|
||||
y += 20
|
||||
// 引文
|
||||
ctx.font = `italic 26px ${F}`
|
||||
ctx.setFillStyle(WHITE_85)
|
||||
let dq = quote
|
||||
if (ctx.measureText(dq).width > maxW - 40) {
|
||||
while (dq.length > 0 && ctx.measureText(dq + '…').width > maxW - 40) dq = dq.slice(0, -1)
|
||||
dq += '…'
|
||||
}
|
||||
const qw = ctx.measureText(dq).width
|
||||
ctx.fillText(dq, (W - qw) / 2, y + 16)
|
||||
y += 44
|
||||
// 下引号
|
||||
ctx.font = `48px Georgia, serif`
|
||||
ctx.setFillStyle(AMBER_FAINT)
|
||||
ctx.fillText('”', W - P - 40, y)
|
||||
y += 56
|
||||
|
||||
// === 品牌底栏 ===
|
||||
// 分割线
|
||||
ctx.setStrokeStyle(WHITE_05)
|
||||
ctx.setLineWidth(1.5)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(P, y)
|
||||
ctx.lineTo(W - P, y)
|
||||
ctx.stroke()
|
||||
y += 40
|
||||
|
||||
// 喝后心得
|
||||
ctx.font = `normal 20px ${F}`
|
||||
ctx.setFillStyle(DIM)
|
||||
ctx.fillText('喝后心得', P, y)
|
||||
y += 36
|
||||
|
||||
const quote = DRINK_QUOTES[Math.floor(Math.random() * DRINK_QUOTES.length)]
|
||||
ctx.font = `italic 24px ${F}`
|
||||
ctx.setFillStyle(SUB)
|
||||
let dq = `"${quote}"`
|
||||
if (ctx.measureText(dq).width > maxW) {
|
||||
while (dq.length > 0 && ctx.measureText(dq + '..."').width > maxW) dq = dq.slice(0, -1)
|
||||
dq += '..."'
|
||||
}
|
||||
ctx.fillText(dq, P, y)
|
||||
y += 56
|
||||
|
||||
// 品牌 + 二维码
|
||||
ctx.font = `bold 22px ${F}`
|
||||
ctx.setFillStyle(DIM)
|
||||
ctx.fillText('🍻 喝了么', P, y + 16)
|
||||
ctx.font = `bold 26px ${F}`
|
||||
ctx.setFillStyle(AMBER_DIM)
|
||||
ctx.fillText('干杯日记', P, y)
|
||||
y += 30
|
||||
ctx.font = `normal 18px ${F}`
|
||||
ctx.setFillStyle(DIM)
|
||||
ctx.fillText('记录每一杯的故事', P, y + 16)
|
||||
ctx.font = `normal 20px ${F}`
|
||||
ctx.setFillStyle(WHITE_30)
|
||||
ctx.fillText('记录每一杯的故事', P, y)
|
||||
|
||||
// 二维码占位框
|
||||
// 二维码占位
|
||||
const qrSize = 80
|
||||
const qrX = W - P - qrSize
|
||||
const qrY = y - 30
|
||||
ctx.setStrokeStyle(colors.borderSubtle)
|
||||
ctx.setLineWidth(2)
|
||||
this.rr(ctx, qrX, qrY, qrSize, qrSize, 8, 'stroke')
|
||||
ctx.setFillStyle(colors.bgPlaceholder)
|
||||
this.rr(ctx, qrX, qrY, qrSize, qrSize, 8, 'fill')
|
||||
const qrY = y - 50
|
||||
ctx.setStrokeStyle('rgba(232,168,56,0.2)')
|
||||
ctx.setLineWidth(1.5)
|
||||
this.rr(ctx, qrX, qrY, qrSize, qrSize, 12, 'stroke')
|
||||
ctx.setFillStyle('rgba(232,168,56,0.04)')
|
||||
this.rr(ctx, qrX, qrY, qrSize, qrSize, 12, 'fill')
|
||||
|
||||
return new Promise(resolve => {
|
||||
ctx.draw(false, () => setTimeout(() => resolve(cardH), 600))
|
||||
@@ -382,6 +515,104 @@ export default {
|
||||
else ctx.fill()
|
||||
},
|
||||
|
||||
// 绘制白酒坛子图标(Canvas 矢量,保证导出海报一致性)
|
||||
drawJarIcon(ctx, cx, cy, s) {
|
||||
ctx.save()
|
||||
ctx.translate(cx - s / 2, cy - s / 2)
|
||||
const u = s / 100
|
||||
|
||||
// 红绸蝴蝶结
|
||||
ctx.setFillStyle('#C62828')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(50 * u, 22 * u)
|
||||
ctx.bezierCurveTo(43 * u, 13 * u, 31 * u, 14 * u, 33 * u, 21 * u)
|
||||
ctx.bezierCurveTo(34.5 * u, 26 * u, 44 * u, 26 * u, 50 * u, 22 * u)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(50 * u, 22 * u)
|
||||
ctx.bezierCurveTo(57 * u, 13 * u, 69 * u, 14 * u, 67 * u, 21 * u)
|
||||
ctx.bezierCurveTo(65.5 * u, 26 * u, 56 * u, 26 * u, 50 * u, 22 * u)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
// 结带飘尾
|
||||
ctx.setFillStyle('#A61B1B')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(44 * u, 24 * u)
|
||||
ctx.lineTo(37 * u, 35 * u)
|
||||
ctx.lineTo(44 * u, 32 * u)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(56 * u, 24 * u)
|
||||
ctx.lineTo(63 * u, 35 * u)
|
||||
ctx.lineTo(56 * u, 32 * u)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
// 结心
|
||||
ctx.setFillStyle('#E53935')
|
||||
ctx.beginPath()
|
||||
ctx.arc(50 * u, 22 * u, 3.5 * u, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
// 坛盖
|
||||
ctx.setFillStyle('#8D6E63')
|
||||
this.rr(ctx, 41 * u, 25 * u, 18 * u, 7 * u, 3.5 * u, 'fill')
|
||||
|
||||
// 坛颈
|
||||
ctx.setFillStyle('#191919')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(43 * u, 31 * u)
|
||||
ctx.lineTo(57 * u, 31 * u)
|
||||
ctx.lineTo(59 * u, 39 * u)
|
||||
ctx.lineTo(41 * u, 39 * u)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
// 坛身
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(41 * u, 39 * u)
|
||||
ctx.bezierCurveTo(27 * u, 45 * u, 23 * u, 58 * u, 25 * u, 69 * u)
|
||||
ctx.bezierCurveTo(27 * u, 82 * u, 37 * u, 91 * u, 50 * u, 91 * u)
|
||||
ctx.bezierCurveTo(63 * u, 91 * u, 73 * u, 82 * u, 75 * u, 69 * u)
|
||||
ctx.bezierCurveTo(77 * u, 58 * u, 73 * u, 45 * u, 59 * u, 39 * u)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
// 釉面高光
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.28)')
|
||||
ctx.setLineWidth(Math.max(1, 4 * u))
|
||||
ctx.setLineCap('round')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(35 * u, 47 * u)
|
||||
ctx.bezierCurveTo(30 * u, 53 * u, 29 * u, 63 * u, 31 * u, 71 * u)
|
||||
ctx.stroke()
|
||||
|
||||
// 红色菱形酒标
|
||||
ctx.setFillStyle('#D32F2F')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(50 * u, 49 * u)
|
||||
ctx.lineTo(64 * u, 65 * u)
|
||||
ctx.lineTo(50 * u, 81 * u)
|
||||
ctx.lineTo(36 * u, 65 * u)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.setStrokeStyle('#F9A825')
|
||||
ctx.setLineWidth(Math.max(1, 1.6 * u))
|
||||
ctx.stroke()
|
||||
|
||||
// 酒字
|
||||
ctx.setFillStyle('#FFF8E1')
|
||||
ctx.font = `bold ${Math.max(6, Math.round(17 * u))}px KaiTi, STKaiti, SimSun, serif`
|
||||
ctx.setTextAlign('center')
|
||||
ctx.setTextBaseline('middle')
|
||||
ctx.fillText('酒', 50 * u, 66 * u)
|
||||
ctx.setTextAlign('left')
|
||||
ctx.setTextBaseline('alphabetic')
|
||||
|
||||
ctx.restore()
|
||||
},
|
||||
|
||||
exportImg(contentH) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cropH = Math.min(contentH || 1600, 1600)
|
||||
@@ -412,7 +643,22 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
shareToFriend() { uni.showToast({ title: '点击右上角分享', icon: 'none' }) },
|
||||
shareToFriend() {
|
||||
// open-type="share" 已直接触发转发,此方法保留作为兜底提示
|
||||
uni.showToast({ title: '点击右上角「···」可分享给好友', icon: 'none' })
|
||||
},
|
||||
|
||||
// 预生成海报图片(分享时用作 imageUrl)
|
||||
async preGeneratePoster() {
|
||||
if (!this.record) return
|
||||
try {
|
||||
const contentH = await this.drawExportCard()
|
||||
const path = await this.exportImg(contentH)
|
||||
this.posterPath = path
|
||||
} catch (e) {
|
||||
console.warn('预生成海报失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
console.log('goBack called, saved:', this.saved)
|
||||
@@ -457,7 +703,24 @@ export default {
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return { title: '今晚的酒局记录 - 喝了么', path: '/pages/index/index' }
|
||||
const rec = this.record
|
||||
const drinks = (rec && rec.drinks) || []
|
||||
const first = drinks[0]
|
||||
const cat = first ? DRINK_CATEGORIES.find(c => c.id === first.category) : null
|
||||
const title = cat
|
||||
? `今晚喝了${cat.name}「${first.brand || ''}」- 干杯日记`
|
||||
: '今晚的酒局记录 - 干杯日记'
|
||||
return {
|
||||
title,
|
||||
path: '/pages/index/index',
|
||||
imageUrl: this.posterPath || undefined
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '今晚的酒局记录 - 干杯日记',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
onBackPress() {
|
||||
this.goBack()
|
||||
@@ -552,17 +815,40 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.close-home-btn {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: $text-tertiary;
|
||||
font-size: $fs-sm;
|
||||
padding: $sp-sm 0;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $sp-xs;
|
||||
background: $bg-elevated;
|
||||
border: 2rpx solid var(--border-subtle, rgba(255, 255, 255, 0.10));
|
||||
border-radius: $radius-full;
|
||||
color: $text-secondary;
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-medium;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
&::after { display: none; }
|
||||
|
||||
&:active {
|
||||
background: $amber-glow;
|
||||
border-color: rgba(232, 168, 56, 0.3);
|
||||
color: $amber;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
.close-home-icon {
|
||||
font-size: $fs-lg;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.export-canvas {
|
||||
|
||||
Reference in New Issue
Block a user