- 添加完整的后端接口文档,包含用户、打卡记录、统计等模块 - 修复应用名称从"喝酒了么"统一更正为"喝了么" - 重构App.vue中的启动逻辑,增加数据迁移和路由守卫功能 - 调整DrinkCalendar组件的UI间距和尺寸参数 - 优化DrinkCard组件的品牌展示文案 - 修改manifest.json中的应用描述和微信小程序配置 - 调整pages.json中的页面路径顺序和tabBar配置 - 更新全局样式文件中的应用名称标识 - 修改mock数据中的用户头像默认值 - 优化卡片导出功能的绘制逻辑和视觉效果
511 lines
12 KiB
Vue
511 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" @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="fab-record" @click="goRecord">
|
||
<text class="fab-icon">+</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>
|
||
</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()
|
||
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`
|
||
})
|
||
},
|
||
goRecord() {
|
||
uni.navigateTo({ url: '/pages/record/record' })
|
||
},
|
||
goProfile() {
|
||
uni.switchTab({ url: '/pages/profile/profile' })
|
||
}
|
||
}
|
||
}
|
||
</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: $fs-lg;
|
||
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 {
|
||
margin-bottom: $sp-md;
|
||
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;
|
||
padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
|
||
font-size: $fs-xs;
|
||
color: $text-tertiary;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
/* 悬浮记录按钮 */
|
||
.fab-record {
|
||
position: fixed;
|
||
right: 40rpx;
|
||
bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||
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;
|
||
z-index: 50;
|
||
|
||
&:active {
|
||
transform: scale(0.92);
|
||
}
|
||
}
|
||
|
||
.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>
|