feat(app): 项目重命名为干杯日记并优化UI设计

- 将应用名称从"喝了么"更改为"干杯日记"
- 更新所有相关文件中的品牌标识和描述信息
- 升级HaveADrink依赖包从1.0.3到1.0.4版本
- 在pages.json中新增分享个人资料页面配置
- 重构DrinkCard组件UI,增加装饰背景、酒类图标支持、饮酒感受徽章等功能
- 优化API错误处理逻辑,增加静默模式支持
- 修改白酒类别图标为 urn emoji,黄酒图标为 tea emoji
- 添加分享功能按钮并优化页面返回交互体验
- 引入新的工具函数用于获取酒类图标和路径判断
- 更新主题混入、常量定义等基础配置文件
- 调整全局样式和设计令牌以匹配新品牌形象
This commit is contained in:
cg
2026-07-20 22:12:27 +08:00
parent adf6b684b9
commit 9f5c69e1e5
25 changed files with 2333 additions and 571 deletions
+492
View File
@@ -0,0 +1,492 @@
<template>
<view :class="themeClass" class="page-container share-page">
<!-- 氛围背景 -->
<view class="ambient">
<view class="glow glow-1"></view>
<view class="glow glow-2"></view>
</view>
<!-- 酒桌名片 -->
<view class="biz-card">
<!-- 品牌条 -->
<view class="card-brand">
<text class="brand-mark">🍻 干杯日记</text>
<text class="brand-label"> </text>
</view>
<!-- 身份区 -->
<view class="identity">
<view class="id-avatar-wrap">
<image
v-if="info.avatar && !avatarError"
class="id-avatar"
:src="info.avatar"
mode="aspectFill"
@error="avatarError = true"
></image>
<view v-else class="id-avatar id-avatar-fallback">
<text class="id-avatar-emoji">🥃</text>
</view>
</view>
<text class="id-name">{{ info.nick }}</text>
<text class="id-slogan">这是我的饮酒档案来交个酒友吧</text>
</view>
<!-- 票券撕边分割线 -->
<view class="perf-divider">
<view class="perf-hole perf-hole-l"></view>
<view class="perf-line"></view>
<view class="perf-hole perf-hole-r"></view>
</view>
<!-- 数据存根 -->
<view class="stats-stub">
<view class="stub-item">
<text class="stub-num text-num">{{ anim.days }}</text>
<text class="stub-label">打卡天数</text>
</view>
<view class="stub-sep"></view>
<view class="stub-item">
<text class="stub-num text-num">{{ anim.records }}</text>
<text class="stub-label">饮酒次数</text>
</view>
<view class="stub-sep"></view>
<view class="stub-item">
<text class="stub-num stub-num-amber text-num">{{ anim.cups }}</text>
<text class="stub-label">标准杯</text>
</view>
</view>
<!-- 连续记录 + 最爱酒类 -->
<view class="extra-row">
<view class="extra-chip">
<text class="extra-chip-emoji">{{ info.streakType === 'drank' ? '🔥' : '💪' }}</text>
<text class="extra-chip-text">{{ info.streakType === 'drank' ? '连续饮酒' : '连续戒酒' }} {{ info.streak }} </text>
</view>
<view v-if="favName" class="extra-chip">
<text class="extra-chip-emoji">{{ favEmoji }}</text>
<text class="extra-chip-text">最爱{{ favName }}</text>
</view>
</view>
<!-- CTA -->
<button class="cta-btn" @click="enterApp">
<text class="cta-text">我也想记 · 打开干杯日记</text>
</button>
</view>
<text class="page-footer">让每一杯酒都有迹可循</text>
</view>
</template>
<script>
import themeMixin from '../../common/theme-mixin'
import { DRINK_CATEGORIES } from '../../common/constants'
export default {
mixins: [themeMixin],
data() {
return {
info: {
nick: '酒友',
avatar: '',
days: 0,
records: 0,
cups: 0,
streak: 0,
streakType: 'drank',
fav: ''
},
anim: { days: 0, records: 0, cups: 0 },
avatarError: false
}
},
computed: {
favCat() {
return DRINK_CATEGORIES.find(c => c.id === this.info.fav) || null
},
favName() {
return this.favCat ? this.favCat.name : ''
},
favEmoji() {
return this.favCat ? this.favCat.emoji : '🍶'
}
},
onLoad(options) {
this.parseOptions(options)
},
onReady() {
this.animateStats()
},
// 好友可继续转发这张名片(病毒式传播,数据保持原始分享者)
onShareAppMessage() {
return {
title: `${this.info.nick}的饮酒名片 - 干杯日记`,
path: `/pages/share-profile/share-profile?${this.buildQuery()}`
}
},
onShareTimeline() {
return {
title: `${this.info.nick}的饮酒名片 - 干杯日记`,
query: this.buildQuery()
}
},
methods: {
parseOptions(options) {
if (!options) return
try {
if (options.nick) this.info.nick = decodeURIComponent(options.nick)
if (options.avatar) this.info.avatar = decodeURIComponent(options.avatar)
this.info.days = parseInt(options.days) || 0
this.info.records = parseInt(options.records) || 0
this.info.cups = parseFloat(options.cups) || 0
this.info.streak = parseInt(options.streak) || 0
if (options.st) this.info.streakType = options.st === 'drank' ? 'drank' : 'abstain'
if (options.fav) this.info.fav = String(options.fav)
} catch (e) {
console.warn('名片参数解析失败:', e)
}
},
buildQuery() {
const parts = [`nick=${encodeURIComponent(this.info.nick)}`]
if (this.info.avatar) parts.push(`avatar=${encodeURIComponent(this.info.avatar)}`)
parts.push(`days=${this.info.days}`)
parts.push(`records=${this.info.records}`)
parts.push(`cups=${this.info.cups}`)
parts.push(`streak=${this.info.streak}`)
parts.push(`st=${this.info.streakType}`)
if (this.info.fav) parts.push(`fav=${this.info.fav}`)
return parts.join('&')
},
// 数字滚动动画
animateStats() {
const t = { days: this.info.days, records: this.info.records, cups: this.info.cups }
const hasDecimal = t.cups % 1 !== 0
const duration = 900
const start = Date.now()
const timer = setInterval(() => {
const p = Math.min((Date.now() - start) / duration, 1)
const ease = 1 - Math.pow(1 - p, 3)
this.anim.days = Math.round(t.days * ease)
this.anim.records = Math.round(t.records * ease)
this.anim.cups = hasDecimal
? Math.round(t.cups * ease * 10) / 10
: Math.round(t.cups * ease)
if (p >= 1) clearInterval(timer)
}, 16)
},
enterApp() {
const isLoggedIn = uni.getStorageSync('is_logged_in')
if (isLoggedIn === 'true') {
uni.switchTab({ url: '/pages/index/index' })
} else {
uni.reLaunch({ url: '/pages/login/login' })
}
}
}
}
</script>
<style lang="scss" scoped>
.share-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: $sp-xl;
position: relative;
overflow: hidden;
}
/* 氛围光 */
.ambient {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 0;
}
.glow {
position: absolute;
border-radius: 50%;
filter: blur(60rpx);
}
.glow-1 {
width: 480rpx;
height: 480rpx;
top: -120rpx;
right: -100rpx;
background: radial-gradient(circle, rgba(232,168,56,0.16) 0%, transparent 70%);
animation: glow-drift 6s ease-in-out infinite alternate;
}
.glow-2 {
width: 400rpx;
height: 400rpx;
bottom: -80rpx;
left: -120rpx;
background: radial-gradient(circle, rgba(139,133,184,0.12) 0%, transparent 70%);
animation: glow-drift 7s ease-in-out infinite alternate-reverse;
}
@keyframes glow-drift {
from { transform: translate(0, 0) scale(1); }
to { transform: translate(30rpx, 40rpx) scale(1.12); }
}
/* 名片主体 */
.biz-card {
width: 100%;
max-width: 620rpx;
position: relative;
z-index: 2;
background: linear-gradient(165deg, var(--card-gradient-start, #1A1510) 0%, var(--card-gradient-end, #0D0B08) 100%);
border: 1rpx solid rgba(232,168,56,0.28);
border-radius: $radius-xl;
padding: $sp-xl $sp-xl $sp-2xl;
box-shadow: $shadow-amber;
overflow: hidden;
animation: card-in 0.6s $ease-out both;
}
@keyframes card-in {
from {
opacity: 0;
transform: translateY(60rpx) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* 品牌条 */
.card-brand {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $sp-xl;
}
.brand-mark {
font-size: $fs-sm;
color: $text-secondary;
font-weight: $fw-medium;
}
.brand-label {
font-size: $fs-xs;
color: $amber;
letter-spacing: 4rpx;
font-weight: $fw-medium;
padding: 6rpx 16rpx;
border: 1rpx solid rgba(232,168,56,0.35);
border-radius: $radius-full;
}
/* 身份区 */
.identity {
display: flex;
flex-direction: column;
align-items: center;
padding: $sp-md 0 $sp-xl;
}
.id-avatar-wrap {
position: relative;
margin-bottom: $sp-lg;
}
.id-avatar-wrap::before {
content: '';
position: absolute;
top: -16rpx;
left: -16rpx;
right: -16rpx;
bottom: -16rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(232,168,56,0.22) 0%, transparent 70%);
filter: blur(12rpx);
}
.id-avatar {
width: 168rpx;
height: 168rpx;
border-radius: 50%;
border: 4rpx solid $amber;
position: relative;
z-index: 1;
}
.id-avatar-fallback {
background: $bg-elevated;
display: flex;
align-items: center;
justify-content: center;
}
.id-avatar-emoji {
font-size: 84rpx;
}
.id-name {
font-size: $fs-2xl;
font-weight: $fw-black;
color: $text-primary;
margin-bottom: $sp-sm;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.id-slogan {
font-size: $fs-sm;
color: $text-tertiary;
}
/* 票券撕边 */
.perf-divider {
position: relative;
display: flex;
align-items: center;
margin: 0 (-$sp-xl);
padding: 0 $sp-md;
}
.perf-line {
flex: 1;
border-top: 2rpx dashed rgba(232,168,56,0.3);
}
.perf-hole {
position: absolute;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: $bg-base;
top: 50%;
transform: translateY(-50%);
}
.perf-hole-l {
left: -22rpx;
}
.perf-hole-r {
right: -22rpx;
}
/* 数据存根 */
.stats-stub {
display: flex;
align-items: center;
justify-content: space-between;
padding: $sp-xl $sp-md;
}
.stub-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-xs;
}
.stub-num {
font-size: $fs-3xl;
font-weight: $fw-black;
color: $text-primary;
line-height: $lh-tight;
}
.stub-num-amber {
color: $amber;
}
.stub-label {
font-size: $fs-xs;
color: $text-tertiary;
letter-spacing: 2rpx;
}
.stub-sep {
width: 1rpx;
height: 56rpx;
background: rgba(232,168,56,0.2);
}
/* 标签行 */
.extra-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: $sp-md;
padding: 0 $sp-md $sp-xl;
}
.extra-chip {
display: inline-flex;
align-items: center;
gap: $sp-xs;
padding: $sp-sm $sp-lg;
background: rgba(232,168,56,0.1);
border: 1rpx solid rgba(232,168,56,0.22);
border-radius: $radius-full;
}
.extra-chip-emoji {
font-size: $fs-base;
}
.extra-chip-text {
font-size: $fs-sm;
color: $text-secondary;
}
/* CTA */
.cta-btn {
margin: 0 $sp-md;
background: linear-gradient(135deg, $amber-light, $amber, $amber-deep);
border-radius: $radius-full;
padding: $sp-md 0;
border: none;
line-height: 1.4;
animation: cta-glow 2.4s ease-in-out infinite;
&::after {
border: none;
}
&:active {
transform: scale(0.97);
opacity: 0.9;
}
}
@keyframes cta-glow {
0%, 100% { box-shadow: 0 8rpx 28rpx rgba(232,168,56,0.28); }
50% { box-shadow: 0 8rpx 44rpx rgba(232,168,56,0.5); }
}
.cta-text {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-on-amber;
}
/* 底部 */
.page-footer {
position: relative;
z-index: 2;
margin-top: $sp-2xl;
font-size: $fs-xs;
color: $text-tertiary;
letter-spacing: 4rpx;
animation: card-in 0.6s $ease-out 0.15s both;
}
</style>