- 添加日夜主题CSS变量定义,支持琥珀夜光和暖白琥珀两种风格 - 实现主题切换逻辑,根据时间自动切换白天(light)和夜间(dark)主题 - 在App.vue中集成主题初始化和token恢复功能 - 更新全局样式类应用主题颜色变量,包括卡片、文本、边框等 - 创建主题混入(mixin)供各页面使用,确保主题同步更新 - 实现导航栏和TabBar主题动态切换,提升用户体验 - 添加API服务层封装HaveADrink SDK,统一处理认证和请求 - 优化DrinkCard组件显示饮酒感受信息,丰富打卡记录展示 - 更新项目依赖配置,集成新的API客户端库
294 lines
6.7 KiB
Vue
294 lines
6.7 KiB
Vue
<template>
|
||
<view class="card-root">
|
||
<!-- 顶部:日期 -->
|
||
<view class="card-header">
|
||
<text class="header-date">{{ record.date }}</text>
|
||
</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"
|
||
:key="i"
|
||
class="photo-cell"
|
||
:class="{ 'photo-span': i === 0 && photos.length % 2 !== 0 }"
|
||
>
|
||
<image :src="photo" mode="aspectFill"></image>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 无照片: emoji -->
|
||
<view v-else class="card-hero">
|
||
<view class="hero-emoji-wrap">
|
||
<text class="hero-emoji">{{ mainEmoji }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 酒水清单 -->
|
||
<view class="drink-list">
|
||
<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>
|
||
<view v-if="record.food" class="drink-row">
|
||
<text class="drink-name">🍲 {{ getFoodName() }}</text>
|
||
<text class="drink-detail"></text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 饮酒感受 -->
|
||
<view v-if="feelingLabel" class="feeling-row">
|
||
<text class="feeling-text">{{ feelingLabel }}</text>
|
||
</view>
|
||
|
||
<!-- 分割线 -->
|
||
<view class="divider"></view>
|
||
|
||
<!-- 喝后心得(酒言酒语) -->
|
||
<view class="card-footer">
|
||
<text class="footer-label">喝后心得</text>
|
||
<text class="footer-quote">"{{ quote }}"</text>
|
||
</view>
|
||
|
||
<!-- 品牌 + 二维码 -->
|
||
<view class="card-brand">
|
||
<view class="brand-info">
|
||
<text class="brand-text">🍻 喝了么</text>
|
||
<text class="brand-slogan">记录每一杯的故事</text>
|
||
</view>
|
||
<view class="brand-qr"></view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../common/constants'
|
||
|
||
const UNIT_NAMES = { ml: 'ml', liang: '两', bottle: '瓶', cup: '杯', can: '听', shot: 'shot' }
|
||
|
||
export default {
|
||
props: {
|
||
record: { type: Object, required: true }
|
||
},
|
||
data() {
|
||
return {
|
||
quote: DRINK_QUOTES[Math.floor(Math.random() * DRINK_QUOTES.length)]
|
||
}
|
||
},
|
||
computed: {
|
||
photos() {
|
||
return (this.record && this.record.photos) || []
|
||
},
|
||
mainEmoji() {
|
||
const first = (this.record.drinks || [])[0]
|
||
return first ? this.getCatEmoji(first.category) : '🥃'
|
||
},
|
||
feelingLabel() {
|
||
const f = FEELINGS.find(fe => fe.id === this.record.feeling)
|
||
return f ? `${f.emoji} ${f.name}` : ''
|
||
}
|
||
},
|
||
methods: {
|
||
getCatEmoji(catId) {
|
||
const cat = DRINK_CATEGORIES.find(c => c.id === catId)
|
||
return cat ? cat.emoji : '🥃'
|
||
},
|
||
getFoodName() {
|
||
const food = FOOD_CATEGORIES.find(f => f.id === this.record.food?.category)
|
||
if (!food) return this.record.food?.name || '美食'
|
||
return this.record.food.name ? `${food.name}·${this.record.food.name}` : food.name
|
||
},
|
||
formatAmount(drink) {
|
||
const unitName = UNIT_NAMES[drink.unit] || drink.unit
|
||
return `${drink.amount}${unitName}`
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.card-root {
|
||
position: relative;
|
||
width: 620rpx;
|
||
border-radius: 32rpx;
|
||
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));
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 顶部 */
|
||
.card-header {
|
||
padding: $sp-lg $sp-xl 0;
|
||
}
|
||
.header-date {
|
||
font-size: $fs-sm;
|
||
color: $text-tertiary;
|
||
font-family: $font-num;
|
||
letter-spacing: 4rpx;
|
||
}
|
||
|
||
/* 照片区域 */
|
||
.card-photos {
|
||
padding: $sp-lg $sp-xl 0;
|
||
}
|
||
|
||
/* 1张:大图 */
|
||
.photo-single {
|
||
width: 100%;
|
||
height: 340rpx;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
/* 2-6张:网格 */
|
||
.photo-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: $sp-sm;
|
||
}
|
||
.photo-cell {
|
||
aspect-ratio: 1;
|
||
border-radius: $radius-md;
|
||
overflow: hidden;
|
||
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;
|
||
}
|
||
.hero-emoji-wrap {
|
||
width: 200rpx;
|
||
height: 200rpx;
|
||
border-radius: $radius-full;
|
||
background: rgba(232, 168, 56, 0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.hero-emoji {
|
||
font-size: 100rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
/* 酒水清单 */
|
||
.drink-list {
|
||
padding: 0 $sp-xl;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $sp-md;
|
||
}
|
||
.drink-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
}
|
||
.drink-name {
|
||
font-size: $fs-base;
|
||
color: $text-primary;
|
||
font-weight: $fw-medium;
|
||
flex-shrink: 0;
|
||
}
|
||
.drink-detail {
|
||
font-size: $fs-sm;
|
||
color: $amber;
|
||
font-family: $font-num;
|
||
font-weight: $fw-bold;
|
||
flex-shrink: 0;
|
||
margin-left: $sp-md;
|
||
}
|
||
|
||
/* 饮酒感受 */
|
||
.feeling-row {
|
||
padding: $sp-sm $sp-xl 0;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.feeling-text {
|
||
font-size: $fs-base;
|
||
color: $amber;
|
||
font-weight: $fw-medium;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
/* 分割线 */
|
||
.divider {
|
||
margin: $sp-lg $sp-xl;
|
||
height: 2rpx;
|
||
background: linear-gradient(90deg, transparent, rgba(232, 168, 56, 0.15), transparent);
|
||
}
|
||
|
||
/* 底部 */
|
||
.card-footer {
|
||
padding: 0 $sp-xl;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: $sp-sm;
|
||
}
|
||
.footer-label {
|
||
font-size: $fs-xs;
|
||
color: $text-tertiary;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
.footer-quote {
|
||
font-size: $fs-sm;
|
||
color: $text-secondary;
|
||
line-height: $lh-loose;
|
||
font-style: italic;
|
||
}
|
||
|
||
/* 品牌 + 二维码 */
|
||
.card-brand {
|
||
padding: $sp-lg $sp-xl $sp-xl;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.brand-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4rpx;
|
||
}
|
||
.brand-text {
|
||
font-size: $fs-sm;
|
||
color: $text-tertiary;
|
||
font-weight: $fw-bold;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
.brand-slogan {
|
||
font-size: $fs-xs;
|
||
color: $text-tertiary;
|
||
}
|
||
.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));
|
||
}
|
||
</style>
|