feat(theme): 实现日夜主题自动切换功能
- 添加日夜主题CSS变量定义,支持琥珀夜光和暖白琥珀两种风格 - 实现主题切换逻辑,根据时间自动切换白天(light)和夜间(dark)主题 - 在App.vue中集成主题初始化和token恢复功能 - 更新全局样式类应用主题颜色变量,包括卡片、文本、边框等 - 创建主题混入(mixin)供各页面使用,确保主题同步更新 - 实现导航栏和TabBar主题动态切换,提升用户体验 - 添加API服务层封装HaveADrink SDK,统一处理认证和请求 - 优化DrinkCard组件显示饮酒感受信息,丰富打卡记录展示 - 更新项目依赖配置,集成新的API客户端库
This commit is contained in:
+85
-21
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page-container card-page">
|
||||
<view :class="themeClass" class="page-container card-page">
|
||||
<view class="card-nav">
|
||||
<view class="step-back" @click="goBack">
|
||||
<text class="step-back-arrow">‹</text>
|
||||
@@ -29,12 +29,16 @@
|
||||
|
||||
<script>
|
||||
import DrinkCard from '../../components/DrinkCard.vue'
|
||||
import client from '../../common/api'
|
||||
import { getRecords } from '../../common/mock-data'
|
||||
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES } from '../../common/constants'
|
||||
import { getThemeColors, calcStandardCups, unitToMl } from '../../common/utils'
|
||||
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../../common/constants'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
const UNIT_NAMES = { ml: 'ml', liang: '两', bottle: '瓶', cup: '杯', can: '听', shot: 'shot' }
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
components: { DrinkCard },
|
||||
data() {
|
||||
return {
|
||||
@@ -43,12 +47,57 @@ export default {
|
||||
saved: false
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
const records = getRecords()
|
||||
if (options && options.recordId) this.record = records.find(r => r.id === options.recordId)
|
||||
if (!this.record) this.record = records.find(r => r.mode === 'drank') || this.fallback()
|
||||
async onLoad(options) {
|
||||
if (options && options.recordId) {
|
||||
try {
|
||||
const resp = await client.GetRecordDetail({ id: options.recordId })
|
||||
const rec = resp.data || resp
|
||||
if (rec) {
|
||||
this.record = {
|
||||
...rec,
|
||||
mode: String(rec.mode),
|
||||
feeling: rec.feeling ? String(rec.feeling) : null,
|
||||
visibility: rec.visibility ? String(rec.visibility) : 'private',
|
||||
drinks: (rec.drinks || []).map(d => ({
|
||||
...d,
|
||||
category: String(d.category),
|
||||
unit: String(d.unit)
|
||||
}))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('获取记录详情失败,降级本地:', e)
|
||||
}
|
||||
}
|
||||
if (!this.record) this.record = this.loadLocalRecord(options.recordId) || this.fallback()
|
||||
},
|
||||
methods: {
|
||||
loadLocalRecord(id) {
|
||||
if (!id) return null
|
||||
try {
|
||||
const records = getRecords()
|
||||
const found = records.find(r => r.id === id)
|
||||
if (!found) return null
|
||||
return {
|
||||
...found,
|
||||
mode: String(found.mode || 'drank'),
|
||||
feeling: found.feeling ? String(found.feeling) : null,
|
||||
visibility: found.visibility ? String(found.visibility) : 'private',
|
||||
drinks: (found.drinks || []).map(d => {
|
||||
const ml = unitToMl(d.amount || 0, d.unit || 'ml')
|
||||
return {
|
||||
...d,
|
||||
category: String(d.category || d.categoryId || ''),
|
||||
unit: String(d.unit || 'ml'),
|
||||
standardCups: d.standardCups || calcStandardCups(ml, d.degree || 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('读取本地记录失败:', e)
|
||||
return null
|
||||
}
|
||||
},
|
||||
fallback() {
|
||||
return {
|
||||
date: new Date().toISOString().slice(0, 10), mode: 'drank',
|
||||
@@ -86,10 +135,11 @@ export default {
|
||||
const W = 750, H = 1600
|
||||
const rec = this.record
|
||||
|
||||
const WHITE = '#F2F2F2'
|
||||
const SUB = '#7E7E96'
|
||||
const DIM = '#4A4A62'
|
||||
const AMBER = '#E8A838'
|
||||
const colors = getThemeColors(this.currentTheme)
|
||||
const WHITE = colors.textPrimary
|
||||
const SUB = colors.textSecondary
|
||||
const DIM = colors.textTertiary
|
||||
const AMBER = colors.amber
|
||||
const F = '-apple-system, PingFang SC, Helvetica Neue, sans-serif'
|
||||
|
||||
const cardW = W
|
||||
@@ -116,12 +166,13 @@ export default {
|
||||
const drinks = rec.drinks || []
|
||||
const drinkH = drinks.length * 52
|
||||
const foodH = rec.food ? 52 : 0
|
||||
const cardH = 48 + 56 + photoAreaH + 32 + drinkH + foodH + 16 + 40 + 36 + 56 + 80 + 40
|
||||
const feelingH = rec.feeling ? 48 : 0
|
||||
const cardH = 48 + 56 + photoAreaH + 32 + drinkH + foodH + feelingH + 16 + 40 + 36 + 56 + 80 + 40
|
||||
|
||||
// 卡片背景渐变(填满整个画布)
|
||||
const grad = ctx.createLinearGradient(0, 0, cardW, cardY + cardH)
|
||||
grad.addColorStop(0, '#1A1510')
|
||||
grad.addColorStop(1, '#0D0B08')
|
||||
grad.addColorStop(0, colors.cardGradStart)
|
||||
grad.addColorStop(1, colors.cardGradEnd)
|
||||
ctx.setFillStyle(grad)
|
||||
ctx.fillRect(0, 0, W, cardH)
|
||||
|
||||
@@ -148,7 +199,7 @@ export default {
|
||||
if (imgs[0]) {
|
||||
try { ctx.drawImage(imgs[0], P, y, maxW, heroH) } catch (e) {}
|
||||
} else {
|
||||
ctx.setFillStyle('rgba(255,255,255,0.03)')
|
||||
ctx.setFillStyle(colors.bgPlaceholder)
|
||||
ctx.fillRect(P, y, maxW, heroH)
|
||||
}
|
||||
ctx.restore()
|
||||
@@ -193,7 +244,7 @@ export default {
|
||||
if (imgs[i]) {
|
||||
try { ctx.drawImage(imgs[i], pos.x, pos.y, pos.w, pos.h) } catch (e) {}
|
||||
} else {
|
||||
ctx.setFillStyle('rgba(255,255,255,0.03)')
|
||||
ctx.setFillStyle(colors.bgPlaceholder)
|
||||
ctx.fillRect(pos.x, pos.y, pos.w, pos.h)
|
||||
}
|
||||
ctx.restore()
|
||||
@@ -203,7 +254,7 @@ export default {
|
||||
// emoji 圆形
|
||||
const eR = 100
|
||||
const eCx = W / 2, eCy = y + eR
|
||||
ctx.setFillStyle('rgba(232,168,56,0.08)')
|
||||
ctx.setFillStyle(colors.emojiBg)
|
||||
ctx.beginPath()
|
||||
ctx.arc(eCx, eCy, eR, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
@@ -222,7 +273,7 @@ export default {
|
||||
const emoji = cat ? cat.emoji : '🥃'
|
||||
const name = `${emoji} ${d.brand}`
|
||||
const unitName = UNIT_NAMES[d.unit] || d.unit
|
||||
const detail = `${d.amount}${unitName}·${d.standardCups}杯`
|
||||
const detail = `${d.amount}${unitName}`
|
||||
|
||||
ctx.font = `500 28px ${F}`
|
||||
ctx.setFillStyle(WHITE)
|
||||
@@ -246,11 +297,24 @@ export default {
|
||||
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 += 16
|
||||
const lineGrad = ctx.createLinearGradient(P, y, W - P, y)
|
||||
lineGrad.addColorStop(0, 'transparent')
|
||||
lineGrad.addColorStop(0.5, 'rgba(232,168,56,0.15)')
|
||||
lineGrad.addColorStop(0.5, colors.borderSubtle)
|
||||
lineGrad.addColorStop(1, 'transparent')
|
||||
ctx.setStrokeStyle(lineGrad)
|
||||
ctx.setLineWidth(2)
|
||||
@@ -290,10 +354,10 @@ export default {
|
||||
const qrSize = 80
|
||||
const qrX = W - P - qrSize
|
||||
const qrY = y - 30
|
||||
ctx.setStrokeStyle('rgba(232,168,56,0.15)')
|
||||
ctx.setStrokeStyle(colors.borderSubtle)
|
||||
ctx.setLineWidth(2)
|
||||
this.rr(ctx, qrX, qrY, qrSize, qrSize, 8, 'stroke')
|
||||
ctx.setFillStyle('rgba(255,255,255,0.03)')
|
||||
ctx.setFillStyle(colors.bgPlaceholder)
|
||||
this.rr(ctx, qrX, qrY, qrSize, qrSize, 8, 'fill')
|
||||
|
||||
return new Promise(resolve => {
|
||||
@@ -453,7 +517,7 @@ export default {
|
||||
border-radius: $radius-full;
|
||||
font-size: $fs-base;
|
||||
background: transparent;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border: 2rpx solid var(--border-active, rgba(255, 255, 255, 0.12));
|
||||
color: $text-secondary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user