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,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
Reference in New Issue
Block a user