init
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<view class="page container">
|
||||
<view class="stage-card">
|
||||
<view class="stage-badge">第 {{ stageDay }}/30 天</view>
|
||||
<text class="stage-label">当前减量计划阶段</text>
|
||||
<text class="stage-name">阶段 {{ stage }} : {{ stageName }}</text>
|
||||
<text class="stage-days">本阶段还剩 {{ daysLeft }} 天</text>
|
||||
<view class="stage-progress-row">
|
||||
<text class="stage-progress-label">阶段进度</text>
|
||||
<text class="stage-progress-value text-primary">{{ Math.round(stageProgress * 100) }}%</text>
|
||||
</view>
|
||||
<view class="stage-progress-bar">
|
||||
<view class="stage-progress-fill" :style="{ width: stageProgress * 100 + '%' }"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-icon">🤖</text>
|
||||
<text class="section-title">每日 AI 分析</text>
|
||||
</view>
|
||||
|
||||
<view class="ai-chat card">
|
||||
<view class="ai-chat-header">
|
||||
<text class="ai-chat-name text-primary">AI 教练</text>
|
||||
<text class="ai-chat-time">· 刚刚</text>
|
||||
</view>
|
||||
<view class="ai-chat-bubble">
|
||||
<text class="ai-chat-text">{{ aiAdvice }}</text>
|
||||
</view>
|
||||
<view class="ai-avatar">
|
||||
<text>🤖</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日目标</text>
|
||||
<text class="section-badge">已完成 {{ completedGoals }}/{{ goals.length }}</text>
|
||||
</view>
|
||||
|
||||
<view class="goals-list">
|
||||
<view
|
||||
v-for="goal in goals"
|
||||
:key="goal.id"
|
||||
class="goal-item card"
|
||||
@tap="toggleGoal(goal)"
|
||||
>
|
||||
<view class="goal-check" :class="{ 'goal-check-done': goal.done }">
|
||||
<text v-if="goal.done">✓</text>
|
||||
</view>
|
||||
<text class="goal-text" :class="{ 'goal-text-done': goal.done }">{{ goal.text }}</text>
|
||||
<text class="goal-icon">{{ goal.icon }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-btn btn btn-primary" @tap="goRecord">
|
||||
<text class="record-icon">➕</text>
|
||||
<text>记录吸烟或烟瘾</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const stageDay = ref(18)
|
||||
const stage = ref(2)
|
||||
const stageName = ref('减量期')
|
||||
const daysLeft = ref(12)
|
||||
const stageProgress = ref(0.4)
|
||||
|
||||
const aiAdvice = ref('早上好 Alex。昨天你的吸烟量比限额少了 2 支。这是一个巨大的胜利!🏆\n\n数据显示你的烟瘾在下午 2 点左右达到顶峰——今天试着那个时候去散散步。')
|
||||
|
||||
const goals = ref([
|
||||
{ id: 1, text: '喝 2 升水', icon: '🏆', done: true },
|
||||
{ id: 2, text: '控制在 5 支烟以内', icon: '', done: false },
|
||||
{ id: 3, text: '阅读激励卡片', icon: '🏆', done: false }
|
||||
])
|
||||
|
||||
const completedGoals = computed(() => {
|
||||
return goals.value.filter(g => g.done).length
|
||||
})
|
||||
|
||||
function toggleGoal(goal) {
|
||||
goal.done = !goal.done
|
||||
}
|
||||
|
||||
function goRecord() {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.stage-card {
|
||||
background: linear-gradient(135deg, rgba(74, 222, 128, 0.1) 0%, rgba(74, 222, 128, 0.05) 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stage-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,...') no-repeat center;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.stage-badge {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stage-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-primary);
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stage-days {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.stage-progress-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.stage-progress-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.stage-progress-value {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stage-progress-bar {
|
||||
height: 12rpx;
|
||||
background-color: rgba(74, 222, 128, 0.2);
|
||||
border-radius: 6rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stage-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-primary), #22C55E);
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
margin-left: auto;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-primary);
|
||||
background-color: rgba(74, 222, 128, 0.1);
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.ai-chat {
|
||||
position: relative;
|
||||
padding: 32rpx;
|
||||
padding-left: 100rpx;
|
||||
}
|
||||
|
||||
.ai-chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.ai-chat-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ai-chat-time {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.ai-chat-bubble {
|
||||
background-color: var(--color-bg-card-light);
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
border-top-left-radius: 8rpx;
|
||||
}
|
||||
|
||||
.ai-chat-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
position: absolute;
|
||||
left: 24rpx;
|
||||
bottom: 32rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background-color: var(--color-bg-card-light);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.goals-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.goal-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.goal-check {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.goal-check-done {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.goal-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.goal-text-done {
|
||||
text-decoration: line-through;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.goal-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.record-btn {
|
||||
position: fixed;
|
||||
bottom: 140rpx;
|
||||
left: 32rpx;
|
||||
right: 32rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.record-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,551 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view v-if="loading" class="skeleton">
|
||||
<view class="skeleton-header"></view>
|
||||
<view class="skeleton-tip"></view>
|
||||
<view class="skeleton-timer"></view>
|
||||
<view class="skeleton-cards"></view>
|
||||
<view class="skeleton-buttons"></view>
|
||||
</view>
|
||||
|
||||
<view v-else class="dashboard">
|
||||
<view class="header">
|
||||
<view class="user-info">
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||
<view class="greeting">
|
||||
<text class="greeting-text">{{ greeting }},{{ userName }}</text>
|
||||
<text class="greeting-sub">保持连胜纪录!</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="settings-btn" @tap="goSettings">
|
||||
<text class="iconfont">⚙</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="showAiTip" class="ai-tip card">
|
||||
<view class="ai-tip-icon">🤖</view>
|
||||
<view class="ai-tip-content">
|
||||
<text class="ai-tip-title">发现规律</text>
|
||||
<text class="ai-tip-desc">{{ aiTipText }}</text>
|
||||
</view>
|
||||
<view class="ai-tip-close" @tap="closeAiTip">×</view>
|
||||
</view>
|
||||
|
||||
<view class="timer-section">
|
||||
<view class="timer-ring">
|
||||
<canvas canvas-id="timerCanvas" class="timer-canvas"></canvas>
|
||||
<view class="timer-content">
|
||||
<text class="timer-label">距上次抽烟</text>
|
||||
<text class="timer-value">{{ timerDisplay }}</text>
|
||||
<view class="next-time" v-if="nextSmokeTimeText">
|
||||
<text class="next-time-icon">✨</text>
|
||||
<text class="next-time-text">下次建议: {{ nextSmokeTimeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-card card">
|
||||
<view class="stat-dot stat-dot-red"></view>
|
||||
<text class="stat-label">今日已抽</text>
|
||||
<view class="stat-value-row">
|
||||
<text class="stat-value">{{ todayCount }}</text>
|
||||
<text class="stat-target">/ {{ dailyTarget }}</text>
|
||||
<view class="stat-change" :class="changeClass">{{ changeText }}</view>
|
||||
</view>
|
||||
<view class="stat-progress">
|
||||
<view class="stat-progress-bar" :style="{ width: progressWidth }"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stat-card card">
|
||||
<view class="stat-dot stat-dot-green"></view>
|
||||
<text class="stat-label">烟瘾发作</text>
|
||||
<view class="stat-value-row">
|
||||
<text class="stat-value">{{ resistedCount }}</text>
|
||||
<text class="stat-unit">已抵抗</text>
|
||||
</view>
|
||||
<view class="stat-progress stat-progress-green">
|
||||
<view class="stat-progress-bar" :style="{ width: resistedProgressWidth }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-buttons">
|
||||
<view class="btn btn-secondary action-btn" @tap="recordSmoke">
|
||||
<text class="action-icon">🚬</text>
|
||||
<text>记录抽烟</text>
|
||||
</view>
|
||||
<view class="btn btn-primary action-btn" @tap="recordResisted">
|
||||
<text class="action-icon">💪</text>
|
||||
<text>想抽忍住了</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useDashboardStore } from '@/stores/dashboard'
|
||||
import { useProfileStore } from '@/stores/profile'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import * as api from '@/api'
|
||||
|
||||
const dashboardStore = useDashboardStore()
|
||||
const profileStore = useProfileStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const showAiTip = ref(true)
|
||||
const aiTipText = ref('你的烟瘾通常在下午2点达到高峰。我们为你准备了一个快速呼吸练习。')
|
||||
const resistedCount = ref(0)
|
||||
|
||||
let timerInterval = null
|
||||
const timerSeconds = ref(0)
|
||||
|
||||
const greeting = computed(() => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 6) return '凌晨好'
|
||||
if (hour < 12) return '早上好'
|
||||
if (hour < 14) return '中午好'
|
||||
if (hour < 18) return '下午好'
|
||||
return '晚上好'
|
||||
})
|
||||
|
||||
const userName = computed(() => {
|
||||
return userStore.user?.nickname || '用户'
|
||||
})
|
||||
|
||||
const userAvatar = computed(() => {
|
||||
return userStore.user?.avatar_url || '/static/icons/default-avatar.png'
|
||||
})
|
||||
|
||||
const todayCount = computed(() => dashboardStore.todayCount)
|
||||
const dailyTarget = computed(() => profileStore.profile?.baseline_cigs_per_day || 10)
|
||||
|
||||
const progressWidth = computed(() => {
|
||||
const percent = Math.min((todayCount.value / dailyTarget.value) * 100, 100)
|
||||
return `${percent}%`
|
||||
})
|
||||
|
||||
const resistedProgressWidth = computed(() => {
|
||||
const percent = Math.min((resistedCount.value / 10) * 100, 100)
|
||||
return `${percent}%`
|
||||
})
|
||||
|
||||
const changeText = computed(() => {
|
||||
return '较昨日 -2'
|
||||
})
|
||||
|
||||
const changeClass = computed(() => {
|
||||
return 'stat-change-down'
|
||||
})
|
||||
|
||||
const timerDisplay = computed(() => {
|
||||
const totalSeconds = dashboardStore.minutesSinceLast * 60 + timerSeconds.value
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
const nextSmokeTimeText = computed(() => {
|
||||
if (!dashboardStore.nextSmokeTime?.suggested_at) return ''
|
||||
const date = new Date(dashboardStore.nextSmokeTime.suggested_at)
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
function startTimer() {
|
||||
timerInterval = setInterval(() => {
|
||||
timerSeconds.value++
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (timerInterval) {
|
||||
clearInterval(timerInterval)
|
||||
timerInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
async function initPage() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const [profileRes, dashboardRes, nextTimeRes] = await Promise.all([
|
||||
api.getProfile(),
|
||||
api.getDashboard(),
|
||||
api.getNextSmokeTime()
|
||||
])
|
||||
|
||||
if (!profileRes.data.exists || !profileRes.data.is_completed) {
|
||||
uni.redirectTo({ url: '/pages/onboarding/index' })
|
||||
return
|
||||
}
|
||||
|
||||
profileStore.exists = profileRes.data.exists
|
||||
profileStore.isCompleted = profileRes.data.is_completed
|
||||
profileStore.profile = profileRes.data.profile
|
||||
|
||||
dashboardStore.setDashboard(dashboardRes.data)
|
||||
dashboardStore.setNextSmokeTime(nextTimeRes.data)
|
||||
|
||||
startTimer()
|
||||
} catch (e) {
|
||||
console.error('initPage error:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goSettings() {
|
||||
uni.navigateTo({ url: '/pages/profile/index' })
|
||||
}
|
||||
|
||||
function closeAiTip() {
|
||||
showAiTip.value = false
|
||||
}
|
||||
|
||||
async function recordSmoke() {
|
||||
try {
|
||||
await api.createLog({
|
||||
smoke_time: new Date().toISOString().split('T')[0],
|
||||
smoke_at: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
num: 1,
|
||||
level: 3
|
||||
})
|
||||
dashboardStore.incrementTodayCount()
|
||||
dashboardStore.resetTimer()
|
||||
timerSeconds.value = 0
|
||||
uni.showToast({ title: '记录成功', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('recordSmoke error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function recordResisted() {
|
||||
try {
|
||||
await api.createResistedLog({
|
||||
smoke_time: new Date().toISOString().split('T')[0],
|
||||
smoke_at: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
remark: '想抽但忍住了'
|
||||
})
|
||||
resistedCount.value++
|
||||
uni.showToast({ title: '太棒了!', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('recordResisted error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPage()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-bg);
|
||||
padding: 32rpx;
|
||||
padding-top: 88rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.skeleton-header,
|
||||
.skeleton-tip,
|
||||
.skeleton-timer,
|
||||
.skeleton-cards,
|
||||
.skeleton-buttons {
|
||||
background: linear-gradient(90deg, var(--color-bg-card) 25%, var(--color-bg-card-light) 50%, var(--color-bg-card) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-header {
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
.skeleton-tip {
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.skeleton-timer {
|
||||
height: 400rpx;
|
||||
border-radius: 50%;
|
||||
margin: 32rpx auto;
|
||||
width: 400rpx;
|
||||
}
|
||||
|
||||
.skeleton-cards {
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.skeleton-buttons {
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.greeting-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.greeting-sub {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.ai-tip {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
background-color: rgba(74, 222, 128, 0.1);
|
||||
border: 2rpx solid rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
|
||||
.ai-tip-icon {
|
||||
font-size: 48rpx;
|
||||
background-color: var(--color-primary);
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai-tip-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ai-tip-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.ai-tip-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ai-tip-close {
|
||||
font-size: 40rpx;
|
||||
color: var(--color-text-muted);
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.timer-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48rpx 0;
|
||||
}
|
||||
|
||||
.timer-ring {
|
||||
position: relative;
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
}
|
||||
|
||||
.timer-canvas {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
}
|
||||
|
||||
.timer-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.timer-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.timer-value {
|
||||
font-size: 72rpx;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
font-family: 'SF Mono', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.next-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 24rpx;
|
||||
background-color: var(--color-bg-card);
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.next-time-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.next-time-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.stat-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.stat-dot-red {
|
||||
background-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.stat-dot-green {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stat-value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-target,
|
||||
.stat-unit {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.stat-change-down {
|
||||
background-color: rgba(74, 222, 128, 0.2);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.stat-change-up {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.stat-progress {
|
||||
height: 8rpx;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: 4rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--color-danger);
|
||||
border-radius: 4rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-progress-green .stat-progress-bar {
|
||||
background-color: var(--color-success);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,312 @@
|
||||
<template>
|
||||
<view class="page container">
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': currentTab === tab.value }"
|
||||
@tap="currentTab = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="timeline">
|
||||
<view v-for="(group, date) in groupedLogs" :key="date" class="timeline-group">
|
||||
<view class="timeline-date">
|
||||
<text class="timeline-date-badge">{{ formatDate(date) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="timeline-items">
|
||||
<view
|
||||
v-for="log in group"
|
||||
:key="log.id"
|
||||
class="timeline-item"
|
||||
@touchstart="onTouchStart"
|
||||
@touchmove="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<view class="timeline-line"></view>
|
||||
<view class="timeline-dot" :class="log.type === 'resisted' ? 'dot-green' : 'dot-smoke'">
|
||||
<text v-if="log.type === 'resisted'">🛡</text>
|
||||
<text v-else>🚬</text>
|
||||
</view>
|
||||
<view class="timeline-content card">
|
||||
<view class="log-header">
|
||||
<text class="log-type">{{ log.type === 'resisted' ? '已忍住' : '已抽烟' }}</text>
|
||||
<text v-if="log.badge" class="log-badge" :class="log.badge.class">{{ log.badge.text }}</text>
|
||||
</view>
|
||||
<text class="log-time">{{ log.time }}</text>
|
||||
<view v-if="log.reason" class="log-reason">
|
||||
<text class="reason-icon">😫</text>
|
||||
<text class="reason-text">{{ log.reason }}</text>
|
||||
</view>
|
||||
<text v-if="log.interval" class="log-interval">间隔 {{ log.interval }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="fab" @tap="addLog">
|
||||
<text class="fab-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const tabs = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '已抽烟', value: 'smoke' },
|
||||
{ label: '已忍住', value: 'resisted' }
|
||||
]
|
||||
|
||||
const currentTab = ref('all')
|
||||
|
||||
const logs = ref([
|
||||
{
|
||||
id: 1,
|
||||
date: '2026-01-25',
|
||||
time: '4:20 PM',
|
||||
type: 'resisted',
|
||||
reason: '压力大',
|
||||
badge: { text: '成功', class: 'badge-success' }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '2026-01-25',
|
||||
time: '1:15 PM',
|
||||
type: 'smoke',
|
||||
reason: '无聊',
|
||||
interval: '1小时30分'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: '2026-01-25',
|
||||
time: '11:45 AM',
|
||||
type: 'smoke',
|
||||
reason: '晨间习惯',
|
||||
badge: { text: '今日第一支', class: 'badge-info' }
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
date: '2026-01-24',
|
||||
time: '10:30 PM',
|
||||
type: 'smoke',
|
||||
reason: '压力大',
|
||||
interval: '4小时12分'
|
||||
}
|
||||
])
|
||||
|
||||
const groupedLogs = computed(() => {
|
||||
const filtered = currentTab.value === 'all'
|
||||
? logs.value
|
||||
: logs.value.filter(l => l.type === currentTab.value)
|
||||
|
||||
return filtered.reduce((groups, log) => {
|
||||
if (!groups[log.date]) {
|
||||
groups[log.date] = []
|
||||
}
|
||||
groups[log.date].push(log)
|
||||
return groups
|
||||
}, {})
|
||||
})
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const date = new Date(dateStr)
|
||||
const today = new Date()
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
|
||||
if (dateStr === today.toISOString().split('T')[0]) {
|
||||
return `今天, ${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
if (dateStr === yesterday.toISOString().split('T')[0]) {
|
||||
return `昨天, ${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
|
||||
function addLog() {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
|
||||
function onTouchStart() {}
|
||||
function onTouchMove() {}
|
||||
function onTouchEnd() {}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text-secondary);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.tab-active {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timeline-group {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.timeline-date-badge {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-primary);
|
||||
background-color: rgba(74, 222, 128, 0.1);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.timeline-items {
|
||||
position: relative;
|
||||
padding-left: 60rpx;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: -36rpx;
|
||||
top: 48rpx;
|
||||
bottom: -24rpx;
|
||||
width: 4rpx;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
.timeline-item:last-child .timeline-line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -48rpx;
|
||||
top: 16rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dot-green {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.dot-smoke {
|
||||
background-color: var(--color-bg-card-light);
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-badge {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: rgba(74, 222, 128, 0.2);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: rgba(96, 165, 250, 0.2);
|
||||
color: #60A5FA;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 26rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.log-reason {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
background-color: var(--color-bg);
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.reason-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.reason-text {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.log-interval {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-muted);
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
bottom: 140rpx;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(74, 222, 128, 0.4);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 48rpx;
|
||||
color: var(--color-bg);
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: progressWidth }"></view>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view v-if="step === 1" class="step">
|
||||
<text class="step-title">你每天抽多少支烟?</text>
|
||||
<text class="step-desc">这将帮助我们为你制定个性化的戒烟计划</text>
|
||||
<view class="input-group">
|
||||
<view class="input-row">
|
||||
<view class="input-btn" @tap="decreaseCigs">-</view>
|
||||
<text class="input-value">{{ formData.baseline_cigs_per_day }}</text>
|
||||
<view class="input-btn" @tap="increaseCigs">+</view>
|
||||
</view>
|
||||
<text class="input-unit">支/天</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="step === 2" class="step">
|
||||
<text class="step-title">你的烟龄是多久?</text>
|
||||
<text class="step-desc">了解你的吸烟历史有助于更好地帮助你</text>
|
||||
<view class="options">
|
||||
<view
|
||||
v-for="option in smokingYearsOptions"
|
||||
:key="option.value"
|
||||
class="option"
|
||||
:class="{ 'option-active': formData.smoking_years === option.value }"
|
||||
@tap="formData.smoking_years = option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="step === 3" class="step">
|
||||
<text class="step-title">你为什么想戒烟?</text>
|
||||
<text class="step-desc">选择对你最重要的原因(可多选)</text>
|
||||
<view class="options options-wrap">
|
||||
<view
|
||||
v-for="option in quitMotivationOptions"
|
||||
:key="option"
|
||||
class="option"
|
||||
:class="{ 'option-active': formData.quit_motivations.includes(option) }"
|
||||
@tap="toggleMotivation(option)"
|
||||
>
|
||||
{{ option }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="step === 4" class="step">
|
||||
<text class="step-title">你通常什么时候起床和睡觉?</text>
|
||||
<text class="step-desc">我们会在你的休息时间避免打扰你</text>
|
||||
<view class="time-row">
|
||||
<view class="time-item">
|
||||
<text class="time-label">起床时间</text>
|
||||
<picker mode="time" :value="formData.wake_up_time" @change="onWakeTimeChange">
|
||||
<view class="time-picker">{{ formData.wake_up_time }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="time-item">
|
||||
<text class="time-label">睡觉时间</text>
|
||||
<picker mode="time" :value="formData.sleep_time" @change="onSleepTimeChange">
|
||||
<view class="time-picker">{{ formData.sleep_time }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="step === 5" class="step">
|
||||
<text class="step-title">每包烟多少钱?</text>
|
||||
<text class="step-desc">我们会帮你计算省下的钱</text>
|
||||
<view class="input-group">
|
||||
<view class="price-input">
|
||||
<text class="price-prefix">¥</text>
|
||||
<input
|
||||
type="digit"
|
||||
v-model="priceYuan"
|
||||
class="price-field"
|
||||
placeholder="0"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-unit">元/包</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer safe-area-bottom">
|
||||
<view v-if="step > 1" class="btn btn-secondary" @tap="prevStep">上一步</view>
|
||||
<view class="btn btn-primary flex-1" @tap="nextStep">
|
||||
{{ step === 5 ? '开始戒烟之旅' : '下一步' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useProfileStore } from '@/stores/profile'
|
||||
|
||||
const profileStore = useProfileStore()
|
||||
|
||||
const step = ref(1)
|
||||
const totalSteps = 5
|
||||
|
||||
const formData = ref({
|
||||
baseline_cigs_per_day: 10,
|
||||
smoking_years: 5,
|
||||
quit_motivations: [],
|
||||
smoke_motivations: [],
|
||||
wake_up_time: '07:30',
|
||||
sleep_time: '23:00',
|
||||
pack_price_cent: 2500
|
||||
})
|
||||
|
||||
const priceYuan = ref('25')
|
||||
|
||||
const progressWidth = computed(() => `${(step.value / totalSteps) * 100}%`)
|
||||
|
||||
const smokingYearsOptions = [
|
||||
{ label: '少于1年', value: 1 },
|
||||
{ label: '1-3年', value: 2 },
|
||||
{ label: '3-5年', value: 4 },
|
||||
{ label: '5-10年', value: 7 },
|
||||
{ label: '10年以上', value: 15 }
|
||||
]
|
||||
|
||||
const quitMotivationOptions = [
|
||||
'身体健康',
|
||||
'家人孩子',
|
||||
'省钱',
|
||||
'形象气质',
|
||||
'工作需要',
|
||||
'伴侣要求'
|
||||
]
|
||||
|
||||
function increaseCigs() {
|
||||
formData.value.baseline_cigs_per_day++
|
||||
}
|
||||
|
||||
function decreaseCigs() {
|
||||
if (formData.value.baseline_cigs_per_day > 1) {
|
||||
formData.value.baseline_cigs_per_day--
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMotivation(option) {
|
||||
const index = formData.value.quit_motivations.indexOf(option)
|
||||
if (index > -1) {
|
||||
formData.value.quit_motivations.splice(index, 1)
|
||||
} else {
|
||||
formData.value.quit_motivations.push(option)
|
||||
}
|
||||
}
|
||||
|
||||
function onWakeTimeChange(e) {
|
||||
formData.value.wake_up_time = e.detail.value
|
||||
}
|
||||
|
||||
function onSleepTimeChange(e) {
|
||||
formData.value.sleep_time = e.detail.value
|
||||
}
|
||||
|
||||
function prevStep() {
|
||||
if (step.value > 1) {
|
||||
step.value--
|
||||
}
|
||||
}
|
||||
|
||||
async function nextStep() {
|
||||
if (step.value < totalSteps) {
|
||||
step.value++
|
||||
return
|
||||
}
|
||||
|
||||
formData.value.pack_price_cent = Math.round(parseFloat(priceYuan.value || '0') * 100)
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
await profileStore.saveProfile(formData.value)
|
||||
uni.hideLoading()
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8rpx;
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: var(--color-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 64rpx 48rpx;
|
||||
}
|
||||
|
||||
.step {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 48rpx;
|
||||
}
|
||||
|
||||
.input-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-bg-card);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 48rpx;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.input-value {
|
||||
font-size: 96rpx;
|
||||
font-weight: 700;
|
||||
min-width: 160rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-unit {
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.options-wrap {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: 24rpx 32rpx;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.options-wrap .option {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.option-active {
|
||||
background-color: rgba(74, 222, 128, 0.1);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.time-row {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 26rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.time-picker {
|
||||
background-color: var(--color-bg-card);
|
||||
padding: 32rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg-card);
|
||||
padding: 24rpx 32rpx;
|
||||
border-radius: 16rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.price-prefix {
|
||||
font-size: 48rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.price-field {
|
||||
font-size: 64rpx;
|
||||
font-weight: 700;
|
||||
width: 200rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx 48rpx;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<view class="page container">
|
||||
<view class="user-section">
|
||||
<view class="avatar-wrapper">
|
||||
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
|
||||
<view class="avatar-edit">📷</view>
|
||||
</view>
|
||||
<text class="user-name">{{ userName }}</text>
|
||||
<view class="goal-badge">
|
||||
<text>目标:{{ goalDate }} 戒烟</text>
|
||||
<text class="goal-icon">🎯</text>
|
||||
</view>
|
||||
<text class="streak-text">已连续戒烟 {{ streakDays }} 天 🔥</text>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">我的进程</text>
|
||||
<view class="menu-list">
|
||||
<view class="menu-item card" @tap="goPage('goal')">
|
||||
<view class="menu-icon menu-icon-green">🎯</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">目标设定</text>
|
||||
<text class="menu-desc">调整每日限额与戒烟日期</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item card" @tap="goPage('ai-plan')">
|
||||
<view class="menu-icon menu-icon-blue">🤖</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">AI 计划调整</text>
|
||||
<text class="menu-desc">个性化辅导风格</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">偏好设置</text>
|
||||
<view class="menu-list">
|
||||
<view class="menu-item card" @tap="goPage('notification')">
|
||||
<view class="menu-icon menu-icon-orange">🔔</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">通知设置</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item card" @tap="goPage('vip')">
|
||||
<view class="menu-icon menu-icon-yellow">💎</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">解锁会员</text>
|
||||
<view class="pro-badge">PRO</view>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">通用</text>
|
||||
<view class="menu-list">
|
||||
<view class="menu-item card" @tap="goPage('settings')">
|
||||
<view class="menu-icon menu-icon-gray">⚙️</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">基础设置</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item card" @tap="goPage('privacy')">
|
||||
<view class="menu-icon menu-icon-gray">🔒</view>
|
||||
<view class="menu-content">
|
||||
<text class="menu-label">隐私与数据</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="logout-btn" @tap="logout">
|
||||
<text class="logout-text">退出登录</text>
|
||||
</view>
|
||||
|
||||
<text class="version">版本 1.0.0</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userName = computed(() => userStore.user?.nickname || 'Alex Doe')
|
||||
const userAvatar = computed(() => userStore.user?.avatar_url || '/static/icons/default-avatar.png')
|
||||
const goalDate = ref('12月1日')
|
||||
const streakDays = ref(12)
|
||||
|
||||
function goPage(page) {
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
function logout() {
|
||||
uni.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
userStore.logout()
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48rpx 0;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid var(--color-primary);
|
||||
}
|
||||
|
||||
.avatar-edit {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.goal-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
background-color: var(--color-danger);
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.goal-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.streak-text {
|
||||
font-size: 26rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.menu-icon-green {
|
||||
background-color: rgba(74, 222, 128, 0.2);
|
||||
}
|
||||
|
||||
.menu-icon-blue {
|
||||
background-color: rgba(96, 165, 250, 0.2);
|
||||
}
|
||||
|
||||
.menu-icon-orange {
|
||||
background-color: rgba(251, 146, 60, 0.2);
|
||||
}
|
||||
|
||||
.menu-icon-yellow {
|
||||
background-color: rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
|
||||
.menu-icon-gray {
|
||||
background-color: rgba(107, 114, 128, 0.2);
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.menu-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.pro-badge {
|
||||
display: inline-block;
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 4rpx;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 36rpx;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
color: var(--color-danger);
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.version {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-muted);
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,355 @@
|
||||
<template>
|
||||
<view class="page container">
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.value"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': currentTab === tab.value }"
|
||||
@tap="currentTab = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="insight-card card">
|
||||
<view class="insight-icon">✨</view>
|
||||
<view class="insight-content">
|
||||
<text class="insight-title">每周洞察</text>
|
||||
<text class="insight-desc">你在周末的吸烟量明显减少。非常棒!试着在这周一保持这个良好的势头。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">吸烟趋势</text>
|
||||
<text class="section-change text-primary">↓ 减少 20%</text>
|
||||
</view>
|
||||
|
||||
<view class="chart-card card">
|
||||
<view class="chart-header">
|
||||
<text class="chart-label">日均吸烟量</text>
|
||||
<view class="chart-value-row">
|
||||
<text class="chart-value">4</text>
|
||||
<text class="chart-unit">支/天</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="chart-placeholder">
|
||||
<view class="chart-bars">
|
||||
<view v-for="(item, index) in weeklyData" :key="index" class="chart-bar-wrapper">
|
||||
<view class="chart-bar" :style="{ height: item.height }"></view>
|
||||
<text class="chart-bar-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">健康与储蓄</text>
|
||||
|
||||
<view class="health-row">
|
||||
<view class="health-card card">
|
||||
<view class="health-ring">
|
||||
<text class="health-value">¥145</text>
|
||||
</view>
|
||||
<text class="health-label">节省金额</text>
|
||||
<text class="health-sub">目标 ¥200</text>
|
||||
</view>
|
||||
|
||||
<view class="health-card card">
|
||||
<view class="health-ring health-ring-purple">
|
||||
<text class="health-value">40%</text>
|
||||
</view>
|
||||
<text class="health-label">肺部功能恢复</text>
|
||||
<text class="health-sub">当前进度</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<view class="mini-stat card">
|
||||
<text class="mini-stat-icon">🔥</text>
|
||||
<text class="mini-stat-label">连续记录</text>
|
||||
<view class="mini-stat-value-row">
|
||||
<text class="mini-stat-value">12</text>
|
||||
<text class="mini-stat-unit">天</text>
|
||||
</view>
|
||||
<text class="mini-stat-sub">未吸烟</text>
|
||||
</view>
|
||||
|
||||
<view class="mini-stat card">
|
||||
<text class="mini-stat-icon">🚫</text>
|
||||
<text class="mini-stat-label">已拒绝</text>
|
||||
<view class="mini-stat-value-row">
|
||||
<text class="mini-stat-value">24</text>
|
||||
<text class="mini-stat-unit">次</text>
|
||||
</view>
|
||||
<text class="mini-stat-sub">对抗烟瘾</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const tabs = [
|
||||
{ label: '周', value: 'week' },
|
||||
{ label: '月', value: 'month' },
|
||||
{ label: '年', value: 'year' }
|
||||
]
|
||||
|
||||
const currentTab = ref('week')
|
||||
|
||||
const weeklyData = [
|
||||
{ label: '一', height: '60%', count: 3 },
|
||||
{ label: '二', height: '40%', count: 2 },
|
||||
{ label: '三', height: '80%', count: 4 },
|
||||
{ label: '四', height: '100%', count: 5 },
|
||||
{ label: '五', height: '60%', count: 3 },
|
||||
{ label: '六', height: '20%', count: 1 },
|
||||
{ label: '日', height: '40%', count: 2 }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
padding-bottom: 120rpx;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.tab-active {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.insight-card {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
background-color: rgba(74, 222, 128, 0.1);
|
||||
border: 2rpx solid rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
|
||||
.insight-icon {
|
||||
font-size: 48rpx;
|
||||
background-color: var(--color-primary);
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.insight-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.insight-title {
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.insight-desc {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-change {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chart-value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.chart-value {
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chart-unit {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
height: 240rpx;
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
|
||||
.chart-bar-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
width: 40rpx;
|
||||
background: linear-gradient(to top, var(--color-primary), rgba(74, 222, 128, 0.5));
|
||||
border-radius: 8rpx 8rpx 0 0;
|
||||
min-height: 8rpx;
|
||||
}
|
||||
|
||||
.chart-bar-label {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.health-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.health-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.health-ring {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
border: 12rpx solid rgba(74, 222, 128, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.health-ring::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
left: -12rpx;
|
||||
right: -12rpx;
|
||||
bottom: -12rpx;
|
||||
border-radius: 50%;
|
||||
border: 12rpx solid transparent;
|
||||
border-top-color: var(--color-primary);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.health-ring-purple::before {
|
||||
border-top-color: #A78BFA;
|
||||
}
|
||||
|
||||
.health-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.health-label {
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.health-sub {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.mini-stat {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.mini-stat-icon {
|
||||
font-size: 36rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.mini-stat-label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.mini-stat-value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.mini-stat-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mini-stat-unit {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.mini-stat-sub {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user