- 移除DrinkCard组件中的模板切换功能和多种布局模式 - 简化DrinkCard组件结构,统一使用琥珀流光样式 - 添加未来日期禁用功能,防止用户选择未来日期 - 优化卡片样式为现代化深色主题设计 - 调整日历单元格点击交互逻辑,过滤未来日期 - 更新Canvas导出尺寸适配新卡片高度 - 优化照片网格布局算法,支持响应式排列 - 简化页面操作按钮,移除冗余功能入口 - 移除情感状态显示,专注核心饮酒记录功能
516 lines
12 KiB
Vue
516 lines
12 KiB
Vue
<template>
|
||
<view class="page-container home">
|
||
<!-- 顶部问候 -->
|
||
<view class="greeting-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="greeting-left">
|
||
<text class="greeting-text">{{ greeting }},{{ userName }}</text>
|
||
<text class="greeting-sub">今晚喝一杯?</text>
|
||
</view>
|
||
<image v-if="userAvatar" class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||
<view v-else class="avatar avatar-placeholder">
|
||
<text class="avatar-placeholder-icon">👤</text>
|
||
</view>
|
||
</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">
|
||
<text class="stat-value text-num text-amber">{{ stats.monthDrinkCount }}</text>
|
||
<text class="stat-label">本月饮酒</text>
|
||
</view>
|
||
<view class="stat-card card">
|
||
<text class="stat-value text-num">{{ stats.monthCups }}</text>
|
||
<text class="stat-label">标准杯</text>
|
||
</view>
|
||
<view class="stat-card card">
|
||
<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 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>
|
||
<button class="btn-ghost day-toggle-btn" @click="toggleToAbstain">改为未饮酒</button>
|
||
</view>
|
||
<view v-else class="day-abstain">
|
||
<text class="day-abstain-emoji">🚫</text>
|
||
<text class="text-caption">今日未饮酒,好样的</text>
|
||
<button class="btn-ghost day-toggle-btn" @click="toggleToDrank">改为饮酒</button>
|
||
</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()
|
||
const sysInfo = uni.getSystemInfoSync()
|
||
return {
|
||
statusBarHeight: sysInfo.statusBarHeight || 25,
|
||
calendarYear: now.getFullYear(),
|
||
calendarMonth: now.getMonth() + 1,
|
||
records: [],
|
||
stats: {},
|
||
greeting: '',
|
||
userName: '',
|
||
userAvatar: '',
|
||
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 || ''
|
||
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`
|
||
})
|
||
},
|
||
// 改为未饮酒
|
||
toggleToAbstain() {
|
||
uni.showModal({
|
||
title: '确认修改',
|
||
content: '确定将这天改为未饮酒吗?饮酒记录将被清除。',
|
||
success: (res) => {
|
||
if (!res.confirm) return
|
||
const idx = this.records.findIndex(r => r.date === this.selectedDate)
|
||
if (idx === -1) return
|
||
this.records[idx].mode = 'abstain'
|
||
this.records[idx].drinks = []
|
||
this.records[idx].food = null
|
||
this.records[idx].feeling = null
|
||
this.records[idx].standardCupsTotal = 0
|
||
this.selectedRecord = this.records[idx]
|
||
this.saveRecords()
|
||
}
|
||
})
|
||
},
|
||
// 改为饮酒(跳转到补录页)
|
||
toggleToDrank() {
|
||
this.showDayDetail = false
|
||
uni.navigateTo({
|
||
url: `/pages/record/record?date=${this.selectedDate}&mode=backfill`
|
||
})
|
||
},
|
||
// 保存记录到本地存储
|
||
saveRecords() {
|
||
uni.setStorageSync('drink_records', JSON.stringify(this.records))
|
||
},
|
||
goRecord() {
|
||
uni.navigateTo({ url: '/pages/record/record' })
|
||
},
|
||
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.home {
|
||
padding: $sp-md;
|
||
padding-top: 0;
|
||
}
|
||
|
||
.greeting-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: $sp-md 0 $sp-sm;
|
||
}
|
||
|
||
.greeting-text {
|
||
display: block;
|
||
font-size:38rpx;
|
||
font-weight: $fw-bold;
|
||
}
|
||
|
||
.greeting-sub {
|
||
display: block;
|
||
font-size: $fs-xs;
|
||
color: $text-secondary;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.avatar {
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: $radius-full;
|
||
border: 4rpx solid $amber-glow;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.avatar-placeholder {
|
||
background: $bg-elevated;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 4rpx dashed rgba(232,168,56,0.3);
|
||
}
|
||
|
||
.avatar-placeholder-icon {
|
||
font-size: 40rpx;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.calendar-section {
|
||
margin-bottom: $sp-md;
|
||
padding: $sp-md;
|
||
}
|
||
|
||
.stats-row {
|
||
display: flex;
|
||
gap: $sp-xs;
|
||
margin-bottom: $sp-md;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: $sp-md $sp-xs;
|
||
}
|
||
|
||
.stat-value {
|
||
display: block;
|
||
font-size: $fs-xl;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: $fs-xs;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.week-summary {
|
||
padding: $sp-md;
|
||
}
|
||
|
||
.week-header {
|
||
margin-bottom: $sp-sm;
|
||
}
|
||
|
||
.week-stats {
|
||
gap: 0;
|
||
}
|
||
|
||
.week-stat {
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
|
||
.week-stat-value {
|
||
display: block;
|
||
font-size: $fs-xl;
|
||
font-weight: $fw-bold;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.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-md;
|
||
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-md;
|
||
border-radius: $radius-lg;
|
||
overflow: hidden;
|
||
padding: $sp-md $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-md;
|
||
}
|
||
|
||
.hero-cta-emoji {
|
||
font-size: 56rpx;
|
||
}
|
||
|
||
.hero-cta-text {
|
||
flex: 1;
|
||
}
|
||
|
||
.hero-cta-title {
|
||
display: block;
|
||
font-size: $fs-xl;
|
||
font-weight: $fw-black;
|
||
color: $text-on-amber;
|
||
letter-spacing: -1rpx;
|
||
}
|
||
|
||
.hero-cta-sub {
|
||
display: block;
|
||
font-size: $fs-xs;
|
||
color: rgba(11, 11, 20, 0.6);
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
.hero-cta-arrow {
|
||
font-size: 44rpx;
|
||
color: $text-on-amber;
|
||
opacity: 0.5;
|
||
font-weight: $fw-bold;
|
||
}
|
||
|
||
.bottom-health-tip {
|
||
text-align: center;
|
||
padding: $sp-sm 0;
|
||
font-size: $fs-xs;
|
||
color: $text-tertiary;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
/* 日期弹窗 */
|
||
.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;
|
||
}
|
||
}
|
||
|
||
.day-toggle-btn {
|
||
margin-top: $sp-lg;
|
||
width: 100%;
|
||
font-size: $fs-sm;
|
||
color: $text-secondary;
|
||
border-color: rgba(255,255,255,0.1);
|
||
}
|
||
</style>
|