feat(app): 项目重命名为干杯日记并优化UI设计
- 将应用名称从"喝了么"更改为"干杯日记" - 更新所有相关文件中的品牌标识和描述信息 - 升级HaveADrink依赖包从1.0.3到1.0.4版本 - 在pages.json中新增分享个人资料页面配置 - 重构DrinkCard组件UI,增加装饰背景、酒类图标支持、饮酒感受徽章等功能 - 优化API错误处理逻辑,增加静默模式支持 - 修改白酒类别图标为 urn emoji,黄酒图标为 tea emoji - 添加分享功能按钮并优化页面返回交互体验 - 引入新的工具函数用于获取酒类图标和路径判断 - 更新主题混入、常量定义等基础配置文件 - 调整全局样式和设计令牌以匹配新品牌形象
This commit is contained in:
@@ -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 {
|
||||
|
||||
<style lang="scss">
|
||||
/* ==========================================
|
||||
* 喝了么 - 全局样式
|
||||
* 干杯日记 - 全局样式
|
||||
* 设计概念: 琥珀夜光 Amber Night
|
||||
* ========================================== */
|
||||
|
||||
@@ -308,8 +308,12 @@ page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
transform: none;
|
||||
background: linear-gradient(90deg, transparent, var(--btn-shimmer, rgba(255, 255, 255, 0.15)), transparent);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
+22
-11
@@ -1,4 +1,4 @@
|
||||
/* 喝了么 - API 服务层
|
||||
/* 干杯日记 - API 服务层
|
||||
* 基于 HaveADrink SDK 封装后端接口
|
||||
*/
|
||||
import HaveADrink from 'HaveADrink'
|
||||
@@ -13,6 +13,8 @@ const API_HOST = 'https://dev.wash-painting.cn'
|
||||
// 参考 goodBooth 项目的 fetchsomething
|
||||
// ==========================================
|
||||
function httpRequest(url, params) {
|
||||
// 支持静默模式(如后台刷新token),不弹 toast 不跳转
|
||||
const silent = params.silent === true
|
||||
return new Promise((resolve, reject) => {
|
||||
// 自动注入 Authorization token
|
||||
const headers = Object.assign({}, params.headers || {})
|
||||
@@ -31,23 +33,31 @@ function httpRequest(url, params) {
|
||||
const code = res.statusCode
|
||||
if (code === 200 || code === 201 || code === 204) {
|
||||
resolve(res.data)
|
||||
} else if (code === 401) {
|
||||
// token 过期,清除登录态并跳转登录页
|
||||
} else if (code === 401 || code === 418) {
|
||||
// token/refresh_token 过期或无效,清除登录态
|
||||
uni.removeStorageSync('auth_token')
|
||||
uni.removeStorageSync('refresh_token')
|
||||
uni.removeStorageSync('is_logged_in')
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
reject({ msg: '登录已过期,请重新登录' })
|
||||
if (!silent) {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none', duration: 2500 })
|
||||
}
|
||||
reject({ msg: '登录已过期,请重新登录', code })
|
||||
} else {
|
||||
const rawMsg = (res.data && res.data.msg) || (res.data && res.data.errmsg) || '请求失败'
|
||||
const errMsg = `[${code}] ${rawMsg}`
|
||||
uni.showToast({ title: errMsg, icon: 'none', duration: 2500 })
|
||||
if (!silent) {
|
||||
uni.showToast({ title: errMsg, icon: 'none', duration: 2500 })
|
||||
}
|
||||
reject({ msg: errMsg, code })
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.showToast({ title: err.errMsg || '网络请求失败', icon: 'none', duration: 2500 })
|
||||
reject({ msg: err.errMsg || '网络请求失败' })
|
||||
const msg = (err && err.errMsg) || '网络请求失败'
|
||||
if (!silent) {
|
||||
uni.showToast({ title: msg, icon: 'none', duration: 2500 })
|
||||
}
|
||||
reject({ msg })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -127,19 +137,20 @@ export function clearAuth() {
|
||||
client.setToken('')
|
||||
}
|
||||
|
||||
/** 尝试用 refresh_token 刷新 token */
|
||||
/** 尝试用 refresh_token 刷新 token(静默,不弹提示不跳转) */
|
||||
export async function refreshAuthToken() {
|
||||
const refreshToken = uni.getStorageSync('refresh_token')
|
||||
if (!refreshToken) return false
|
||||
try {
|
||||
const res = await client.RefreshToken({ refresh_token: refreshToken })
|
||||
const res = await client.RefreshToken({ refresh_token: refreshToken, silent: true })
|
||||
if (res && res.token) {
|
||||
saveAuthTokens(res)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (e) {
|
||||
console.warn('刷新 token 失败:', e)
|
||||
// 静默失败:refresh token 无效时清除残留凭证
|
||||
uni.removeStorageSync('refresh_token')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
/* 喝了么 - 常量定义 */
|
||||
/* 干杯日记 - 常量定义 */
|
||||
|
||||
// 酒类分类
|
||||
export const DRINK_CATEGORIES = [
|
||||
{ id: 'baijiu', name: '白酒', emoji: '🥃', defaultDegree: 52 },
|
||||
{ id: 'baijiu', name: '白酒', emoji: '⚱️', icon: '/static/icons/baijiu.svg', defaultDegree: 52 },
|
||||
{ id: 'beer', name: '啤酒', emoji: '🍺', defaultDegree: 5 },
|
||||
{ id: 'wine', name: '红酒', emoji: '🍷', defaultDegree: 13 },
|
||||
{ id: 'whisky', name: '洋酒', emoji: '🥂', defaultDegree: 40 },
|
||||
{ id: 'huangjiu', name: '黄酒', emoji: '🍶', defaultDegree: 15 },
|
||||
{ id: 'huangjiu', name: '黄酒', emoji: '🫖', defaultDegree: 15 },
|
||||
{ id: 'sake', name: '清酒', emoji: '🍶', defaultDegree: 15 },
|
||||
{ id: 'fruit', name: '果酒', emoji: '🍹', defaultDegree: 8 },
|
||||
{ id: 'cocktail', name: '调酒', emoji: '🍸', defaultDegree: 15 },
|
||||
@@ -176,7 +176,7 @@ export const ACHIEVEMENTS = [
|
||||
id: 'baijiu_master',
|
||||
name: '白酒达人',
|
||||
desc: '白酒累计打卡20次',
|
||||
icon: '🥃',
|
||||
icon: '⚱️',
|
||||
condition: { type: 'category_records', category: 'baijiu', value: 20 }
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/* 喝了么 - Mock 数据 */
|
||||
/* 干杯日记 - Mock 数据 */
|
||||
import { formatDate, calcStandardCups, unitToMl } from './utils'
|
||||
|
||||
// 模拟用户数据
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 喝了么 - 主题混入 (Theme Mixin)
|
||||
/* 干杯日记 - 主题混入 (Theme Mixin)
|
||||
* 每个页面混入此mixin,自动在onShow时刷新日夜主题
|
||||
* 页面根视图需绑定 :class="themeClass"
|
||||
*/
|
||||
|
||||
+16
-1
@@ -1,4 +1,19 @@
|
||||
/* 喝了么 - 工具函数 */
|
||||
/* 干杯日记 - 工具函数 */
|
||||
|
||||
import { DRINK_CATEGORIES } from './constants'
|
||||
|
||||
/**
|
||||
* 获取酒类图标:优先返回自定义图片路径,否则返回 emoji
|
||||
*/
|
||||
export function getCatIcon(catId) {
|
||||
const cat = DRINK_CATEGORIES.find(c => c.id === catId)
|
||||
if (!cat) return '🥃'
|
||||
return cat.icon || cat.emoji
|
||||
}
|
||||
|
||||
export function isIconPath(val) {
|
||||
return typeof val === 'string' && val.charAt(0) === '/'
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准杯换算引擎
|
||||
|
||||
+294
-123
@@ -1,17 +1,25 @@
|
||||
<template>
|
||||
<view class="card-root">
|
||||
<!-- 顶部:日期 -->
|
||||
<view class="card-header">
|
||||
<text class="header-date">{{ record.date }}</text>
|
||||
<!-- 装饰背景层 -->
|
||||
<view class="card-decor">
|
||||
<view class="decor-circle decor-circle-1"></view>
|
||||
<view class="decor-circle decor-circle-2"></view>
|
||||
<view class="decor-circle decor-circle-3"></view>
|
||||
<view class="decor-glow"></view>
|
||||
</view>
|
||||
|
||||
<!-- 照片区域 -->
|
||||
<!-- 顶部日期 -->
|
||||
<view class="card-top">
|
||||
<view class="date-line"></view>
|
||||
<text class="header-date">{{ record.date }}</text>
|
||||
<view class="date-line"></view>
|
||||
</view>
|
||||
|
||||
<!-- 主视觉区:照片或图标 -->
|
||||
<view v-if="photos.length > 0" class="card-photos">
|
||||
<!-- 1张: 大图 -->
|
||||
<view v-if="photos.length === 1" class="photo-single">
|
||||
<image :src="photos[0]" mode="aspectFill"></image>
|
||||
</view>
|
||||
<!-- 2-6张: 网格 -->
|
||||
<view v-else class="photo-grid">
|
||||
<view
|
||||
v-for="(photo, i) in photos"
|
||||
@@ -23,43 +31,65 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 无照片: emoji -->
|
||||
<view v-else class="card-hero">
|
||||
<view class="hero-emoji-wrap">
|
||||
<text class="hero-emoji">{{ mainEmoji }}</text>
|
||||
<view class="hero-ring"></view>
|
||||
<view class="hero-inner">
|
||||
<image v-if="isIconPath(mainIcon)" :src="mainIcon" class="hero-icon-img" mode="aspectFit"></image>
|
||||
<text v-else class="hero-emoji">{{ mainIcon }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 饮酒感受徽章 -->
|
||||
<view v-if="feelingData" class="feeling-badge">
|
||||
<text class="feeling-emoji">{{ feelingData.emoji }}</text>
|
||||
<text class="feeling-name">{{ feelingData.name }}</text>
|
||||
<text class="feeling-desc">{{ feelingData.desc }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 酒水清单 -->
|
||||
<view class="drink-list">
|
||||
<view class="list-title-row">
|
||||
<view class="list-dot"></view>
|
||||
<text class="list-title">今夜菜单</text>
|
||||
<view class="list-dot"></view>
|
||||
</view>
|
||||
<view v-for="(drink, i) in record.drinks" :key="i" class="drink-row">
|
||||
<text class="drink-name">{{ getCatEmoji(drink.category) }} {{ drink.brand }}</text>
|
||||
<text class="drink-detail">{{ formatAmount(drink) }}</text>
|
||||
<view class="drink-left">
|
||||
<image v-if="isIconPath(getCatIcon(drink.category))" :src="getCatIcon(drink.category)" class="drink-icon-img" mode="aspectFit"></image>
|
||||
<text v-else class="drink-emoji">{{ getCatEmoji(drink.category) }}</text>
|
||||
<text class="drink-name">{{ drink.brand }}</text>
|
||||
</view>
|
||||
<view class="drink-dots"></view>
|
||||
<text class="drink-amount">{{ formatAmount(drink) }}</text>
|
||||
</view>
|
||||
<view v-if="record.food" class="drink-row">
|
||||
<text class="drink-name">🍲 {{ getFoodName() }}</text>
|
||||
<text class="drink-detail"></text>
|
||||
<view class="drink-left">
|
||||
<text class="drink-emoji">🍲</text>
|
||||
<text class="drink-name">{{ getFoodName() }}</text>
|
||||
</view>
|
||||
<view class="drink-dots"></view>
|
||||
<text class="drink-amount">佐酒</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 饮酒感受 -->
|
||||
<view v-if="feelingLabel" class="feeling-row">
|
||||
<text class="feeling-text">{{ feelingLabel }}</text>
|
||||
<!-- 分割装饰 -->
|
||||
<view class="divider-decor">
|
||||
<view class="divider-line"></view>
|
||||
<text class="divider-icon">🥂</text>
|
||||
<view class="divider-line"></view>
|
||||
</view>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<view class="divider"></view>
|
||||
|
||||
<!-- 喝后心得(酒言酒语) -->
|
||||
<view class="card-footer">
|
||||
<text class="footer-label">喝后心得</text>
|
||||
<text class="footer-quote">"{{ quote }}"</text>
|
||||
<!-- 酒言酒语 -->
|
||||
<view class="quote-section">
|
||||
<text class="quote-mark">"</text>
|
||||
<text class="quote-text">{{ quote }}</text>
|
||||
<text class="quote-mark quote-mark-end">"</text>
|
||||
</view>
|
||||
|
||||
<!-- 品牌 + 二维码 -->
|
||||
<!-- 品牌底栏 -->
|
||||
<view class="card-brand">
|
||||
<view class="brand-info">
|
||||
<text class="brand-text">🍻 喝了么</text>
|
||||
<view class="brand-left">
|
||||
<text class="brand-name">干杯日记</text>
|
||||
<text class="brand-slogan">记录每一杯的故事</text>
|
||||
</view>
|
||||
<view class="brand-qr"></view>
|
||||
@@ -69,6 +99,7 @@
|
||||
|
||||
<script>
|
||||
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../common/constants'
|
||||
import { getCatIcon, isIconPath } from '../common/utils'
|
||||
|
||||
const UNIT_NAMES = { ml: 'ml', liang: '两', bottle: '瓶', cup: '杯', can: '听', shot: 'shot' }
|
||||
|
||||
@@ -85,16 +116,17 @@ export default {
|
||||
photos() {
|
||||
return (this.record && this.record.photos) || []
|
||||
},
|
||||
mainEmoji() {
|
||||
mainIcon() {
|
||||
const first = (this.record.drinks || [])[0]
|
||||
return first ? this.getCatEmoji(first.category) : '🥃'
|
||||
return first ? getCatIcon(first.category) : '🥃'
|
||||
},
|
||||
feelingLabel() {
|
||||
const f = FEELINGS.find(fe => fe.id === this.record.feeling)
|
||||
return f ? `${f.emoji} ${f.name}` : ''
|
||||
feelingData() {
|
||||
return FEELINGS.find(fe => fe.id === this.record.feeling) || null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCatIcon,
|
||||
isIconPath,
|
||||
getCatEmoji(catId) {
|
||||
const cat = DRINK_CATEGORIES.find(c => c.id === catId)
|
||||
return cat ? cat.emoji : '🥃'
|
||||
@@ -116,178 +148,317 @@ export default {
|
||||
.card-root {
|
||||
position: relative;
|
||||
width: 620rpx;
|
||||
border-radius: 32rpx;
|
||||
border-radius: 40rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(170deg, var(--card-gradient-start, #1A1510) 0%, var(--card-gradient-end, #0D0B08) 100%);
|
||||
border: 2rpx solid var(--border-subtle, rgba(255, 255, 255, 0.10));
|
||||
box-shadow: var(--shadow-lg, 0 16rpx 48rpx rgba(0, 0, 0, 0.45)), 0 0 60rpx var(--amber-glow, rgba(232, 168, 56, 0.15));
|
||||
background: linear-gradient(175deg, #1C1428 0%, #0F0A1A 40%, #0A0710 100%);
|
||||
border: 2rpx solid rgba(232, 168, 56, 0.2);
|
||||
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.6), 0 0 80rpx rgba(232, 168, 56, 0.08), inset 0 1rpx 0 rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 56rpx 48rpx 40rpx;
|
||||
}
|
||||
|
||||
/* 顶部 */
|
||||
.card-header {
|
||||
padding: $sp-lg $sp-xl 0;
|
||||
/* === 装饰背景 === */
|
||||
.card-decor {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.decor-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 1.5rpx solid rgba(232, 168, 56, 0.12);
|
||||
}
|
||||
.decor-circle-1 {
|
||||
width: 300rpx; height: 300rpx;
|
||||
top: -80rpx; right: -60rpx;
|
||||
}
|
||||
.decor-circle-2 {
|
||||
width: 200rpx; height: 200rpx;
|
||||
bottom: 200rpx; left: -60rpx;
|
||||
border-color: rgba(139, 133, 184, 0.1);
|
||||
}
|
||||
.decor-circle-3 {
|
||||
width: 120rpx; height: 120rpx;
|
||||
top: 320rpx; right: 40rpx;
|
||||
background: rgba(232, 168, 56, 0.03);
|
||||
}
|
||||
.decor-glow {
|
||||
position: absolute;
|
||||
width: 400rpx; height: 400rpx;
|
||||
top: -100rpx; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: radial-gradient(circle, rgba(232, 168, 56, 0.06) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* === 顶部日期 === */
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 40rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.date-line {
|
||||
width: 60rpx;
|
||||
height: 2rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(232, 168, 56, 0.5));
|
||||
}
|
||||
.date-line:last-child {
|
||||
background: linear-gradient(90deg, rgba(232, 168, 56, 0.5), transparent);
|
||||
}
|
||||
.header-date {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
font-size: 22rpx;
|
||||
color: rgba(232, 168, 56, 0.8);
|
||||
font-family: $font-num;
|
||||
letter-spacing: 4rpx;
|
||||
letter-spacing: 6rpx;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
|
||||
/* 照片区域 */
|
||||
/* === 照片区域 === */
|
||||
.card-photos {
|
||||
padding: $sp-lg $sp-xl 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
/* 1张:大图 */
|
||||
.photo-single {
|
||||
width: 100%;
|
||||
height: 340rpx;
|
||||
border-radius: $radius-lg;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.4);
|
||||
image { width: 100%; height: 100%; }
|
||||
}
|
||||
|
||||
/* 2-6张:网格 */
|
||||
.photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $sp-sm;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.photo-cell {
|
||||
aspect-ratio: 1;
|
||||
border-radius: $radius-md;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
image { width: 100%; height: 100%; }
|
||||
}
|
||||
/* 奇数张时第一张占满两列 */
|
||||
.photo-span {
|
||||
grid-column: span 2;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
/* emoji占位 */
|
||||
/* === 主视觉图标 === */
|
||||
.card-hero {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: $sp-xl $sp-xl $sp-lg;
|
||||
padding: 24rpx 0 32rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.hero-emoji-wrap {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: $radius-full;
|
||||
background: rgba(232, 168, 56, 0.08);
|
||||
.hero-ring {
|
||||
position: absolute;
|
||||
width: 220rpx; height: 220rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(232, 168, 56, 0.15);
|
||||
animation: pulse-ring 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-ring {
|
||||
0%, 100% { transform: scale(1); opacity: 0.6; }
|
||||
50% { transform: scale(1.08); opacity: 1; }
|
||||
}
|
||||
.hero-inner {
|
||||
width: 180rpx; height: 180rpx;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 35%, rgba(232, 168, 56, 0.15), rgba(232, 168, 56, 0.04));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(232, 168, 56, 0.1), inset 0 0 40rpx rgba(232, 168, 56, 0.05);
|
||||
}
|
||||
.hero-emoji {
|
||||
font-size: 100rpx;
|
||||
font-size: 88rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.hero-icon-img {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
}
|
||||
|
||||
/* 酒水清单 */
|
||||
.drink-list {
|
||||
padding: 0 $sp-xl;
|
||||
/* === 感受徽章 === */
|
||||
.feeling-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $sp-md;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin: 0 auto 32rpx;
|
||||
padding: 14rpx 32rpx;
|
||||
background: linear-gradient(135deg, rgba(232, 168, 56, 0.12), rgba(232, 168, 56, 0.04));
|
||||
border: 1.5rpx solid rgba(232, 168, 56, 0.25);
|
||||
border-radius: 999rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.feeling-emoji {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.feeling-name {
|
||||
font-size: 26rpx;
|
||||
color: #E8A838;
|
||||
font-weight: $fw-bold;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.feeling-desc {
|
||||
font-size: 20rpx;
|
||||
color: rgba(232, 168, 56, 0.6);
|
||||
}
|
||||
|
||||
/* === 酒水清单 === */
|
||||
.drink-list {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 24rpx;
|
||||
border: 1.5rpx solid rgba(255, 255, 255, 0.05);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.list-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
.list-dot {
|
||||
width: 8rpx; height: 8rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(232, 168, 56, 0.5);
|
||||
}
|
||||
.list-title {
|
||||
font-size: 22rpx;
|
||||
color: rgba(232, 168, 56, 0.7);
|
||||
letter-spacing: 8rpx;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
.drink-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14rpx 0;
|
||||
}
|
||||
.drink-name {
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-medium;
|
||||
.drink-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.drink-detail {
|
||||
font-size: $fs-sm;
|
||||
color: $amber;
|
||||
.drink-icon-img {
|
||||
width: 34rpx; height: 34rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.drink-emoji {
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.drink-name {
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
.drink-dots {
|
||||
flex: 1;
|
||||
height: 2rpx;
|
||||
margin: 0 16rpx;
|
||||
background: repeating-linear-gradient(90deg, rgba(255,255,255,0.15) 0, rgba(255,255,255,0.15) 4rpx, transparent 4rpx, transparent 10rpx);
|
||||
}
|
||||
.drink-amount {
|
||||
font-size: 26rpx;
|
||||
color: #E8A838;
|
||||
font-family: $font-num;
|
||||
font-weight: $fw-bold;
|
||||
flex-shrink: 0;
|
||||
margin-left: $sp-md;
|
||||
}
|
||||
|
||||
/* 饮酒感受 */
|
||||
.feeling-row {
|
||||
padding: $sp-sm $sp-xl 0;
|
||||
/* === 分割装饰 === */
|
||||
.divider-decor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 32rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.feeling-text {
|
||||
font-size: $fs-base;
|
||||
color: $amber;
|
||||
font-weight: $fw-medium;
|
||||
letter-spacing: 2rpx;
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1.5rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(232, 168, 56, 0.2), transparent);
|
||||
}
|
||||
.divider-icon {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.divider {
|
||||
margin: $sp-lg $sp-xl;
|
||||
height: 2rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(232, 168, 56, 0.15), transparent);
|
||||
/* === 酒言酒语 === */
|
||||
.quote-section {
|
||||
text-align: center;
|
||||
padding: 0 16rpx;
|
||||
margin-bottom: 40rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.card-footer {
|
||||
padding: 0 $sp-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $sp-sm;
|
||||
.quote-mark {
|
||||
font-size: 48rpx;
|
||||
color: rgba(232, 168, 56, 0.3);
|
||||
font-family: Georgia, serif;
|
||||
line-height: 1;
|
||||
}
|
||||
.footer-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
.quote-mark-end {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
.quote-text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
line-height: 1.8;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.footer-quote {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
line-height: $lh-loose;
|
||||
padding: 8rpx 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 品牌 + 二维码 */
|
||||
/* === 品牌底栏 === */
|
||||
.card-brand {
|
||||
padding: $sp-lg $sp-xl $sp-xl;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 32rpx;
|
||||
border-top: 1.5rpx solid rgba(255, 255, 255, 0.05);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.brand-info {
|
||||
.brand-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.brand-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
.brand-name {
|
||||
font-size: 26rpx;
|
||||
color: rgba(232, 168, 56, 0.8);
|
||||
font-weight: $fw-bold;
|
||||
letter-spacing: 2rpx;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
.brand-slogan {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.brand-qr {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: $radius-sm;
|
||||
border: 2rpx solid var(--border-subtle, rgba(255, 255, 255, 0.10));
|
||||
background: var(--bg-subtle, rgba(255, 255, 255, 0.03));
|
||||
border-radius: 12rpx;
|
||||
border: 1.5rpx solid rgba(232, 168, 56, 0.2);
|
||||
background: rgba(232, 168, 56, 0.04);
|
||||
}
|
||||
</style>
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
# 喝了么 - 后端接口文档
|
||||
# 干杯日记 - 后端接口文档
|
||||
|
||||
> 版本: v1.0
|
||||
> 日期: 2026-07-13
|
||||
> 小程序: 喝了么(uni-app 微信小程序)
|
||||
> 小程序: 干杯日记(uni-app 微信小程序)
|
||||
> 基础URL: `https://your-domain.com/api/v1`
|
||||
|
||||
---
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name" : "喝了么",
|
||||
"name" : "干杯日记",
|
||||
"appid" : "__UNI__871D23D",
|
||||
"description" : "让每一杯酒都有迹可循",
|
||||
"description" : "让每一杯酒都有迹可循 - 干杯日记",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
|
||||
+3
-3
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -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**
|
||||
|
||||
+13
-1
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "HaveADrink",
|
||||
"type": "module",
|
||||
"version": "v1.0.3",
|
||||
"version": "v1.0.4",
|
||||
"description": "喝酒了么 API 服务",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
Generated
+4
-4
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"HaveADrink": "^1.0.3"
|
||||
"HaveADrink": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -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",
|
||||
|
||||
+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 {
|
||||
|
||||
+177
-40
@@ -91,24 +91,37 @@
|
||||
<view v-if="selectedRecord.mode === 'drank'" class="day-record-info">
|
||||
<view class="day-drinks">
|
||||
<view v-for="(drink, i) in selectedRecord.drinks" :key="i" class="day-drink-item">
|
||||
<text>{{ getCatEmoji(drink.category) }} {{ drink.brand }} {{ drink.product }}</text>
|
||||
<view class="day-drink-row">
|
||||
<view class="day-drink-name">
|
||||
<image v-if="isIconPath(getCatIcon(drink.category))" :src="getCatIcon(drink.category)" class="day-drink-icon" mode="aspectFit"></image>
|
||||
<text v-else>{{ getCatEmoji(drink.category) }}</text>
|
||||
<text>{{ drink.brand }} {{ drink.product }}</text>
|
||||
</view>
|
||||
<text v-if="drink.customAmount != null" class="day-drink-amount text-num">{{ drink.customAmount }}{{ getUnitLabel(drink.customUnit) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="day-meta flex-between">
|
||||
<text class="text-caption">{{ getFeelingName(selectedRecord.feeling) }}</text>
|
||||
<text class="text-caption text-amber text-num">{{ selectedRecord.standardCupsTotal }} 标准杯</text>
|
||||
</view>
|
||||
<button class="btn-ghost day-toggle-btn" @click="toggleToAbstain">改为未饮酒</button>
|
||||
<view class="day-action-row">
|
||||
<button class="btn-ghost day-action-btn" @click="editRecord">修改</button>
|
||||
<button class="btn-ghost day-action-btn" @click="toggleToAbstain">改为未饮酒</button>
|
||||
</view>
|
||||
<button class="btn-danger day-delete-btn" @click="deleteRecord">删除记录</button>
|
||||
</view>
|
||||
<view v-else class="day-abstain">
|
||||
<text class="day-abstain-emoji">🚫</text>
|
||||
<text class="text-caption">今日未饮酒,好样的</text>
|
||||
<button class="btn-ghost day-toggle-btn" @click="toggleToDrank">改为饮酒</button>
|
||||
<button class="btn-danger day-delete-btn" @click="deleteRecord">删除记录</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="day-empty">
|
||||
<text class="text-caption">当天无记录</text>
|
||||
<button class="btn-ghost" @click="backfillDate">补打卡</button>
|
||||
<button v-if="isSelectedToday" class="btn-primary day-record-btn" @click="goRecordToday">去记录</button>
|
||||
<button v-else class="btn-ghost" @click="backfillDate">补打卡</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -118,9 +131,9 @@
|
||||
<script>
|
||||
import DrinkCalendar from '../../components/DrinkCalendar.vue'
|
||||
import client from '../../common/api'
|
||||
import { GetCalendarReq } from 'HaveADrink'
|
||||
import { getGreeting, formatDate, getWeekStart } from '../../common/utils'
|
||||
import { FEELINGS, DRINK_CATEGORIES } from '../../common/constants'
|
||||
import { GetCalendarReq, DeleteRecordReq, CreateRecordReq, GetRecordDetailReq } from 'HaveADrink'
|
||||
import { getGreeting, formatDate, getWeekStart, getCatIcon, isIconPath } from '../../common/utils'
|
||||
import { FEELINGS, DRINK_CATEGORIES, DRINK_UNITS } from '../../common/constants'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
@@ -152,6 +165,9 @@ export default {
|
||||
healthPercent() {
|
||||
// 建议每周不超过14标准杯
|
||||
return Math.min((this.stats.weekCups / 14) * 100, 100)
|
||||
},
|
||||
isSelectedToday() {
|
||||
return this.selectedDate === formatDate(new Date(), 'YYYY-MM-DD')
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -163,6 +179,18 @@ export default {
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '干杯日记 - 让每一杯酒都有迹可循',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '干杯日记 - 让每一杯酒都有迹可循',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.greeting = getGreeting()
|
||||
@@ -178,15 +206,8 @@ export default {
|
||||
if (userResp.status === 'fulfilled' && userResp.value) {
|
||||
const user = userResp.value
|
||||
this.userName = user.nickname || '酒友'
|
||||
uni.setStorageSync('user_info', JSON.stringify(user))
|
||||
} else {
|
||||
// 降级读取本地缓存
|
||||
try {
|
||||
const cached = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
this.userName = cached.nickname || '酒友'
|
||||
} catch (e) {
|
||||
this.userName = '酒友'
|
||||
}
|
||||
this.userName = '酒友'
|
||||
}
|
||||
|
||||
// 统计概览
|
||||
@@ -218,7 +239,9 @@ export default {
|
||||
standardCupsTotal: d.standardCupsTotal || 0,
|
||||
drinks: (d.drinks || []).map(dr => ({
|
||||
...dr,
|
||||
category: String(dr.category)
|
||||
category: String(dr.category),
|
||||
customAmount: dr.customAmount,
|
||||
customUnit: dr.customUnit ? String(dr.customUnit) : ''
|
||||
})),
|
||||
food: d.food ? { category: String(d.food.category), name: d.food.name || '' } : null,
|
||||
feeling: d.feeling ? String(d.feeling) : null,
|
||||
@@ -228,13 +251,7 @@ export default {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载首页数据失败:', e)
|
||||
// 降级:读取本地缓存
|
||||
try {
|
||||
const cached = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
this.userName = cached.nickname || '酒友'
|
||||
} catch (err) {
|
||||
this.userName = '酒友'
|
||||
}
|
||||
this.userName = '酒友'
|
||||
}
|
||||
},
|
||||
onMonthChange({ year, month }) {
|
||||
@@ -255,7 +272,9 @@ export default {
|
||||
standardCupsTotal: d.standardCupsTotal || 0,
|
||||
drinks: (d.drinks || []).map(dr => ({
|
||||
...dr,
|
||||
category: String(dr.category)
|
||||
category: String(dr.category),
|
||||
customAmount: dr.customAmount,
|
||||
customUnit: dr.customUnit ? String(dr.customUnit) : ''
|
||||
})),
|
||||
food: d.food ? { category: String(d.food.category), name: d.food.name || '' } : null,
|
||||
feeling: d.feeling ? String(d.feeling) : null,
|
||||
@@ -266,10 +285,37 @@ export default {
|
||||
console.warn('加载月度记录失败:', e)
|
||||
}
|
||||
},
|
||||
onDateSelect(dateStr) {
|
||||
async onDateSelect(dateStr) {
|
||||
this.selectedDate = dateStr
|
||||
this.selectedRecord = this.records.find(r => r.date === dateStr) || null
|
||||
this.showDayDetail = true
|
||||
const localRecord = this.records.find(r => r.date === dateStr) || null
|
||||
// 如果有记录且有id,拉取完整详情(确保拿到 customAmount/customUnit 等全量字段)
|
||||
if (localRecord && localRecord.id && localRecord.mode === 'drank') {
|
||||
this.selectedRecord = localRecord // 先用本地数据展示
|
||||
this.showDayDetail = true
|
||||
try {
|
||||
const resp = await client.GetRecordDetail(new GetRecordDetailReq(localRecord.id))
|
||||
const rec = resp.data || resp
|
||||
if (rec) {
|
||||
this.selectedRecord = {
|
||||
...localRecord,
|
||||
...rec,
|
||||
mode: String(rec.mode || localRecord.mode),
|
||||
drinks: (rec.drinks || localRecord.drinks || []).map(d => ({
|
||||
...d,
|
||||
category: String(d.category),
|
||||
customUnit: d.customUnit ? String(d.customUnit) : ''
|
||||
})),
|
||||
feeling: rec.feeling ? String(rec.feeling) : localRecord.feeling,
|
||||
visibility: rec.visibility ? String(rec.visibility) : localRecord.visibility
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('获取记录详情失败,使用本地数据:', e)
|
||||
}
|
||||
} else {
|
||||
this.selectedRecord = localRecord
|
||||
this.showDayDetail = true
|
||||
}
|
||||
},
|
||||
getFeelingName(feelingId) {
|
||||
const f = FEELINGS.find(f => f.id === feelingId)
|
||||
@@ -279,28 +325,45 @@ export default {
|
||||
const cat = DRINK_CATEGORIES.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
|
||||
},
|
||||
backfillDate() {
|
||||
this.showDayDetail = false
|
||||
uni.navigateTo({
|
||||
url: `/pages/record/record?date=${this.selectedDate}&mode=backfill`
|
||||
})
|
||||
},
|
||||
// 修改记录(跳转到记录页编辑)
|
||||
editRecord() {
|
||||
this.showDayDetail = false
|
||||
uni.navigateTo({
|
||||
url: `/pages/record/record?date=${this.selectedDate}&mode=edit`
|
||||
})
|
||||
},
|
||||
// 改为未饮酒
|
||||
toggleToAbstain() {
|
||||
uni.showModal({
|
||||
title: '确认修改',
|
||||
content: '确定将这天改为未饮酒吗?饮酒记录将被清除。',
|
||||
success: (res) => {
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
const idx = this.records.findIndex(r => r.date === this.selectedDate)
|
||||
if (idx === -1) return
|
||||
this.records[idx].mode = 'abstain'
|
||||
this.records[idx].drinks = []
|
||||
this.records[idx].food = null
|
||||
this.records[idx].feeling = null
|
||||
this.records[idx].standardCupsTotal = 0
|
||||
this.selectedRecord = this.records[idx]
|
||||
this.saveRecords()
|
||||
const recordId = this.selectedRecord && this.selectedRecord.id
|
||||
try {
|
||||
if (recordId) {
|
||||
await client.DeleteRecord(new DeleteRecordReq(recordId))
|
||||
}
|
||||
uni.showToast({ title: '已修改', icon: 'success' })
|
||||
this.showDayDetail = false
|
||||
this.loadData()
|
||||
} catch (e) {
|
||||
console.warn('修改为未饮酒失败:', e)
|
||||
uni.showToast({ title: '修改失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -311,13 +374,37 @@ export default {
|
||||
url: `/pages/record/record?date=${this.selectedDate}&mode=backfill`
|
||||
})
|
||||
},
|
||||
// 保存记录到本地存储
|
||||
saveRecords() {
|
||||
uni.setStorageSync('drink_records', JSON.stringify(this.records))
|
||||
// 删除记录
|
||||
deleteRecord() {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定删除 ${this.selectedDate} 的记录吗?删除后不可恢复。`,
|
||||
confirmColor: '#e85d3a',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
const recordId = this.selectedRecord && this.selectedRecord.id
|
||||
try {
|
||||
if (recordId) {
|
||||
await client.DeleteRecord(new DeleteRecordReq(recordId))
|
||||
}
|
||||
this.showDayDetail = false
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
// 重新加载统计和日历数据
|
||||
this.loadData()
|
||||
} catch (e) {
|
||||
console.warn('删除记录失败:', e)
|
||||
uni.showToast({ title: '删除失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
goRecord() {
|
||||
uni.navigateTo({ url: '/pages/record/record' })
|
||||
},
|
||||
goRecordToday() {
|
||||
this.showDayDetail = false
|
||||
uni.navigateTo({ url: '/pages/record/record' })
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
@@ -555,6 +642,33 @@ export default {
|
||||
border-bottom: 2rpx solid var(--border-micro, rgba(255, 255, 255, 0.05));
|
||||
}
|
||||
|
||||
.day-drink-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.day-drink-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.day-drink-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.day-drink-amount {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
white-space: nowrap;
|
||||
margin-left: $sp-sm;
|
||||
}
|
||||
|
||||
.day-meta {
|
||||
margin-top: $sp-lg;
|
||||
}
|
||||
@@ -574,11 +688,34 @@ export default {
|
||||
text-align: center;
|
||||
padding: $sp-xl 0;
|
||||
|
||||
.btn-ghost {
|
||||
.btn-ghost, .day-record-btn {
|
||||
margin-top: $sp-lg;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.day-action-row {
|
||||
display: flex;
|
||||
gap: $sp-sm;
|
||||
margin-top: $sp-lg;
|
||||
}
|
||||
|
||||
.day-action-btn {
|
||||
flex: 1;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
border-color: var(--border-active, rgba(255, 255, 255, 0.12));
|
||||
}
|
||||
|
||||
.day-delete-btn {
|
||||
margin-top: $sp-md;
|
||||
width: 100%;
|
||||
font-size: $fs-sm;
|
||||
color: #e85d3a;
|
||||
background: rgba(232, 93, 58, 0.08);
|
||||
border: 2rpx solid rgba(232, 93, 58, 0.2);
|
||||
}
|
||||
|
||||
.day-toggle-btn {
|
||||
margin-top: $sp-lg;
|
||||
width: 100%;
|
||||
|
||||
+13
-1
@@ -6,7 +6,7 @@
|
||||
<view class="brand-icon">
|
||||
<text class="brand-emoji">🍻</text>
|
||||
</view>
|
||||
<text class="brand-name">喝了么</text>
|
||||
<text class="brand-name">干杯日记</text>
|
||||
<text class="brand-slogan">让每一杯酒都有迹可循</text>
|
||||
</view>
|
||||
|
||||
@@ -103,6 +103,18 @@ export default {
|
||||
nickname: ''
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '干杯日记 - 让每一杯酒都有迹可循',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '干杯日记 - 让每一杯酒都有迹可循',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
acceptNotice() {
|
||||
this.showNotice = false
|
||||
|
||||
@@ -98,6 +98,18 @@ export default {
|
||||
const sys = uni.getSystemInfoSync()
|
||||
this.swiperHeight = sys.windowHeight - 60 - 200
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '干杯日记 - 让每一杯酒都有迹可循',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '干杯日记 - 让每一杯酒都有迹可循',
|
||||
query: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSlideChange(e) {
|
||||
this.currentSlide = e.detail.current
|
||||
|
||||
+703
-219
File diff suppressed because it is too large
Load Diff
+68
-6
@@ -36,7 +36,8 @@
|
||||
:class="{ 'category-active': currentDrink.category === cat.id }"
|
||||
@click="selectCategory(cat)"
|
||||
>
|
||||
<text class="category-emoji">{{ cat.emoji }}</text>
|
||||
<image v-if="isIconPath(cat.icon)" :src="cat.icon" class="category-icon-img" mode="aspectFit"></image>
|
||||
<text v-else class="category-emoji">{{ cat.emoji }}</text>
|
||||
<text class="category-name">{{ cat.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -112,6 +113,7 @@
|
||||
@click="currentDrink.unit = u.id; onAmountChange()"
|
||||
>
|
||||
<text>{{ u.name }}</text>
|
||||
<text v-if="u.id !== 'ml'" class="unit-ml">≈{{ u.toMl }}ml</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -133,7 +135,11 @@
|
||||
</view>
|
||||
<view v-for="(d, i) in drinks" :key="i" class="added-drink-item card">
|
||||
<view class="flex-between">
|
||||
<text>{{ getCatEmoji(d.category) }} {{ d.brand }} {{ d.product }} · {{ d.amount }}{{ d.unit }}</text>
|
||||
<view class="added-drink-name">
|
||||
<image v-if="isIconPath(getCatIcon(d.category))" :src="getCatIcon(d.category)" class="inline-icon-img" mode="aspectFit"></image>
|
||||
<text v-else>{{ getCatEmoji(d.category) }}</text>
|
||||
<text>{{ d.brand }} {{ d.product }} · {{ d.customAmount || d.amount }}{{ getUnitLabel(d.customUnit || d.unit) }}</text>
|
||||
</view>
|
||||
<view class="added-drink-right">
|
||||
<text class="text-amber text-num">{{ d.standardCups }}杯</text>
|
||||
<view class="added-drink-remove" @click="removeDrink(i)">
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
<template>
|
||||
<view :class="themeClass" class="page-container share-page">
|
||||
<!-- 氛围背景 -->
|
||||
<view class="ambient">
|
||||
<view class="glow glow-1"></view>
|
||||
<view class="glow glow-2"></view>
|
||||
</view>
|
||||
|
||||
<!-- 酒桌名片 -->
|
||||
<view class="biz-card">
|
||||
<!-- 品牌条 -->
|
||||
<view class="card-brand">
|
||||
<text class="brand-mark">🍻 干杯日记</text>
|
||||
<text class="brand-label">饮 酒 名 片</text>
|
||||
</view>
|
||||
|
||||
<!-- 身份区 -->
|
||||
<view class="identity">
|
||||
<view class="id-avatar-wrap">
|
||||
<image
|
||||
v-if="info.avatar && !avatarError"
|
||||
class="id-avatar"
|
||||
:src="info.avatar"
|
||||
mode="aspectFill"
|
||||
@error="avatarError = true"
|
||||
></image>
|
||||
<view v-else class="id-avatar id-avatar-fallback">
|
||||
<text class="id-avatar-emoji">🥃</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="id-name">{{ info.nick }}</text>
|
||||
<text class="id-slogan">这是我的饮酒档案,来交个酒友吧</text>
|
||||
</view>
|
||||
|
||||
<!-- 票券撕边分割线 -->
|
||||
<view class="perf-divider">
|
||||
<view class="perf-hole perf-hole-l"></view>
|
||||
<view class="perf-line"></view>
|
||||
<view class="perf-hole perf-hole-r"></view>
|
||||
</view>
|
||||
|
||||
<!-- 数据存根 -->
|
||||
<view class="stats-stub">
|
||||
<view class="stub-item">
|
||||
<text class="stub-num text-num">{{ anim.days }}</text>
|
||||
<text class="stub-label">打卡天数</text>
|
||||
</view>
|
||||
<view class="stub-sep"></view>
|
||||
<view class="stub-item">
|
||||
<text class="stub-num text-num">{{ anim.records }}</text>
|
||||
<text class="stub-label">饮酒次数</text>
|
||||
</view>
|
||||
<view class="stub-sep"></view>
|
||||
<view class="stub-item">
|
||||
<text class="stub-num stub-num-amber text-num">{{ anim.cups }}</text>
|
||||
<text class="stub-label">标准杯</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 连续记录 + 最爱酒类 -->
|
||||
<view class="extra-row">
|
||||
<view class="extra-chip">
|
||||
<text class="extra-chip-emoji">{{ info.streakType === 'drank' ? '🔥' : '💪' }}</text>
|
||||
<text class="extra-chip-text">{{ info.streakType === 'drank' ? '连续饮酒' : '连续戒酒' }} {{ info.streak }} 天</text>
|
||||
</view>
|
||||
<view v-if="favName" class="extra-chip">
|
||||
<text class="extra-chip-emoji">{{ favEmoji }}</text>
|
||||
<text class="extra-chip-text">最爱{{ favName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- CTA -->
|
||||
<button class="cta-btn" @click="enterApp">
|
||||
<text class="cta-text">我也想记 · 打开干杯日记</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<text class="page-footer">让每一杯酒都有迹可循</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
import { DRINK_CATEGORIES } from '../../common/constants'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
return {
|
||||
info: {
|
||||
nick: '酒友',
|
||||
avatar: '',
|
||||
days: 0,
|
||||
records: 0,
|
||||
cups: 0,
|
||||
streak: 0,
|
||||
streakType: 'drank',
|
||||
fav: ''
|
||||
},
|
||||
anim: { days: 0, records: 0, cups: 0 },
|
||||
avatarError: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
favCat() {
|
||||
return DRINK_CATEGORIES.find(c => c.id === this.info.fav) || null
|
||||
},
|
||||
favName() {
|
||||
return this.favCat ? this.favCat.name : ''
|
||||
},
|
||||
favEmoji() {
|
||||
return this.favCat ? this.favCat.emoji : '🍶'
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.parseOptions(options)
|
||||
},
|
||||
onReady() {
|
||||
this.animateStats()
|
||||
},
|
||||
// 好友可继续转发这张名片(病毒式传播,数据保持原始分享者)
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: `${this.info.nick}的饮酒名片 - 干杯日记`,
|
||||
path: `/pages/share-profile/share-profile?${this.buildQuery()}`
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: `${this.info.nick}的饮酒名片 - 干杯日记`,
|
||||
query: this.buildQuery()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseOptions(options) {
|
||||
if (!options) return
|
||||
try {
|
||||
if (options.nick) this.info.nick = decodeURIComponent(options.nick)
|
||||
if (options.avatar) this.info.avatar = decodeURIComponent(options.avatar)
|
||||
this.info.days = parseInt(options.days) || 0
|
||||
this.info.records = parseInt(options.records) || 0
|
||||
this.info.cups = parseFloat(options.cups) || 0
|
||||
this.info.streak = parseInt(options.streak) || 0
|
||||
if (options.st) this.info.streakType = options.st === 'drank' ? 'drank' : 'abstain'
|
||||
if (options.fav) this.info.fav = String(options.fav)
|
||||
} catch (e) {
|
||||
console.warn('名片参数解析失败:', e)
|
||||
}
|
||||
},
|
||||
buildQuery() {
|
||||
const parts = [`nick=${encodeURIComponent(this.info.nick)}`]
|
||||
if (this.info.avatar) parts.push(`avatar=${encodeURIComponent(this.info.avatar)}`)
|
||||
parts.push(`days=${this.info.days}`)
|
||||
parts.push(`records=${this.info.records}`)
|
||||
parts.push(`cups=${this.info.cups}`)
|
||||
parts.push(`streak=${this.info.streak}`)
|
||||
parts.push(`st=${this.info.streakType}`)
|
||||
if (this.info.fav) parts.push(`fav=${this.info.fav}`)
|
||||
return parts.join('&')
|
||||
},
|
||||
// 数字滚动动画
|
||||
animateStats() {
|
||||
const t = { days: this.info.days, records: this.info.records, cups: this.info.cups }
|
||||
const hasDecimal = t.cups % 1 !== 0
|
||||
const duration = 900
|
||||
const start = Date.now()
|
||||
const timer = setInterval(() => {
|
||||
const p = Math.min((Date.now() - start) / duration, 1)
|
||||
const ease = 1 - Math.pow(1 - p, 3)
|
||||
this.anim.days = Math.round(t.days * ease)
|
||||
this.anim.records = Math.round(t.records * ease)
|
||||
this.anim.cups = hasDecimal
|
||||
? Math.round(t.cups * ease * 10) / 10
|
||||
: Math.round(t.cups * ease)
|
||||
if (p >= 1) clearInterval(timer)
|
||||
}, 16)
|
||||
},
|
||||
enterApp() {
|
||||
const isLoggedIn = uni.getStorageSync('is_logged_in')
|
||||
if (isLoggedIn === 'true') {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.share-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $sp-xl;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 氛围光 */
|
||||
.ambient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60rpx);
|
||||
}
|
||||
|
||||
.glow-1 {
|
||||
width: 480rpx;
|
||||
height: 480rpx;
|
||||
top: -120rpx;
|
||||
right: -100rpx;
|
||||
background: radial-gradient(circle, rgba(232,168,56,0.16) 0%, transparent 70%);
|
||||
animation: glow-drift 6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.glow-2 {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
bottom: -80rpx;
|
||||
left: -120rpx;
|
||||
background: radial-gradient(circle, rgba(139,133,184,0.12) 0%, transparent 70%);
|
||||
animation: glow-drift 7s ease-in-out infinite alternate-reverse;
|
||||
}
|
||||
|
||||
@keyframes glow-drift {
|
||||
from { transform: translate(0, 0) scale(1); }
|
||||
to { transform: translate(30rpx, 40rpx) scale(1.12); }
|
||||
}
|
||||
|
||||
/* 名片主体 */
|
||||
.biz-card {
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
background: linear-gradient(165deg, var(--card-gradient-start, #1A1510) 0%, var(--card-gradient-end, #0D0B08) 100%);
|
||||
border: 1rpx solid rgba(232,168,56,0.28);
|
||||
border-radius: $radius-xl;
|
||||
padding: $sp-xl $sp-xl $sp-2xl;
|
||||
box-shadow: $shadow-amber;
|
||||
overflow: hidden;
|
||||
animation: card-in 0.6s $ease-out both;
|
||||
}
|
||||
|
||||
@keyframes card-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(60rpx) scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 品牌条 */
|
||||
.card-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
|
||||
.brand-label {
|
||||
font-size: $fs-xs;
|
||||
color: $amber;
|
||||
letter-spacing: 4rpx;
|
||||
font-weight: $fw-medium;
|
||||
padding: 6rpx 16rpx;
|
||||
border: 1rpx solid rgba(232,168,56,0.35);
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
/* 身份区 */
|
||||
.identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: $sp-md 0 $sp-xl;
|
||||
}
|
||||
|
||||
.id-avatar-wrap {
|
||||
position: relative;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.id-avatar-wrap::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -16rpx;
|
||||
left: -16rpx;
|
||||
right: -16rpx;
|
||||
bottom: -16rpx;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(232,168,56,0.22) 0%, transparent 70%);
|
||||
filter: blur(12rpx);
|
||||
}
|
||||
|
||||
.id-avatar {
|
||||
width: 168rpx;
|
||||
height: 168rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid $amber;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.id-avatar-fallback {
|
||||
background: $bg-elevated;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.id-avatar-emoji {
|
||||
font-size: 84rpx;
|
||||
}
|
||||
|
||||
.id-name {
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-primary;
|
||||
margin-bottom: $sp-sm;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.id-slogan {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 票券撕边 */
|
||||
.perf-divider {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 (-$sp-xl);
|
||||
padding: 0 $sp-md;
|
||||
}
|
||||
|
||||
.perf-line {
|
||||
flex: 1;
|
||||
border-top: 2rpx dashed rgba(232,168,56,0.3);
|
||||
}
|
||||
|
||||
.perf-hole {
|
||||
position: absolute;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-base;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.perf-hole-l {
|
||||
left: -22rpx;
|
||||
}
|
||||
|
||||
.perf-hole-r {
|
||||
right: -22rpx;
|
||||
}
|
||||
|
||||
/* 数据存根 */
|
||||
.stats-stub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $sp-xl $sp-md;
|
||||
}
|
||||
|
||||
.stub-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
}
|
||||
|
||||
.stub-num {
|
||||
font-size: $fs-3xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-primary;
|
||||
line-height: $lh-tight;
|
||||
}
|
||||
|
||||
.stub-num-amber {
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.stub-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.stub-sep {
|
||||
width: 1rpx;
|
||||
height: 56rpx;
|
||||
background: rgba(232,168,56,0.2);
|
||||
}
|
||||
|
||||
/* 标签行 */
|
||||
.extra-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: $sp-md;
|
||||
padding: 0 $sp-md $sp-xl;
|
||||
}
|
||||
|
||||
.extra-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
padding: $sp-sm $sp-lg;
|
||||
background: rgba(232,168,56,0.1);
|
||||
border: 1rpx solid rgba(232,168,56,0.22);
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
.extra-chip-emoji {
|
||||
font-size: $fs-base;
|
||||
}
|
||||
|
||||
.extra-chip-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
/* CTA */
|
||||
.cta-btn {
|
||||
margin: 0 $sp-md;
|
||||
background: linear-gradient(135deg, $amber-light, $amber, $amber-deep);
|
||||
border-radius: $radius-full;
|
||||
padding: $sp-md 0;
|
||||
border: none;
|
||||
line-height: 1.4;
|
||||
animation: cta-glow 2.4s ease-in-out infinite;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cta-glow {
|
||||
0%, 100% { box-shadow: 0 8rpx 28rpx rgba(232,168,56,0.28); }
|
||||
50% { box-shadow: 0 8rpx 44rpx rgba(232,168,56,0.5); }
|
||||
}
|
||||
|
||||
.cta-text {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.page-footer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: $sp-2xl;
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
letter-spacing: 4rpx;
|
||||
animation: card-in 0.6s $ease-out 0.15s both;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<!-- 红绸蝴蝶结 -->
|
||||
<path d="M50 22 C43 13 31 14 33 21 C34.5 26 44 26 50 22 Z" fill="#C62828"/>
|
||||
<path d="M50 22 C57 13 69 14 67 21 C65.5 26 56 26 50 22 Z" fill="#C62828"/>
|
||||
<path d="M44 24 L37 35 L44 32 Z" fill="#A61B1B"/>
|
||||
<path d="M56 24 L63 35 L56 32 Z" fill="#A61B1B"/>
|
||||
<circle cx="50" cy="22" r="3.5" fill="#E53935"/>
|
||||
<!-- 金色丝带边 -->
|
||||
<path d="M33 21 C34.5 26 44 26 50 22" fill="none" stroke="#F9A825" stroke-width="1.2"/>
|
||||
<path d="M67 21 C65.5 26 56 26 50 22" fill="none" stroke="#F9A825" stroke-width="1.2"/>
|
||||
<!-- 坛盖 -->
|
||||
<rect x="41" y="25" width="18" height="7" rx="3.5" fill="#8D6E63"/>
|
||||
<!-- 坛颈 -->
|
||||
<path d="M43 31 L57 31 L59 39 L41 39 Z" fill="#191919"/>
|
||||
<!-- 坛身 -->
|
||||
<path d="M41 39 C27 45 23 58 25 69 C27 82 37 91 50 91 C63 91 73 82 75 69 C77 58 73 45 59 39 Z" fill="#191919"/>
|
||||
<!-- 釉面高光 -->
|
||||
<path d="M35 47 C30 53 29 63 31 71" stroke="rgba(255,255,255,0.28)" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||
<!-- 红色菱形酒标 -->
|
||||
<path d="M50 49 L64 65 L50 81 L36 65 Z" fill="#D32F2F" stroke="#F9A825" stroke-width="1.6"/>
|
||||
<text x="50" y="72" text-anchor="middle" font-size="17" font-weight="bold" fill="#FFF8E1" font-family="KaiTi, STKaiti, SimSun, serif">酒</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user