feat(app): 初始化喝酒了么小程序基础架构
- 添加项目配置文件(.editorconfig, .gitignore) - 创建App.vue主应用文件,包含启动时Mock数据初始化 - 添加index.html和main.js应用入口文件 - 配置manifest.json应用基本信息和权限设置 - 设置pages.json页面路由和全局样式 - 添加uni.promisify.adaptor.js适配器文件 - 创建uni.scss设计令牌系统,定义琥珀夜光主题色彩 - 新增constants.js常量定义,包含酒类分类和品牌数据
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -0,0 +1,3 @@
|
||||
unpackage/
|
||||
.DS_Store
|
||||
.hbuilderx/
|
||||
@@ -0,0 +1,305 @@
|
||||
<script>
|
||||
export default {
|
||||
onLaunch() {
|
||||
console.log('喝酒了么 App Launch')
|
||||
// 初始化Mock数据(仅首次)
|
||||
const hasData = uni.getStorageSync('drink_records')
|
||||
if (!hasData) {
|
||||
import('./common/mock-data').then(({ initMockData }) => {
|
||||
initMockData()
|
||||
console.log('Mock数据已初始化')
|
||||
})
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* ==========================================
|
||||
* 喝酒了么 - 全局样式
|
||||
* 设计概念: 琥珀夜光 Amber Night
|
||||
* ========================================== */
|
||||
|
||||
page {
|
||||
background-color: $bg-base;
|
||||
color: $text-primary;
|
||||
font-family: $font-base;
|
||||
font-size: $fs-base;
|
||||
line-height: $lh-normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* --- 通用工具类 --- */
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-base;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
padding: $sp-lg;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.card-elevated {
|
||||
background-color: $bg-elevated;
|
||||
border-radius: $radius-lg;
|
||||
padding: $sp-lg;
|
||||
box-shadow: $shadow-md;
|
||||
}
|
||||
|
||||
/* --- 文本层级 --- */
|
||||
.text-hero {
|
||||
font-size: $fs-hero;
|
||||
font-weight: $fw-black;
|
||||
line-height: $lh-tight;
|
||||
letter-spacing: -2rpx;
|
||||
}
|
||||
|
||||
.text-display {
|
||||
font-size: $fs-3xl;
|
||||
font-weight: $fw-black;
|
||||
line-height: $lh-tight;
|
||||
}
|
||||
|
||||
.text-h1 {
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-bold;
|
||||
line-height: $lh-tight;
|
||||
}
|
||||
|
||||
.text-h2 {
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
line-height: $lh-tight;
|
||||
}
|
||||
|
||||
.text-h3 {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-medium;
|
||||
line-height: $lh-normal;
|
||||
}
|
||||
|
||||
.text-body {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-normal;
|
||||
line-height: $lh-normal;
|
||||
}
|
||||
|
||||
.text-caption {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
line-height: $lh-normal;
|
||||
}
|
||||
|
||||
.text-tiny {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* --- 数字专用 --- */
|
||||
.text-num {
|
||||
font-family: $font-num;
|
||||
font-weight: $fw-bold;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
/* --- 色彩文字 --- */
|
||||
.text-amber {
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.text-mint {
|
||||
color: $mint;
|
||||
}
|
||||
|
||||
.text-coral {
|
||||
color: $coral;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
/* --- 按钮系统 --- */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
color: $text-on-amber;
|
||||
font-size: $fs-md;
|
||||
font-weight: $fw-bold;
|
||||
padding: $sp-md $sp-xl;
|
||||
border-radius: $radius-full;
|
||||
border: none;
|
||||
text-align: center;
|
||||
box-shadow: $shadow-amber;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: 0 4rpx 16rpx rgba(232, 168, 56, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary-lg {
|
||||
@extend .btn-primary;
|
||||
padding: $sp-lg $sp-xl;
|
||||
font-size: $fs-lg;
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: $bg-elevated;
|
||||
color: $text-primary;
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-medium;
|
||||
padding: $sp-md $sp-xl;
|
||||
border-radius: $radius-full;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.08);
|
||||
text-align: center;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
&:active {
|
||||
background-color: $bg-card-alt;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background-color: transparent;
|
||||
color: $amber;
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-medium;
|
||||
padding: $sp-sm $sp-lg;
|
||||
border: 2rpx solid $amber;
|
||||
border-radius: $radius-full;
|
||||
text-align: center;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
&:active {
|
||||
background-color: $amber-glow;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $text-secondary;
|
||||
font-size: $fs-sm;
|
||||
padding: $sp-xs $sp-sm;
|
||||
}
|
||||
|
||||
/* --- 琥珀CTA大按钮(签名元素) --- */
|
||||
.btn-cta {
|
||||
background: linear-gradient(135deg, $amber-light, $amber, $amber-deep);
|
||||
color: $text-on-amber;
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
padding: $sp-lg 0;
|
||||
border-radius: $radius-xl;
|
||||
border: none;
|
||||
text-align: center;
|
||||
box-shadow: $shadow-amber;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
/* --- 标签 --- */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: $sp-xs $sp-md;
|
||||
border-radius: $radius-full;
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-medium;
|
||||
background-color: $bg-elevated;
|
||||
color: $text-secondary;
|
||||
|
||||
&.tag-active {
|
||||
background-color: $amber-glow;
|
||||
color: $amber;
|
||||
border: 2rpx solid rgba(232, 168, 56, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- 分隔线 --- */
|
||||
.divider {
|
||||
height: 2rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.06), transparent);
|
||||
margin: $sp-lg 0;
|
||||
}
|
||||
|
||||
/* --- 弹性布局辅助 --- */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* --- 安全区域 --- */
|
||||
.safe-bottom {
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 120rpx);
|
||||
}
|
||||
|
||||
/* --- 健康提示条 --- */
|
||||
.health-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $sp-sm;
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
|
||||
text {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
/* 喝酒了么 - 常量定义 */
|
||||
|
||||
// 酒类分类
|
||||
export const DRINK_CATEGORIES = [
|
||||
{ id: 'baijiu', name: '白酒', emoji: '🥃', defaultDegree: 52 },
|
||||
{ id: 'beer', name: '啤酒', emoji: '🍺', defaultDegree: 5 },
|
||||
{ id: 'wine', name: '红酒', emoji: '🍷', defaultDegree: 13 },
|
||||
{ id: 'whisky', name: '洋酒', emoji: '🥂', defaultDegree: 40 },
|
||||
{ id: 'huangjiu', name: '黄酒', emoji: '🍶', defaultDegree: 15 },
|
||||
{ id: 'sake', name: '清酒', emoji: '🍶', defaultDegree: 15 },
|
||||
{ id: 'fruit', name: '果酒', emoji: '🍹', defaultDegree: 8 },
|
||||
{ id: 'cocktail', name: '调酒', emoji: '🍸', defaultDegree: 15 },
|
||||
{ id: 'other', name: '其他', emoji: '🫗', defaultDegree: 10 }
|
||||
]
|
||||
|
||||
// 饮用量单位
|
||||
export const DRINK_UNITS = [
|
||||
{ id: 'ml', name: 'ml', toMl: 1 },
|
||||
{ id: 'liang', name: '两', toMl: 50 },
|
||||
{ id: 'bottle', name: '瓶', toMl: 500 },
|
||||
{ id: 'cup', name: '杯', toMl: 150 },
|
||||
{ id: 'can', name: '听', toMl: 330 },
|
||||
{ id: 'shot', name: 'shot', toMl: 30 }
|
||||
]
|
||||
|
||||
// 预设品牌库 (各酒类常见品牌)
|
||||
export const BRANDS = {
|
||||
baijiu: [
|
||||
{ name: '茅台', product: '飞天茅台53度', degree: 53 },
|
||||
{ name: '五粮液', product: '普五52度', degree: 52 },
|
||||
{ name: '泸州老窖', product: '国窖1573', degree: 52 },
|
||||
{ name: '洋河', product: '梦之蓝M6', degree: 52 },
|
||||
{ name: '汾酒', product: '青花汾酒20', degree: 53 },
|
||||
{ name: '郎酒', product: '红花郎', degree: 53 },
|
||||
{ name: '剑南春', product: '水晶剑南春', degree: 52 },
|
||||
{ name: '牛栏山', product: '陈酿42度', degree: 42 },
|
||||
{ name: '红星', product: '二锅头56度', degree: 56 },
|
||||
{ name: '江小白', product: '表达瓶', degree: 40 }
|
||||
],
|
||||
beer: [
|
||||
{ name: '青岛啤酒', product: '经典', degree: 4.3 },
|
||||
{ name: '雪花', product: '勇闯天涯', degree: 3.3 },
|
||||
{ name: '百威', product: '经典', degree: 5.0 },
|
||||
{ name: '喜力', product: '星银', degree: 5.0 },
|
||||
{ name: '科罗娜', product: 'Extra', degree: 4.5 },
|
||||
{ name: '嘉士伯', product: '特醇', degree: 5.0 },
|
||||
{ name: '燕京', product: 'U8', degree: 2.5 },
|
||||
{ name: '哈尔滨', product: '冰纯', degree: 4.5 },
|
||||
{ name: '精酿', product: '自选', degree: 6.0 },
|
||||
{ name: '朝日', product: '超爽', degree: 5.0 }
|
||||
],
|
||||
wine: [
|
||||
{ name: '拉菲', product: '传奇', degree: 13 },
|
||||
{ name: '奔富', product: 'Bin 389', degree: 14.5 },
|
||||
{ name: '长城', product: '五星赤霞珠', degree: 13 },
|
||||
{ name: '张裕', product: '解百纳', degree: 12.5 },
|
||||
{ name: '杰卡斯', product: '西拉', degree: 14 },
|
||||
{ name: '黄尾袋鼠', product: '西拉', degree: 13.5 },
|
||||
{ name: '蒙特斯', product: '紫天使', degree: 14.5 },
|
||||
{ name: '云雾之湾', product: '长相思', degree: 13 }
|
||||
],
|
||||
whisky: [
|
||||
{ name: '麦卡伦', product: '12年雪莉桶', degree: 40 },
|
||||
{ name: '格兰菲迪', product: '12年', degree: 40 },
|
||||
{ name: '杰克丹尼', product: 'No.7', degree: 40 },
|
||||
{ name: '芝华士', product: '12年', degree: 40 },
|
||||
{ name: '尊尼获加', product: '黑牌', degree: 40 },
|
||||
{ name: '山崎', product: '12年', degree: 43 },
|
||||
{ name: '百龄坛', product: '12年', degree: 40 },
|
||||
{ name: '人头马', product: 'XO', degree: 40 }
|
||||
],
|
||||
huangjiu: [
|
||||
{ name: '古越龙山', product: '花雕五年', degree: 15 },
|
||||
{ name: '会稽山', product: '纯正五年', degree: 15 },
|
||||
{ name: '塔牌', product: '手工冬酿', degree: 14 },
|
||||
{ name: '女儿红', product: '三年陈', degree: 15 }
|
||||
],
|
||||
sake: [
|
||||
{ name: '獺祭', product: '纯米大吟酿45', degree: 15 },
|
||||
{ name: '久保田', product: '纯米吟酿', degree: 15 },
|
||||
{ name: '松竹梅', product: '上撰', degree: 15 }
|
||||
],
|
||||
fruit: [
|
||||
{ name: '梅乃宿', product: '柚子酒', degree: 8 },
|
||||
{ name: 'RIO', product: '微醺', degree: 3 },
|
||||
{ name: '冰锐', product: '朗姆预调', degree: 4.5 },
|
||||
{ name: '梅见', product: '青梅酒', degree: 12 }
|
||||
],
|
||||
cocktail: [
|
||||
{ name: '自制', product: 'Mojito', degree: 15 },
|
||||
{ name: '自制', product: 'Gin Tonic', degree: 12 },
|
||||
{ name: '自制', product: 'Old Fashioned', degree: 30 }
|
||||
],
|
||||
other: []
|
||||
}
|
||||
|
||||
// 配餐分类
|
||||
export const FOOD_CATEGORIES = [
|
||||
{ id: 'hotpot', name: '火锅', emoji: '🍲' },
|
||||
{ id: 'bbq', name: '烧烤', emoji: '🍢' },
|
||||
{ id: 'stirfry', name: '炒菜', emoji: '🥘' },
|
||||
{ id: 'seafood', name: '海鲜', emoji: '🦐' },
|
||||
{ id: 'japanese', name: '日料', emoji: '🍣' },
|
||||
{ id: 'western', name: '西餐', emoji: '🥩' },
|
||||
{ id: 'snack', name: '小吃卤味', emoji: '🥜' },
|
||||
{ id: 'junk', name: '零食', emoji: '🍿' },
|
||||
{ id: 'none', name: '无配餐', emoji: '🚫' }
|
||||
]
|
||||
|
||||
// 饮用感受
|
||||
export const FEELINGS = [
|
||||
{ id: 'tipsy', name: '微醺', emoji: '😊', desc: '刚刚好的快乐' },
|
||||
{ id: 'buzzed', name: '到位', emoji: '😌', desc: '今夜刚刚好' },
|
||||
{ id: 'drunk', name: '醉了', emoji: '😵', desc: '有点上头了' },
|
||||
{ id: 'blackout', name: '断片', emoji: '🫠', desc: '什么都不记得了' }
|
||||
]
|
||||
|
||||
// 酒言酒语
|
||||
export const DRINK_QUOTES = [
|
||||
'人生得意须尽欢,莫使金樽空对月',
|
||||
'微醺是最好的状态',
|
||||
'今朝有酒今朝醉,明日愁来明日愁',
|
||||
'对酒当歌,人生几何',
|
||||
'举杯邀明月,对影成三人',
|
||||
'酒逢知己千杯少',
|
||||
'晚来天欲雪,能饮一杯无',
|
||||
'醉后不知天在水,满船清梦压星河',
|
||||
'白日放歌须纵酒,青春作伴好还乡',
|
||||
'且将新火试新茶,诗酒趁年华',
|
||||
'一壶浊酒喜相逢,古今多少事,都付笑谈中',
|
||||
'何以解忧?唯有杜康',
|
||||
'把酒祝东风,且共从容',
|
||||
'明月几时有,把酒问青天',
|
||||
'劝君更尽一杯酒,西出阳关无故人',
|
||||
'葡萄美酒夜光杯,欲饮琵琶马上催',
|
||||
'借问酒家何处有,牧童遥指杏花村',
|
||||
'花间一壶酒,独酌无相亲',
|
||||
'金樽清酒斗十千,玉盘珍羞直万钱',
|
||||
'东篱把酒黄昏后,有暗香盈袖',
|
||||
'今夜不设防,快乐至上',
|
||||
'干了这杯,烦恼全忘',
|
||||
'好友配好酒,人间值得',
|
||||
'生活有点苦,需要加点酒',
|
||||
'酒杯太浅,敬不到来日方长',
|
||||
'世间万物,唯有美食与酒不可辜负',
|
||||
'把酒言欢,不醉不归',
|
||||
'夜色温柔,酒也温柔',
|
||||
'小酌怡情,大饮伤身,今日刚好',
|
||||
'酒是粮食精,越喝越年轻'
|
||||
]
|
||||
|
||||
// 成就定义
|
||||
export const ACHIEVEMENTS = [
|
||||
{
|
||||
id: 'first_checkin',
|
||||
name: '初来乍到',
|
||||
desc: '完成第一次打卡,开启你的饮酒记录之旅',
|
||||
icon: '🏅',
|
||||
condition: { type: 'total_records', value: 1 }
|
||||
},
|
||||
{
|
||||
id: 'hundred_cups',
|
||||
name: '百杯不醉',
|
||||
desc: '累计饮用100标准杯',
|
||||
icon: '🏆',
|
||||
condition: { type: 'total_standard_cups', value: 100 }
|
||||
},
|
||||
{
|
||||
id: 'streak_30',
|
||||
name: '签到王者',
|
||||
desc: '连续打卡30天,自律的记录者',
|
||||
icon: '👑',
|
||||
condition: { type: 'streak_days', value: 30 }
|
||||
},
|
||||
{
|
||||
id: 'baijiu_master',
|
||||
name: '白酒达人',
|
||||
desc: '白酒累计打卡20次',
|
||||
icon: '🥃',
|
||||
condition: { type: 'category_records', category: 'baijiu', value: 20 }
|
||||
},
|
||||
{
|
||||
id: 'tasting_master',
|
||||
name: '品鉴师',
|
||||
desc: '尝试过5种以上不同类型的酒',
|
||||
icon: '🌈',
|
||||
condition: { type: 'category_variety', value: 5 }
|
||||
}
|
||||
]
|
||||
|
||||
// 可见范围选项
|
||||
export const VISIBILITY_OPTIONS = [
|
||||
{ id: 'public', name: '公开', desc: '所有人都能看到' },
|
||||
{ id: 'friends', name: '仅好友', desc: '只有好友可见' },
|
||||
{ id: 'private', name: '仅自己', desc: '只有自己能看' }
|
||||
]
|
||||
|
||||
// 打卡模式
|
||||
export const CHECKIN_MODES = [
|
||||
{ id: 'drank', name: '今日喝了', color: '$amber' },
|
||||
{ id: 'abstain', name: '今日未喝', color: '$mint' }
|
||||
]
|
||||
@@ -0,0 +1,150 @@
|
||||
/* 喝酒了么 - Mock 数据 */
|
||||
import { formatDate, calcStandardCups, unitToMl } from './utils'
|
||||
|
||||
// 模拟用户数据
|
||||
export const MOCK_USER = {
|
||||
id: 'user_001',
|
||||
nickname: '酒友小王',
|
||||
avatar: '/static/logo.png',
|
||||
preferences: {
|
||||
favoriteCategories: ['baijiu', 'beer'],
|
||||
frequency: 'often'
|
||||
},
|
||||
joinedAt: '2026-01-15'
|
||||
}
|
||||
|
||||
// 生成过去一个月的模拟打卡记录
|
||||
export function generateMockRecords() {
|
||||
const records = []
|
||||
const today = new Date()
|
||||
|
||||
// 预设一些打卡日
|
||||
const drinkDays = [0, 1, 3, 5, 6, 8, 10, 12, 13, 15, 17, 19, 20, 22, 24, 25, 27, 29]
|
||||
const abstainDays = [2, 4, 7, 9, 11, 14, 16, 18, 21, 23, 26, 28]
|
||||
|
||||
const drinkTemplates = [
|
||||
{
|
||||
drinks: [{ category: 'baijiu', brand: '茅台', product: '飞天茅台53度', amount: 100, unit: 'ml', degree: 53 }],
|
||||
food: { category: 'hotpot', name: '重庆老火锅' },
|
||||
feeling: 'tipsy'
|
||||
},
|
||||
{
|
||||
drinks: [
|
||||
{ category: 'beer', brand: '青岛啤酒', product: '经典', amount: 2, unit: 'bottle', degree: 4.3 },
|
||||
],
|
||||
food: { category: 'bbq', name: '烤串配毛豆' },
|
||||
feeling: 'buzzed'
|
||||
},
|
||||
{
|
||||
drinks: [{ category: 'wine', brand: '奔富', product: 'Bin 389', amount: 2, unit: 'cup', degree: 14.5 }],
|
||||
food: { category: 'western', name: '牛排' },
|
||||
feeling: 'tipsy'
|
||||
},
|
||||
{
|
||||
drinks: [
|
||||
{ category: 'baijiu', brand: '五粮液', product: '普五52度', amount: 150, unit: 'ml', degree: 52 },
|
||||
{ category: 'beer', brand: '雪花', product: '勇闯天涯', amount: 1, unit: 'bottle', degree: 3.3 }
|
||||
],
|
||||
food: { category: 'stirfry', name: '水煮鱼+回锅肉' },
|
||||
feeling: 'drunk'
|
||||
},
|
||||
{
|
||||
drinks: [{ category: 'whisky', brand: '麦卡伦', product: '12年雪莉桶', amount: 3, unit: 'shot', degree: 40 }],
|
||||
food: { category: 'snack', name: '坚果拼盘' },
|
||||
feeling: 'tipsy'
|
||||
},
|
||||
{
|
||||
drinks: [{ category: 'beer', brand: '百威', product: '经典', amount: 3, unit: 'bottle', degree: 5.0 }],
|
||||
food: { category: 'seafood', name: '小龙虾' },
|
||||
feeling: 'buzzed'
|
||||
},
|
||||
{
|
||||
drinks: [{ category: 'fruit', brand: '梅见', product: '青梅酒', amount: 200, unit: 'ml', degree: 12 }],
|
||||
food: { category: 'japanese', name: '刺身拼盘' },
|
||||
feeling: 'tipsy'
|
||||
}
|
||||
]
|
||||
|
||||
drinkDays.forEach(daysAgo => {
|
||||
const date = new Date(today)
|
||||
date.setDate(date.getDate() - daysAgo)
|
||||
const dateStr = formatDate(date, 'YYYY-MM-DD')
|
||||
|
||||
const template = drinkTemplates[daysAgo % drinkTemplates.length]
|
||||
const standardCupsTotal = template.drinks.reduce((sum, d) => {
|
||||
const ml = unitToMl(d.amount, d.unit)
|
||||
return sum + calcStandardCups(ml, d.degree)
|
||||
}, 0)
|
||||
|
||||
records.push({
|
||||
id: `record_${daysAgo}`,
|
||||
date: dateStr,
|
||||
mode: 'drank',
|
||||
drinks: template.drinks.map(d => ({ ...d, categoryId: d.category })),
|
||||
food: template.food,
|
||||
feeling: template.feeling,
|
||||
photos: [],
|
||||
visibility: 'friends',
|
||||
standardCupsTotal: Math.round(standardCupsTotal * 100) / 100,
|
||||
createdAt: date.toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
abstainDays.forEach(daysAgo => {
|
||||
const date = new Date(today)
|
||||
date.setDate(date.getDate() - daysAgo)
|
||||
const dateStr = formatDate(date, 'YYYY-MM-DD')
|
||||
|
||||
records.push({
|
||||
id: `record_abs_${daysAgo}`,
|
||||
date: dateStr,
|
||||
mode: 'abstain',
|
||||
drinks: [],
|
||||
food: null,
|
||||
feeling: null,
|
||||
photos: [],
|
||||
visibility: 'private',
|
||||
standardCupsTotal: 0,
|
||||
createdAt: date.toISOString()
|
||||
})
|
||||
})
|
||||
|
||||
return records.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
}
|
||||
|
||||
// 初始化并缓存Mock数据
|
||||
export function initMockData() {
|
||||
const records = generateMockRecords()
|
||||
uni.setStorageSync('drink_records', JSON.stringify(records))
|
||||
uni.setStorageSync('user_info', JSON.stringify(MOCK_USER))
|
||||
uni.setStorageSync('is_first_launch', 'true')
|
||||
return records
|
||||
}
|
||||
|
||||
// 获取记录
|
||||
export function getRecords() {
|
||||
const data = uni.getStorageSync('drink_records')
|
||||
if (data) {
|
||||
try { return JSON.parse(data) } catch (e) { return [] }
|
||||
}
|
||||
return initMockData()
|
||||
}
|
||||
|
||||
// 添加记录
|
||||
export function addRecord(record) {
|
||||
const records = getRecords()
|
||||
record.id = `record_${Date.now()}`
|
||||
record.createdAt = new Date().toISOString()
|
||||
records.unshift(record)
|
||||
uni.setStorageSync('drink_records', JSON.stringify(records))
|
||||
return record
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
const data = uni.getStorageSync('user_info')
|
||||
if (data) {
|
||||
try { return JSON.parse(data) } catch (e) { return MOCK_USER }
|
||||
}
|
||||
return MOCK_USER
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/* 喝酒了么 - 工具函数 */
|
||||
|
||||
/**
|
||||
* 标准杯换算引擎
|
||||
* 核心公式: 标准杯 = (饮用量ml × 酒精度数% × 0.8) / 10
|
||||
* 1标准杯 = 10g纯酒精
|
||||
*/
|
||||
export function calcStandardCups(amountMl, degreePercent) {
|
||||
if (!amountMl || !degreePercent) return 0
|
||||
return Math.round((amountMl * (degreePercent / 100) * 0.8 / 10) * 100) / 100
|
||||
}
|
||||
|
||||
/**
|
||||
* 单位换算为ml
|
||||
*/
|
||||
const UNIT_TO_ML = {
|
||||
ml: 1,
|
||||
liang: 50, // 1两 = 50ml
|
||||
bottle: 500, // 1瓶 = 500ml (啤酒默认)
|
||||
cup: 150, // 1杯 = 150ml
|
||||
can: 330, // 1听 = 330ml
|
||||
shot: 30 // 1shot = 30ml
|
||||
}
|
||||
|
||||
export function unitToMl(amount, unit) {
|
||||
const factor = UNIT_TO_ML[unit] || 1
|
||||
return amount * factor
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算一组饮品的总标准杯
|
||||
*/
|
||||
export function calcTotalStandardCups(drinks) {
|
||||
return drinks.reduce((sum, d) => {
|
||||
const ml = unitToMl(d.amount, d.unit)
|
||||
return sum + calcStandardCups(ml, d.degree)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式化
|
||||
*/
|
||||
export function formatDate(date, format = 'YYYY-MM-DD') {
|
||||
const d = new Date(date)
|
||||
const year = d.getFullYear()
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
const hours = String(d.getHours()).padStart(2, '0')
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('YYYY', year)
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定月份的日历数据
|
||||
* @param {number} year
|
||||
* @param {number} month 1-12
|
||||
* @returns {Array} 日历数组,包含前置空白、日期、后置空白
|
||||
*/
|
||||
export function getCalendarDays(year, month) {
|
||||
const firstDay = new Date(year, month - 1, 1)
|
||||
const lastDay = new Date(year, month, 0)
|
||||
const daysInMonth = lastDay.getDate()
|
||||
// 周一为一周起始 (0=周一, 6=周日)
|
||||
let startWeekday = firstDay.getDay() - 1
|
||||
if (startWeekday < 0) startWeekday = 6
|
||||
|
||||
const days = []
|
||||
// 前置空白
|
||||
for (let i = 0; i < startWeekday; i++) {
|
||||
days.push({ day: null, date: null })
|
||||
}
|
||||
// 日期
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(i).padStart(2, '0')}`
|
||||
days.push({
|
||||
day: i,
|
||||
date: dateStr,
|
||||
isToday: dateStr === formatDate(new Date(), 'YYYY-MM-DD')
|
||||
})
|
||||
}
|
||||
return days
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本周一的日期
|
||||
*/
|
||||
export function getWeekStart(date = new Date()) {
|
||||
const d = new Date(date)
|
||||
const day = d.getDay()
|
||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1)
|
||||
return new Date(d.setDate(diff))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间问候语
|
||||
*/
|
||||
export function getGreeting() {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 6) return '夜深了'
|
||||
if (hour < 12) return '早上好'
|
||||
if (hour < 14) return '中午好'
|
||||
if (hour < 18) return '下午好'
|
||||
if (hour < 22) return '晚上好'
|
||||
return '夜深了'
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机获取一条酒言酒语
|
||||
*/
|
||||
export function getRandomQuote(quotes) {
|
||||
return quotes[Math.floor(Math.random() * quotes.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计数据计算
|
||||
*/
|
||||
export function calcStats(records) {
|
||||
const now = new Date()
|
||||
const today = formatDate(now, 'YYYY-MM-DD')
|
||||
|
||||
// 本周统计
|
||||
const weekStart = getWeekStart(now)
|
||||
const weekRecords = records.filter(r => new Date(r.date) >= weekStart)
|
||||
const weekDrinkCount = weekRecords.filter(r => r.mode === 'drank').length
|
||||
const weekCups = weekRecords.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
|
||||
|
||||
// 本月统计
|
||||
const monthStr = formatDate(now, 'YYYY-MM')
|
||||
const monthRecords = records.filter(r => r.date && r.date.startsWith(monthStr))
|
||||
const monthDrinkCount = monthRecords.filter(r => r.mode === 'drank').length
|
||||
const monthCups = monthRecords.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
|
||||
|
||||
// 连续天数
|
||||
let streak = 0
|
||||
let streakType = 'drank'
|
||||
const d = new Date()
|
||||
// 检查今天是否有记录
|
||||
const todayRecord = records.find(r => r.date === today)
|
||||
if (todayRecord) {
|
||||
streakType = todayRecord.mode
|
||||
streak = 1
|
||||
// 向前追溯
|
||||
for (let i = 1; i < 365; i++) {
|
||||
const prevDate = new Date(d)
|
||||
prevDate.setDate(prevDate.getDate() - i)
|
||||
const prevStr = formatDate(prevDate, 'YYYY-MM-DD')
|
||||
const prev = records.find(r => r.date === prevStr)
|
||||
if (prev && prev.mode === streakType) {
|
||||
streak++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 总打卡天数
|
||||
const totalDays = new Set(records.filter(r => r.mode === 'drank').map(r => r.date)).size
|
||||
const totalRecords = records.filter(r => r.mode === 'drank').length
|
||||
const totalCups = records.reduce((sum, r) => sum + (r.standardCupsTotal || 0), 0)
|
||||
|
||||
// 最爱酒类
|
||||
const categoryCount = {}
|
||||
records.forEach(r => {
|
||||
if (r.drinks) {
|
||||
r.drinks.forEach(d => {
|
||||
categoryCount[d.category] = (categoryCount[d.category] || 0) + 1
|
||||
})
|
||||
}
|
||||
})
|
||||
const favCategory = Object.entries(categoryCount).sort((a, b) => b[1] - a[1])[0]
|
||||
|
||||
return {
|
||||
weekDrinkCount,
|
||||
weekCups: Math.round(weekCups * 10) / 10,
|
||||
monthDrinkCount,
|
||||
monthCups: Math.round(monthCups * 10) / 10,
|
||||
streak,
|
||||
streakType,
|
||||
totalDays,
|
||||
totalRecords,
|
||||
totalCups: Math.round(totalCups * 10) / 10,
|
||||
favCategory: favCategory ? favCategory[0] : null,
|
||||
categoryVariety: Object.keys(categoryCount).length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查成就解锁
|
||||
*/
|
||||
export function checkAchievements(stats, achievements) {
|
||||
return achievements.map(a => {
|
||||
let unlocked = false
|
||||
const { type, value, category } = a.condition
|
||||
|
||||
switch (type) {
|
||||
case 'total_records':
|
||||
unlocked = stats.totalRecords >= value
|
||||
break
|
||||
case 'total_standard_cups':
|
||||
unlocked = stats.totalCups >= value
|
||||
break
|
||||
case 'streak_days':
|
||||
unlocked = stats.streak >= value
|
||||
break
|
||||
case 'category_variety':
|
||||
unlocked = stats.categoryVariety >= value
|
||||
break
|
||||
case 'category_records':
|
||||
// 需要更细粒度的数据,这里简化处理
|
||||
unlocked = false
|
||||
break
|
||||
}
|
||||
|
||||
return { ...a, unlocked }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<view
|
||||
class="badge"
|
||||
:class="{ 'badge-unlocked': achievement.unlocked, 'badge-locked': !achievement.unlocked }"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<view class="badge-icon-wrap">
|
||||
<text class="badge-icon">{{ achievement.icon }}</text>
|
||||
<view v-if="!achievement.unlocked" class="badge-lock">
|
||||
<text class="badge-lock-icon">🔒</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="badge-name">{{ achievement.name }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
achievement: { type: Object, required: true }
|
||||
},
|
||||
emits: ['click']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
padding: $sp-lg $sp-sm;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.badge-unlocked {
|
||||
.badge-icon-wrap {
|
||||
box-shadow: 0 0 24rpx rgba(232,168,56,0.3);
|
||||
border-color: rgba(232,168,56,0.3);
|
||||
}
|
||||
|
||||
.badge-name {
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.badge-locked {
|
||||
opacity: 0.5;
|
||||
|
||||
.badge-icon {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge-icon-wrap {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
background: $bg-elevated;
|
||||
border-radius: $radius-full;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3rpx solid rgba(255,255,255,0.06);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.badge-lock {
|
||||
position: absolute;
|
||||
bottom: -4rpx;
|
||||
right: -4rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background: $bg-base;
|
||||
border-radius: $radius-full;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.badge-lock-icon {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.badge-name {
|
||||
font-size: $fs-xs;
|
||||
color: $text-secondary;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<view class="calendar">
|
||||
<!-- 月份导航 -->
|
||||
<view class="cal-header">
|
||||
<view class="cal-nav" @click="prevMonth">
|
||||
<text class="cal-nav-arrow">‹</text>
|
||||
</view>
|
||||
<text class="cal-title">{{ year }}年{{ month }}月</text>
|
||||
<view class="cal-nav" @click="nextMonth">
|
||||
<text class="cal-nav-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 星期头 -->
|
||||
<view class="cal-weekdays">
|
||||
<text v-for="day in weekLabels" :key="day" class="cal-weekday">{{ day }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 日期网格 -->
|
||||
<view class="cal-grid">
|
||||
<view
|
||||
v-for="(cell, index) in calendarDays"
|
||||
:key="index"
|
||||
class="cal-cell"
|
||||
:class="{
|
||||
'cal-cell-empty': !cell.day,
|
||||
'cal-cell-today': cell.isToday,
|
||||
'cal-cell-drank': getRecordMode(cell.date) === 'drank',
|
||||
'cal-cell-abstain': getRecordMode(cell.date) === 'abstain'
|
||||
}"
|
||||
@click="cell.day && onSelectDate(cell.date)"
|
||||
>
|
||||
<text v-if="cell.day" class="cal-day-num">{{ cell.day }}</text>
|
||||
<view v-if="getRecordMode(cell.date) === 'drank'" class="cal-dot cal-dot-drank"></view>
|
||||
<view v-if="getRecordMode(cell.date) === 'abstain'" class="cal-dot cal-dot-abstain"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCalendarDays } from '../common/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
year: Number,
|
||||
month: Number,
|
||||
records: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
weekLabels: ['一', '二', '三', '四', '五', '六', '日']
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
calendarDays() {
|
||||
return getCalendarDays(this.year, this.month)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRecordMode(date) {
|
||||
if (!date) return null
|
||||
const record = this.records.find(r => r.date === date)
|
||||
return record ? record.mode : null
|
||||
},
|
||||
prevMonth() {
|
||||
let y = this.year
|
||||
let m = this.month - 1
|
||||
if (m < 1) { y--; m = 12 }
|
||||
this.$emit('change-month', { year: y, month: m })
|
||||
},
|
||||
nextMonth() {
|
||||
let y = this.year
|
||||
let m = this.month + 1
|
||||
if (m > 12) { y++; m = 1 }
|
||||
this.$emit('change-month', { year: y, month: m })
|
||||
},
|
||||
onSelectDate(date) {
|
||||
this.$emit('select-date', date)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.calendar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.cal-nav {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $radius-full;
|
||||
background: $bg-elevated;
|
||||
}
|
||||
|
||||
.cal-nav-arrow {
|
||||
font-size: $fs-xl;
|
||||
color: $text-secondary;
|
||||
font-weight: $fw-bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cal-title {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
font-family: $font-num;
|
||||
}
|
||||
|
||||
.cal-weekdays {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.cal-weekday {
|
||||
text-align: center;
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
padding: $sp-xs 0;
|
||||
}
|
||||
|
||||
.cal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: $sp-xs;
|
||||
}
|
||||
|
||||
.cal-cell {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $radius-md;
|
||||
position: relative;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
&.cal-cell-empty {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.cal-cell-today {
|
||||
border: 2rpx solid $amber;
|
||||
}
|
||||
|
||||
&:active:not(.cal-cell-empty) {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
}
|
||||
|
||||
.cal-day-num {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-medium;
|
||||
color: $text-primary;
|
||||
font-family: $font-num;
|
||||
}
|
||||
|
||||
.cal-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: $radius-full;
|
||||
margin-top: $sp-xs;
|
||||
|
||||
&.cal-dot-drank {
|
||||
background: $amber;
|
||||
box-shadow: 0 0 12rpx rgba(232, 168, 56, 0.5);
|
||||
}
|
||||
|
||||
&.cal-dot-abstain {
|
||||
background: $mint;
|
||||
box-shadow: 0 0 12rpx rgba(94, 198, 160, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* 饮酒日高亮背景 */
|
||||
.cal-cell-drank {
|
||||
background: rgba(232, 168, 56, 0.08);
|
||||
}
|
||||
|
||||
.cal-cell-abstain {
|
||||
background: rgba(94, 198, 160, 0.06);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,767 @@
|
||||
<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>
|
||||
|
||||
<!-- ====== 无照片: 纯文字排版 ====== -->
|
||||
<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]" mode="aspectFill"></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>
|
||||
|
||||
<!-- 照片拼贴区域 -->
|
||||
<view class="photo-collage" :class="'collage-' + photoLayout">
|
||||
<!-- 2张: 左大右小 -->
|
||||
<template v-if="photos.length === 2">
|
||||
<view class="collage-item collage-large">
|
||||
<image :src="photos[0]" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
<view class="collage-item collage-small">
|
||||
<image :src="photos[1]" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 3张: 左大右两小 -->
|
||||
<template v-else-if="photos.length === 3">
|
||||
<view class="collage-item c3-left">
|
||||
<image :src="photos[0]" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
<view class="c3-right">
|
||||
<view class="collage-item c3-right-top">
|
||||
<image :src="photos[1]" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
<view class="collage-item c3-right-bottom">
|
||||
<image :src="photos[2]" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 4张: 田字格 -->
|
||||
<template v-else-if="photos.length === 4">
|
||||
<view v-for="(p, i) in photos.slice(0, 4)" :key="i" class="collage-item collage-quarter">
|
||||
<image :src="p" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- 5+张: 瀑布流 -->
|
||||
<template v-else>
|
||||
<view class="collage-item collage-hero">
|
||||
<image :src="photos[0]" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
<view class="collage-strip">
|
||||
<view v-for="(p, i) in photos.slice(1, 5)" :key="i" class="collage-item collage-strip-item">
|
||||
<image :src="p" mode="aspectFill" class="collage-img"></image>
|
||||
</view>
|
||||
<view v-if="photos.length > 5" class="collage-more">
|
||||
<text>+{{ photos.length - 5 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</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">
|
||||
<text class="brand-name">🍻 喝酒了么</text>
|
||||
<view class="brand-dot"></view>
|
||||
<text class="brand-slogan">记录每一杯的故事</text>
|
||||
</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 === 2) return 'two'
|
||||
if (len === 3) return 'three'
|
||||
if (len === 4) return 'four'
|
||||
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: 100%;
|
||||
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: center;
|
||||
gap: $sp-sm;
|
||||
padding-top: $sp-lg;
|
||||
margin-top: auto;
|
||||
}
|
||||
.brand-name {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
.brand-dot {
|
||||
width: 4rpx;
|
||||
height: 4rpx;
|
||||
border-radius: $radius-full;
|
||||
background: $text-tertiary;
|
||||
}
|
||||
.brand-slogan {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* ========== 无照片布局 ========== */
|
||||
.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 加回来让品牌栏有空间 */
|
||||
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%;
|
||||
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);
|
||||
backdrop-filter: blur(10px);
|
||||
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-collage {
|
||||
border-radius: $radius-lg;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 2张: 左大右小 */
|
||||
.collage-two {
|
||||
display: flex;
|
||||
gap: 6rpx;
|
||||
height: 420rpx;
|
||||
}
|
||||
.collage-two .collage-large {
|
||||
flex: 3;
|
||||
}
|
||||
.collage-two .collage-small {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
/* 3张: 左大右两小 */
|
||||
.collage-three {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
height: 440rpx;
|
||||
}
|
||||
.collage-three .c3-left {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
.collage-three .c3-right {
|
||||
width: 45%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.collage-three .c3-right-top,
|
||||
.collage-three .c3-right-bottom {
|
||||
flex: 1;
|
||||
border-radius: $radius-md;
|
||||
}
|
||||
|
||||
/* 4张: 田字格 */
|
||||
.collage-four {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6rpx;
|
||||
height: 460rpx;
|
||||
}
|
||||
.collage-four .collage-quarter {
|
||||
width: calc(50% - 3rpx);
|
||||
height: calc(50% - 3rpx);
|
||||
}
|
||||
|
||||
/* 5+张: 瀑布流 */
|
||||
.collage-many {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.collage-many .collage-hero {
|
||||
height: 320rpx;
|
||||
border-radius: $radius-md;
|
||||
overflow: hidden;
|
||||
}
|
||||
.collage-strip {
|
||||
display: flex;
|
||||
gap: 6rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
.collage-strip-item {
|
||||
flex: 1;
|
||||
border-radius: $radius-sm;
|
||||
overflow: hidden;
|
||||
}
|
||||
.collage-more {
|
||||
width: 100rpx;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: $radius-sm;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.collage-item {
|
||||
overflow: hidden;
|
||||
border-radius: $radius-sm;
|
||||
}
|
||||
|
||||
.collage-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 底部信息 */
|
||||
.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>
|
||||
@@ -0,0 +1,559 @@
|
||||
# 喝酒了么 - 微信小程序产品规划方案
|
||||
|
||||
|
||||
|
||||
## 一、产品定位与用户画像
|
||||
|
||||
|
||||
|
||||
### 1.1 一句话定位
|
||||
|
||||
**"喝酒了么"是一款以饮酒打卡为入口、以酒友社交为粘性、以趣味分享为裂变引擎的轻量级生活方式小程序。**
|
||||
|
||||
|
||||
|
||||
### 1.2 核心价值主张
|
||||
|
||||
- **记录价值**:帮用户建立个人饮酒档案,让每一杯酒都有迹可循
|
||||
|
||||
- **社交价值**:让喝酒从"一个人闷"变成"一群人嗨",满足分享欲和归属感
|
||||
|
||||
- **趣味价值**:用游戏化机制消解"饮酒=不健康"的负面联想,塑造轻松有趣的品牌调性
|
||||
|
||||
- **数据价值**:长期积累后,用户可获得个人饮酒画像和年度报告,满足自我认知需求
|
||||
|
||||
|
||||
|
||||
### 1.3 目标用户画像
|
||||
|
||||
|
||||
|
||||
| 画像类型 | 年龄 | 饮酒场景 | 核心诉求 | 活跃时段 |
|
||||
|
||||
|---------|------|---------|---------|---------|
|
||||
|
||||
| **社交酒友** | 25-35岁 | 朋友聚会、公司团建、约饭 | 记录酒局、分享给朋友看、存图晒图 | 周五/周六晚 18:00-02:00 |
|
||||
|
||||
| **独酌爱好者** | 28-40岁 | 下班小酌、周末独饮 | 记录品酒心得、建立品酒档案 | 工作日 20:00-23:00 |
|
||||
|
||||
| **酒类入门者** | 22-28岁 | 尝试不同酒类、学习品酒 | 了解酒类知识、记录成长轨迹 | 不定时 |
|
||||
|
||||
| **轻度社交用户** | 20-40岁 | 偶尔应酬、节日聚会 | 快速记录、看看朋友喝了啥 | 节假日 |
|
||||
|
||||
|
||||
|
||||
### 1.4 使用场景拆解
|
||||
|
||||
|
||||
|
||||
**核心场景A:酒局进行时**
|
||||
|
||||
> 小王周五晚上和朋友聚餐吃火锅,开了两瓶白酒。他打开"喝酒了么",选择"火锅"配餐,录入"茅台·53度飞天 200ml",拍了一张碰杯照片,一键生成带滤镜的酒局卡片分享到微信群。朋友们看到后也纷纷打开小程序加入打卡。
|
||||
|
||||
|
||||
|
||||
**核心场景B:周末回顾**
|
||||
|
||||
> 周日晚上,小李收到小程序推送:"本周你喝了3次,白酒2次啤酒1次,打败了78%的酒友"。他点开查看本周饮酒日历,觉得有趣,截图发到朋友圈。
|
||||
|
||||
|
||||
|
||||
**核心场景C:年度盘点**
|
||||
|
||||
> 年底,小程序生成"2026年度饮酒报告":全年喝了186次,白酒占比62%,最爱配餐是烧烤,最爱品牌是青岛啤酒。排行榜显示他是朋友圈"年度酒神"第3名。他分享后获得专属年度徽章。
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 二、核心功能模块细化
|
||||
|
||||
|
||||
|
||||
### 2.1 每日饮酒打卡(基础入口)
|
||||
|
||||
|
||||
|
||||
**功能点:**
|
||||
|
||||
- **快速打卡**:首页醒目大按钮"今日喝了",一键标记当天饮酒状态
|
||||
|
||||
- **饮酒日历**:月视图日历,用不同颜色标记饮酒日(浅黄=未喝/深红=喝了),直观展示月度饮酒频率
|
||||
|
||||
- **连续打卡统计**:显示"连续饮酒 X 天"或"连续戒酒 X 天",支持用户自主切换追踪模式
|
||||
|
||||
- **补打卡**:支持补录过去7天内的饮酒记录(防止遗漏)
|
||||
|
||||
- **未喝也打卡**:支持"今日未喝"打卡,满足戒酒/控制饮酒用户的需求
|
||||
|
||||
|
||||
|
||||
### 2.2 酒水详情记录(核心数据层)
|
||||
|
||||
|
||||
|
||||
**酒类分类体系:**
|
||||
|
||||
- 白酒 | 啤酒 | 红酒 | 洋酒(威士忌/白兰地/伏特加/朗姆酒/金酒/龙舌兰)| 黄酒 | 清酒 | 果酒/气泡酒 | 自制调酒 | 其他
|
||||
|
||||
|
||||
|
||||
**记录字段:**
|
||||
|
||||
- 酒类类型(必选,下拉选择)
|
||||
|
||||
- 品牌名称(支持搜索+自定义输入,建立品牌数据库)
|
||||
|
||||
- 具体产品名称/型号(如"飞天茅台53度"、"百威经典")
|
||||
|
||||
- 饮用量(核心功能,见2.3)
|
||||
|
||||
- 酒精度数(自动匹配或手动输入)
|
||||
|
||||
- 饮用感受(可选:微醺/到位/醉了/断片,用表情化选择降低输入门槛)
|
||||
|
||||
- 照片(最多9张,支持滤镜)
|
||||
|
||||
- 饮用地点标签(家/餐厅/酒吧/KTV/户外 等)
|
||||
|
||||
|
||||
|
||||
### 2.3 饮用量换算引擎(差异化亮点)
|
||||
|
||||
|
||||
|
||||
**核心逻辑:统一换算为"标准杯"(1标准杯=10g纯酒精)**
|
||||
|
||||
|
||||
|
||||
| 酒类 | 常见规格 | 标准杯换算 |
|
||||
|
||||
|------|---------|-----------|
|
||||
|
||||
| 白酒(52°) | 1两=50ml | ≈2.1标准杯 |
|
||||
|
||||
| 啤酒(5°) | 1瓶=500ml | ≈2.0标准杯 |
|
||||
|
||||
| 红酒(13°) | 1杯=150ml | ≈1.5标准杯 |
|
||||
|
||||
| 威士忌(40°) | 1 shot=30ml | ≈1.0标准杯 |
|
||||
|
||||
|
||||
|
||||
**功能表现:**
|
||||
|
||||
- 输入时支持多种单位(ml/两/瓶/杯/听/shot),自动换算标准杯
|
||||
|
||||
- 单次酒局展示"累计 XX 标准杯",帮助用户感知饮酒量
|
||||
|
||||
- 健康参考线:标注"建议每日不超过2标准杯(《中国居民膳食指南》)"
|
||||
|
||||
|
||||
|
||||
### 2.4 配餐记录
|
||||
|
||||
|
||||
|
||||
- **菜品分类快捷选择**:火锅、烧烤、炒菜、海鲜、日料、西餐、小吃卤味、零食、无配餐
|
||||
|
||||
- **具体菜名输入**:支持文字输入+历史记录联想
|
||||
|
||||
- **酒配餐推荐**(后期AI功能):根据酒类推荐经典搭配(如"红酒配牛排"、"啤酒配烧烤")
|
||||
|
||||
- **配餐热力图**:统计分析"你最爱的下酒菜 TOP 5"
|
||||
|
||||
|
||||
|
||||
### 2.5 饮酒健康/安全提醒(社会责任模块)
|
||||
|
||||
|
||||
|
||||
- **单次提醒**:当日累计超过3标准杯时,弹出温和提醒"今日已饮用X标准杯,适度为宜"
|
||||
|
||||
- **周度报告**:每周总结饮酒总量和频率,给予健康建议
|
||||
|
||||
- **代驾快捷入口**:打卡页面底部固定"叫代驾"入口(对接滴滴/ e代驾小程序跳转)
|
||||
|
||||
- **安全提示**:首次使用展示《理性饮酒须知》,打卡页面底部常驻"过量饮酒有害健康"提示
|
||||
|
||||
- **戒酒挑战模式**:用户可设定"戒酒X天"目标,每日签到确认,失败可重置,完成获得成就徽章
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 三、进阶与社交功能建议
|
||||
|
||||
|
||||
|
||||
### 3.1 酒局卡片(核心分享载体)
|
||||
|
||||
|
||||
|
||||
每次完成打卡后,自动生成一张精美图片卡片,包含:
|
||||
|
||||
- 酒水信息(类型+品牌+饮用量)
|
||||
|
||||
- 配餐信息
|
||||
|
||||
- 饮用感受表情
|
||||
|
||||
- 日期和地点
|
||||
|
||||
- 一句随机"酒言酒语"文案(如"人生得意须尽欢"、"微醺是最好的状态")
|
||||
|
||||
- 小程序二维码水印(引流)
|
||||
|
||||
|
||||
|
||||
提供 5-8 种卡片模板风格(简约/国潮/文艺/酷炫等),支持一键保存图片或分享到微信聊天/朋友圈。
|
||||
|
||||
|
||||
|
||||
### 3.2 酒友圈(社区Feed流)
|
||||
|
||||
|
||||
|
||||
- **关注制内容流**:用户可选择关注微信好友或平台推荐用户
|
||||
|
||||
- **动态展示**:展示关注用户的公开打卡记录(以酒局卡片形式)
|
||||
|
||||
- **互动功能**:点赞、评论(仅文字,控制内容风险)
|
||||
|
||||
- **隐私控制**:每条打卡可设为"公开/仅好友可见/仅自己可见"
|
||||
|
||||
- **热门推荐**:算法推荐的优质打卡内容(需人工审核机制)
|
||||
|
||||
|
||||
|
||||
### 3.3 成就与徽章系统(游戏化)
|
||||
|
||||
|
||||
|
||||
| 徽章类别 | 徽章示例 | 获取条件 |
|
||||
|
||||
|---------|---------|---------|
|
||||
|
||||
| 打卡类 | 初来乍到、百杯不醉、签到王者 | 首次打卡/累计100次/连续30天打卡 |
|
||||
|
||||
| 酒类类 | 白酒达人、啤酒之王、红酒绅士 | 某类酒累计打卡N次 |
|
||||
|
||||
| 场景类 | 火锅搭档、烧烤之王、日料控 | 某类配餐累计打卡N次 |
|
||||
|
||||
| 挑战类 | 戒酒勇士、自律达人 | 完成戒酒挑战 |
|
||||
|
||||
| 稀有类 | 跨年酒神、中秋月饮 | 特定节日打卡 |
|
||||
|
||||
| 社交类 | 人气之星、酒友满天下 | 获得N个赞/关注N个好友 |
|
||||
|
||||
|
||||
|
||||
### 3.4 排行榜(社交裂变引擎)
|
||||
|
||||
|
||||
|
||||
- **好友排行(核心)**:基于微信好友关系链,排行维度包括:
|
||||
|
||||
- 本月饮酒次数
|
||||
|
||||
- 本月累计标准杯
|
||||
|
||||
- 酒类丰富度(喝过的不同酒类数量)
|
||||
|
||||
- **全平台排行**:匿名化展示,仅显示昵称首尾字符
|
||||
|
||||
- **警惕设计**:排行榜不鼓励过量饮酒,强调"丰富度"和"品鉴力"而非单纯的"喝得多"
|
||||
|
||||
- **排行卡片**:支持生成个人排行卡片分享(含排名和趣味称号)
|
||||
|
||||
|
||||
|
||||
### 3.5 年度饮酒报告(年度爆款功能)
|
||||
|
||||
|
||||
|
||||
参照网易云音乐年度报告的模式,包含:
|
||||
|
||||
- 年度饮酒天数/次数
|
||||
|
||||
- 最爱酒类 & 最爱品牌
|
||||
|
||||
- 最爱配餐 TOP 3
|
||||
|
||||
- 累计标准杯(换算成趣味等价物,如"相当于喝了XX瓶啤酒")
|
||||
|
||||
- 年度关键词/标签(如"白酒老炮"、"精酿探索者")
|
||||
|
||||
- 好友排行中的年度位置
|
||||
|
||||
- 月度饮酒趋势曲线
|
||||
|
||||
|
||||
|
||||
### 3.6 酒局组局(社交场景延伸)
|
||||
|
||||
|
||||
|
||||
- 发起酒局:设置时间、地点、主题,邀请好友参与
|
||||
|
||||
- 参与者共同打卡,自动关联到同一酒局
|
||||
|
||||
- 酒局结束后生成"酒局战报":参与人数、总消耗、人均标准杯、MVP(喝得最多的)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 四、核心用户交互流程
|
||||
|
||||
|
||||
|
||||
### 4.1 首次使用流程
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
用户搜索/扫码进入小程序
|
||||
|
||||
↓
|
||||
|
||||
欢迎页(3屏滑动引导:记录饮酒 / 分享酒局 / 成就徽章)
|
||||
|
||||
↓
|
||||
|
||||
微信一键授权登录(获取头像昵称)
|
||||
|
||||
↓
|
||||
|
||||
《理性饮酒须知》确认(仅首次,含健康提示和内容规范)
|
||||
|
||||
↓
|
||||
|
||||
设置饮酒偏好(可选跳过):
|
||||
|
||||
· 常饮酒类(多选)
|
||||
|
||||
· 饮酒频率(每天/经常/偶尔/很少)
|
||||
|
||||
↓
|
||||
|
||||
进入首页(今日打卡主界面)
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 4.2 一次完整"酒局打卡"流程
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
【首页】点击大按钮"🍻 开始记录"
|
||||
|
||||
↓
|
||||
|
||||
【Step 1 - 选酒】选择酒类类型 → 搜索/输入品牌 → 选择具体产品
|
||||
|
||||
↓ (支持快速添加多种酒,如同时喝白酒和啤酒)
|
||||
|
||||
↓
|
||||
|
||||
【Step 2 - 记量】输入饮用量 → 自动换算标准杯 → 实时显示累计
|
||||
|
||||
↓
|
||||
|
||||
【Step 3 - 配餐】选择配餐类型 → 可选输入具体菜名
|
||||
|
||||
↓
|
||||
|
||||
【Step 4 - 感受】选择饮用感受表情(微醺/到位/醉了/断片/自定义)
|
||||
|
||||
↓
|
||||
|
||||
【Step 5 - 照片】拍照或从相册选择(最多9张)→ 可选滤镜
|
||||
|
||||
↓
|
||||
|
||||
【Step 6 - 发布】设置可见范围 → 预览酒局卡片 → 确认发布
|
||||
|
||||
↓
|
||||
|
||||
【完成页】展示酒局卡片 → 一键分享微信好友/朋友圈 → 返回首页
|
||||
|
||||
↓
|
||||
|
||||
【首页更新】日历标记今日已喝 → 更新连续打卡天数 → 解锁新成就弹窗
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 4.3 浏览与互动流程
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
【底部导航】
|
||||
|
||||
首页(打卡) | 酒友圈(发现) | +(发布) | 排行榜 | 我的
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
- **首页**:日历视图 + 快速打卡按钮 + 饮酒统计卡片(本周/本月)
|
||||
|
||||
- **酒友圈**:关注用户的打卡动态Feed + 热门内容推荐
|
||||
|
||||
- **+号**:发布按钮,快速发起打卡
|
||||
|
||||
- **排行榜**:好友排行/全平台排行,切换维度
|
||||
|
||||
- **我的**:个人主页、饮酒档案、成就墙、设置
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 五、MVP版本规划与迭代路线
|
||||
|
||||
|
||||
|
||||
### 5.1 MVP V1.0 —— 核心闭环(建议4-6周开发)
|
||||
|
||||
|
||||
|
||||
**必须上线的功能:**
|
||||
|
||||
|
||||
|
||||
| 模块 | 功能点 | 优先级理由 |
|
||||
|
||||
|------|--------|-----------|
|
||||
|
||||
| 登录体系 | 微信一键授权登录 | 基础能力 |
|
||||
|
||||
| 饮酒打卡 | 快速打卡+日历视图+补打卡 | 核心功能闭环 |
|
||||
|
||||
| 酒水记录 | 酒类选择+品牌输入+饮用量(含标准杯换算) | 数据基础 |
|
||||
|
||||
| 配餐记录 | 配餐分类选择+文字输入 | 数据基础 |
|
||||
|
||||
| 酒局卡片 | 自动生成分享卡片(2-3种模板) | 社交裂变最小单元 |
|
||||
|
||||
| 我的页面 | 基础统计(打卡天数/总次数)+ 简单成就(3-5个) | 留存基础 |
|
||||
|
||||
|
||||
|
||||
**V1.0 不做的:**
|
||||
|
||||
- 酒友圈Feed流(内容审核成本高,先靠卡片分享裂变)
|
||||
|
||||
- 排行榜(依赖好友关系链数据积累)
|
||||
|
||||
- 年度报告(需要数据积累)
|
||||
|
||||
- 酒局组局
|
||||
|
||||
|
||||
|
||||
### 5.2 V1.1 —— 社交激活(+3-4周)
|
||||
|
||||
|
||||
|
||||
- 排行榜功能上线(好友排行优先)
|
||||
|
||||
- 成就徽章扩展至 15-20 个
|
||||
|
||||
- 卡片模板增加至 5-6 种
|
||||
|
||||
- 健康提醒 & 代驾入口
|
||||
|
||||
- "戒酒挑战"模式
|
||||
|
||||
|
||||
|
||||
### 5.3 V1.2 —— 社区建设(+3-4周)
|
||||
|
||||
|
||||
|
||||
- 酒友圈Feed流上线
|
||||
|
||||
- 点赞评论互动
|
||||
|
||||
- 关注/粉丝体系
|
||||
|
||||
- 内容举报与审核后台
|
||||
|
||||
|
||||
|
||||
### 5.4 V2.0 —— 生态扩展(+6-8周)
|
||||
|
||||
|
||||
|
||||
- 年度饮酒报告
|
||||
|
||||
- 酒局组局功能 `
|
||||
|
||||
- 酒配餐智能推荐
|
||||
|
||||
- 品牌数据库完善(UGC+人工维护)
|
||||
|
||||
- 个性化推送
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 六、关键风险与应对
|
||||
|
||||
|
||||
|
||||
| 风险点 | 应对策略 |
|
||||
|
||||
|--------|---------|
|
||||
|
||||
| **内容合规风险** | 所有页面底部标注"过量饮酒有害健康";不出现劝酒、拼酒等引导性内容;建立UGC内容审核机制 |
|
||||
|
||||
| **微信审核风险** | 避免"酒"字过度突出,小程序简介侧重"生活方式记录";不涉及在线酒类销售 |
|
||||
|
||||
| **用户隐私** | 打卡默认"仅好友可见";不强制获取位置等敏感权限;隐私政策清晰可读 |
|
||||
|
||||
| **留存难题** | 用成就徽章+连续打卡+排行榜制造回访钩子;周报推送保持存在感 |
|
||||
|
||||
| **冷启动** | V1.0重点打磨卡片分享体验,让每次分享都是一次拉新;可考虑种子用户内测+邀请制 |
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## 七、下一步行动建议
|
||||
|
||||
|
||||
|
||||
1. **竞品调研**:体验"DrinkNote"、"Untappd"(啤酒领域标杆)、"Vivino"(红酒领域),重点看它们的分享卡片和社区机制
|
||||
|
||||
2. **品牌名确认**:"喝酒了么"有记忆点,但需确认微信小程序命名规范(避免"酒"字被限制),备选名可考虑"饮记"、"微醺日记"等
|
||||
|
||||
3. **技术选型决策**:确定前端框架(原生/uniapp/Taro)和后端技术栈,建议 uniapp 以兼顾多端能力
|
||||
|
||||
4. **数据库设计**:酒类品牌库是核心资产,需尽早规划数据结构(酒类→品牌→产品的三级体系)
|
||||
|
||||
5. **视觉风格确定**:建议年轻化、轻量化的设计风格,避免传统白酒的"厚重感"
|
||||
|
||||
6. **MVP原型设计**:根据本方案V1.0范围,产出交互原型图(可使用墨刀/Figma),进行内部评审
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
import App from './App'
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import {
|
||||
createSSRApp
|
||||
} from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"name": "喝酒了么",
|
||||
"appid": "__UNI__871D23D",
|
||||
"description": "让每一杯酒都有迹可循",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"modules": {},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>"
|
||||
]
|
||||
},
|
||||
"ios": {},
|
||||
"sdkConfigs": {}
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"usingComponents": true,
|
||||
"permission": {},
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
}
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"vueVersion": "3"
|
||||
}
|
||||
{
|
||||
"name" : "hekjiu",
|
||||
"appid" : "__UNI__871D23D",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/onboarding/onboarding",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/record/record",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/card/card",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "喝酒了么",
|
||||
"navigationBarBackgroundColor": "#0B0B14",
|
||||
"backgroundColor": "#0B0B14",
|
||||
"backgroundColorTop": "#0B0B14",
|
||||
"backgroundColorBottom": "#0B0B14"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "uni-app"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<view class="page-container card-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="card-nav">
|
||||
<view class="step-back" @click="goBack">
|
||||
<text class="step-back-arrow">‹</text>
|
||||
</view>
|
||||
<text class="card-nav-title">你的酒局卡片</text>
|
||||
</view>
|
||||
|
||||
<!-- 照片提示 -->
|
||||
<view class="photo-hint" v-if="record">
|
||||
<text class="photo-hint-text">
|
||||
{{ photoHintText }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 模板切换 -->
|
||||
<view class="template-switcher">
|
||||
<view
|
||||
v-for="t in templates"
|
||||
:key="t.id"
|
||||
class="template-option"
|
||||
:class="{ 'template-active': currentTemplate === t.id }"
|
||||
@click="currentTemplate = t.id"
|
||||
>
|
||||
<view class="template-swatch" :style="{ background: t.swatch }">
|
||||
<view class="swatch-dot" v-if="currentTemplate === t.id"></view>
|
||||
</view>
|
||||
<text class="template-label">{{ t.name }}</text>
|
||||
<text class="template-desc">{{ t.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 卡片预览 -->
|
||||
<view class="card-preview-area">
|
||||
<DrinkCard v-if="record" :record="record" :template="currentTemplate" />
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="card-actions">
|
||||
<button class="btn-cta" @click="saveToAlbum">
|
||||
💾 保存到相册
|
||||
</button>
|
||||
<view class="action-row">
|
||||
<button class="btn-secondary action-btn" @click="shareToFriend">
|
||||
🔗 分享好友
|
||||
</button>
|
||||
<button class="btn-secondary action-btn" @click="goHome">
|
||||
🏠 返回首页
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<view v-if="showSuccess" class="success-toast">
|
||||
<text>✅ 已保存到相册</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DrinkCard from '../../components/DrinkCard.vue'
|
||||
import { getRecords } from '../../common/mock-data'
|
||||
|
||||
export default {
|
||||
components: { DrinkCard },
|
||||
data() {
|
||||
return {
|
||||
currentTemplate: 'amber',
|
||||
record: null,
|
||||
showSuccess: false,
|
||||
templates: [
|
||||
{
|
||||
id: 'amber',
|
||||
name: '琥珀流光',
|
||||
desc: '温暖金色调',
|
||||
swatch: 'linear-gradient(135deg, #1A1308, #0F0B06)'
|
||||
},
|
||||
{
|
||||
id: 'aurora',
|
||||
name: '暗夜极光',
|
||||
desc: '冷色氛围感',
|
||||
swatch: 'linear-gradient(135deg, #0A0E1A, #0E0A1A)'
|
||||
},
|
||||
{
|
||||
id: 'film',
|
||||
name: '胶片记忆',
|
||||
desc: '复古胶片风',
|
||||
swatch: 'linear-gradient(135deg, #141210, #0D0C0A)'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
photoHintText() {
|
||||
if (!this.record) return ''
|
||||
const photos = this.record.photos || []
|
||||
if (photos.length === 0) return '📷 无照片 · 纯文字排版'
|
||||
if (photos.length === 1) return '📸 单张照片 · 沉浸式全屏'
|
||||
return `🖼️ ${photos.length}张照片 · 拼贴布局`
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
const records = getRecords()
|
||||
if (options && options.recordId) {
|
||||
this.record = records.find(r => r.id === options.recordId)
|
||||
}
|
||||
if (!this.record) {
|
||||
this.record = records.find(r => r.mode === 'drank') || this.getFallbackRecord()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFallbackRecord() {
|
||||
return {
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
mode: 'drank',
|
||||
drinks: [{ category: 'beer', brand: '青岛啤酒', product: '经典', amount: 2, unit: 'bottle', degree: 4.3, standardCups: 3.4 }],
|
||||
food: { category: 'bbq', name: '烤串' },
|
||||
feeling: 'tipsy',
|
||||
photos: [],
|
||||
standardCupsTotal: 3.4
|
||||
}
|
||||
},
|
||||
saveToAlbum() {
|
||||
this.showSuccess = true
|
||||
uni.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
setTimeout(() => { this.showSuccess = false }, 2000)
|
||||
},
|
||||
shareToFriend() {
|
||||
uni.showToast({ title: '点击右上角分享', icon: 'none' })
|
||||
},
|
||||
goHome() {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '今晚的酒局记录 - 喝酒了么',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-page {
|
||||
padding: $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + $sp-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 导航 */
|
||||
.card-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
padding: $sp-md 0 $sp-lg;
|
||||
}
|
||||
|
||||
.step-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $radius-full;
|
||||
background: $bg-card;
|
||||
}
|
||||
|
||||
.step-back-arrow {
|
||||
font-size: $fs-xl;
|
||||
color: $text-secondary;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.card-nav-title {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
/* 照片数量提示 */
|
||||
.photo-hint {
|
||||
text-align: center;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.photo-hint-text {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 模板切换 - 横排卡片选择 */
|
||||
.template-switcher {
|
||||
display: flex;
|
||||
gap: $sp-md;
|
||||
margin-bottom: $sp-xl;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.template-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
padding: $sp-sm $sp-md;
|
||||
border-radius: $radius-lg;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
transition: all $duration-fast $ease-out;
|
||||
flex: 1;
|
||||
max-width: 220rpx;
|
||||
|
||||
&.template-active {
|
||||
border-color: $amber;
|
||||
background: rgba(232, 168, 56, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.template-swatch {
|
||||
width: 64rpx;
|
||||
height: 80rpx;
|
||||
border-radius: $radius-md;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.swatch-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: $radius-full;
|
||||
background: $amber;
|
||||
}
|
||||
|
||||
.template-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-primary;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
font-size: 18rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 卡片预览 */
|
||||
.card-preview-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin-bottom: $sp-xl;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.card-actions {
|
||||
padding-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: $sp-md;
|
||||
margin-top: $sp-md;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.success-toast {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
padding: $sp-lg $sp-xl;
|
||||
border-radius: $radius-lg;
|
||||
font-size: $fs-base;
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,540 @@
|
||||
<template>
|
||||
<view class="page-container home safe-bottom">
|
||||
<!-- 顶部问候 -->
|
||||
<view class="greeting-bar">
|
||||
<view class="greeting-left">
|
||||
<text class="greeting-text">{{ greeting }},{{ userName }}</text>
|
||||
<text class="greeting-sub">今晚喝一杯?</text>
|
||||
</view>
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||
</view>
|
||||
|
||||
<!-- 核心CTA - 首页最醒目位置 -->
|
||||
<view class="hero-cta" @click="goRecord">
|
||||
<view class="hero-cta-bg"></view>
|
||||
<view class="hero-cta-content">
|
||||
<text class="hero-cta-emoji">🍻</text>
|
||||
<view class="hero-cta-text">
|
||||
<text class="hero-cta-title">开始记录</text>
|
||||
<text class="hero-cta-sub">记录今晚的酒局</text>
|
||||
</view>
|
||||
<text class="hero-cta-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<view class="stats-row">
|
||||
<view class="stat-card card" @click="goProfile">
|
||||
<text class="stat-value text-num text-amber">{{ stats.monthDrinkCount }}</text>
|
||||
<text class="stat-label">本月饮酒</text>
|
||||
</view>
|
||||
<view class="stat-card card" @click="goProfile">
|
||||
<text class="stat-value text-num">{{ stats.monthCups }}</text>
|
||||
<text class="stat-label">标准杯</text>
|
||||
</view>
|
||||
<view class="stat-card card" @click="goProfile">
|
||||
<text class="stat-value text-num" :class="stats.streakType === 'drank' ? 'text-amber' : 'text-mint'">
|
||||
{{ stats.streak }}
|
||||
</text>
|
||||
<text class="stat-label">{{ stats.streakType === 'drank' ? '连喝天数' : '戒酒天数' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 饮酒日历 -->
|
||||
<view class="calendar-section card">
|
||||
<DrinkCalendar
|
||||
:year="calendarYear"
|
||||
:month="calendarMonth"
|
||||
:records="records"
|
||||
@change-month="onMonthChange"
|
||||
@select-date="onDateSelect"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 本周摘要 -->
|
||||
<view class="week-summary card">
|
||||
<view class="week-header flex-between">
|
||||
<text class="text-h3">本周摘要</text>
|
||||
<text class="text-caption">{{ weekRange }}</text>
|
||||
</view>
|
||||
<view class="week-stats flex">
|
||||
<view class="week-stat">
|
||||
<text class="week-stat-value text-num">{{ stats.weekDrinkCount }}</text>
|
||||
<text class="week-stat-label">次饮酒</text>
|
||||
</view>
|
||||
<view class="week-divider"></view>
|
||||
<view class="week-stat">
|
||||
<text class="week-stat-value text-num text-amber">{{ stats.weekCups }}</text>
|
||||
<text class="week-stat-label">标准杯</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="health-ref" v-if="stats.weekCups > 0">
|
||||
<view class="health-bar-fill" :style="{ width: healthPercent + '%' }"></view>
|
||||
<text class="health-text">
|
||||
{{ stats.weekCups > 14 ? '本周超标了,注意休息' : '本周控制得不错' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部健康提示 -->
|
||||
<view class="bottom-health-tip">
|
||||
<text>过量饮酒有害健康 · 请理性记录</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item tab-active" @click="goHome">
|
||||
<text class="tab-icon">🏠</text>
|
||||
<text class="tab-label">首页</text>
|
||||
</view>
|
||||
<view class="tab-center" @click="goRecord">
|
||||
<view class="tab-fab">
|
||||
<text class="tab-fab-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tab-item" @click="goProfile">
|
||||
<text class="tab-icon">👤</text>
|
||||
<text class="tab-label">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期记录弹窗 -->
|
||||
<view v-if="showDayDetail" class="day-overlay" @click="showDayDetail = false">
|
||||
<view class="day-detail card-elevated" @click.stop>
|
||||
<view class="day-detail-header flex-between">
|
||||
<text class="text-h3">{{ selectedDate }}</text>
|
||||
<text class="btn-text" @click="showDayDetail = false">关闭</text>
|
||||
</view>
|
||||
<view v-if="selectedRecord">
|
||||
<view v-if="selectedRecord.mode === 'drank'" class="day-record-info">
|
||||
<view class="day-drinks">
|
||||
<view v-for="(drink, i) in selectedRecord.drinks" :key="i" class="day-drink-item">
|
||||
<text>{{ drink.emoji || '🥃' }} {{ drink.brand }} {{ drink.product }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="day-meta flex-between">
|
||||
<text class="text-caption">{{ getFeelingName(selectedRecord.feeling) }}</text>
|
||||
<text class="text-caption text-amber text-num">{{ selectedRecord.standardCupsTotal }} 标准杯</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="day-abstain">
|
||||
<text class="day-abstain-emoji">🚫</text>
|
||||
<text class="text-caption">今日未饮酒,好样的</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="day-empty">
|
||||
<text class="text-caption">当天无记录</text>
|
||||
<button class="btn-ghost" @click="backfillDate">补打卡</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DrinkCalendar from '../../components/DrinkCalendar.vue'
|
||||
import { getRecords, getUserInfo } from '../../common/mock-data'
|
||||
import { calcStats, getGreeting, formatDate, getWeekStart } from '../../common/utils'
|
||||
import { FEELINGS } from '../../common/constants'
|
||||
|
||||
export default {
|
||||
components: { DrinkCalendar },
|
||||
data() {
|
||||
const now = new Date()
|
||||
return {
|
||||
calendarYear: now.getFullYear(),
|
||||
calendarMonth: now.getMonth() + 1,
|
||||
records: [],
|
||||
stats: {},
|
||||
greeting: '',
|
||||
userName: '',
|
||||
userAvatar: '/static/logo.png',
|
||||
showDayDetail: false,
|
||||
selectedDate: '',
|
||||
selectedRecord: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
weekRange() {
|
||||
const weekStart = getWeekStart()
|
||||
const weekEnd = new Date(weekStart)
|
||||
weekEnd.setDate(weekEnd.getDate() + 6)
|
||||
return `${formatDate(weekStart, 'MM/DD')} - ${formatDate(weekEnd, 'MM/DD')}`
|
||||
},
|
||||
healthPercent() {
|
||||
// 建议每周不超过14标准杯
|
||||
return Math.min((this.stats.weekCups / 14) * 100, 100)
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// 首次启动跳转引导页
|
||||
const isFirst = uni.getStorageSync('is_first_launch')
|
||||
const isLoggedIn = uni.getStorageSync('is_logged_in')
|
||||
if (isFirst === 'true' || !isLoggedIn) {
|
||||
uni.reLaunch({ url: '/pages/onboarding/onboarding' })
|
||||
return
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.records = getRecords()
|
||||
const user = getUserInfo()
|
||||
this.userName = user.nickname || '酒友'
|
||||
this.userAvatar = user.avatar || '/static/logo.png'
|
||||
this.greeting = getGreeting()
|
||||
this.stats = calcStats(this.records)
|
||||
},
|
||||
onMonthChange({ year, month }) {
|
||||
this.calendarYear = year
|
||||
this.calendarMonth = month
|
||||
},
|
||||
onDateSelect(dateStr) {
|
||||
this.selectedDate = dateStr
|
||||
this.selectedRecord = this.records.find(r => r.date === dateStr) || null
|
||||
this.showDayDetail = true
|
||||
},
|
||||
getFeelingName(feelingId) {
|
||||
const f = FEELINGS.find(f => f.id === feelingId)
|
||||
return f ? `${f.emoji} ${f.name}` : ''
|
||||
},
|
||||
backfillDate() {
|
||||
this.showDayDetail = false
|
||||
uni.navigateTo({
|
||||
url: `/pages/record/record?date=${this.selectedDate}&mode=backfill`
|
||||
})
|
||||
},
|
||||
goRecord() {
|
||||
uni.navigateTo({ url: '/pages/record/record' })
|
||||
},
|
||||
goHome() {
|
||||
// 已在首页
|
||||
},
|
||||
goProfile() {
|
||||
uni.navigateTo({ url: '/pages/profile/profile' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
padding: $sp-lg;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.greeting-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $sp-xl 0 $sp-lg;
|
||||
}
|
||||
|
||||
.greeting-text {
|
||||
display: block;
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.greeting-sub {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
margin-top: $sp-xs;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: $radius-full;
|
||||
border: 4rpx solid $amber-glow;
|
||||
}
|
||||
|
||||
.calendar-section {
|
||||
margin-bottom: $sp-lg;
|
||||
padding: $sp-lg;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: $sp-lg $sp-sm;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
margin-bottom: $sp-xs;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.week-summary {
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.week-header {
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.week-stats {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.week-stat {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.week-stat-value {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-bold;
|
||||
margin-bottom: $sp-xs;
|
||||
}
|
||||
|
||||
.week-stat-label {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.week-divider {
|
||||
width: 2rpx;
|
||||
background: rgba(255,255,255,0.06);
|
||||
margin: $sp-xs 0;
|
||||
}
|
||||
|
||||
.health-ref {
|
||||
margin-top: $sp-lg;
|
||||
position: relative;
|
||||
height: 12rpx;
|
||||
background: $bg-elevated;
|
||||
border-radius: $radius-full;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.health-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: $radius-full;
|
||||
background: linear-gradient(90deg, $mint, $amber);
|
||||
transition: width $duration-slow $ease-out;
|
||||
}
|
||||
|
||||
.health-text {
|
||||
display: block;
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
margin-top: $sp-xs;
|
||||
}
|
||||
|
||||
/* 核心CTA - 首页英雄区域 */
|
||||
.hero-cta {
|
||||
position: relative;
|
||||
margin-bottom: $sp-lg;
|
||||
border-radius: $radius-xl;
|
||||
overflow: hidden;
|
||||
padding: $sp-xl $sp-lg;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.hero-cta-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, $amber-light, $amber, $amber-deep);
|
||||
z-index: 0;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.12), transparent);
|
||||
animation: shimmer 4s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
.hero-cta-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-lg;
|
||||
}
|
||||
|
||||
.hero-cta-emoji {
|
||||
font-size: 72rpx;
|
||||
}
|
||||
|
||||
.hero-cta-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hero-cta-title {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-on-amber;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.hero-cta-sub {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: rgba(11, 11, 20, 0.6);
|
||||
margin-top: $sp-xs;
|
||||
}
|
||||
|
||||
.hero-cta-arrow {
|
||||
font-size: 56rpx;
|
||||
color: $text-on-amber;
|
||||
opacity: 0.5;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.bottom-health-tip {
|
||||
text-align: center;
|
||||
padding: $sp-lg 0;
|
||||
padding-bottom: 160rpx;
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* 底部TabBar */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120rpx;
|
||||
background: $bg-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
border-top: 2rpx solid rgba(255,255,255,0.04);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: $fs-xl;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.tab-active {
|
||||
.tab-icon { opacity: 1; }
|
||||
.tab-label { color: $amber; }
|
||||
}
|
||||
|
||||
.tab-center {
|
||||
position: relative;
|
||||
top: -20rpx;
|
||||
}
|
||||
|
||||
.tab-fab {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, $amber-light, $amber-deep);
|
||||
border-radius: $radius-full;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-amber;
|
||||
}
|
||||
|
||||
.tab-fab-icon {
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
}
|
||||
|
||||
/* 日期弹窗 */
|
||||
.day-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
z-index: 100;
|
||||
padding: $sp-lg;
|
||||
}
|
||||
|
||||
.day-detail {
|
||||
width: 100%;
|
||||
padding: $sp-xl;
|
||||
border-radius: $radius-xl $radius-xl 0 0;
|
||||
}
|
||||
|
||||
.day-detail-header {
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.day-drink-item {
|
||||
padding: $sp-sm 0;
|
||||
font-size: $fs-base;
|
||||
border-bottom: 2rpx solid rgba(255,255,255,0.04);
|
||||
}
|
||||
|
||||
.day-meta {
|
||||
margin-top: $sp-lg;
|
||||
}
|
||||
|
||||
.day-abstain {
|
||||
text-align: center;
|
||||
padding: $sp-xl 0;
|
||||
}
|
||||
|
||||
.day-abstain-emoji {
|
||||
display: block;
|
||||
font-size: $fs-hero;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.day-empty {
|
||||
text-align: center;
|
||||
padding: $sp-xl 0;
|
||||
|
||||
.btn-ghost {
|
||||
margin-top: $sp-lg;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<view class="page-container login-page">
|
||||
<!-- 顶部品牌区 -->
|
||||
<view class="brand-area">
|
||||
<view class="brand-glow"></view>
|
||||
<view class="brand-icon">
|
||||
<text class="brand-emoji">🍻</text>
|
||||
</view>
|
||||
<text class="brand-name">喝酒了么</text>
|
||||
<text class="brand-slogan">让每一杯酒都有迹可循</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录区域 -->
|
||||
<view class="login-area">
|
||||
<!-- 理性饮酒须知 -->
|
||||
<view v-if="showNotice" class="notice-card card">
|
||||
<view class="notice-header">
|
||||
<text class="notice-icon">📋</text>
|
||||
<text class="notice-title">理性饮酒须知</text>
|
||||
</view>
|
||||
<view class="notice-content">
|
||||
<text class="notice-item">• 过量饮酒有害健康,请理性记录</text>
|
||||
<text class="notice-item">• 本应用不鼓励未成年人使用</text>
|
||||
<text class="notice-item">• 不涉及任何酒类销售与推荐</text>
|
||||
<text class="notice-item">• 您的数据仅用于个人记录</text>
|
||||
</view>
|
||||
<button class="btn-primary notice-btn" @click="acceptNotice">我已了解</button>
|
||||
</view>
|
||||
|
||||
<!-- 主登录按钮 -->
|
||||
<view v-else class="login-actions">
|
||||
<button class="btn-cta login-btn" @click="handleLogin">
|
||||
<text class="login-btn-icon">📱</text>
|
||||
<text class="login-btn-text">微信一键登录</text>
|
||||
</button>
|
||||
|
||||
<view class="login-divider">
|
||||
<view class="login-divider-line"></view>
|
||||
<text class="login-divider-text">或</text>
|
||||
<view class="login-divider-line"></view>
|
||||
</view>
|
||||
|
||||
<button class="btn-secondary" @click="handleGuest">
|
||||
先看看再说
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 饮酒偏好(登录后显示) -->
|
||||
<view v-if="showPreferences" class="pref-overlay">
|
||||
<view class="pref-card card-elevated">
|
||||
<view class="pref-header">
|
||||
<text class="text-h2">你的饮酒偏好</text>
|
||||
<text class="text-caption">可跳过,稍后在设置中修改</text>
|
||||
</view>
|
||||
|
||||
<view class="pref-section">
|
||||
<text class="pref-label">常喝的酒类</text>
|
||||
<view class="pref-tags">
|
||||
<view
|
||||
v-for="cat in drinkCategories"
|
||||
:key="cat.id"
|
||||
class="tag"
|
||||
:class="{ 'tag-active': selectedCategories.includes(cat.id) }"
|
||||
@click="toggleCategory(cat.id)"
|
||||
>
|
||||
<text>{{ cat.emoji }} {{ cat.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pref-section">
|
||||
<text class="pref-label">饮酒频率</text>
|
||||
<view class="pref-options">
|
||||
<view
|
||||
v-for="freq in frequencies"
|
||||
:key="freq.id"
|
||||
class="freq-option"
|
||||
:class="{ 'freq-active': selectedFrequency === freq.id }"
|
||||
@click="selectedFrequency = freq.id"
|
||||
>
|
||||
<text>{{ freq.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pref-actions">
|
||||
<button class="btn-text" @click="skipPreferences">跳过</button>
|
||||
<button class="btn-primary" @click="savePreferences">保存并进入</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部协议 -->
|
||||
<view class="agreement">
|
||||
<text class="text-tiny">登录即代表同意</text>
|
||||
<text class="text-tiny text-amber">《用户协议》</text>
|
||||
<text class="text-tiny">和</text>
|
||||
<text class="text-tiny text-amber">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DRINK_CATEGORIES } from '../../common/constants'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showNotice: true,
|
||||
showPreferences: false,
|
||||
drinkCategories: DRINK_CATEGORIES,
|
||||
selectedCategories: [],
|
||||
selectedFrequency: '',
|
||||
frequencies: [
|
||||
{ id: 'daily', name: '几乎每天' },
|
||||
{ id: 'often', name: '经常喝' },
|
||||
{ id: 'sometimes', name: '偶尔喝' },
|
||||
{ id: 'rarely', name: '很少喝' }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
acceptNotice() {
|
||||
this.showNotice = false
|
||||
},
|
||||
handleLogin() {
|
||||
// Mock登录,直接显示偏好设置
|
||||
this.showPreferences = true
|
||||
},
|
||||
handleGuest() {
|
||||
uni.setStorageSync('is_logged_in', 'true')
|
||||
uni.setStorageSync('is_first_launch', 'false')
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
toggleCategory(id) {
|
||||
const idx = this.selectedCategories.indexOf(id)
|
||||
if (idx > -1) {
|
||||
this.selectedCategories.splice(idx, 1)
|
||||
} else {
|
||||
this.selectedCategories.push(id)
|
||||
}
|
||||
},
|
||||
skipPreferences() {
|
||||
this.finishLogin()
|
||||
},
|
||||
savePreferences() {
|
||||
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
user.preferences = {
|
||||
favoriteCategories: this.selectedCategories,
|
||||
frequency: this.selectedFrequency
|
||||
}
|
||||
uni.setStorageSync('user_info', JSON.stringify(user))
|
||||
this.finishLogin()
|
||||
},
|
||||
finishLogin() {
|
||||
uni.setStorageSync('is_logged_in', 'true')
|
||||
uni.setStorageSync('is_first_launch', 'false')
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: $sp-xl;
|
||||
}
|
||||
|
||||
.brand-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding-top: 120rpx;
|
||||
}
|
||||
|
||||
.brand-glow {
|
||||
position: absolute;
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
background: radial-gradient(circle, rgba(232,168,56,0.15) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -60%);
|
||||
filter: blur(30rpx);
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-xl;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-md;
|
||||
margin-bottom: $sp-xl;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.brand-emoji {
|
||||
font-size: 96rpx;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: $fs-hero;
|
||||
font-weight: $fw-black;
|
||||
color: $text-primary;
|
||||
letter-spacing: -4rpx;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.brand-slogan {
|
||||
font-size: $fs-md;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.login-area {
|
||||
padding-bottom: $sp-3xl;
|
||||
}
|
||||
|
||||
/* 理性饮酒须知 */
|
||||
.notice-card {
|
||||
padding: $sp-xl;
|
||||
}
|
||||
|
||||
.notice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.notice-icon {
|
||||
font-size: $fs-xl;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
line-height: $lh-loose;
|
||||
}
|
||||
|
||||
.notice-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $sp-lg;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $sp-sm;
|
||||
}
|
||||
|
||||
.login-btn-icon {
|
||||
font-size: $fs-xl;
|
||||
}
|
||||
|
||||
.login-btn-text {
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
}
|
||||
|
||||
.login-divider-line {
|
||||
flex: 1;
|
||||
height: 2rpx;
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
.login-divider-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 偏好设置弹层 */
|
||||
.pref-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
z-index: 100;
|
||||
padding: $sp-lg;
|
||||
}
|
||||
|
||||
.pref-card {
|
||||
width: 100%;
|
||||
padding: $sp-xl;
|
||||
border-radius: $radius-xl $radius-xl 0 0;
|
||||
}
|
||||
|
||||
.pref-header {
|
||||
margin-bottom: $sp-xl;
|
||||
|
||||
.text-h2 {
|
||||
display: block;
|
||||
margin-bottom: $sp-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.pref-section {
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.pref-label {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-medium;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.pref-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $sp-sm;
|
||||
}
|
||||
|
||||
.pref-options {
|
||||
display: flex;
|
||||
gap: $sp-sm;
|
||||
}
|
||||
|
||||
.freq-option {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: $sp-md $sp-sm;
|
||||
border-radius: $radius-md;
|
||||
background: $bg-card;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all $duration-fast $ease-out;
|
||||
|
||||
&.freq-active {
|
||||
background: $amber-glow;
|
||||
color: $amber;
|
||||
border-color: rgba(232,168,56,0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.pref-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: $sp-lg;
|
||||
|
||||
.btn-primary {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部协议 */
|
||||
.agreement {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $sp-xs;
|
||||
padding: $sp-lg 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<view class="page-container onboarding">
|
||||
<!-- 跳过按钮 -->
|
||||
<view class="skip-bar">
|
||||
<text class="btn-text" @click="goLogin">跳过</text>
|
||||
</view>
|
||||
|
||||
<!-- 滑动引导 -->
|
||||
<swiper
|
||||
class="swiper"
|
||||
:style="{ height: swiperHeight + 'px' }"
|
||||
:current="currentSlide"
|
||||
@change="onSlideChange"
|
||||
circular
|
||||
>
|
||||
<swiper-item v-for="(slide, index) in slides" :key="index">
|
||||
<view class="slide">
|
||||
<!-- 图标区域 -->
|
||||
<view class="slide-icon-area">
|
||||
<view class="slide-icon">
|
||||
<text class="slide-emoji">{{ slide.emoji }}</text>
|
||||
</view>
|
||||
<!-- 装饰光环 -->
|
||||
<view class="slide-glow" :style="{ background: slide.glowColor }"></view>
|
||||
</view>
|
||||
|
||||
<!-- 文案区域 -->
|
||||
<view class="slide-text">
|
||||
<text class="slide-title">{{ slide.title }}</text>
|
||||
<text class="slide-desc">{{ slide.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 底部控制区 -->
|
||||
<view class="bottom-controls">
|
||||
<!-- 指示器 -->
|
||||
<view class="dots">
|
||||
<view
|
||||
v-for="(slide, index) in slides"
|
||||
:key="index"
|
||||
class="dot"
|
||||
:class="{ 'dot-active': currentSlide === index }"
|
||||
></view>
|
||||
</view>
|
||||
|
||||
<!-- 按钮 -->
|
||||
<view class="action-area">
|
||||
<button
|
||||
v-if="currentSlide < slides.length - 1"
|
||||
class="btn-secondary"
|
||||
@click="nextSlide"
|
||||
>下一步</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn-cta"
|
||||
@click="goLogin"
|
||||
>开始体验</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentSlide: 0,
|
||||
swiperHeight: 600,
|
||||
slides: [
|
||||
{
|
||||
emoji: '🍻',
|
||||
title: '记录每一杯',
|
||||
desc: '轻松打卡,让每一杯酒都有迹可循。白酒、啤酒、红酒…你的饮酒档案从这里开始',
|
||||
glowColor: 'radial-gradient(circle, rgba(232,168,56,0.2) 0%, transparent 70%)'
|
||||
},
|
||||
{
|
||||
emoji: '📸',
|
||||
title: '分享你的酒局',
|
||||
desc: '自动生成精美卡片,配上酒言酒语,一键分享到微信群和朋友圈',
|
||||
glowColor: 'radial-gradient(circle, rgba(139,133,184,0.2) 0%, transparent 70%)'
|
||||
},
|
||||
{
|
||||
emoji: '🏆',
|
||||
title: '解锁成就徽章',
|
||||
desc: '从"初来乍到"到"年度酒神",每一个成就都记录着你的饮酒故事',
|
||||
glowColor: 'radial-gradient(circle, rgba(94,198,160,0.2) 0%, transparent 70%)'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 计算swiper高度: 屏幕高度 - 顶部跳过按钮 - 底部控制区
|
||||
const sys = uni.getSystemInfoSync()
|
||||
this.swiperHeight = sys.windowHeight - 60 - 200
|
||||
},
|
||||
methods: {
|
||||
onSlideChange(e) {
|
||||
this.currentSlide = e.detail.current
|
||||
},
|
||||
nextSlide() {
|
||||
if (this.currentSlide < this.slides.length - 1) {
|
||||
this.currentSlide++
|
||||
}
|
||||
},
|
||||
goLogin() {
|
||||
uni.reLaunch({ url: '/pages/login/login' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.onboarding {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skip-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: $sp-xl $sp-lg $sp-sm;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slide {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 $sp-xl;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
.slide-icon-area {
|
||||
position: relative;
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: $sp-2xl;
|
||||
}
|
||||
|
||||
.slide-icon {
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-xl;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-md;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.slide-emoji {
|
||||
font-size: 120rpx;
|
||||
}
|
||||
|
||||
.slide-glow {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
filter: blur(40rpx);
|
||||
}
|
||||
|
||||
.slide-text {
|
||||
text-align: center;
|
||||
max-width: 560rpx;
|
||||
}
|
||||
|
||||
.slide-title {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-black;
|
||||
color: $text-primary;
|
||||
margin-bottom: $sp-lg;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.slide-desc {
|
||||
display: block;
|
||||
font-size: $fs-md;
|
||||
color: $text-secondary;
|
||||
line-height: $lh-loose;
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: $sp-xl $sp-xl;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + $sp-xl);
|
||||
background: linear-gradient(to top, $bg-base 60%, transparent);
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $sp-sm;
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: $radius-full;
|
||||
background-color: $text-tertiary;
|
||||
transition: all $duration-base $ease-out;
|
||||
|
||||
&.dot-active {
|
||||
width: 40rpx;
|
||||
background-color: $amber;
|
||||
}
|
||||
}
|
||||
|
||||
.action-area {
|
||||
.btn-secondary, .btn-cta {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<view class="page-container profile-page safe-bottom">
|
||||
<!-- 用户头部 -->
|
||||
<view class="profile-header">
|
||||
<view class="profile-glow"></view>
|
||||
<image class="profile-avatar" :src="user.avatar" mode="aspectFill"></image>
|
||||
<text class="profile-name">{{ user.nickname }}</text>
|
||||
<text class="profile-joined">加入于 {{ user.joinedAt || '2026' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 饮酒档案统计 -->
|
||||
<view class="stats-section">
|
||||
<text class="section-title">饮酒档案</text>
|
||||
<view class="stats-grid">
|
||||
<view class="profile-stat card">
|
||||
<text class="profile-stat-value text-num">{{ stats.totalDays }}</text>
|
||||
<text class="profile-stat-label">打卡天数</text>
|
||||
</view>
|
||||
<view class="profile-stat card">
|
||||
<text class="profile-stat-value text-num">{{ stats.totalRecords }}</text>
|
||||
<text class="profile-stat-label">饮酒次数</text>
|
||||
</view>
|
||||
<view class="profile-stat card">
|
||||
<text class="profile-stat-value text-num text-amber">{{ stats.totalCups }}</text>
|
||||
<text class="profile-stat-label">标准杯</text>
|
||||
</view>
|
||||
<view class="profile-stat card">
|
||||
<text class="profile-stat-value text-num">{{ favCategoryName }}</text>
|
||||
<text class="profile-stat-label">最爱酒类</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 连续打卡 -->
|
||||
<view class="streak-card card">
|
||||
<view class="flex-between">
|
||||
<view>
|
||||
<text class="streak-label">{{ stats.streakType === 'drank' ? '连续饮酒' : '连续戒酒' }}</text>
|
||||
<text class="streak-value text-num" :class="stats.streakType === 'drank' ? 'text-amber' : 'text-mint'">
|
||||
{{ stats.streak }} 天
|
||||
</text>
|
||||
</view>
|
||||
<text class="streak-emoji">{{ stats.streakType === 'drank' ? '🔥' : '💪' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成就墙 -->
|
||||
<view class="achievements-section">
|
||||
<text class="section-title">成就徽章</text>
|
||||
<view class="achievements-grid">
|
||||
<AchievementBadge
|
||||
v-for="a in achievementList"
|
||||
:key="a.id"
|
||||
:achievement="a"
|
||||
@click="showAchievementDetail(a)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设置入口 -->
|
||||
<view class="settings-section card">
|
||||
<view class="settings-item" @click="showComingSoon('偏好设置')">
|
||||
<text class="settings-icon">⚙️</text>
|
||||
<text class="settings-name">饮酒偏好设置</text>
|
||||
<text class="settings-arrow">›</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="settings-item" @click="showComingSoon('隐私设置')">
|
||||
<text class="settings-icon">🔒</text>
|
||||
<text class="settings-name">隐私设置</text>
|
||||
<text class="settings-arrow">›</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="settings-item" @click="showComingSoon('关于')">
|
||||
<text class="settings-icon">ℹ️</text>
|
||||
<text class="settings-name">关于喝酒了么</text>
|
||||
<text class="settings-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部TabBar -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item" @click="goHome">
|
||||
<text class="tab-icon">🏠</text>
|
||||
<text class="tab-label">首页</text>
|
||||
</view>
|
||||
<view class="tab-center" @click="goRecord">
|
||||
<view class="tab-fab">
|
||||
<text class="tab-fab-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tab-item tab-active" @click="goProfile">
|
||||
<text class="tab-icon">👤</text>
|
||||
<text class="tab-label">我的</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成就详情弹窗 -->
|
||||
<view v-if="showDetail" class="detail-overlay" @click="showDetail = false">
|
||||
<view class="detail-card card-elevated" @click.stop>
|
||||
<text class="detail-emoji">{{ detailAchievement.icon }}</text>
|
||||
<text class="detail-name">{{ detailAchievement.name }}</text>
|
||||
<text class="detail-desc">{{ detailAchievement.desc }}</text>
|
||||
<view class="detail-status" :class="detailAchievement.unlocked ? 'status-unlocked' : 'status-locked'">
|
||||
<text>{{ detailAchievement.unlocked ? '✓ 已解锁' : '🔒 未解锁' }}</text>
|
||||
</view>
|
||||
<button class="btn-secondary" style="width: 100%; margin-top: 24rpx;" @click="showDetail = false">关闭</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AchievementBadge from '../../components/AchievementBadge.vue'
|
||||
import { getRecords, getUserInfo } from '../../common/mock-data'
|
||||
import { calcStats, checkAchievements } from '../../common/utils'
|
||||
import { ACHIEVEMENTS, DRINK_CATEGORIES } from '../../common/constants'
|
||||
|
||||
export default {
|
||||
components: { AchievementBadge },
|
||||
data() {
|
||||
return {
|
||||
user: {},
|
||||
stats: {},
|
||||
achievementList: [],
|
||||
showDetail: false,
|
||||
detailAchievement: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
favCategoryName() {
|
||||
if (!this.stats.favCategory) return '-'
|
||||
const cat = DRINK_CATEGORIES.find(c => c.id === this.stats.favCategory)
|
||||
return cat ? cat.name : '-'
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadData()
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
this.user = getUserInfo()
|
||||
const records = getRecords()
|
||||
this.stats = calcStats(records)
|
||||
this.achievementList = checkAchievements(this.stats, ACHIEVEMENTS)
|
||||
},
|
||||
showAchievementDetail(a) {
|
||||
this.detailAchievement = a
|
||||
this.showDetail = true
|
||||
},
|
||||
showComingSoon(name) {
|
||||
uni.showToast({ title: `${name}即将上线`, icon: 'none' })
|
||||
},
|
||||
goHome() {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
},
|
||||
goRecord() {
|
||||
uni.navigateTo({ url: '/pages/record/record' })
|
||||
},
|
||||
goProfile() {
|
||||
// 已在个人页面
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-page {
|
||||
padding: $sp-lg;
|
||||
padding-bottom: calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
.profile-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: $sp-2xl 0 $sp-xl;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.profile-glow {
|
||||
position: absolute;
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
background: radial-gradient(circle, rgba(232,168,56,0.12) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
top: 0;
|
||||
filter: blur(30rpx);
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: $radius-full;
|
||||
border: 4rpx solid $amber;
|
||||
margin-bottom: $sp-lg;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
margin-bottom: $sp-xs;
|
||||
}
|
||||
|
||||
.profile-joined {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
/* 统计 */
|
||||
.stats-section {
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $sp-md;
|
||||
}
|
||||
|
||||
.profile-stat {
|
||||
text-align: center;
|
||||
padding: $sp-lg;
|
||||
}
|
||||
|
||||
.profile-stat-value {
|
||||
display: block;
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-bold;
|
||||
margin-bottom: $sp-xs;
|
||||
}
|
||||
|
||||
.profile-stat-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
/* 连续打卡 */
|
||||
.streak-card {
|
||||
margin-bottom: $sp-xl;
|
||||
padding: $sp-xl;
|
||||
}
|
||||
|
||||
.streak-label {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.streak-value {
|
||||
display: block;
|
||||
font-size: $fs-3xl;
|
||||
font-weight: $fw-black;
|
||||
}
|
||||
|
||||
.streak-emoji {
|
||||
font-size: $fs-hero;
|
||||
}
|
||||
|
||||
/* 成就 */
|
||||
.achievements-section {
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.achievements-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: $sp-md;
|
||||
}
|
||||
|
||||
/* 设置 */
|
||||
.settings-section {
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
padding: $sp-lg 0;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: $fs-xl;
|
||||
}
|
||||
|
||||
.settings-name {
|
||||
flex: 1;
|
||||
font-size: $fs-base;
|
||||
}
|
||||
|
||||
.settings-arrow {
|
||||
font-size: $fs-xl;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* TabBar */
|
||||
.tab-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 120rpx;
|
||||
background: $bg-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
border-top: 2rpx solid rgba(255,255,255,0.04);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: $fs-xl;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.tab-active {
|
||||
.tab-icon { opacity: 1; }
|
||||
.tab-label { color: $amber; }
|
||||
}
|
||||
|
||||
.tab-center {
|
||||
position: relative;
|
||||
top: -20rpx;
|
||||
}
|
||||
|
||||
.tab-fab {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(135deg, $amber-light, $amber-deep);
|
||||
border-radius: $radius-full;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-amber;
|
||||
}
|
||||
|
||||
.tab-fab-icon {
|
||||
font-size: $fs-2xl;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
}
|
||||
|
||||
/* 成就详情弹窗 */
|
||||
.detail-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
padding: $sp-xl;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
text-align: center;
|
||||
padding: $sp-2xl $sp-xl;
|
||||
}
|
||||
|
||||
.detail-emoji {
|
||||
font-size: 120rpx;
|
||||
display: block;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
display: block;
|
||||
font-size: $fs-xl;
|
||||
font-weight: $fw-bold;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
margin-bottom: $sp-lg;
|
||||
line-height: $lh-loose;
|
||||
}
|
||||
|
||||
.detail-status {
|
||||
display: inline-flex;
|
||||
padding: $sp-sm $sp-lg;
|
||||
border-radius: $radius-full;
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-medium;
|
||||
|
||||
&.status-unlocked {
|
||||
background: rgba(94,198,160,0.15);
|
||||
color: $mint;
|
||||
}
|
||||
|
||||
&.status-locked {
|
||||
background: $bg-elevated;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,14 @@
|
||||
uni.addInterceptor({
|
||||
returnValue(res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then ===
|
||||
"function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
/* ==========================================
|
||||
* 喝酒了么 - 设计令牌 (Design Tokens)
|
||||
* 设计概念: 琥珀夜光 Amber Night
|
||||
* ========================================== */
|
||||
|
||||
/* --- 色彩系统 --- */
|
||||
$bg-base: #0B0B14; // 深夜底色
|
||||
$bg-card: #151520; // 卡片底色
|
||||
$bg-card-alt: #1C1C2A; // 卡片悬浮/选中态
|
||||
$bg-elevated: #222234; // 弹出层/输入框底色
|
||||
|
||||
$amber: #E8A838; // 琥珀金 - 品牌主色
|
||||
$amber-light: #F5C563; // 浅金 - 高亮/数字
|
||||
$amber-deep: #C47F17; // 深琥珀 - 按压态
|
||||
$amber-glow: rgba(232, 168, 56, 0.15); // 琥珀光晕
|
||||
|
||||
$coral: #FF6B6B; // 珊瑚红 - 警告/健康提醒
|
||||
$mint: #5EC6A0; // 薄荷绿 - 成功/正面
|
||||
$lavender: #8B85B8; // 淡紫灰 - 次要文本
|
||||
|
||||
$text-primary: #FFFFFF; // 主文本
|
||||
$text-secondary: #7E7E96; // 次要文本
|
||||
$text-tertiary: #4A4A62; // 辅助文本
|
||||
$text-on-amber: #0B0B14; // 琥珀色上的文字
|
||||
|
||||
/* --- 字体 --- */
|
||||
$font-base: -apple-system, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', sans-serif;
|
||||
$font-num: 'DIN Alternate', 'Helvetica Neue', -apple-system, sans-serif;
|
||||
|
||||
$fs-xs: 20rpx;
|
||||
$fs-sm: 24rpx;
|
||||
$fs-base: 28rpx;
|
||||
$fs-md: 30rpx;
|
||||
$fs-lg: 34rpx;
|
||||
$fs-xl: 40rpx;
|
||||
$fs-2xl: 52rpx;
|
||||
$fs-3xl: 64rpx;
|
||||
$fs-hero: 88rpx;
|
||||
|
||||
$fw-normal: 400;
|
||||
$fw-medium: 500;
|
||||
$fw-bold: 700;
|
||||
$fw-black: 800;
|
||||
|
||||
$lh-tight: 1.2;
|
||||
$lh-normal: 1.5;
|
||||
$lh-loose: 1.8;
|
||||
|
||||
/* --- 间距 --- */
|
||||
$sp-xs: 8rpx;
|
||||
$sp-sm: 16rpx;
|
||||
$sp-md: 24rpx;
|
||||
$sp-lg: 32rpx;
|
||||
$sp-xl: 48rpx;
|
||||
$sp-2xl: 64rpx;
|
||||
$sp-3xl: 96rpx;
|
||||
|
||||
/* --- 圆角 --- */
|
||||
$radius-sm: 12rpx;
|
||||
$radius-md: 20rpx;
|
||||
$radius-lg: 28rpx;
|
||||
$radius-xl: 40rpx;
|
||||
$radius-full: 999rpx;
|
||||
|
||||
/* --- 阴影 --- */
|
||||
$shadow-sm: 0 4rpx 12rpx rgba(0, 0, 0, 0.25);
|
||||
$shadow-md: 0 8rpx 32rpx rgba(0, 0, 0, 0.35);
|
||||
$shadow-amber: 0 8rpx 32rpx rgba(232, 168, 56, 0.25);
|
||||
$shadow-lg: 0 16rpx 48rpx rgba(0, 0, 0, 0.45);
|
||||
|
||||
/* --- 动画 --- */
|
||||
$ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
$ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
$duration-fast: 0.15s;
|
||||
$duration-base: 0.3s;
|
||||
$duration-slow: 0.5s;
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
@@ -0,0 +1,12 @@
|
||||
// 本文件用于,使用JQL语法操作项目关联的uniCloud空间的数据库,方便开发调试和远程数据库管理
|
||||
// 编写clientDB的js API(也支持常规js语法,比如var),可以对云数据库进行增删改查操作。不支持uniCloud-db组件写法
|
||||
// 可以全部运行,也可以选中部分代码运行。点击工具栏上的运行按钮或者按下【F5】键运行代码
|
||||
// 如果文档中存在多条JQL语句,只有最后一条语句生效
|
||||
// 如果混写了普通js,最后一条语句需是数据库操作语句
|
||||
// 此处代码运行不受DB Schema的权限控制,移植代码到实际业务中注意在schema中配好permission
|
||||
// 不支持clientDB的action
|
||||
// 数据库查询有最大返回条数限制,详见:https://uniapp.dcloud.net.cn/uniCloud/cf-database.html#limit
|
||||
// 详细JQL语法,请参考:https://uniapp.dcloud.net.cn/uniCloud/jql.html
|
||||
|
||||
// 下面示例查询uni-id-users表的所有数据
|
||||
db.collection('uni-id-users').get();
|
||||
@@ -0,0 +1,6 @@
|
||||
## 0.0.3(2022-11-11)
|
||||
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
|
||||
## 0.0.2(2021-04-16)
|
||||
- 修改插件package信息
|
||||
## 0.0.1(2021-03-15)
|
||||
- 初始化项目
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"id": "uni-config-center",
|
||||
"displayName": "uni-config-center",
|
||||
"version": "0.0.3",
|
||||
"description": "uniCloud 配置中心",
|
||||
"keywords": [
|
||||
"配置",
|
||||
"配置中心"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"type": "unicloud-template-function"
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../../scripts/dist"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y"
|
||||
},
|
||||
"client": {
|
||||
"App": {
|
||||
"app-vue": "u",
|
||||
"app-nvue": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "u",
|
||||
"Android Browser": "u",
|
||||
"微信浏览器(Android)": "u",
|
||||
"QQ浏览器(Android)": "u"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "u",
|
||||
"IE": "u",
|
||||
"Edge": "u",
|
||||
"Firefox": "u",
|
||||
"Safari": "u"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "u",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
},
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
# 为什么使用uni-config-center
|
||||
|
||||
实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
|
||||
|
||||
```bash
|
||||
cloudfunctions
|
||||
└─────common 公共模块
|
||||
├─plugin-a // 插件A对应的目录
|
||||
│ ├─index.js
|
||||
│ ├─config.json // plugin-a对应的配置文件
|
||||
│ └─other-file.cert // plugin-a依赖的其他文件
|
||||
└─plugin-b // plugin-b对应的目录
|
||||
├─index.js
|
||||
└─config.json // plugin-b对应的配置文件
|
||||
```
|
||||
|
||||
假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
|
||||
|
||||
uni-config-center就是用了统一管理这些配置文件的,使用uni-config-center后的目录结构如下
|
||||
|
||||
```bash
|
||||
cloudfunctions
|
||||
└─────common 公共模块
|
||||
├─plugin-a // 插件A对应的目录
|
||||
│ └─index.js
|
||||
├─plugin-b // plugin-b对应的目录
|
||||
│ └─index.js
|
||||
└─uni-config-center
|
||||
├─index.js // config-center入口文件
|
||||
├─plugin-a
|
||||
│ ├─config.json // plugin-a对应的配置文件
|
||||
│ └─other-file.cert // plugin-a依赖的其他文件
|
||||
└─plugin-b
|
||||
└─config.json // plugin-b对应的配置文件
|
||||
```
|
||||
|
||||
使用uni-config-center后的优势
|
||||
|
||||
- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
|
||||
- 支持对config.json设置schema,插件使用者在HBuilderX内编写config.json文件时会有更好的提示(后续HBuilderX会提供支持)
|
||||
|
||||
# 用法
|
||||
|
||||
在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖,请参考:[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
|
||||
|
||||
```js
|
||||
const createConfig = require('uni-config-center')
|
||||
|
||||
const uniIdConfig = createConfig({
|
||||
pluginId: 'uni-id', // 插件id
|
||||
defaultConfig: { // 默认配置
|
||||
tokenExpiresIn: 7200,
|
||||
tokenExpiresThreshold: 600,
|
||||
},
|
||||
customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
|
||||
// defaudltConfig 默认配置
|
||||
// userConfig 用户配置
|
||||
return Object.assign(defaultConfig, userConfig)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 以如下配置为例
|
||||
// {
|
||||
// "tokenExpiresIn": 7200,
|
||||
// "passwordErrorLimit": 6,
|
||||
// "bindTokenToDevice": false,
|
||||
// "passwordErrorRetryTime": 3600,
|
||||
// "app-plus": {
|
||||
// "tokenExpiresIn": 2592000
|
||||
// },
|
||||
// "service": {
|
||||
// "sms": {
|
||||
// "codeExpiresIn": 300
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// 获取配置
|
||||
uniIdConfig.config() // 获取全部配置,注意:uni-config-center内不存在对应插件目录时会返回空对象
|
||||
uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置,返回:7200
|
||||
uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置,返回:300
|
||||
uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置,如果不存在则取传入的默认值,返回:600
|
||||
|
||||
// 获取文件绝对路径
|
||||
uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
|
||||
|
||||
// 引用文件(require)
|
||||
uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined,文件内有其他错误导致require失败时会抛出错误。
|
||||
|
||||
// 判断是否包含某文件
|
||||
uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件,true: 文件存在,false: 文件不存在
|
||||
```
|
||||
+1
File diff suppressed because one or more lines are too long
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "uni-config-center",
|
||||
"version": "0.0.3",
|
||||
"description": "配置中心",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"author": "DCloud",
|
||||
"license": "Apache-2.0",
|
||||
"origin-plugin-dev-name": "uni-config-center",
|
||||
"origin-plugin-version": "0.0.3",
|
||||
"plugin-dev-name": "uni-config-center",
|
||||
"plugin-version": "0.0.3"
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
## 1.0.19(2025-12-16)
|
||||
- 增加配置参数缺失时的错误提示,指明配置文件路径
|
||||
## 1.0.18(2024-07-08)
|
||||
- checkToken时如果传入的token为空则返回uni-id-check-token-failed错误码以便uniIdRouter能正常跳转
|
||||
## 1.0.17(2024-04-26)
|
||||
- 兼容uni-app-x对客户端uniPlatform的调整(uni-app-x内uniPlatform区分app-android、app-ios)
|
||||
## 1.0.16(2023-04-25)
|
||||
- 新增maxTokenLength配置,用于限制数据库用户记录token数组的最大长度
|
||||
## 1.0.15(2023-04-06)
|
||||
- 修复部分语言国际化出错的Bug
|
||||
## 1.0.14(2023-03-07)
|
||||
- 修复 admin用户包含其他角色时未包含在token的Bug
|
||||
## 1.0.13(2022-07-21)
|
||||
- 修复 创建token时未传角色权限信息生成的token不正确的bug
|
||||
## 1.0.12(2022-07-15)
|
||||
- 提升与旧版本uni-id的兼容性(补充读取配置文件时回退平台app-plus、h5),但是仍推荐使用新平台名进行配置(app、web)
|
||||
## 1.0.11(2022-07-14)
|
||||
- 修复 部分情况下报`read property 'reduce' of undefined`的错误
|
||||
## 1.0.10(2022-07-11)
|
||||
- 将token存储在用户表的token字段内,与旧版本uni-id保持一致
|
||||
## 1.0.9(2022-07-01)
|
||||
- checkToken兼容token内未缓存角色权限的情况,此时将查库获取角色权限
|
||||
## 1.0.8(2022-07-01)
|
||||
- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug
|
||||
## 1.0.7(2022-06-30)
|
||||
- 修复config文件不合法时未抛出具体错误的Bug
|
||||
## 1.0.6(2022-06-28)
|
||||
- 移除插件内的数据表schema
|
||||
## 1.0.5(2022-06-27)
|
||||
- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug
|
||||
## 1.0.4(2022-06-27)
|
||||
- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945)
|
||||
## 1.0.2(2022-06-23)
|
||||
- 对齐旧版本uni-id默认配置
|
||||
## 1.0.1(2022-06-22)
|
||||
- 补充对uni-config-center的依赖
|
||||
## 1.0.0(2022-06-21)
|
||||
- 提供uni-id token创建、校验、刷新接口,简化旧版uni-id公共模块
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"id": "uni-id-common",
|
||||
"displayName": "uni-id-common",
|
||||
"version": "1.0.19",
|
||||
"description": "包含uni-id token生成、校验、刷新功能的云函数公共模块",
|
||||
"keywords": [
|
||||
"uni-id-common",
|
||||
"uniCloud",
|
||||
"token",
|
||||
"权限"
|
||||
],
|
||||
"repository": "https://gitcode.net/dcloud/uni-id-common",
|
||||
"engines": {
|
||||
"uni-app": "^3.1.0",
|
||||
"uni-app-x": "^3.1.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"type": "unicloud-template-function",
|
||||
"darkmode": "-",
|
||||
"i18n": "-",
|
||||
"widescreen": "-"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"uni-config-center"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "-",
|
||||
"vue3": "-"
|
||||
},
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"vue": "-",
|
||||
"nvue": "-",
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "-",
|
||||
"chrome": "-"
|
||||
},
|
||||
"app": {
|
||||
"android": "-",
|
||||
"ios": "-",
|
||||
"harmony": "-"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# uni-id-common
|
||||
|
||||
文档请参考:[uni-id-common](https://uniapp.dcloud.net.cn/uniCloud/uni-id-common.html)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "uni-id-common",
|
||||
"version": "1.0.19",
|
||||
"description": "uni-id token生成、校验、刷新",
|
||||
"main": "index.js",
|
||||
"homepage": "https:\/\/uniapp.dcloud.io\/uniCloud\/uni-id-common.html",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https:\/\/gitee.com\/dcloud\/uni-id-common.git"
|
||||
},
|
||||
"author": "DCloud",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"uni-config-center": "file:..\/..\/..\/..\/..\/uni-config-center\/uniCloud\/cloudfunctions\/common\/uni-config-center"
|
||||
},
|
||||
"origin-plugin-dev-name": "uni-id-common",
|
||||
"origin-plugin-version": "1.0.19",
|
||||
"plugin-dev-name": "uni-id-common",
|
||||
"plugin-version": "1.0.19"
|
||||
}
|
||||
Reference in New Issue
Block a user