Files
hejiu/components/DrinkCard.vue
T
cg b95243c8f8 docs(api): 更新API接口文档并调整应用配置
- 添加完整的后端接口文档,包含用户、打卡记录、统计等模块
- 修复应用名称从"喝酒了么"统一更正为"喝了么"
- 重构App.vue中的启动逻辑,增加数据迁移和路由守卫功能
- 调整DrinkCalendar组件的UI间距和尺寸参数
- 优化DrinkCard组件的品牌展示文案
- 修改manifest.json中的应用描述和微信小程序配置
- 调整pages.json中的页面路径顺序和tabBar配置
- 更新全局样式文件中的应用名称标识
- 修改mock数据中的用户头像默认值
- 优化卡片导出功能的绘制逻辑和视觉效果
2026-07-14 23:27:23 +08:00

493 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="card-root" :class="['tpl-' + template, 'photos-' + photoLayout]">
<!-- 装饰背景层 -->
<view class="card-bg-deco">
<view class="bg-circle bg-circle-1"></view>
<view class="bg-circle bg-circle-2"></view>
</view>
<!-- ====== 0: 纯文字排版 ====== -->
<view v-if="photos.length === 0" class="card-no-photo">
<view class="no-photo-header">
<view class="date-badge"><text class="date-text">{{ record.date }}</text></view>
<view class="feeling-tag" v-if="record.feeling">
<text>{{ getFeelingEmoji() }} {{ getFeelingName() }}</text>
</view>
</view>
<view class="no-photo-hero">
<view class="hero-drink-list">
<view v-for="(drink, i) in record.drinks" :key="i" class="hero-drink-item">
<text class="hero-emoji">{{ getCatEmoji(drink.category) }}</text>
<view class="hero-drink-info">
<text class="hero-brand">{{ drink.brand }}</text>
<text class="hero-product">{{ drink.product }} · {{ drink.amount }}{{ drink.unit }}</text>
</view>
<text class="hero-cups">{{ drink.standardCups }}</text>
</view>
</view>
</view>
<view class="no-photo-stats">
<view class="stat-pill">
<text class="stat-num">{{ record.standardCupsTotal }}</text>
<text class="stat-label">标准杯</text>
</view>
<view class="stat-divider"></view>
<view class="stat-pill" v-if="record.food">
<text class="stat-icon">🍲</text>
<text class="stat-label">{{ getFoodName() }}</text>
</view>
</view>
<view class="no-photo-quote">
<text class="quote-mark">"</text>
<text class="quote-text">{{ quote }}</text>
</view>
</view>
<!-- ====== 1张: 全屏沉浸式 ====== -->
<view v-else-if="photos.length === 1" class="card-one-photo">
<image class="one-photo-bg" :src="photos[0]"></image>
<view class="one-photo-overlay"></view>
<view class="one-photo-top">
<view class="date-badge light"><text class="date-text">{{ record.date }}</text></view>
</view>
<view class="one-photo-bottom">
<view class="photo-drink-chips">
<view v-for="(drink, i) in record.drinks" :key="i" class="drink-chip">
<text>{{ getCatEmoji(drink.category) }} {{ drink.brand }} {{ drink.standardCups }}杯</text>
</view>
</view>
<view class="photo-stats-row">
<text class="photo-total-cups">{{ record.standardCupsTotal }} 标准杯</text>
<text v-if="record.feeling" class="photo-feeling">{{ getFeelingEmoji() }} {{ getFeelingName() }}</text>
</view>
<view class="photo-quote-line">
<text class="quote-mark light">"</text>
<text class="quote-text light">{{ quote }}</text>
</view>
</view>
</view>
<!-- ====== 多张: 拼贴布局 ====== -->
<view v-else class="card-multi-photo">
<view class="multi-top">
<view class="date-badge"><text class="date-text">{{ record.date }}</text></view>
<view class="multi-cups-badge">
<text class="cups-badge-num">{{ record.standardCupsTotal }}</text>
<text class="cups-badge-label">标准杯</text>
</view>
</view>
<!-- 2: 左6右4 -->
<view v-if="photos.length === 2" class="photo-grid grid-2">
<view class="grid-item gi-large"><image :src="photos[0]" class="grid-img"></image></view>
<view class="grid-item gi-medium"><image :src="photos[1]" class="grid-img"></image></view>
</view>
<!-- 3: 左大右两小 -->
<view v-else-if="photos.length === 3" class="photo-grid grid-3">
<view class="grid-item gi-hero-left"><image :src="photos[0]" class="grid-img"></image></view>
<view class="gi-right-col">
<view class="grid-item gi-half"><image :src="photos[1]" class="grid-img"></image></view>
<view class="grid-item gi-half"><image :src="photos[2]" class="grid-img"></image></view>
</view>
</view>
<!-- 4: 2×2 -->
<view v-else-if="photos.length === 4" class="photo-grid grid-2x2">
<view v-for="(p, i) in photos" :key="i" class="grid-item gi-quarter">
<image :src="p" class="grid-img"></image>
</view>
</view>
<!-- 5: 上3下2 -->
<view v-else-if="photos.length === 5" class="photo-grid grid-rows">
<view class="grid-row grid-row-3">
<view v-for="(p, i) in photos.slice(0, 3)" :key="i" class="grid-item gi-third">
<image :src="p" class="grid-img"></image>
</view>
</view>
<view class="grid-row grid-row-2">
<view v-for="(p, i) in photos.slice(3, 5)" :key="i" class="grid-item gi-half-row">
<image :src="p" class="grid-img"></image>
</view>
</view>
</view>
<!-- 6: 3+3 -->
<view v-else-if="photos.length === 6" class="photo-grid grid-rows">
<view class="grid-row grid-row-3">
<view v-for="(p, i) in photos.slice(0, 3)" :key="i" class="grid-item gi-third">
<image :src="p" class="grid-img"></image>
</view>
</view>
<view class="grid-row grid-row-3">
<view v-for="(p, i) in photos.slice(3, 6)" :key="i" class="grid-item gi-third">
<image :src="p" class="grid-img"></image>
</view>
</view>
</view>
<!-- 7: 3+4 -->
<view v-else-if="photos.length === 7" class="photo-grid grid-rows">
<view class="grid-row grid-row-3">
<view v-for="(p, i) in photos.slice(0, 3)" :key="i" class="grid-item gi-third">
<image :src="p" class="grid-img"></image>
</view>
</view>
<view class="grid-row grid-row-4">
<view v-for="(p, i) in photos.slice(3, 7)" :key="i" class="grid-item gi-quarter-row">
<image :src="p" class="grid-img"></image>
</view>
</view>
</view>
<!-- 8: 4+4 -->
<view v-else-if="photos.length === 8" class="photo-grid grid-rows">
<view class="grid-row grid-row-4">
<view v-for="(p, i) in photos.slice(0, 4)" :key="i" class="grid-item gi-quarter-row">
<image :src="p" class="grid-img"></image>
</view>
</view>
<view class="grid-row grid-row-4">
<view v-for="(p, i) in photos.slice(4, 8)" :key="i" class="grid-item gi-quarter-row">
<image :src="p" class="grid-img"></image>
</view>
</view>
</view>
<!-- 9: 3×3 -->
<view v-else class="photo-grid grid-3x3">
<view v-for="(p, i) in photos.slice(0, 9)" :key="i" class="grid-item gi-ninth">
<image :src="p" class="grid-img"></image>
</view>
</view>
<!-- 底部信息 -->
<view class="multi-bottom">
<view class="multi-drinks-row">
<view v-for="(drink, i) in record.drinks" :key="i" class="multi-drink-tag">
<text>{{ getCatEmoji(drink.category) }} {{ drink.brand }} · {{ drink.standardCups }}</text>
</view>
</view>
<view class="multi-meta-row">
<text v-if="record.food" class="multi-meta-item">🍲 {{ getFoodName() }}</text>
<text v-if="record.feeling" class="multi-meta-item">{{ getFeelingEmoji() }} {{ getFeelingName() }}</text>
</view>
<view class="multi-quote">
<text class="quote-mark">"</text>
<text class="quote-text">{{ quote }}</text>
</view>
</view>
</view>
<!-- 统一底部品牌 + 二维码 -->
<view class="card-brand-bar">
<view class="brand-left">
<text class="brand-name">🍻 喝了么</text>
<text class="brand-slogan">记录每一杯的故事</text>
</view>
<view class="brand-qr">
<text class="brand-qr-text">扫码</text>
</view>
</view>
</view>
</template>
<script>
import { DRINK_CATEGORIES, FOOD_CATEGORIES, FEELINGS, DRINK_QUOTES } from '../common/constants'
export default {
props: {
record: { type: Object, required: true },
template: { type: String, default: 'amber' }
},
data() {
return {
quote: DRINK_QUOTES[Math.floor(Math.random() * DRINK_QUOTES.length)]
}
},
computed: {
photos() {
return (this.record && this.record.photos) || []
},
photoLayout() {
const len = this.photos.length
if (len === 0) return 'none'
if (len === 1) return 'one'
if (len <= 4) return 'few'
return 'many'
}
},
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 ''
return this.record.food.name ? `${food.name}·${this.record.food.name}` : food.name
},
getFeelingEmoji() {
const f = FEELINGS.find(f => f.id === this.record.feeling)
return f ? f.emoji : ''
},
getFeelingName() {
const f = FEELINGS.find(f => f.id === this.record.feeling)
return f ? f.name : ''
}
}
}
</script>
<style lang="scss" scoped>
/* ========== 根容器 ========== */
.card-root {
position: relative;
width: 650rpx;
border-radius: $radius-xl;
overflow: hidden;
padding: $sp-lg;
min-height: 700rpx;
display: flex;
flex-direction: column;
}
/* --- 模板: 琥珀流光 --- */
.tpl-amber {
background: linear-gradient(160deg, #1A1308 0%, #0F0B06 40%, #1A1308 100%);
border: 2rpx solid rgba(232, 168, 56, 0.25);
}
.tpl-amber .bg-circle-1 { background: radial-gradient(circle, rgba(232, 168, 56, 0.12), transparent 70%); }
.tpl-amber .bg-circle-2 { background: radial-gradient(circle, rgba(245, 197, 99, 0.08), transparent 70%); }
/* --- 模板: 暗夜极光 --- */
.tpl-aurora {
background: linear-gradient(160deg, #0A0E1A 0%, #0B0B14 40%, #0E0A1A 100%);
border: 2rpx solid rgba(139, 133, 184, 0.2);
}
.tpl-aurora .bg-circle-1 { background: radial-gradient(circle, rgba(94, 198, 160, 0.1), transparent 70%); }
.tpl-aurora .bg-circle-2 { background: radial-gradient(circle, rgba(139, 133, 184, 0.1), transparent 70%); }
.tpl-aurora .date-badge { background: rgba(139, 133, 184, 0.15); border-color: rgba(139, 133, 184, 0.2); }
.tpl-aurora .stat-pill .stat-num,
.tpl-aurora .cups-badge-num,
.tpl-aurora .hero-cups,
.tpl-aurora .photo-total-cups { color: $mint; }
/* --- 模板: 胶片记忆 --- */
.tpl-film {
background: linear-gradient(160deg, #141210 0%, #0D0C0A 40%, #141210 100%);
border: 2rpx solid rgba(200, 180, 140, 0.15);
}
.tpl-film .bg-circle-1 { background: radial-gradient(circle, rgba(200, 180, 140, 0.08), transparent 70%); }
.tpl-film .bg-circle-2 { background: radial-gradient(circle, rgba(180, 160, 120, 0.06), transparent 70%); }
.tpl-film .date-badge { background: rgba(200, 180, 140, 0.1); border-color: rgba(200, 180, 140, 0.15); }
/* 装饰背景 */
.card-bg-deco { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 0; }
.bg-circle { position: absolute; border-radius: 50%; }
.bg-circle-1 { width: 400rpx; height: 400rpx; top: -100rpx; right: -100rpx; }
.bg-circle-2 { width: 300rpx; height: 300rpx; bottom: 100rpx; left: -80rpx; }
/* ========== 公共元素 ========== */
.date-badge {
display: inline-flex; align-items: center;
padding: $sp-xs $sp-md;
background: rgba(232, 168, 56, 0.1);
border: 1rpx solid rgba(232, 168, 56, 0.15);
border-radius: $radius-full;
}
.date-badge.light { background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.2); }
.date-text { font-size: $fs-xs; color: $text-secondary; font-family: $font-num; }
.date-badge.light .date-text { color: rgba(255, 255, 255, 0.85); }
.feeling-tag {
display: inline-flex; align-items: center;
padding: $sp-xs $sp-md;
background: rgba(255, 255, 255, 0.05);
border-radius: $radius-full;
font-size: $fs-xs; color: $text-secondary;
}
.quote-mark { font-size: $fs-3xl; color: $amber; opacity: 0.3; line-height: 1; margin-right: $sp-xs; }
.quote-mark.light { color: rgba(255, 255, 255, 0.4); }
.quote-text { font-size: $fs-sm; color: $text-secondary; font-style: italic; flex: 1; line-height: $lh-loose; }
.quote-text.light { color: rgba(255, 255, 255, 0.7); }
/* 品牌底栏 */
.card-brand-bar {
position: relative; z-index: 1;
display: flex; align-items: center; justify-content: space-between;
padding-top: $sp-lg; margin-top: auto;
border-top: 2rpx solid rgba(255, 255, 255, 0.04);
}
.brand-left {
display: flex; flex-direction: column; gap: 4rpx;
}
.brand-name { font-size: $fs-xs; color: $text-tertiary; font-weight: $fw-medium; }
.brand-slogan { font-size: 18rpx; color: $text-tertiary; }
.brand-qr {
width: 80rpx; height: 80rpx;
background: rgba(255, 255, 255, 0.06);
border: 2rpx solid rgba(255, 255, 255, 0.08);
border-radius: $radius-md;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.brand-qr-text {
font-size: 18rpx; color: $text-tertiary;
}
/* ========== 0张: 纯文字 ========== */
.card-no-photo {
position: relative; z-index: 1; flex: 1;
display: flex; flex-direction: column; gap: $sp-lg;
}
.no-photo-header { display: flex; justify-content: space-between; align-items: center; }
.no-photo-hero { padding: $sp-md 0; }
.hero-drink-list { display: flex; flex-direction: column; gap: $sp-md; }
.hero-drink-item {
display: flex; align-items: center; gap: $sp-md;
padding: $sp-md;
background: rgba(255, 255, 255, 0.03);
border-radius: $radius-lg;
border: 1rpx solid rgba(255, 255, 255, 0.04);
}
.hero-emoji { font-size: 56rpx; flex-shrink: 0; }
.hero-drink-info { flex: 1; }
.hero-brand { display: block; font-size: $fs-base; font-weight: $fw-bold; color: $text-primary; }
.hero-product { display: block; font-size: $fs-xs; color: $text-secondary; margin-top: 4rpx; }
.hero-cups { font-size: $fs-lg; font-weight: $fw-black; color: $amber; font-family: $font-num; flex-shrink: 0; }
.no-photo-stats {
display: flex; align-items: center; gap: $sp-lg;
padding: $sp-md; background: rgba(255, 255, 255, 0.03); border-radius: $radius-lg;
}
.stat-pill { display: flex; align-items: center; gap: $sp-xs; }
.stat-num { font-size: $fs-2xl; font-weight: $fw-black; color: $amber; font-family: $font-num; }
.stat-label { font-size: $fs-xs; color: $text-secondary; }
.stat-icon { font-size: $fs-base; }
.stat-divider { width: 2rpx; height: 40rpx; background: rgba(255, 255, 255, 0.08); }
.no-photo-quote { display: flex; align-items: flex-start; padding: $sp-sm 0; }
/* ========== 1张: 沉浸式 ========== */
.card-one-photo {
position: relative; z-index: 1; flex: 1;
min-height: 600rpx; border-radius: $radius-lg;
overflow: hidden;
margin: -#{$sp-lg};
padding: $sp-lg; padding-bottom: 0;
display: flex; flex-direction: column; justify-content: space-between;
}
.one-photo-bg { position: absolute; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0; }
.one-photo-overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(180deg, rgba(0,0,0,0.3) 0%, transparent 30%, transparent 50%, rgba(0,0,0,0.75) 100%);
z-index: 1;
}
.one-photo-top { position: relative; z-index: 2; display: flex; justify-content: flex-start; padding-top: $sp-sm; }
.one-photo-bottom { position: relative; z-index: 2; display: flex; flex-direction: column; gap: $sp-md; padding-bottom: $sp-md; }
.photo-drink-chips { display: flex; flex-wrap: wrap; gap: $sp-sm; }
.drink-chip {
display: inline-flex; align-items: center;
padding: $sp-xs $sp-md;
background: rgba(0, 0, 0, 0.45);
border-radius: $radius-full;
border: 1rpx solid rgba(255, 255, 255, 0.1);
font-size: $fs-xs; color: rgba(255, 255, 255, 0.9);
}
.photo-stats-row { display: flex; align-items: center; gap: $sp-lg; }
.photo-total-cups { font-size: $fs-xl; font-weight: $fw-black; color: $amber-light; font-family: $font-num; }
.photo-feeling { font-size: $fs-sm; color: rgba(255, 255, 255, 0.7); }
.photo-quote-line { display: flex; align-items: flex-start; }
/* ========== 多张: 公共 ========== */
.card-multi-photo {
position: relative; z-index: 1; flex: 1;
display: flex; flex-direction: column; gap: $sp-lg;
}
.multi-top { display: flex; justify-content: space-between; align-items: center; }
.multi-cups-badge {
display: flex; align-items: baseline; gap: $sp-xs;
padding: $sp-xs $sp-md;
background: rgba(232, 168, 56, 0.1);
border: 1rpx solid rgba(232, 168, 56, 0.2);
border-radius: $radius-full;
}
.cups-badge-num { font-size: $fs-lg; font-weight: $fw-black; color: $amber; font-family: $font-num; }
.cups-badge-label { font-size: $fs-xs; color: $text-secondary; }
/* 照片网格公共 */
.photo-grid { border-radius: $radius-lg; overflow: hidden; }
.grid-item {
overflow: hidden;
border-radius: $radius-sm;
position: relative;
}
.grid-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.grid-row { display: flex; gap: 6rpx; }
/* --- 2张: 左6右4 --- */
.grid-2 {
display: flex; gap: 8rpx; height: 420rpx;
}
.grid-2 .gi-large { flex: 6; }
.grid-2 .gi-medium { flex: 4; }
/* --- 3张: 左大右两小 --- */
.grid-3 {
display: flex; gap: 8rpx; height: 440rpx;
}
.grid-3 .gi-hero-left { flex: 1; }
.grid-3 .gi-right-col {
width: 45%; display: flex; flex-direction: column; gap: 8rpx;
}
.grid-3 .gi-half { flex: 1; border-radius: $radius-md; }
/* --- 4张: 2×2 --- */
.grid-2x2 {
display: flex; flex-wrap: wrap; gap: 6rpx; height: 460rpx;
}
.grid-2x2 .gi-quarter {
width: calc(50% - 3rpx); height: calc(50% - 3rpx);
}
/* --- 行式网格 (5-8张) --- */
.grid-rows {
display: flex; flex-direction: column; gap: 6rpx;
}
.grid-row-3 { display: flex; gap: 6rpx; height: 210rpx; }
.grid-row-2 { display: flex; gap: 6rpx; height: 210rpx; }
.grid-row-4 { display: flex; gap: 6rpx; height: 170rpx; }
.gi-third { flex: 1; }
.gi-half-row { flex: 1; }
.gi-quarter-row { flex: 1; }
/* --- 9张: 3×3 --- */
.grid-3x3 {
display: flex; flex-wrap: wrap; gap: 6rpx;
}
.grid-3x3 .gi-ninth {
width: calc(33.33% - 4rpx); height: 190rpx;
}
/* 底部信息 */
.multi-bottom { display: flex; flex-direction: column; gap: $sp-md; }
.multi-drinks-row { display: flex; flex-wrap: wrap; gap: $sp-sm; }
.multi-drink-tag {
display: inline-flex; padding: $sp-xs $sp-md;
background: rgba(255, 255, 255, 0.04);
border: 1rpx solid rgba(255, 255, 255, 0.06);
border-radius: $radius-full;
font-size: $fs-xs; color: $text-secondary;
}
.multi-meta-row { display: flex; gap: $sp-lg; }
.multi-meta-item { font-size: $fs-sm; color: $text-secondary; }
.multi-quote { display: flex; align-items: flex-start; }
</style>