From 9f5c69e1e5bf1e95b2bab6d6e246d9f4b55695ed Mon Sep 17 00:00:00 2001 From: cheng <545895878@qq.com> Date: Mon, 20 Jul 2026 22:12:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E9=A1=B9=E7=9B=AE=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E4=B8=BA=E5=B9=B2=E6=9D=AF=E6=97=A5=E8=AE=B0?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96UI=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将应用名称从"喝了么"更改为"干杯日记" - 更新所有相关文件中的品牌标识和描述信息 - 升级HaveADrink依赖包从1.0.3到1.0.4版本 - 在pages.json中新增分享个人资料页面配置 - 重构DrinkCard组件UI,增加装饰背景、酒类图标支持、饮酒感受徽章等功能 - 优化API错误处理逻辑,增加静默模式支持 - 修改白酒类别图标为 urn emoji,黄酒图标为 tea emoji - 添加分享功能按钮并优化页面返回交互体验 - 引入新的工具函数用于获取酒类图标和路径判断 - 更新主题混入、常量定义等基础配置文件 - 调整全局样式和设计令牌以匹配新品牌形象 --- App.vue | 8 +- common/api.js | 33 +- common/constants.js | 8 +- common/mock-data.js | 2 +- common/theme-mixin.js | 2 +- common/utils.js | 17 +- components/DrinkCard.vue | 417 ++++++++---- docs/API接口文档.md | 4 +- manifest.json | 4 +- node_modules/.package-lock.json | 6 +- node_modules/HaveADrink/README.md | 37 +- node_modules/HaveADrink/index.d.ts | 14 +- node_modules/HaveADrink/package.json | 2 +- package-lock.json | 8 +- package.json | 2 +- pages.json | 9 +- pages/card/card.vue | 576 ++++++++++++---- pages/index/index.vue | 217 ++++-- pages/login/login.vue | 14 +- pages/onboarding/onboarding.vue | 12 + pages/profile/profile.vue | 922 ++++++++++++++++++++------ pages/record/record.vue | 74 ++- pages/share-profile/share-profile.vue | 492 ++++++++++++++ static/icons/baijiu.svg | 22 + uni.scss | 2 +- 25 files changed, 2333 insertions(+), 571 deletions(-) create mode 100644 pages/share-profile/share-profile.vue create mode 100644 static/icons/baijiu.svg diff --git a/App.vue b/App.vue index 8a02f6c..bdc5d19 100644 --- a/App.vue +++ b/App.vue @@ -4,7 +4,7 @@ import client, { refreshAuthToken } from './common/api' export default { onLaunch() { - console.log('喝了么 App Launch') + console.log('干杯日记 App Launch') // 初始化主题 this.initTheme() // 尝试刷新 token @@ -74,7 +74,7 @@ export default { diff --git a/docs/API接口文档.md b/docs/API接口文档.md index b94adca..bebdff2 100644 --- a/docs/API接口文档.md +++ b/docs/API接口文档.md @@ -1,8 +1,8 @@ -# 喝了么 - 后端接口文档 +# 干杯日记 - 后端接口文档 > 版本: v1.0 > 日期: 2026-07-13 -> 小程序: 喝了么(uni-app 微信小程序) +> 小程序: 干杯日记(uni-app 微信小程序) > 基础URL: `https://your-domain.com/api/v1` --- diff --git a/manifest.json b/manifest.json index aa503d5..f6d8e5d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { - "name" : "喝了么", + "name" : "干杯日记", "appid" : "__UNI__871D23D", - "description" : "让每一杯酒都有迹可循", + "description" : "让每一杯酒都有迹可循 - 干杯日记", "versionName" : "1.0.0", "versionCode" : "100", "transformPx" : false, diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index f9cc112..f930d21 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -4,9 +4,9 @@ "requires": true, "packages": { "node_modules/HaveADrink": { - "version": "1.0.3", - "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.3.tgz", - "integrity": "sha512-Mp4WRwqFmrf9CQhlF+gdJQ4YGRZkrBSbcu1l+XF4nq5dZmrFW7iggzFH4XSIXewBME1svk+cMhN6vetZVVOA6Q==", + "version": "1.0.4", + "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.4.tgz", + "integrity": "sha512-kSSpTZrQGjRueftSAopurZcwXGMS3oKQ0mvP3/sePJh7odO5ev9FT31isXX0LLmL1HojzB4nrIeDb/PTdGhYuA==", "license": "ISC" } } diff --git a/node_modules/HaveADrink/README.md b/node_modules/HaveADrink/README.md index 4b1a0f1..d0744e1 100644 --- a/node_modules/HaveADrink/README.md +++ b/node_modules/HaveADrink/README.md @@ -14,7 +14,7 @@ * 6. 社交模块 - 朋友圈动态、点赞 -**版本:** v1.0.3 +**版本:** v1.0.4 ## 安装 @@ -283,6 +283,8 @@ client.GetAchievements(req).then(...).catch(...) |unit|`Unit`| 单位| |degree|`number`| 酒精度数(%)| |standardCups|`number`| 标准杯数(前端计算,后端应校验)| +|customAmount|`number`| 自定义饮酒数量| +|customUnit|`Unit`| 自定义饮酒单位| **Category** @@ -310,6 +312,17 @@ client.GetAchievements(req).then(...).catch(...) |Shot|"shot"|`Unit`|shot| +**Unit** +|名称|值|类型|说明| +|:-|:-:|:-|:-| +|Ml|"ml"|`Unit`|毫升| +|Liang|"liang"|`Unit`|两| +|Bottle|"bottle"|`Unit`|瓶| +|Cup|"cup"|`Unit`|杯| +|Can|"can"|`Unit`|听| +|Shot|"shot"|`Unit`|shot| + + **FoodInfo** |名称|类型|说明| |:-|:-|:-| @@ -473,6 +486,8 @@ client.UploadPhotos(req).then(...).catch(...) |unit|`Unit`|required| 单位| |degree|`number`|required,gte=0,lte=100| 酒精度数(%)| |standardCups|`number`|| 标准杯数(前端计算,后端应校验)| +|customAmount|`number`|| 自定义饮酒数量| +|customUnit|`Unit`|| 自定义饮酒单位| **Category** @@ -502,6 +517,18 @@ client.UploadPhotos(req).then(...).catch(...) +**Unit** +|名称|值|类型|说明| +|:-|:-|:-|:-| +|Ml|"ml"|`Unit`|毫升| +|Liang|"liang"|`Unit`|两| +|Bottle|"bottle"|`Unit`|瓶| +|Cup|"cup"|`Unit`|杯| +|Can|"can"|`Unit`|听| +|Shot|"shot"|`Unit`|shot| + + + **FoodInfo** |名称|类型|校验规则|说明| |:-|:-|:-|:-| @@ -565,6 +592,8 @@ client.UploadPhotos(req).then(...).catch(...) |unit|`Unit`| 单位| |degree|`number`| 酒精度数(%)| |standardCups|`number`| 标准杯数(前端计算,后端应校验)| +|customAmount|`number`| 自定义饮酒数量| +|customUnit|`Unit`| 自定义饮酒单位| **FoodInfo** @@ -654,6 +683,8 @@ client.CreateRecord(req).then(...).catch(...) |unit|`Unit`| 单位| |degree|`number`| 酒精度数(%)| |standardCups|`number`| 标准杯数(前端计算,后端应校验)| +|customAmount|`number`| 自定义饮酒数量| +|customUnit|`Unit`| 自定义饮酒单位| **FoodInfo** @@ -740,6 +771,8 @@ client.GetRecords(req).then(...).catch(...) |unit|`Unit`| 单位| |degree|`number`| 酒精度数(%)| |standardCups|`number`| 标准杯数(前端计算,后端应校验)| +|customAmount|`number`| 自定义饮酒数量| +|customUnit|`Unit`| 自定义饮酒单位| **FoodInfo** @@ -856,6 +889,8 @@ client.DeleteRecord(req).then(...).catch(...) |unit|`Unit`| 单位| |degree|`number`| 酒精度数(%)| |standardCups|`number`| 标准杯数(前端计算,后端应校验)| +|customAmount|`number`| 自定义饮酒数量| +|customUnit|`Unit`| 自定义饮酒单位| **FoodInfo** diff --git a/node_modules/HaveADrink/index.d.ts b/node_modules/HaveADrink/index.d.ts index 192ac28..9cd4cd6 100644 --- a/node_modules/HaveADrink/index.d.ts +++ b/node_modules/HaveADrink/index.d.ts @@ -1133,6 +1133,14 @@ export declare class DrinkItem { * 标准杯数(前端计算,后端应校验) */ standardCups: number; + /** + * 自定义饮酒数量 + */ + customAmount: number; + /** + * 自定义饮酒单位 + */ + customUnit: Unit; /** * @param category Category 酒类分类ID @@ -1142,8 +1150,10 @@ export declare class DrinkItem { * @param unit Unit 单位 * @param degree number 酒精度数(%) * @param standardCups number 标准杯数(前端计算,后端应校验) + * @param customAmount number 自定义饮酒数量 + * @param customUnit Unit 自定义饮酒单位 */ - constructor(category: Category,brand: string,product: string,amount: number,unit: Unit,degree: number,standardCups: number,); + constructor(category: Category,brand: string,product: string,amount: number,unit: Unit,degree: number,standardCups: number,customAmount: number,customUnit: Unit,); /** * 从对象创建 DrinkItem * @@ -1155,6 +1165,8 @@ export declare class DrinkItem { * - unit: Unit, // 单位 * - degree: number, // 酒精度数(%) * - standardCups: number, // 标准杯数(前端计算,后端应校验) + * - customAmount: number, // 自定义饮酒数量 + * - customUnit: Unit, // 自定义饮酒单位 */ static fromObject(o: Object): DrinkItem; } diff --git a/node_modules/HaveADrink/package.json b/node_modules/HaveADrink/package.json index 39e6d7d..a9ad47c 100644 --- a/node_modules/HaveADrink/package.json +++ b/node_modules/HaveADrink/package.json @@ -1,7 +1,7 @@ { "name": "HaveADrink", "type": "module", - "version": "v1.0.3", + "version": "v1.0.4", "description": "喝酒了么 API 服务", "main": "index.js", "scripts": { diff --git a/package-lock.json b/package-lock.json index a39a457..6f3a24d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,13 +5,13 @@ "packages": { "": { "dependencies": { - "HaveADrink": "^1.0.3" + "HaveADrink": "^1.0.4" } }, "node_modules/HaveADrink": { - "version": "1.0.3", - "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.3.tgz", - "integrity": "sha512-Mp4WRwqFmrf9CQhlF+gdJQ4YGRZkrBSbcu1l+XF4nq5dZmrFW7iggzFH4XSIXewBME1svk+cMhN6vetZVVOA6Q==", + "version": "1.0.4", + "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.4.tgz", + "integrity": "sha512-kSSpTZrQGjRueftSAopurZcwXGMS3oKQ0mvP3/sePJh7odO5ev9FT31isXX0LLmL1HojzB4nrIeDb/PTdGhYuA==", "license": "ISC" } } diff --git a/package.json b/package.json index d37c6c3..da8c42f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "HaveADrink": "^1.0.3" + "HaveADrink": "^1.0.4" } } diff --git a/pages.json b/pages.json index 9be7938..c6342d5 100644 --- a/pages.json +++ b/pages.json @@ -41,11 +41,18 @@ "navigationStyle": "custom", "navigationBarTitleText": "" } + }, + { + "path": "pages/share-profile/share-profile", + "style": { + "navigationStyle": "custom", + "navigationBarTitleText": "" + } } ], "globalStyle": { "navigationBarTextStyle": "white", - "navigationBarTitleText": "喝了么", + "navigationBarTitleText": "干杯日记", "navigationBarBackgroundColor": "#0B0B14", "backgroundColor": "#0B0B14", "backgroundColorTop": "#0B0B14", diff --git a/pages/card/card.vue b/pages/card/card.vue index f39514f..4d81b2b 100644 --- a/pages/card/card.vue +++ b/pages/card/card.vue @@ -18,11 +18,12 @@ - - @@ -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 { diff --git a/pages/index/index.vue b/pages/index/index.vue index cd2be9f..303d98c 100644 --- a/pages/index/index.vue +++ b/pages/index/index.vue @@ -91,24 +91,37 @@ - {{ getCatEmoji(drink.category) }} {{ drink.brand }} {{ drink.product }} + + + + {{ getCatEmoji(drink.category) }} + {{ drink.brand }} {{ drink.product }} + + {{ drink.customAmount }}{{ getUnitLabel(drink.customUnit) }} + {{ getFeelingName(selectedRecord.feeling) }} {{ selectedRecord.standardCupsTotal }} 标准杯 - + + + + + 🚫 今日未饮酒,好样的 + 当天无记录 - + + @@ -118,9 +131,9 @@ diff --git a/pages/record/record.vue b/pages/record/record.vue index 28815a2..5934063 100644 --- a/pages/record/record.vue +++ b/pages/record/record.vue @@ -36,7 +36,8 @@ :class="{ 'category-active': currentDrink.category === cat.id }" @click="selectCategory(cat)" > - {{ cat.emoji }} + + {{ cat.emoji }} {{ cat.name }} @@ -112,6 +113,7 @@ @click="currentDrink.unit = u.id; onAmountChange()" > {{ u.name }} + ≈{{ u.toMl }}ml @@ -133,7 +135,11 @@ - {{ getCatEmoji(d.category) }} {{ d.brand }} {{ d.product }} · {{ d.amount }}{{ d.unit }} + + + {{ getCatEmoji(d.category) }} + {{ d.brand }} {{ d.product }} · {{ d.customAmount || d.amount }}{{ getUnitLabel(d.customUnit || d.unit) }} + {{ d.standardCups }}杯 @@ -308,7 +314,7 @@ import { DRINK_CATEGORIES, DRINK_UNITS, BRANDS, FOOD_CATEGORIES, FEELINGS, VISIBILITY_OPTIONS } from '../../common/constants' -import { calcStandardCups, unitToMl, formatDate } from '../../common/utils' +import { calcStandardCups, unitToMl, formatDate, getCatIcon, isIconPath } from '../../common/utils' import client from '../../common/api' import { addRecord } from '../../common/mock-data' import themeMixin from '../../common/theme-mixin' @@ -383,6 +389,18 @@ export default { this.backfillDate = options.date } }, + onShareAppMessage() { + return { + title: '干杯日记 - 记录每一杯酒', + path: '/pages/index/index' + } + }, + onShareTimeline() { + return { + title: '干杯日记 - 记录每一杯酒', + query: '' + } + }, onBackPress() { if (this.currentStep > 1 || this.hasFormData()) { uni.showModal({ @@ -420,6 +438,13 @@ export default { const cat = this.drinkCategories.find(c => c.id === catId) return cat ? cat.emoji : '🥃' }, + getCatIcon, + isIconPath, + getUnitLabel(unitId) { + if (!unitId) return '' + const u = DRINK_UNITS.find(u => u.id === unitId) + return u ? u.name : unitId + }, getFoodName() { const food = this.foodCategories.find(f => f.id === this.selectedFood) if (!food) return '' @@ -489,7 +514,9 @@ export default { ...this.currentDrink, amount: parseFloat(this.amountStr) || 0, degree: parseFloat(this.degreeStr) || 0, - standardCups: calcStandardCups(ml, parseFloat(this.degreeStr) || 0) + standardCups: calcStandardCups(ml, parseFloat(this.degreeStr) || 0), + customAmount: parseFloat(this.amountStr) || 0, + customUnit: this.currentDrink.unit }) // 重置表单,准备添加下一种 this.currentDrink = { category: '', brand: '', product: '', amount: 0, unit: 'ml', degree: 0 } @@ -508,11 +535,13 @@ export default { amount: d.amount, unit: d.unit, degree: d.degree, - standardCups: d.standardCups + standardCups: d.standardCups, + customAmount: d.customAmount || d.amount, + customUnit: d.customUnit || d.unit })), food: this.selectedFood ? { category: this.selectedFood, name: this.foodName || '' } : null, feeling: this.selectedFeeling || null, - photos: [], + photos: this.photos, visibility: 'friends', quote: '' } @@ -658,6 +687,12 @@ export default { margin-bottom: $sp-sm; } +.category-icon-img { + width: 56rpx; + height: 56rpx; + margin-bottom: $sp-sm; +} + .category-name { font-size: $fs-sm; font-weight: $fw-medium; @@ -780,6 +815,9 @@ export default { } .unit-btn { + display: flex; + align-items: baseline; + gap: 6rpx; padding: $sp-xs $sp-md; background: $bg-elevated; border-radius: $radius-full; @@ -792,9 +830,19 @@ export default { background: $amber-glow; color: $amber; border-color: rgba(232,168,56,0.3); + + .unit-ml { + color: rgba(232, 168, 56, 0.6); + } } } +.unit-ml { + font-size: 20rpx; + color: $text-tertiary; + font-family: $font-num; +} + .degree-input { width: 100rpx; text-align: center; @@ -1224,6 +1272,20 @@ export default { border-radius: $radius-full; } +.added-drink-name { + display: flex; + align-items: center; + gap: $sp-xs; + flex: 1; + min-width: 0; +} + +.inline-icon-img { + width: 36rpx; + height: 36rpx; + flex-shrink: 0; +} + .added-drink-right { display: flex; align-items: center; diff --git a/pages/share-profile/share-profile.vue b/pages/share-profile/share-profile.vue new file mode 100644 index 0000000..62df12a --- /dev/null +++ b/pages/share-profile/share-profile.vue @@ -0,0 +1,492 @@ + + + + + diff --git a/static/icons/baijiu.svg b/static/icons/baijiu.svg new file mode 100644 index 0000000..b3280ac --- /dev/null +++ b/static/icons/baijiu.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/uni.scss b/uni.scss index c9ec30c..c62a011 100644 --- a/uni.scss +++ b/uni.scss @@ -1,5 +1,5 @@ /* ========================================== - * 喝了么 - 设计令牌 (Design Tokens) + * 干杯日记 - 设计令牌 (Design Tokens) * 设计概念: 琥珀夜光 Amber Night / 暖白琥珀 Warm Day * 支持日夜主题自动切换 * ========================================== */