Files
hejiu/components/DrinkCard.vue
T
cg c9413ff5fa feat(brand): 在品牌区域添加二维码展示功能
- 在 DrinkCard 组件中添加二维码图片显示
- 更新品牌区域样式以支持二维码展示
- 在卡片页面预加载二维码图片资源
- 实现画布中二维码的绘制功能,包括白色圆角背景
- 优化二维码路径规范化处理以确保正确加载
- 更新空状态操作逻辑,区分游客和已登录用户的按钮行为
2026-09-22 23:23:24 +08:00

473 lines
11 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">
<!-- 装饰背景层 -->
<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">
<view v-if="photos.length === 1" class="photo-single">
<image :src="photos[0]" mode="aspectFill"></image>
</view>
<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>
<view v-else class="card-hero">
<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">
<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">
<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 class="divider-decor">
<view class="divider-line"></view>
<text class="divider-icon">🥂</text>
<view class="divider-line"></view>
</view>
<!-- 酒言酒语 -->
<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-left">
<text class="brand-name">碰盏日记</text>
<text class="brand-slogan">记录每一杯的故事</text>
</view>
<view class="brand-qr">
<image class="brand-qr-img" src="/static/qrcode.jpg" mode="aspectFill"></image>
</view>
</view>
</view>
</template>
<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' }
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) || []
},
mainIcon() {
const first = (this.record.drinks || [])[0]
return first ? getCatIcon(first.category) : '🥃'
},
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 : '🥃'
},
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: 40rpx;
overflow: hidden;
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-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: 22rpx;
color: rgba(232, 168, 56, 0.8);
font-family: $font-num;
letter-spacing: 6rpx;
font-weight: $fw-medium;
}
/* === 照片区域 === */
.card-photos {
position: relative;
z-index: 1;
margin-bottom: 32rpx;
}
.photo-single {
width: 100%;
height: 340rpx;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.4);
image { width: 100%; height: 100%; }
}
.photo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12rpx;
}
.photo-cell {
aspect-ratio: 1;
border-radius: 16rpx;
overflow: hidden;
image { width: 100%; height: 100%; }
}
.photo-span {
grid-column: span 2;
aspect-ratio: 16 / 9;
}
/* === 主视觉图标 === */
.card-hero {
display: flex;
justify-content: center;
align-items: center;
padding: 24rpx 0 32rpx;
position: relative;
z-index: 1;
}
.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: 88rpx;
line-height: 1;
}
.hero-icon-img {
width: 110rpx;
height: 110rpx;
}
/* === 感受徽章 === */
.feeling-badge {
display: flex;
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: center;
padding: 14rpx 0;
}
.drink-left {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.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;
}
/* === 分割装饰 === */
.divider-decor {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 32rpx;
position: relative;
z-index: 1;
}
.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;
}
/* === 酒言酒语 === */
.quote-section {
text-align: center;
padding: 0 16rpx;
margin-bottom: 40rpx;
position: relative;
z-index: 1;
}
.quote-mark {
font-size: 48rpx;
color: rgba(232, 168, 56, 0.3);
font-family: Georgia, serif;
line-height: 1;
}
.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;
padding: 8rpx 0;
font-style: italic;
}
/* === 品牌底栏 === */
.card-brand {
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-left {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.brand-name {
font-size: 26rpx;
color: rgba(232, 168, 56, 0.8);
font-weight: $fw-bold;
letter-spacing: 4rpx;
}
.brand-slogan {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.3);
letter-spacing: 2rpx;
}
.brand-qr {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
background: #FFFFFF;
overflow: hidden;
flex-shrink: 0;
}
.brand-qr-img {
width: 100%;
height: 100%;
display: block;
}
</style>