feat: 添加模式选择功能与页面更新
- 在 onboarding 页面中新增使用模式选择功能,用户可选择“戒烟打卡”或“记录抽烟”模式 - 更新个人资料页面以显示当前模式并允许用户切换模式 - 在 pages.json 中注册新的模式选择页面 - 优化首页和其他相关页面以适应新模式功能
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
# smt 双模式改造方案
|
||||
|
||||
> 创建时间:2026-03-17
|
||||
> 状态:待开发
|
||||
> 相关文档:[[戒烟产品分析-smt-vs-quit-checkin]]
|
||||
|
||||
---
|
||||
|
||||
## 📁 现有项目结构
|
||||
|
||||
```
|
||||
smt/
|
||||
├── api/ # 接口封装
|
||||
│ ├── auth.js # 登录认证
|
||||
│ ├── smoke.js # 抽烟记录 API
|
||||
│ ├── profile.js # 用户资料 API
|
||||
│ ├── request.js # 请求封装
|
||||
│ └── index.js # 统一导出
|
||||
├── components/ # 业务组件
|
||||
│ └── smoke-record-dialog/ # 记录弹框组件
|
||||
├── config/ # 环境配置
|
||||
│ └── index.js # BASE_URL 配置
|
||||
├── hooks/ # 组合式逻辑
|
||||
│ └── useLogin.js # 登录相关 hook
|
||||
├── pages/ # 页面
|
||||
│ ├── index/ # 首页 ⭐ 需要改造
|
||||
│ ├── stats/ # 统计页
|
||||
│ ├── logs/ # 历史记录页
|
||||
│ ├── ai/ # AI 建议
|
||||
│ ├── ai_summary/ # AI 总结
|
||||
│ ├── share/ # 分享页
|
||||
│ ├── profile/ # 个人中心
|
||||
│ ├── quit-plan/ # 戒烟计划
|
||||
│ └── onboarding/ # 新用户引导 ⭐ 需要改造
|
||||
├── stores/ # Pinia 状态管理
|
||||
│ ├── user.js # 用户状态 ⭐ 需要扩展
|
||||
│ ├── profile.js # 用户资料 ⭐ 需要扩展
|
||||
│ ├── logs.js # 记录状态
|
||||
│ ├── dashboard.js # 首页数据
|
||||
│ └── index.js # 统一导出
|
||||
├── utils/ # 工具函数
|
||||
│ ├── storage.js # 本地存储 ⭐ 需要扩展
|
||||
│ ├── time.js # 时间处理
|
||||
│ └── format.js # 格式化
|
||||
├── static/ # 静态资源
|
||||
├── App.vue # 应用入口
|
||||
├── main.js # 主入口
|
||||
├── pages.json # 页面配置 ⭐ 需要修改
|
||||
└── manifest.json # 小程序配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 需要修改/新增的文件
|
||||
|
||||
### 新增文件
|
||||
|
||||
| 文件路径 | 说明 |
|
||||
|----------|------|
|
||||
| `pages/mode-select/index.vue` | **新增** - 模式选择引导页 |
|
||||
| `pages/quit-home/index.vue` | **新增** - 戒烟打卡模式首页 |
|
||||
| `pages/record-home/index.vue` | **新增** - 记录抽烟模式首页 |
|
||||
| `components/quit-checkin-btn/index.vue` | **新增** - 打卡按钮组件 |
|
||||
| `components/simple-counter/index.vue` | **新增** - 简易计数器组件 |
|
||||
|
||||
### 需要修改的文件
|
||||
|
||||
| 文件路径 | 修改内容 |
|
||||
|----------|----------|
|
||||
| `stores/user.js` | 添加 `mode` 字段存储用户模式 |
|
||||
| `stores/profile.js` | 添加 `quitDays` 戒烟天数计算 |
|
||||
| `utils/storage.js` | 添加 `USER_MODE_KEY` 常量 |
|
||||
| `pages.json` | 添加新页面路由,修改首页逻辑 |
|
||||
| `pages/onboarding/index.vue` | 完成引导后跳转到模式选择页 |
|
||||
| `pages/profile/index.vue` | 添加模式切换入口 |
|
||||
| `pages/index/index.vue` | 根据模式渲染不同首页(可选方案) |
|
||||
|
||||
---
|
||||
|
||||
## 📝 详细代码修改
|
||||
|
||||
### 1. `utils/storage.js` - 新增常量
|
||||
|
||||
```javascript
|
||||
export const USER_MODE_KEY = 'user_mode' // 新增
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `stores/user.js` - 扩展用户模式
|
||||
|
||||
```javascript
|
||||
import { defineStore } from 'pinia'
|
||||
import { storage, USER_KEY, SESSION_KEY, USER_MODE_KEY } from '@/utils/storage'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
user: storage.get(USER_KEY),
|
||||
sessionKey: storage.get(SESSION_KEY),
|
||||
isLoggedIn: !!storage.get(SESSION_KEY),
|
||||
mode: storage.get(USER_MODE_KEY) || null // 'quit' | 'record' | null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setUser(user, sessionKey) {
|
||||
this.user = user
|
||||
this.sessionKey = sessionKey
|
||||
this.isLoggedIn = true
|
||||
storage.set(USER_KEY, user)
|
||||
storage.set(SESSION_KEY, sessionKey)
|
||||
},
|
||||
|
||||
setMode(mode) {
|
||||
this.mode = mode
|
||||
storage.set(USER_MODE_KEY, mode)
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.user = null
|
||||
this.sessionKey = null
|
||||
this.isLoggedIn = false
|
||||
storage.remove(USER_KEY)
|
||||
storage.remove(SESSION_KEY)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `pages.json` - 添加新页面
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/mode-select/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/quit-home/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/record-home/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "记录抽烟"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. `pages/mode-select/index.vue` - 模式选择页
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="content">
|
||||
<text class="title">你想怎么戒烟?</text>
|
||||
<text class="subtitle">选择适合你的方式</text>
|
||||
|
||||
<view class="options">
|
||||
<view class="option option-quit" @tap="selectMode('quit')">
|
||||
<text class="option-icon">🌟</text>
|
||||
<view class="option-text">
|
||||
<text class="option-title">我要戒烟打卡</text>
|
||||
<text class="option-desc">记录坚持的天数,获得成就感</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="option option-record" @tap="selectMode('record')">
|
||||
<text class="option-icon">📊</text>
|
||||
<view class="option-text">
|
||||
<text class="option-title">我要记录抽烟</text>
|
||||
<text class="option-desc">跟踪抽烟频率,分析戒烟进度</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
function selectMode(mode) {
|
||||
userStore.setMode(mode)
|
||||
|
||||
if (mode === 'quit') {
|
||||
uni.redirectTo({ url: '/pages/quit-home/index' })
|
||||
} else {
|
||||
uni.redirectTo({ url: '/pages/record-home/index' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. `pages/quit-home/index.vue` - 戒烟打卡首页
|
||||
|
||||
**界面设计:**
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ 🔥 已坚持 23 天 │
|
||||
│ │
|
||||
│ ┌─────────┐ │
|
||||
│ │ 打卡 │ │
|
||||
│ │ 今天没抽│ │
|
||||
│ └─────────┘ │
|
||||
│ │
|
||||
│ 💰 已省下 184 元 │
|
||||
│ 🫁 肺部正在恢复中... │
|
||||
│ │
|
||||
│ ───────────────────── │
|
||||
│ 今日打卡 ✓ 08:30 │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**核心代码:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="header">
|
||||
<text class="days-label">已坚持</text>
|
||||
<text class="days-value">{{ quitDays }}</text>
|
||||
<text class="days-unit">天</text>
|
||||
</view>
|
||||
|
||||
<view class="checkin-section">
|
||||
<view class="checkin-btn" :class="{ 'checked': todayChecked }" @tap="checkin">
|
||||
<text class="checkin-icon">{{ todayChecked ? '✓' : '🔥' }}</text>
|
||||
<text class="checkin-text">{{ todayChecked ? '今日已打卡' : '打卡' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">¥{{ savedMoney }}</text>
|
||||
<text class="stat-label">已省下</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ healthProgress }}%</text>
|
||||
<text class="stat-label">健康恢复</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="health-tips">
|
||||
<text class="tip-icon">🫁</text>
|
||||
<text class="tip-text">{{ healthTip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useProfileStore } from '@/stores/profile'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const profileStore = useProfileStore()
|
||||
|
||||
const todayChecked = ref(false)
|
||||
const quitStartDate = ref(null)
|
||||
|
||||
const quitDays = computed(() => {
|
||||
if (!quitStartDate.value) return 0
|
||||
const diff = Date.now() - new Date(quitStartDate.value).getTime()
|
||||
return Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
})
|
||||
|
||||
const savedMoney = computed(() => {
|
||||
const pricePerPack = profileStore.profile?.pack_price_cent || 2500
|
||||
const cigsPerDay = profileStore.profile?.baseline_cigs_per_day || 10
|
||||
const savedCigs = quitDays.value * cigsPerDay
|
||||
return ((savedCigs / 20) * (pricePerPack / 100)).toFixed(0)
|
||||
})
|
||||
|
||||
const healthProgress = computed(() => {
|
||||
if (quitDays.value >= 365) return 100
|
||||
if (quitDays.value >= 180) return 85
|
||||
if (quitDays.value >= 90) return 70
|
||||
if (quitDays.value >= 30) return 50
|
||||
if (quitDays.value >= 14) return 30
|
||||
if (quitDays.value >= 7) return 15
|
||||
return 5
|
||||
})
|
||||
|
||||
const healthTip = computed(() => {
|
||||
if (quitDays.value >= 365) return '恭喜!你的肺部功能已基本恢复'
|
||||
if (quitDays.value >= 180) return '你的血液循环已显著改善'
|
||||
if (quitDays.value >= 30) return '你的味觉和嗅觉正在恢复'
|
||||
if (quitDays.value >= 7) return '你的肺活量开始增加'
|
||||
return '坚持下去,身体正在恢复中'
|
||||
})
|
||||
|
||||
function checkin() {
|
||||
if (todayChecked.value) return
|
||||
todayChecked.value = true
|
||||
// TODO: 调用 API 保存打卡记录
|
||||
uni.showToast({ title: '打卡成功!', icon: 'success' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 获取戒烟开始日期
|
||||
// TODO: 从 API 或本地存储获取
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. `pages/record-home/index.vue` - 记录抽烟首页
|
||||
|
||||
**界面设计:**
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ 今天抽了 3 根 │
|
||||
│ │
|
||||
│ ┌─────────┐ │
|
||||
│ │ +1 根 │ │
|
||||
│ │ 点击记录│ │
|
||||
│ └─────────┘ │
|
||||
│ │
|
||||
│ 昨日:4 根 │
|
||||
│ 本周:18 根 │
|
||||
│ │
|
||||
│ ───────────────────── │
|
||||
│ 历史记录 > │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**核心代码:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="counter-section">
|
||||
<text class="counter-label">今天抽了</text>
|
||||
<text class="counter-value">{{ todayCount }}</text>
|
||||
<text class="counter-unit">根</text>
|
||||
</view>
|
||||
|
||||
<view class="add-btn" @tap="addOne">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">记录一根</text>
|
||||
</view>
|
||||
|
||||
<view class="summary">
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">昨日</text>
|
||||
<text class="summary-value">{{ yesterdayCount }} 根</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-label">本周</text>
|
||||
<text class="summary-value">{{ weekCount }} 根</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import * as api from '@/api'
|
||||
|
||||
const todayCount = ref(0)
|
||||
const yesterdayCount = ref(0)
|
||||
const weekCount = ref(0)
|
||||
|
||||
async function addOne() {
|
||||
todayCount.value++
|
||||
// TODO: 调用 API 记录
|
||||
try {
|
||||
await api.createLog({
|
||||
num: 1,
|
||||
smoke_time: new Date().toISOString(),
|
||||
smoke_at: new Date().toISOString()
|
||||
})
|
||||
uni.showToast({ title: '已记录', icon: 'success', duration: 1000 })
|
||||
} catch (e) {
|
||||
console.error('记录失败:', e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add-btn {
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #EF4444, #F87171);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 60rpx auto;
|
||||
box-shadow: 0 20rpx 40rpx rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 80rpx;
|
||||
color: #FFF;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. `pages/profile/index.vue` - 添加模式切换
|
||||
|
||||
在个人中心添加:
|
||||
|
||||
```vue
|
||||
<view class="menu-item" @tap="showModeSwitch">
|
||||
<text class="menu-icon">⚙️</text>
|
||||
<text class="menu-text">模式切换</text>
|
||||
<text class="menu-value">{{ modeText }}</text>
|
||||
</view>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const modeText = computed(() => {
|
||||
return userStore.mode === 'quit' ? '戒烟打卡' : '记录抽烟'
|
||||
})
|
||||
|
||||
function showModeSwitch() {
|
||||
uni.showActionSheet({
|
||||
itemList: ['戒烟打卡模式', '记录抽烟模式'],
|
||||
success: (res) => {
|
||||
const modes = ['quit', 'record']
|
||||
userStore.setMode(modes[res.tapIndex])
|
||||
uni.showToast({ title: '已切换', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 开发顺序
|
||||
|
||||
| 阶段 | 任务 | 预估时间 | 状态 |
|
||||
|------|------|----------|------|
|
||||
| **第一阶段** | 1. 修改 `utils/storage.js`<br>2. 修改 `stores/user.js`<br>3. 创建 `pages/mode-select/index.vue`<br>4. 修改 `pages.json` | 1-2h | ⬜ 待开始 |
|
||||
| **第二阶段** | 1. 创建 `pages/quit-home/index.vue`<br>2. 打卡 API 对接<br>3. 省钱金额计算 | 2-3h | ⬜ 待开始 |
|
||||
| **第三阶段** | 1. 创建 `pages/record-home/index.vue`<br>2. 一键记录 API 对接 | 1-2h | ⬜ 待开始 |
|
||||
| **第四阶段** | 1. 修改 `pages/profile/index.vue` 模式切换<br>2. 修改引导流程跳转逻辑 | 1h | ⬜ 待开始 |
|
||||
|
||||
**总预估:5-8 小时**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
### 1. TabBar 问题
|
||||
|
||||
如果要让两种模式共用 TabBar,首页需要根据 mode 动态渲染:
|
||||
|
||||
```vue
|
||||
<!-- pages/index/index.vue -->
|
||||
<template>
|
||||
<QuitHome v-if="userStore.mode === 'quit'" />
|
||||
<RecordHome v-else />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. 数据兼容
|
||||
|
||||
老用户没有 mode 字段,需要引导选择模式:
|
||||
|
||||
```javascript
|
||||
// App.vue 或首页
|
||||
if (!userStore.mode) {
|
||||
uni.redirectTo({ url: '/pages/mode-select/index' })
|
||||
}
|
||||
```
|
||||
|
||||
### 3. API 对接
|
||||
|
||||
打卡功能需要后端新增接口:
|
||||
|
||||
```
|
||||
POST /api/checkin
|
||||
{
|
||||
"date": "2026-03-17",
|
||||
"quit_days": 23
|
||||
}
|
||||
```
|
||||
|
||||
或复用现有 resisted 接口。
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关文档
|
||||
|
||||
- [[戒烟产品分析-smt-vs-quit-checkin]]
|
||||
- [[smt - 技术文档]]
|
||||
- [[quit-checkin - 技术文档]]
|
||||
|
||||
---
|
||||
|
||||
#技术开发 #smt #戒烟 #小程序 #双模式
|
||||
Reference in New Issue
Block a user