Files
hejiu/pages/card/card.vue
T
cg 9f5c69e1e5 feat(app): 项目重命名为干杯日记并优化UI设计
- 将应用名称从"喝了么"更改为"干杯日记"
- 更新所有相关文件中的品牌标识和描述信息
- 升级HaveADrink依赖包从1.0.3到1.0.4版本
- 在pages.json中新增分享个人资料页面配置
- 重构DrinkCard组件UI,增加装饰背景、酒类图标支持、饮酒感受徽章等功能
- 优化API错误处理逻辑,增加静默模式支持
- 修改白酒类别图标为 urn emoji,黄酒图标为 tea emoji
- 添加分享功能按钮并优化页面返回交互体验
- 引入新的工具函数用于获取酒类图标和路径判断
- 更新主题混入、常量定义等基础配置文件
- 调整全局样式和设计令牌以匹配新品牌形象
2026-07-20 22:12:27 +08:00

860 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view :class="themeClass" class="page-container card-page">
<view class="card-nav" :style="{ paddingTop: (statusBarHeight + 12) + 'px' }">
<view class="step-back" @click="goBack">
<text class="step-back-arrow"></text>
</view>
<text class="card-nav-title">酒局卡片</text>
</view>
<!-- 大卡片预览 -->
<view class="card-preview-area">
<DrinkCard v-if="record" :record="record" />
<view v-else class="card-empty">暂无记录</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<button class="btn-cta save-btn" :loading="saving" :disabled="saving" @click="saveToAlbum">
<text>{{ saving ? '保存中...' : '保存到相册' }}</text>
</button>
<button class="btn-secondary share-btn" open-type="share">
<text>分享给好友</text>
</button>
<button class="close-home-btn" @click="closeToHome">
<text class="close-home-icon"></text>
<text>完成返回首页</text>
</button>
</view>
<canvas canvas-id="shareCanvas" class="export-canvas" :style="{width:'750px',height:'1600px'}"></canvas>
</view>
</template>
<script>
import DrinkCard from '../../components/DrinkCard.vue'
import client from '../../common/api'
import { getRecords } from '../../common/mock-data'
import { calcStandardCups, unitToMl } from '../../common/utils'
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../../common/constants'
import themeMixin from '../../common/theme-mixin'
const UNIT_NAMES = { ml: 'ml', liang: '两', bottle: '瓶', cup: '杯', can: '听', shot: 'shot' }
export default {
mixins: [themeMixin],
components: { DrinkCard },
data() {
const sysInfo = uni.getSystemInfoSync()
return {
statusBarHeight: sysInfo.statusBarHeight || 25,
record: null,
saving: false,
saved: false,
posterPath: '' // 预生成的海报图片路径
}
},
async onLoad(options) {
if (options && options.recordId) {
try {
const resp = await client.GetRecordDetail({ id: options.recordId })
const rec = resp.data || resp
if (rec) {
this.record = {
...rec,
mode: String(rec.mode),
feeling: rec.feeling ? String(rec.feeling) : null,
visibility: rec.visibility ? String(rec.visibility) : 'private',
drinks: (rec.drinks || []).map(d => ({
...d,
category: String(d.category),
unit: String(d.unit)
}))
}
}
} catch (e) {
console.warn('获取记录详情失败,降级本地:', e)
}
}
if (!this.record) this.record = this.loadLocalRecord(options.recordId) || this.fallback()
},
onReady() {
// 页面渲染完成后预生成海报,供分享使用
this.preGeneratePoster()
},
methods: {
loadLocalRecord(id) {
if (!id) return null
try {
const records = getRecords()
const found = records.find(r => r.id === id)
if (!found) return null
return {
...found,
mode: String(found.mode || 'drank'),
feeling: found.feeling ? String(found.feeling) : null,
visibility: found.visibility ? String(found.visibility) : 'private',
drinks: (found.drinks || []).map(d => {
const ml = unitToMl(d.amount || 0, d.unit || 'ml')
return {
...d,
category: String(d.category || d.categoryId || ''),
unit: String(d.unit || 'ml'),
standardCups: d.standardCups || calcStandardCups(ml, d.degree || 0)
}
})
}
} catch (e) {
console.warn('读取本地记录失败:', e)
return null
}
},
fallback() {
return {
date: new Date().toISOString().slice(0, 10), mode: 'drank',
drinks: [{ category: 'beer', brand: '青岛啤酒', product: '经典', amount: 2, unit: 'bottle', degree: 4.3, standardCups: 3.4 }],
food: { category: 'bbq', name: '烤串' }, feeling: 'tipsy', photos: [], standardCupsTotal: 3.4
}
},
// ===== 保存流程 =====
async saveToAlbum() {
if (this.saving) return
this.saving = true
try {
const contentH = await this.drawExportCard()
const path = await this.exportImg(contentH)
await this.saveImg(path)
this.saved = true
uni.showToast({ title: '已保存到相册', icon: 'success' })
} catch (e) {
console.error('保存失败:', e)
uni.showToast({ title: e.message || e.msg || '保存失败', icon: 'none', duration: 3000 })
} finally { this.saving = false }
},
loadImg(src) {
return new Promise(r => {
if (!src) return r(null)
uni.getImageInfo({ src, success: res => r(res.path), fail: () => r(null) })
})
},
// ===== 海报绘制(醉美夜色风格) =====
async drawExportCard() {
const ctx = uni.createCanvasContext('shareCanvas', this)
const W = 750
const rec = this.record
const P = 56 // 内边距
const maxW = W - P * 2
const F = '-apple-system, PingFang SC, Helvetica Neue, sans-serif'
// 色彩定义
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
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 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 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)
// 装饰圆形
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}`
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) {
const heroH = 340
ctx.save()
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(WHITE_05); ctx.fillRect(P, y, maxW, heroH) }
ctx.restore()
y += heroH + 32
} else {
const positions = []
if (photos.length % 2 !== 0) {
const heroH = 240
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 })
}
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 })
}
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, 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(WHITE_05); ctx.fillRect(pos.x, pos.y, pos.w, pos.h) }
ctx.restore()
}
}
} else {
// 图标圆形 + 光环
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
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 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(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 - 12 - dw, y)
y += 52
})
// 配餐
if (rec.food) {
const food = FOOD_CATEGORIES.find(f => f.id === rec.food.category)
const foodName = food ? food.name : '美食'
ctx.font = `26px ${F}`
ctx.fillText('🍲', P + 12, y)
ctx.font = `500 28px ${F}`
ctx.setFillStyle(WHITE)
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
}
y += 32
// === 分割装饰 ===
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 += 36
ctx.font = `bold 26px ${F}`
ctx.setFillStyle(AMBER_DIM)
ctx.fillText('干杯日记', P, y)
y += 30
ctx.font = `normal 20px ${F}`
ctx.setFillStyle(WHITE_30)
ctx.fillText('记录每一杯的故事', P, y)
// 二维码占位
const qrSize = 80
const qrX = W - P - qrSize
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))
})
},
rr(ctx, x, y, w, h, r, act) {
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.arcTo(x + w, y, x + w, y + h, r)
ctx.arcTo(x + w, y + h, x, y + h, r)
ctx.arcTo(x, y + h, x, y, r)
ctx.arcTo(x, y, x + w, y, r)
ctx.closePath()
if (act === 'stroke') ctx.stroke()
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)
uni.canvasToTempFilePath({
canvasId: 'shareCanvas', quality: 1,
x: 0, y: 0, width: 750, height: cropH,
destWidth: 750, destHeight: cropH,
success: res => resolve(res.tempFilePath),
fail: err => reject({ msg: '导出失败: ' + (err.errMsg || '') })
}, this)
})
},
saveImg(fp) {
return new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({
filePath: fp, success: () => resolve(),
fail: err => {
if (err.errMsg && err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示', content: '需要授权保存相册',
confirmText: '去设置', success: r => { if (r.confirm) uni.openSetting() }
})
reject({ msg: '请授权相册' })
} else reject({ msg: '保存失败' })
}
})
})
},
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)
if (this.saved) {
this._doBack()
return
}
uni.showModal({
title: '离开前保存海报?',
content: '海报还未保存到相册,离开后需要重新生成。',
confirmText: '保存并离开',
cancelText: '直接离开',
success: async (res) => {
console.log('modal result:', res)
if (res.confirm) {
try { await this.saveToAlbum() } catch (e) { /* ignore */ }
}
this._doBack()
},
fail: (err) => {
console.error('showModal fail:', err)
this._doBack()
}
})
},
closeToHome() {
uni.switchTab({ url: '/pages/index/index' })
},
_doBack() {
console.log('_doBack called, pages:', getCurrentPages().length)
if (getCurrentPages().length > 1) {
uni.navigateBack({
fail: (err) => {
console.error('navigateBack fail:', err)
uni.reLaunch({ url: '/pages/index/index' })
}
})
} else {
console.log('no pages to go back to, reLaunch to index')
uni.reLaunch({ url: '/pages/index/index' })
}
}
},
onShareAppMessage() {
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()
return true
}
}
</script>
<style lang="scss" scoped>
.card-page {
padding: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
background: $bg-base;
}
.card-nav {
display: flex;
align-items: center;
gap: $sp-md;
padding: $sp-md $sp-lg;
padding-top: 0;
}
.step-back {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: $radius-full;
background: $bg-card;
flex-shrink: 0;
}
.step-back-arrow {
font-size: $fs-xl;
color: $text-secondary;
font-weight: $fw-bold;
}
.card-nav-title {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-primary;
}
/* 大卡片预览区 */
.card-preview-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: $sp-lg;
min-height: 0;
}
.card-empty {
color: $text-tertiary;
font-size: $fs-md;
}
/* 操作按钮 */
.card-actions {
padding: $sp-lg;
padding-bottom: calc($sp-lg + env(safe-area-inset-bottom));
display: flex;
flex-direction: column;
gap: $sp-md;
}
.save-btn {
width: 100%;
height: 92rpx;
border-radius: $radius-full;
font-size: $fs-lg;
font-weight: $fw-bold;
display: flex;
align-items: center;
justify-content: center;
}
.share-btn {
width: 100%;
height: 92rpx;
border-radius: $radius-full;
font-size: $fs-base;
background: transparent;
border: 2rpx solid var(--border-active, rgba(255, 255, 255, 0.12));
color: $text-secondary;
display: flex;
align-items: center;
justify-content: center;
&::after {
border: none;
}
}
.close-home-btn {
width: 100%;
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 {
position: fixed;
left: -9999px;
top: -9999px;
}
</style>