Files
hejiu/components/DrinkCard.vue
T
cg 93e19caa75 refactor(components): 重构饮酒卡片组件的多照片布局和样式结构
- 将多照片布局从拼贴模式改为网格系统,支持2-9张照片的不同排列
- 优化卡片宽度为650rpx并调整整体布局结构
- 更新品牌底栏为左右分栏布局并添加二维码区域
- 简化模板数量计算逻辑,合并少照片情况的处理
- 为单照片模式添加object-fit: cover确保图片填充效果
- 移除多余的换行和注释,精简代码结构
- 调整CSS样式以适应新的网格布局系统
2026-07-13 23:12:10 +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>