feat: 添加戒烟计划前端页面
- 新增 pages/quit-plan/index.vue 戒烟计划页面 - 展示30天戒烟计划总览和进度 - 显示当前阶段(记录期/减量期/巩固期) - 展示每日目标和建议 - 支持生成计划和重置计划功能 - 在 api/smoke.js 添加相关 API 调用 - 在 pages.json 注册路由
This commit is contained in:
@@ -63,3 +63,20 @@ export function getShareData(shareToken, params = {}) {
|
|||||||
export function revokeShare(shareToken) {
|
export function revokeShare(shareToken) {
|
||||||
return request.post(`/smoke/share/${shareToken}/revoke`)
|
return request.post(`/smoke/share/${shareToken}/revoke`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 戒烟计划 API
|
||||||
|
export function generateQuitPlan() {
|
||||||
|
return request.post('/smoke/quit-plan/generate')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQuitPlan(params = {}) {
|
||||||
|
return request.get('/smoke/quit-plan', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQuitPlanDays(planId) {
|
||||||
|
return request.get('/smoke/quit-plan/days', { plan_id: planId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetQuitPlan() {
|
||||||
|
return request.post('/smoke/quit-plan/reset')
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,12 @@
|
|||||||
"navigationBarTitleText": "个人中心"
|
"navigationBarTitleText": "个人中心"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/quit-plan/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "戒烟计划"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/onboarding/index",
|
"path": "pages/onboarding/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -0,0 +1,823 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||||
|
|
||||||
|
<view class="container">
|
||||||
|
<view v-if="pageLoading" class="skeleton">
|
||||||
|
<view class="skeleton-card"></view>
|
||||||
|
<view class="skeleton-card"></view>
|
||||||
|
<view class="skeleton-list">
|
||||||
|
<view v-for="i in 3" :key="i" class="skeleton-row"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else>
|
||||||
|
<!-- 无计划状态 -->
|
||||||
|
<view v-if="!planData" class="no-plan-card">
|
||||||
|
<view class="no-plan-icon">📋</view>
|
||||||
|
<text class="no-plan-title">暂无戒烟计划</text>
|
||||||
|
<text class="no-plan-desc">生成专属30天戒烟计划,按阶段轻松戒烟</text>
|
||||||
|
<view class="generate-btn" @tap="handleGenerate">
|
||||||
|
<text class="generate-btn-text">生成戒烟计划</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 有计划状态 -->
|
||||||
|
<view v-else>
|
||||||
|
<!-- 计划总览卡片 -->
|
||||||
|
<view class="stage-card">
|
||||||
|
<view class="stage-badge">第 {{ currentDay }}/30 天</view>
|
||||||
|
<text class="stage-label">戒烟计划进度</text>
|
||||||
|
<text class="stage-name">{{ stageName }}</text>
|
||||||
|
<text class="stage-days">{{ stageDesc }}</text>
|
||||||
|
<view class="stage-progress-row">
|
||||||
|
<text class="stage-progress-label">计划进度</text>
|
||||||
|
<text class="stage-progress-value">{{ Math.round(planProgress * 100) }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="stage-progress-bar">
|
||||||
|
<view class="stage-progress-fill" :style="{ width: planProgress * 100 + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 阶段说明卡片 -->
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">当前阶段</text>
|
||||||
|
</view>
|
||||||
|
<view class="stage-info-card">
|
||||||
|
<view class="stage-item" :class="{ 'stage-item-active': planData.current_stage === 'recording' }">
|
||||||
|
<view class="stage-number">1</view>
|
||||||
|
<view class="stage-content">
|
||||||
|
<text class="stage-item-title">记录期</text>
|
||||||
|
<text class="stage-item-desc">记录每日吸烟情况,了解习惯</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="stage-line"></view>
|
||||||
|
<view class="stage-item" :class="{ 'stage-item-active': planData.current_stage === 'reducing' }">
|
||||||
|
<view class="stage-number">2</view>
|
||||||
|
<view class="stage-content">
|
||||||
|
<text class="stage-item-title">减量期</text>
|
||||||
|
<text class="stage-item-desc">逐步减少吸烟数量</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="stage-line"></view>
|
||||||
|
<view class="stage-item" :class="{ 'stage-item-active': planData.current_stage === 'consolidating' }">
|
||||||
|
<view class="stage-number">3</view>
|
||||||
|
<view class="stage-content">
|
||||||
|
<text class="stage-item-title">巩固期</text>
|
||||||
|
<text class="stage-item-desc">保持成果,彻底戒烟</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 每日目标和建议 -->
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">每日目标</text>
|
||||||
|
<text class="section-badge">{{ todayTarget }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="daily-tips-card">
|
||||||
|
<text class="daily-tips-title">今日建议</text>
|
||||||
|
<text class="daily-tips-text">{{ dailyTip }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 每日计划列表 -->
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">每日计划详情</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="daysLoading" class="days-loading">
|
||||||
|
<text class="days-loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="daysList.length > 0" class="days-list">
|
||||||
|
<view
|
||||||
|
v-for="day in daysList"
|
||||||
|
:key="day.day"
|
||||||
|
class="day-item"
|
||||||
|
:class="{ 'day-item-today': day.isToday, 'day-item-past': day.isPast }"
|
||||||
|
@tap="showDayDetail(day)"
|
||||||
|
>
|
||||||
|
<view class="day-header">
|
||||||
|
<text class="day-number">第 {{ day.day }} 天</text>
|
||||||
|
<text v-if="day.isToday" class="day-today-badge">今天</text>
|
||||||
|
<text v-else-if="day.isPast" class="day-past-badge">已完成</text>
|
||||||
|
</view>
|
||||||
|
<view class="day-target">
|
||||||
|
<text class="day-target-label">目标:</text>
|
||||||
|
<text class="day-target-value">{{ day.target_cigs }} 支</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="day.tip" class="day-tip">
|
||||||
|
<text class="day-tip-text">{{ day.tip }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="days-empty">
|
||||||
|
<text class="days-empty-text">暂无计划详情</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<view class="actions">
|
||||||
|
<view class="reset-btn" @tap="handleReset">
|
||||||
|
<text class="reset-btn-text">重置计划</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 每日详情弹窗 -->
|
||||||
|
<view v-if="showDayModal" class="modal-mask" @tap="closeDayModal">
|
||||||
|
<view class="modal-content" @tap.stop>
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">第 {{ selectedDay.day }} 天计划</text>
|
||||||
|
<text class="modal-close" @tap="closeDayModal">×</text>
|
||||||
|
</view>
|
||||||
|
<view class="modal-body">
|
||||||
|
<view class="modal-item">
|
||||||
|
<text class="modal-label">目标吸烟量</text>
|
||||||
|
<text class="modal-value">{{ selectedDay.target_cigs }} 支</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="selectedDay.tip" class="modal-item">
|
||||||
|
<text class="modal-label">建议</text>
|
||||||
|
<text class="modal-value">{{ selectedDay.tip }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { onShareAppMessage } from '@dcloudio/uni-app'
|
||||||
|
import { useLogin } from '@/hooks/useLogin'
|
||||||
|
import * as api from '@/api'
|
||||||
|
|
||||||
|
const { waitForLogin } = useLogin()
|
||||||
|
|
||||||
|
const statusBarHeight = ref(0)
|
||||||
|
const pageLoading = ref(true)
|
||||||
|
const daysLoading = ref(false)
|
||||||
|
const generating = ref(false)
|
||||||
|
|
||||||
|
const planData = ref(null)
|
||||||
|
const daysList = ref([])
|
||||||
|
|
||||||
|
const showDayModal = ref(false)
|
||||||
|
const selectedDay = ref({})
|
||||||
|
|
||||||
|
// 阶段名称映射
|
||||||
|
const stageNames = {
|
||||||
|
recording: '记录期',
|
||||||
|
reducing: '减量期',
|
||||||
|
consolidating: '巩固期'
|
||||||
|
}
|
||||||
|
|
||||||
|
const stageDescs = {
|
||||||
|
recording: '记录每日吸烟情况,了解您的吸烟习惯',
|
||||||
|
reducing: '按计划逐步减少吸烟数量',
|
||||||
|
consolidating: '保持戒烟成果,彻底摆脱烟瘾'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前是第几天
|
||||||
|
const currentDay = computed(() => {
|
||||||
|
if (!planData.value?.plan_start_date) return 1
|
||||||
|
const start = new Date(planData.value.plan_start_date)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = Math.floor((now - start) / (24 * 60 * 60 * 1000))
|
||||||
|
return Math.min(Math.max(diff + 1, 1), 30)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计划进度
|
||||||
|
const planProgress = computed(() => {
|
||||||
|
return currentDay.value / 30
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前阶段名称
|
||||||
|
const stageName = computed(() => {
|
||||||
|
if (!planData.value) return ''
|
||||||
|
return stageNames[planData.value.current_stage] || '记录期'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 阶段描述
|
||||||
|
const stageDesc = computed(() => {
|
||||||
|
if (!planData.value) return ''
|
||||||
|
return stageDescs[planData.value.current_stage] || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 今日目标
|
||||||
|
const todayTarget = computed(() => {
|
||||||
|
const today = daysList.value.find(d => d.isToday)
|
||||||
|
return today ? `${today.target_cigs} 支` : '--'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 每日建议
|
||||||
|
const dailyTip = computed(() => {
|
||||||
|
const today = daysList.value.find(d => d.isToday)
|
||||||
|
return today?.tip || '按计划执行,保持决心!'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取戒烟计划
|
||||||
|
async function fetchQuitPlan() {
|
||||||
|
try {
|
||||||
|
const res = await api.getQuitPlan()
|
||||||
|
planData.value = res?.data || null
|
||||||
|
if (planData.value?.id) {
|
||||||
|
await fetchDays()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('fetchQuitPlan error:', e)
|
||||||
|
planData.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每日计划
|
||||||
|
async function fetchDays() {
|
||||||
|
if (!planData.value?.id) return
|
||||||
|
|
||||||
|
daysLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.getQuitPlanDays(planData.value.id)
|
||||||
|
const days = res?.data || []
|
||||||
|
|
||||||
|
// 计算今天的日期
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
daysList.value = days.map(day => {
|
||||||
|
const dayDate = new Date(planData.value.plan_start_date)
|
||||||
|
dayDate.setDate(dayDate.getDate() + day.day - 1)
|
||||||
|
dayDate.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...day,
|
||||||
|
isToday: dayDate.getTime() === today.getTime(),
|
||||||
|
isPast: dayDate.getTime() < today.getTime()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('fetchDays error:', e)
|
||||||
|
daysList.value = []
|
||||||
|
} finally {
|
||||||
|
daysLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成计划
|
||||||
|
async function handleGenerate() {
|
||||||
|
if (generating.value) return
|
||||||
|
|
||||||
|
generating.value = true
|
||||||
|
try {
|
||||||
|
await api.generateQuitPlan()
|
||||||
|
uni.showToast({
|
||||||
|
title: '计划生成成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
await fetchQuitPlan()
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({
|
||||||
|
title: e?.message || '生成失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
generating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置计划
|
||||||
|
function handleReset() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认重置',
|
||||||
|
content: '重置后将清除当前计划,重新开始,确定要重置吗?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
try {
|
||||||
|
await api.resetQuitPlan()
|
||||||
|
uni.showToast({
|
||||||
|
title: '计划已重置',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
planData.value = null
|
||||||
|
daysList.value = []
|
||||||
|
} catch (e) {
|
||||||
|
uni.showToast({
|
||||||
|
title: e?.message || '重置失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示每日详情
|
||||||
|
function showDayDetail(day) {
|
||||||
|
selectedDay.value = day
|
||||||
|
showDayModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
function closeDayModal() {
|
||||||
|
showDayModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化页面
|
||||||
|
async function initPage() {
|
||||||
|
pageLoading.value = true
|
||||||
|
try {
|
||||||
|
const sys = uni.getSystemInfoSync()
|
||||||
|
statusBarHeight.value = sys.statusBarHeight || 0
|
||||||
|
|
||||||
|
await waitForLogin()
|
||||||
|
await fetchQuitPlan()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('initPage error:', e)
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initPage()
|
||||||
|
})
|
||||||
|
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: '戒烟助手 - 30天戒烟计划',
|
||||||
|
path: 'pages/index/index'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(to bottom, #D1FAE5 0%, #F0FDF4 45%, #FFFFFF 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
background: linear-gradient(to bottom, #D1FAE5, #E9FDF2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 24rpx 32rpx 180rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card,
|
||||||
|
.skeleton-row {
|
||||||
|
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 50%, #E5E7EB 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.6s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-card {
|
||||||
|
height: 260rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-list {
|
||||||
|
padding: 24rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-row {
|
||||||
|
height: 92rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无计划状态 */
|
||||||
|
.no-plan-card {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 60rpx 40rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(16, 185, 129, 0.12);
|
||||||
|
border: 2rpx solid #ECFDF3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-plan-icon {
|
||||||
|
font-size: 80rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-plan-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-plan-desc {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
background: linear-gradient(135deg, #10B981, #059669);
|
||||||
|
padding: 24rpx 60rpx;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阶段卡片 */
|
||||||
|
.stage-card {
|
||||||
|
background: #FFFFFF;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(16, 185, 129, 0.12);
|
||||||
|
border: 2rpx solid #ECFDF3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 24rpx;
|
||||||
|
right: 24rpx;
|
||||||
|
background-color: #10B981;
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #059669;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-name {
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-days {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-progress-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-progress-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-progress-value {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-progress-bar {
|
||||||
|
height: 12rpx;
|
||||||
|
background-color: #E5E7EB;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #10B981, #34D399);
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 阶段信息 */
|
||||||
|
.section {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-badge {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #059669;
|
||||||
|
background-color: #ECFDF3;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-info-card {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
border: 2rpx solid #ECFDF3;
|
||||||
|
box-shadow: 0 8rpx 22rpx rgba(16, 185, 129, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item-active .stage-number {
|
||||||
|
background-color: #10B981;
|
||||||
|
color: #FFFFFF;
|
||||||
|
border-color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item-active .stage-item-title {
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-number {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3rpx solid #D1D5DB;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6B7280;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-item-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-line {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
background-color: #E5E7EB;
|
||||||
|
margin: 16rpx 0 16rpx 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每日目标 */
|
||||||
|
.daily-tips-card {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
border: 2rpx solid #ECFDF3;
|
||||||
|
box-shadow: 0 8rpx 22rpx rgba(16, 185, 129, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-tips-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #059669;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-tips-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每日计划列表 */
|
||||||
|
.days-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-item {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
border: 2rpx solid #ECFDF3;
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba(16, 185, 129, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-item-today {
|
||||||
|
border-color: #10B981;
|
||||||
|
background: linear-gradient(135deg, #ECFDF5, #F0FDF4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-item-past {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-number {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-today-badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #10B981;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-past-badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-target {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-target-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-target-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-tip {
|
||||||
|
padding-top: 12rpx;
|
||||||
|
border-top: 1rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-tip-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-loading,
|
||||||
|
.days-empty {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-loading-text,
|
||||||
|
.days-empty-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮 */
|
||||||
|
.actions {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #EF4444;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #EF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗 */
|
||||||
|
.modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
width: 600rpx;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 32rpx 28rpx 24rpx;
|
||||||
|
border-bottom: 1rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-item {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user