refactor: trim smoke home API usage
This commit is contained in:
+2
-2
@@ -254,9 +254,9 @@ function calculateMoneySaved(packPriceCent, cigsPerPack, baselineCigsPerDay, act
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 激励语生成(后端统一)
|
## 7. 激励语生成(首页内联返回)
|
||||||
|
|
||||||
接口:`GET /api/v1/smoke/motivation`(详见 `docs/smoke/API.md`)
|
接口:`GET /api/v1/smoke/home` 的 `motivation` 字段。
|
||||||
|
|
||||||
根据用户状态生成不同的激励语:
|
根据用户状态生成不同的激励语:
|
||||||
|
|
||||||
|
|||||||
+15
-42
@@ -60,18 +60,14 @@
|
|||||||
├─────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────┤
|
||||||
│ 0ms ─ 页面骨架屏渲染 │
|
│ 0ms ─ 页面骨架屏渲染 │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ├──── 并行请求 ──────────────────────────────────── │
|
│ ├──── 串行守卫 + 单接口数据 ──────────────────────── │
|
||||||
│ │ ├── /profile (检查用户状态) │
|
│ │ ├── /profile (检查用户状态) │
|
||||||
│ │ ├── /dashboard (核心数据) │
|
│ │ └── /home (首页核心数据) │
|
||||||
│ │ └── /next_smoke_time (建议时间) │
|
|
||||||
│ │ │
|
│ │ │
|
||||||
│ 200ms ─ 核心数据返回,渲染计时器+统计卡片 │
|
│ 200ms ─ 核心数据返回,渲染计时器+统计卡片 │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ 300ms ─ 首屏渲染完成 │
|
│ 300ms ─ 首屏渲染完成 │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ │ ┌── 延迟加载 ────────────────────────────── │
|
|
||||||
│ │ └── /ai/advice (AI提示卡片) │
|
|
||||||
│ │ │
|
|
||||||
│ 500ms ─ 完整页面渲染 │
|
│ 500ms ─ 完整页面渲染 │
|
||||||
└─────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
@@ -79,29 +75,14 @@
|
|||||||
### 3.2 缓存策略
|
### 3.2 缓存策略
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// stores/dashboard.js
|
// 首页使用 /smoke/home 单接口返回当前屏所需字段。
|
||||||
import { defineStore } from 'pinia'
|
// 页面级刷新由 onShow 触发,避免维护额外 dashboard store 和重复请求。
|
||||||
|
const homeData = ref(null)
|
||||||
|
|
||||||
export const useDashboardStore = defineStore('dashboard', {
|
async function fetchRecordHomeData() {
|
||||||
state: () => ({
|
const res = await api.getHome()
|
||||||
todayCount: 0,
|
homeData.value = res.data || {}
|
||||||
minutesSinceLast: 0,
|
}
|
||||||
weekly: [],
|
|
||||||
nextSmokeTime: null,
|
|
||||||
lastFetchTime: 0,
|
|
||||||
cacheExpiry: 30 * 1000
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async fetchDashboard(forceRefresh = false) {
|
|
||||||
const now = Date.now()
|
|
||||||
if (!forceRefresh && now - this.lastFetchTime < this.cacheExpiry) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 发起请求...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3 计时器优化
|
### 3.3 计时器优化
|
||||||
@@ -240,35 +221,27 @@ export const request = {
|
|||||||
```javascript
|
```javascript
|
||||||
// pages/index/index.vue
|
// pages/index/index.vue
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useDashboardStore } from '@/stores/dashboard'
|
import * as api from '@/api'
|
||||||
import * as api from '@/api/smoke'
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const dashboardStore = useDashboardStore()
|
const homeData = ref(null)
|
||||||
|
|
||||||
async function initPage() {
|
async function initPage() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [profileRes, dashboardRes, nextTimeRes] = await Promise.all([
|
const profileRes = await api.getSmokeProfile()
|
||||||
api.getProfile(),
|
if (!profileRes.exists || !profileRes.is_completed) {
|
||||||
api.getDashboard(),
|
|
||||||
api.getNextSmokeTime()
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!profileRes.data.exists || !profileRes.data.is_completed) {
|
|
||||||
uni.redirectTo({ url: '/pages/onboarding/index' })
|
uni.redirectTo({ url: '/pages/onboarding/index' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboardStore.setDashboard(dashboardRes.data)
|
const homeRes = await api.getHome()
|
||||||
dashboardStore.setNextSmokeTime(nextTimeRes.data)
|
homeData.value = homeRes.data || {}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(loadAiAdvice, 300)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(initPage)
|
onMounted(initPage)
|
||||||
|
|||||||
+17
-288
@@ -54,18 +54,7 @@ curl -X POST 'http://127.0.0.1:8080/api/v1/smoke/logs' \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2) 获取单条记录
|
## 2) 列表查询(分页)
|
||||||
|
|
||||||
`GET /api/v1/smoke/logs/:id`
|
|
||||||
|
|
||||||
curl 示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
|
||||||
-H 'Authorization: Bearer wx-session-key'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3) 列表查询(分页)
|
|
||||||
|
|
||||||
`GET /api/v1/smoke/logs?page=1&page_size=20&start=2025-12-01&end=2025-12-31&type=all`
|
`GET /api/v1/smoke/logs?page=1&page_size=20&start=2025-12-01&end=2025-12-31&type=all`
|
||||||
|
|
||||||
@@ -93,71 +82,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4) 获取看板概览
|
## 3) 更新记录
|
||||||
|
|
||||||
`GET /api/v1/smoke/dashboard?start=2026-01-01&end=2026-01-07`
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- `start`:起始日期(含,格式 `YYYY-MM-DD`),默认“本周一”
|
|
||||||
- `end`:截止日期(含,格式 `YYYY-MM-DD`),默认“本周日”。若只传 `start`,`end` 默认为 `start + 6 天`。
|
|
||||||
|
|
||||||
成功响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"today_count": 6,
|
|
||||||
"minutes_since_last": 42,
|
|
||||||
"weekly": [
|
|
||||||
{ "date": "2026-01-01", "count": 2, "is_today": false },
|
|
||||||
{ "date": "2026-01-02", "count": 1, "is_today": false },
|
|
||||||
{ "date": "2026-01-03", "count": 0, "is_today": false },
|
|
||||||
{ "date": "2026-01-04", "count": 0, "is_today": false },
|
|
||||||
{ "date": "2026-01-05", "count": 3, "is_today": true },
|
|
||||||
{ "date": "2026-01-06", "count": 0, "is_today": false },
|
|
||||||
{ "date": "2026-01-07", "count": 0, "is_today": false }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
字段说明:
|
|
||||||
- `today_count`:当天吸烟总支数(累加 `num`)
|
|
||||||
- `minutes_since_last`:距最后一次“实际抽烟”(忽略 `level=0 && num=0` 的忍住记录)的分钟数,通过最近一条 `smoke_at/smoke_time/createtime` 计算;若历史为空则字段不存在
|
|
||||||
- `weekly`:起止日期内每日汇总,`count` 为当日总支数,`is_today` 标记当前日期(即便不在 `start/end` 范围内也会标记为 `false`)
|
|
||||||
|
|
||||||
## 5) 最近记录列表(轻量版)
|
|
||||||
|
|
||||||
`GET /api/v1/smoke/logs/latest?limit=20`
|
|
||||||
|
|
||||||
参数:
|
|
||||||
- `limit`:返回条数,默认 `20`,最大 `100`
|
|
||||||
|
|
||||||
成功响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"smoke_time": "2026-01-05T00:00:00+08:00",
|
|
||||||
"smoke_at": "2026-01-05T09:12:00+08:00",
|
|
||||||
"remark": "压力大",
|
|
||||||
"level": 3,
|
|
||||||
"num": 2,
|
|
||||||
"createtime": 1736049120
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6) 更新记录
|
|
||||||
|
|
||||||
`POST /api/v1/smoke/logs/:id`
|
`POST /api/v1/smoke/logs/:id`
|
||||||
|
|
||||||
@@ -178,7 +103,7 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
|||||||
- 如果你想“清空 smoke_at”,请传空字符串:`{"smoke_at":""}`。
|
- 如果你想“清空 smoke_at”,请传空字符串:`{"smoke_at":""}`。
|
||||||
- 如果传 `null` 或者不传 `smoke_time`,后端会认为你没有修改该字段。
|
- 如果传 `null` 或者不传 `smoke_time`,后端会认为你没有修改该字段。
|
||||||
|
|
||||||
## 7) 删除记录(软删除)
|
## 4) 删除记录(软删除)
|
||||||
|
|
||||||
`DELETE /api/v1/smoke/logs/:id`
|
`DELETE /api/v1/smoke/logs/:id`
|
||||||
|
|
||||||
@@ -194,157 +119,53 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8) 获取 AI 戒烟建议(会员 + 广告解锁并行)
|
## 5) 首页整合接口(Home)
|
||||||
|
|
||||||
`GET /api/v1/smoke/ai/advice?date=2026-01-02`
|
|
||||||
|
|
||||||
说明:
|
|
||||||
- `date` 可选,默认“昨天”(建议针对哪一天的数据)。
|
|
||||||
- 权限:会员用户直接可用;非会员需要先对该 `date` 完成“看广告解锁”(见下一个接口)。
|
|
||||||
- 建议结果会按 `uid + date + prompt_version` 缓存(表:`fa_smoke_ai_advice`)。
|
|
||||||
|
|
||||||
## 9) 首页整合接口(Home Dashboard)
|
|
||||||
|
|
||||||
`GET /api/v1/smoke/home`
|
`GET /api/v1/smoke/home`
|
||||||
|
|
||||||
此接口把首页 UI 所需核心模块一次返回,避免前端串行请求多个接口。返回示例:
|
此接口只返回首页、AI 时间页和 AI 日总结页正在消费的核心字段,避免生成或传递无用模块。返回示例:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"message": "success",
|
"message": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"greeting": {
|
|
||||||
"title": "早安,Alex",
|
|
||||||
"subtitle": "今天也是清爽的一天",
|
|
||||||
"nickname": "Alex",
|
|
||||||
"time_of_day": "morning",
|
|
||||||
"avatar_url": "https://example.com/avatar.jpg"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"exists": true,
|
|
||||||
"is_completed": true,
|
|
||||||
"awake_minutes": 900,
|
|
||||||
"baseline_interval_minutes": 60,
|
|
||||||
"profile": {
|
|
||||||
"baseline_cigs_per_day": 10,
|
|
||||||
"pack_price_cent": 3200,
|
|
||||||
"wake_up_time": "07:20",
|
|
||||||
"sleep_time": "23:30"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"advice_card": {
|
|
||||||
"title": "智能控烟建议",
|
|
||||||
"date": "2026-01-04",
|
|
||||||
"message": "根据你的习惯,下午2点是烟瘾高峰,可以试试短暂散步。",
|
|
||||||
"status": "available"
|
|
||||||
},
|
|
||||||
"campaign_card": {
|
|
||||||
"title": "绿色生活,从戒烟开始",
|
|
||||||
"subtitle": "BRAND CAMPAIGN",
|
|
||||||
"badge": "广告"
|
|
||||||
},
|
|
||||||
"timer": {
|
"timer": {
|
||||||
"label": "距上次抽烟",
|
|
||||||
"last_smoke_at": "2026-01-05T07:42:00+08:00",
|
|
||||||
"seconds_since_last": 9900,
|
"seconds_since_last": 9900,
|
||||||
"next_suggested_at": "2026-01-05T10:30:00+08:00",
|
"next_suggested_at": "2026-01-05T10:30:00+08:00",
|
||||||
"next_suggested_clock": "10:30",
|
"next_suggested_clock": "10:30",
|
||||||
"not_before_at": "2026-01-05T10:30:00+08:00",
|
"suggestion_source": "default"
|
||||||
"suggestion_source": "default",
|
|
||||||
"suggestion_algorithm": "staircase_delay_v1"
|
|
||||||
},
|
},
|
||||||
"summary": {
|
"summary": {
|
||||||
"today_count": 3,
|
"today_count": 3,
|
||||||
"daily_target": 10,
|
"daily_target": 10,
|
||||||
"resisted_count": 1,
|
"resisted_count": 1,
|
||||||
"reduced_from_yesterday": 2,
|
"reduced_from_yesterday": 2,
|
||||||
"exceeded_yesterday": false,
|
"exceeded_yesterday": false
|
||||||
"profile_completed": true
|
|
||||||
},
|
},
|
||||||
"motivation": {
|
"motivation": {
|
||||||
"message": "太棒了!你刚刚成功抵抗了一次烟瘾",
|
"message": "太棒了!你刚刚成功抵抗了一次烟瘾",
|
||||||
"type": "praise"
|
"type": "praise"
|
||||||
},
|
|
||||||
"quick_actions": [
|
|
||||||
{ "type": "log_smoke", "title": "记录抽烟", "primary": false },
|
|
||||||
{ "type": "resist", "title": "想抽忍住了", "primary": true }
|
|
||||||
],
|
|
||||||
"data_sources": {
|
|
||||||
"ai_advice_date": "2026-01-04",
|
|
||||||
"plan_date": "2026-01-05"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
字段说明:
|
字段说明:
|
||||||
- `greeting.title`:问候语 + 昵称(如“下午好,Alex”)。
|
|
||||||
- `greeting.subtitle`:副标题/心情提示文案。
|
|
||||||
- `greeting.nickname`:昵称(无昵称时使用“朋友”)。
|
|
||||||
- `greeting.time_of_day`:时间段标识(`morning`/`noon`/`afternoon`/`evening`)。
|
|
||||||
- `greeting.avatar_url`:头像 URL。
|
|
||||||
- `profile.exists`:是否存在用户档案。
|
|
||||||
- `profile.profile`:档案详情对象(可能为空)。
|
|
||||||
- `profile.is_completed`:是否已完成 onboarding。
|
|
||||||
- `profile.awake_minutes`:清醒时长(分钟)。
|
|
||||||
- `profile.baseline_interval_minutes`:基准间隔(分钟)。
|
|
||||||
- `advice_card.title`:AI 提示卡片标题。
|
|
||||||
- `advice_card.date`:建议对应日期。
|
|
||||||
- `advice_card.message`:AI 建议内容。
|
|
||||||
- `advice_card.status`:`available`、`locked`(需解锁)、`unavailable`(AI 服务未配置)、`no_data`(所需日期没有记录)、`empty`(初始化)。
|
|
||||||
- `advice_card.model`:模型名称(有则返回)。
|
|
||||||
- `campaign_card.title`:活动标题。
|
|
||||||
- `campaign_card.subtitle`:活动副标题。
|
|
||||||
- `campaign_card.badge`:活动角标(如“广告”)。
|
|
||||||
- `timer.label`:展示标题(如“距上次抽烟”)。
|
|
||||||
- `timer.last_smoke_at`:最近一次实际抽烟时间(RFC3339)。
|
|
||||||
- `timer.seconds_since_last`:距上次抽烟的秒数(无记录返回 `-1`)。
|
- `timer.seconds_since_last`:距上次抽烟的秒数(无记录返回 `-1`)。
|
||||||
- `timer.next_suggested_at`:建议下次抽烟时间(RFC3339)。
|
- `timer.next_suggested_at`:建议下次抽烟时间(RFC3339)。
|
||||||
- `timer.next_suggested_clock`:仅时分显示(如“16:30”)。
|
- `timer.next_suggested_clock`:仅时分显示(如“16:30”)。
|
||||||
- `timer.not_before_at`:不早于的时间点(当前与 `next_suggested_at` 一致)。
|
|
||||||
- `timer.suggestion_source`:建议来源(`default`/`ai`)。
|
- `timer.suggestion_source`:建议来源(`default`/`ai`)。
|
||||||
- `timer.suggestion_algorithm`:算法版本(`staircase_delay_v1`)。
|
|
||||||
- `timer` 说明:`seconds_since_last` 基于服务器当前时间计算,`last_smoke_at` 若补录未来时间会截断到 `as_of`;当 `plan_date=今天` 时会补齐过期间隔确保 `next_suggested_at` 在未来。
|
|
||||||
- `summary.today_count`:今日吸烟支数累加。
|
- `summary.today_count`:今日吸烟支数累加。
|
||||||
- `summary.daily_target`:每日目标(线性递减:以 `onboarding_completed_at` 为起点,按 `quit_date` 线性下降到 0)。
|
- `summary.daily_target`:每日目标。
|
||||||
- `summary.resisted_count`:今日忍住次数。
|
- `summary.resisted_count`:今日忍住次数。
|
||||||
- `summary.reduced_from_yesterday`:与昨日的绝对差值(非负)。
|
- `summary.reduced_from_yesterday`:与昨日的绝对差值(非负)。
|
||||||
- `summary.exceeded_yesterday`:是否比昨天多。
|
- `summary.exceeded_yesterday`:是否比昨天多。
|
||||||
- `summary.profile_completed`:是否已完成基础信息。
|
- `daily_summary`:当天已缓存的 AI 日总结;无缓存时为 `null`。
|
||||||
- `motivation.message`:激励语文案。
|
- `motivation.message`:激励语文案。
|
||||||
- `motivation.type`:激励语类型。
|
- `motivation.type`:激励语类型。
|
||||||
- `quick_actions[].type`:动作类型(`log_smoke`/`resist`)。
|
|
||||||
- `quick_actions[].title`:按钮文案。
|
|
||||||
- `quick_actions[].primary`:是否主按钮。
|
|
||||||
- `data_sources.ai_advice_date`:AI 建议日期。
|
|
||||||
- `data_sources.plan_date`:当前计划日期。
|
|
||||||
|
|
||||||
如需 AI 时间节点完整版,可继续调用 `GET /ai/next_smoke_time`;首页接口只返回默认建议,避免额外的 AI 生成成本。
|
如需生成 AI 时间节点,请调用 `GET /api/v1/smoke/ai/next_smoke_time`;首页接口只读取缓存,不主动生成 AI 建议,避免额外性能成本。
|
||||||
|
|
||||||
未满足权限时的建议响应(示例):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 403,
|
|
||||||
"message": "需要会员或观看广告解锁后才可生成建议",
|
|
||||||
"data": {
|
|
||||||
"date": "2026-01-02",
|
|
||||||
"need": "vip_or_ad"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应(示例):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"date": "2026-01-02",
|
|
||||||
"advice": "..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10) 看广告解锁(用于非会员)
|
## 10) 看广告解锁(用于非会员)
|
||||||
|
|
||||||
@@ -451,33 +272,13 @@ curl -X GET 'http://127.0.0.1:8080/api/v1/smoke/logs/5202' \
|
|||||||
|
|
||||||
成功响应:同 `GET /api/v1/smoke/profile`(返回最新 `profile` + `is_completed` + `baseline_interval_minutes`)。
|
成功响应:同 `GET /api/v1/smoke/profile`(返回最新 `profile` + `is_completed` + `baseline_interval_minutes`)。
|
||||||
|
|
||||||
## 13) 想抽但忍住了(写入一条 level=0,num=0 的记录)
|
## 13) 获取 AI 下次抽烟建议
|
||||||
|
|
||||||
`POST /api/v1/smoke/logs/resisted`
|
`GET /api/v1/smoke/ai/next_smoke_time`
|
||||||
|
|
||||||
请求体(示例):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"smoke_time": "2026-01-05",
|
|
||||||
"smoke_at": "2026-01-05 10:20:00",
|
|
||||||
"remark": "压力大,想抽但忍住了",
|
|
||||||
"level": 0,
|
|
||||||
"num": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
- 该接口会在 `fa_smoke_log` 中新增一条记录:`level=0` 且 `num=0`,用于更直观记录“想抽/忍住”的过程。
|
- 用于 AI 建议页生成当天时间节点。
|
||||||
- 这类记录不会影响 `today_count/weekly.count` 的支数统计(因为 `num=0`)。
|
- 首页只通过 `GET /smoke/home` 读取已缓存的 AI 结果,不主动生成 AI,避免首页加载时产生额外性能成本。
|
||||||
|
|
||||||
## 14) 获取“下次抽烟记录时间”(默认 + AI 自动切换)
|
|
||||||
|
|
||||||
`GET /api/v1/smoke/next_smoke_time`
|
|
||||||
|
|
||||||
说明:
|
|
||||||
- 用于首页展示“建议的下次记录时间”。
|
|
||||||
- 已整合首页所需汇总字段(上次抽烟时间/今日抽烟支数/今日克制次数/较昨日减少支数)。
|
|
||||||
- 如果指定日期存在 AI 给出的时间节点(`time_nodes` 不为空),则优先使用 AI 的建议;否则使用默认策略。
|
|
||||||
- 可选参数:
|
- 可选参数:
|
||||||
- `date`:计划日期(默认今天),支持 `YYYY-MM-DD` 或 `today/tomorrow`。
|
- `date`:计划日期(默认今天),支持 `YYYY-MM-DD` 或 `today/tomorrow`。
|
||||||
- `mode`(默认 `auto`)
|
- `mode`(默认 `auto`)
|
||||||
@@ -495,74 +296,21 @@ AI 生成说明:
|
|||||||
- 当 `mode=ai` 时,会把最近 3 天的抽烟数据(含“忍住记录”)作为输入提供给 AI,用于更贴合近期模式生成时间节点。
|
- 当 `mode=ai` 时,会把最近 3 天的抽烟数据(含“忍住记录”)作为输入提供给 AI,用于更贴合近期模式生成时间节点。
|
||||||
- 未解锁时会返回 `403`:提示需要观看广告解锁。
|
- 未解锁时会返回 `403`:提示需要观看广告解锁。
|
||||||
|
|
||||||
成功响应(示例:回落到默认):
|
成功响应(示例):
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"source": "default",
|
|
||||||
"not_before_at": "2026-01-05T10:18:00+08:00",
|
|
||||||
"suggested_at": "2026-01-05T10:18:00+08:00",
|
|
||||||
"last_smoke_at": "2026-01-05T09:30:00+08:00",
|
|
||||||
"today_count": 3,
|
|
||||||
"resisted_count": 1,
|
|
||||||
"reduced_from_yesterday": 2,
|
|
||||||
"exceeded_yesterday": false,
|
|
||||||
"default": {
|
|
||||||
"last_smoke_at": "2026-01-05T09:30:00+08:00",
|
|
||||||
"next_smoke_at": "2026-01-05T10:18:00+08:00",
|
|
||||||
"base_interval_minutes": 48,
|
|
||||||
"interval_minutes": 48,
|
|
||||||
"stage": 0,
|
|
||||||
"resisted_7d": 3,
|
|
||||||
"sleep_adjusted": false,
|
|
||||||
"algorithm": "staircase_delay_v1",
|
|
||||||
"as_of": "2026-01-05T10:00:00+08:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
当存在 AI 建议且包含 `time_nodes` 时,响应会是(示例):
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"message": "success",
|
"message": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"source": "ai",
|
"source": "ai",
|
||||||
"not_before_at": "2026-01-05T10:18:00+08:00",
|
|
||||||
"suggested_at": "2026-01-05T10:28:00+08:00",
|
"suggested_at": "2026-01-05T10:28:00+08:00",
|
||||||
"last_smoke_at": "2026-01-05T09:30:00+08:00",
|
|
||||||
"today_count": 3,
|
|
||||||
"resisted_count": 1,
|
|
||||||
"reduced_from_yesterday": 2,
|
|
||||||
"exceeded_yesterday": false,
|
|
||||||
"time_nodes": ["10:30", "11:10", "14:00", "16:30"],
|
"time_nodes": ["10:30", "11:10", "14:00", "16:30"],
|
||||||
"advice": "先把这次冲动延后到10:28,期间做一次5分钟快走+喝水,压力场景用深呼吸替代。",
|
"advice": "先把这次冲动延后到10:28,期间做一次5分钟快走+喝水,压力场景用深呼吸替代。"
|
||||||
"default": { "algorithm": "staircase_delay_v1" },
|
|
||||||
"ai": {
|
|
||||||
"plan_date": "2026-01-05",
|
|
||||||
"not_before_at": "2026-01-05T10:18:00+08:00",
|
|
||||||
"suggested_at": "2026-01-05T10:28:00+08:00",
|
|
||||||
"time_nodes": ["10:30", "11:10", "14:00", "16:30"],
|
|
||||||
"advice": "先把这次冲动延后到10:28,期间做一次5分钟快走+喝水,压力场景用深呼吸替代。",
|
|
||||||
"prompt_version": "v1",
|
|
||||||
"model": "gpt-4.1-mini",
|
|
||||||
"provider": "openai-compatible"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
字段说明(新增首页字段):
|
## 14) 数据统计分析(趋势 + 健康 + 省钱)
|
||||||
- `last_smoke_at`:上次“实际抽烟”时间(忽略忍住记录),格式 `RFC3339`(含时区)。
|
|
||||||
- `today_count`:今日抽烟支数(累加 `num`)。
|
|
||||||
- `resisted_count`:今日克制次数(`num=0`)。
|
|
||||||
- `reduced_from_yesterday`:较昨日减少的支数(允许为负数;为负时表示“今天超出昨日”)。
|
|
||||||
- `exceeded_yesterday`:是否超出昨日(`true` 表示今天超出昨日,前端可用作单独标识)。
|
|
||||||
|
|
||||||
## 15) 数据统计分析(趋势 + 健康 + 省钱)
|
|
||||||
|
|
||||||
`GET /api/v1/smoke/stats?range=week|month|year&date=2026-01-07`
|
`GET /api/v1/smoke/stats?range=week|month|year&date=2026-01-07`
|
||||||
|
|
||||||
@@ -624,22 +372,3 @@ AI 生成说明:
|
|||||||
- `money.expected_total`:按“统计周期内有记录的天数”×`baseline_cigs_per_day` 计算;不统计无日志的天数。
|
- `money.expected_total`:按“统计周期内有记录的天数”×`baseline_cigs_per_day` 计算;不统计无日志的天数。
|
||||||
- `money.saved_cent`:按 `max(expected_total - actual_total, 0)` 计算,避免出现负值。
|
- `money.saved_cent`:按 `max(expected_total - actual_total, 0)` 计算,避免出现负值。
|
||||||
- `health.available=false`:表示无历史记录。
|
- `health.available=false`:表示无历史记录。
|
||||||
|
|
||||||
## 16) 激励语(后端统一生成)
|
|
||||||
|
|
||||||
`GET /api/v1/smoke/motivation`
|
|
||||||
|
|
||||||
说明:
|
|
||||||
- 基于当日数据(如 `today_count`、`resisted_count`、`last_smoke_at`)与 `quit_motivations` 生成一句激励语。
|
|
||||||
|
|
||||||
成功响应(示例):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"data": {
|
|
||||||
"message": "今天的表现很稳,继续保持!记住你的目标:身体健康。",
|
|
||||||
"type": "encourage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -3,30 +3,14 @@ import { BASE_URL } from '@/config'
|
|||||||
|
|
||||||
const BASE_URL_V2 = BASE_URL.replace('/v1', '/v2')
|
const BASE_URL_V2 = BASE_URL.replace('/v1', '/v2')
|
||||||
|
|
||||||
export function getDashboard(params = {}) {
|
|
||||||
return request.get('/smoke/dashboard', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHome(params = {}) {
|
export function getHome(params = {}) {
|
||||||
return request.get('/smoke/home', params)
|
return request.get('/smoke/home', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNextSmokeTime(params = {}) {
|
|
||||||
return request.get('/smoke/next_smoke_time', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLogs(params = {}) {
|
export function getLogs(params = {}) {
|
||||||
return request.get('/smoke/logs', params)
|
return request.get('/smoke/logs', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLatestLogs(limit = 20) {
|
|
||||||
return request.get('/smoke/logs/latest', { limit })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLog(id) {
|
|
||||||
return request.get(`/smoke/logs/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createLog(data) {
|
export function createLog(data) {
|
||||||
return request.post('/smoke/logs', data)
|
return request.post('/smoke/logs', data)
|
||||||
}
|
}
|
||||||
@@ -39,14 +23,6 @@ export function deleteLog(id) {
|
|||||||
return request.delete(`/smoke/logs/${id}`)
|
return request.delete(`/smoke/logs/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createResistedLog(data) {
|
|
||||||
return request.post('/smoke/logs/resisted', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAiAdvice(date) {
|
|
||||||
return request.get('/smoke/ai/advice', { date })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unlockAiAdvice(data) {
|
export function unlockAiAdvice(data) {
|
||||||
return request.post('/smoke/ai/advice_unlocks', data)
|
return request.post('/smoke/ai/advice_unlocks', data)
|
||||||
}
|
}
|
||||||
@@ -71,10 +47,6 @@ export function getShareData(shareToken, params = {}) {
|
|||||||
return request.get(`/smoke/share/${shareToken}`, params)
|
return request.get(`/smoke/share/${shareToken}`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function revokeShare(shareToken) {
|
|
||||||
return request.post(`/smoke/share/${shareToken}/revoke`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 戒烟计划 API
|
// 戒烟计划 API
|
||||||
export function generateQuitPlan() {
|
export function generateQuitPlan() {
|
||||||
return request.post('/smoke/quit-plan/generate')
|
return request.post('/smoke/quit-plan/generate')
|
||||||
|
|||||||
+13
-127
@@ -204,26 +204,6 @@
|
|||||||
<text class="record-bio-metric-name">{{ item.name }}</text>
|
<text class="record-bio-metric-name">{{ item.name }}</text>
|
||||||
<text class="record-bio-metric-value">{{ item.value }}</text>
|
<text class="record-bio-metric-value">{{ item.value }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="record-bio-divider"></view>
|
|
||||||
<view class="record-bio-life-row">
|
|
||||||
<text class="record-bio-life-value">+{{ recordLifeSaved }} 小时</text>
|
|
||||||
<text class="record-bio-life-label">生命已回收</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="record-bio-trend-card">
|
|
||||||
<view class="record-bio-section-head record-bio-section-head-compact">
|
|
||||||
<text class="record-bio-section-title">7 天节奏</text>
|
|
||||||
<text class="record-bio-section-chip">{{ recordStatusLabel }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="record-bio-trend-chart">
|
|
||||||
<view v-for="item in recordTrendItems" :key="item.label" class="record-bio-trend-col">
|
|
||||||
<view class="record-bio-trend-line" :style="{ height: item.height }">
|
|
||||||
<view class="record-bio-trend-dot" :class="{ 'record-bio-trend-dot-active': item.active }"></view>
|
|
||||||
</view>
|
|
||||||
<text class="record-bio-trend-label">{{ item.label }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -330,6 +310,7 @@ const timerSeconds = ref(0) // 页面存活期间累计秒数
|
|||||||
const isQuitMode = computed(() => userStore.mode === 'quit')
|
const isQuitMode = computed(() => userStore.mode === 'quit')
|
||||||
const homeSummary = computed(() => homeData.value?.summary || {})
|
const homeSummary = computed(() => homeData.value?.summary || {})
|
||||||
const homeTimer = computed(() => homeData.value?.timer || {})
|
const homeTimer = computed(() => homeData.value?.timer || {})
|
||||||
|
const homeMotivation = computed(() => homeData.value?.motivation || {})
|
||||||
const quitSummary = computed(() => quitHomeData.value?.summary || {})
|
const quitSummary = computed(() => quitHomeData.value?.summary || {})
|
||||||
const quitDailyStatus = computed(() => quitHomeData.value?.daily_status || {})
|
const quitDailyStatus = computed(() => quitHomeData.value?.daily_status || {})
|
||||||
|
|
||||||
@@ -340,6 +321,7 @@ const packPriceYuan = computed(() => (profileStore.profile?.pack_price_cent || 2
|
|||||||
// ========== 记录模式 ==========
|
// ========== 记录模式 ==========
|
||||||
|
|
||||||
const todayCount = computed(() => homeSummary.value.today_count ?? 0)
|
const todayCount = computed(() => homeSummary.value.today_count ?? 0)
|
||||||
|
const resistedCount = computed(() => homeSummary.value.resisted_count ?? 0)
|
||||||
const dailyTarget = computed(() => {
|
const dailyTarget = computed(() => {
|
||||||
const target = homeSummary.value.daily_target
|
const target = homeSummary.value.daily_target
|
||||||
if (target != null) return target
|
if (target != null) return target
|
||||||
@@ -386,12 +368,6 @@ const todayCountRingStyle = computed(() => {
|
|||||||
|
|
||||||
const recordHasData = computed(() => todayCount.value > 0 || timerBaseSeconds.value >= 0)
|
const recordHasData = computed(() => todayCount.value > 0 || timerBaseSeconds.value >= 0)
|
||||||
|
|
||||||
// 今日节省(比目标少抽的部分换算)
|
|
||||||
const recordSavedMoney = computed(() => {
|
|
||||||
const saved = Math.max(0, (dailyTarget.value - todayCount.value) * (packPriceYuan.value / 20))
|
|
||||||
return saved.toFixed(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
const recordHeroTitle = computed(() => {
|
const recordHeroTitle = computed(() => {
|
||||||
if (!recordHasData.value) return '先记录第一刻'
|
if (!recordHasData.value) return '先记录第一刻'
|
||||||
if (todayCount.value === 0) return '今天还没抽烟'
|
if (todayCount.value === 0) return '今天还没抽烟'
|
||||||
@@ -408,25 +384,19 @@ const recordRhythmText = computed(() => {
|
|||||||
|
|
||||||
const achievementCardTitle = computed(() => isQuitMode.value ? '无烟等级' : '长期记录等级')
|
const achievementCardTitle = computed(() => isQuitMode.value ? '无烟等级' : '长期记录等级')
|
||||||
|
|
||||||
// 今日延长生命时长(每少一支 = 11 分钟)
|
|
||||||
const recordLifeSaved = computed(() => {
|
|
||||||
const minutes = Math.max(0, (dailyTarget.value - todayCount.value) * 11)
|
|
||||||
return Math.round(minutes / 60 * 10) / 10
|
|
||||||
})
|
|
||||||
|
|
||||||
const reducePercent = computed(() => {
|
const reducePercent = computed(() => {
|
||||||
if (dailyTarget.value <= 0) return 0
|
if (dailyTarget.value <= 0) return 0
|
||||||
return Math.round(Math.max(0, dailyTarget.value - todayCount.value) / dailyTarget.value * 100)
|
return Math.round(Math.max(0, dailyTarget.value - todayCount.value) / dailyTarget.value * 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
const recordHealthTip = computed(() => {
|
const recordHealthTip = computed(() => {
|
||||||
|
if (homeMotivation.value.message) return homeMotivation.value.message
|
||||||
if (todayCount.value === 0) return '今天还没抽烟,继续保持!'
|
if (todayCount.value === 0) return '今天还没抽烟,继续保持!'
|
||||||
if (todayCount.value < dailyTarget.value) return `今天比目标少抽了${dailyTarget.value - todayCount.value}根,很棒!`
|
if (todayCount.value < dailyTarget.value) return `今天比目标少抽了${dailyTarget.value - todayCount.value}根,很棒!`
|
||||||
if (todayCount.value === dailyTarget.value) return '今天已达到目标,加油!'
|
if (todayCount.value === dailyTarget.value) return '今天已达到目标,加油!'
|
||||||
return '今天超标了,明天继续努力'
|
return '今天超标了,明天继续努力'
|
||||||
})
|
})
|
||||||
|
|
||||||
const recordGoalLeft = computed(() => Math.max(0, dailyTarget.value - todayCount.value))
|
|
||||||
const recordControlScore = computed(() => {
|
const recordControlScore = computed(() => {
|
||||||
if (!dailyTarget.value || dailyTarget.value <= 0) return todayCount.value > 0 ? 40 : 88
|
if (!dailyTarget.value || dailyTarget.value <= 0) return todayCount.value > 0 ? 40 : 88
|
||||||
const score = Math.round((1 - Math.min(todayCount.value / dailyTarget.value, 1)) * 72) + 18
|
const score = Math.round((1 - Math.min(todayCount.value / dailyTarget.value, 1)) * 72) + 18
|
||||||
@@ -450,26 +420,20 @@ const recordControlRingStyle = computed(() => {
|
|||||||
return { background: `conic-gradient(${accent} 0deg ${angle}deg, rgba(226, 232, 240, 0.78) ${angle}deg 360deg)` }
|
return { background: `conic-gradient(${accent} 0deg ${angle}deg, rgba(226, 232, 240, 0.78) ${angle}deg 360deg)` }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const recordYesterdayText = computed(() => {
|
||||||
|
const reduced = Number(homeSummary.value.reduced_from_yesterday)
|
||||||
|
if (Number.isNaN(reduced)) return '暂无对比'
|
||||||
|
if (reduced === 0) return '持平'
|
||||||
|
return homeSummary.value.exceeded_yesterday ? `多 ${reduced} 根` : `少 ${reduced} 根`
|
||||||
|
})
|
||||||
|
|
||||||
const recordBioHealthItems = computed(() => [
|
const recordBioHealthItems = computed(() => [
|
||||||
{ name: '今日目标', value: `${todayCount.value}/${dailyTarget.value || '-'} 根` },
|
{ name: '今日目标', value: `${todayCount.value}/${dailyTarget.value || '-'} 根` },
|
||||||
{ name: '剩余额度', value: `${recordGoalLeft.value} 根` },
|
{ name: '今日忍住', value: `${resistedCount.value} 次` },
|
||||||
{ name: '节省金额', value: `¥${recordSavedMoney.value}` }
|
{ name: '较昨日', value: recordYesterdayText.value },
|
||||||
|
{ name: '建议时间', value: nextSmokeTimeText.value || '--:--' }
|
||||||
])
|
])
|
||||||
|
|
||||||
const recordTrendItems = computed(() => {
|
|
||||||
const labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
|
||||||
const target = Math.max(dailyTarget.value || baselineCigsPerDay.value || 1, 1)
|
|
||||||
return labels.map((label, index) => {
|
|
||||||
const estimated = Math.max(0, todayCount.value + index - 6)
|
|
||||||
const value = index === labels.length - 1 ? todayCount.value : Math.min(target, estimated)
|
|
||||||
return {
|
|
||||||
label,
|
|
||||||
height: `${Math.max(24, Math.round((value / target) * 112))}rpx`,
|
|
||||||
active: index === labels.length - 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// ========== 戒烟模式 ==========
|
// ========== 戒烟模式 ==========
|
||||||
|
|
||||||
// 服务端连续无烟天数优先,失败时从本地缓存推算
|
// 服务端连续无烟天数优先,失败时从本地缓存推算
|
||||||
@@ -2793,7 +2757,6 @@ onShareAppMessage(() => ({
|
|||||||
.record-bio-status-card,
|
.record-bio-status-card,
|
||||||
.record-bio-console-card,
|
.record-bio-console-card,
|
||||||
.record-bio-health-card,
|
.record-bio-health-card,
|
||||||
.record-bio-trend-card,
|
|
||||||
.record-bio-achievement-card,
|
.record-bio-achievement-card,
|
||||||
.record-bio-tip-card {
|
.record-bio-tip-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -3060,7 +3023,6 @@ onShareAppMessage(() => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-bio-health-card,
|
.record-bio-health-card,
|
||||||
.record-bio-trend-card,
|
|
||||||
.record-bio-achievement-card {
|
.record-bio-achievement-card {
|
||||||
padding: 28rpx;
|
padding: 28rpx;
|
||||||
border-radius: 34rpx;
|
border-radius: 34rpx;
|
||||||
@@ -3170,82 +3132,6 @@ onShareAppMessage(() => ({
|
|||||||
color: #0F766E;
|
color: #0F766E;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-bio-divider {
|
|
||||||
height: 1rpx;
|
|
||||||
margin: 12rpx 0 14rpx;
|
|
||||||
background: rgba(148, 163, 184, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-life-row {
|
|
||||||
@include flex-col;
|
|
||||||
gap: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-life-value {
|
|
||||||
font-size: 34rpx;
|
|
||||||
line-height: 1.2;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--record-gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-life-label {
|
|
||||||
font-size: 21rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #64748B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-trend-card {
|
|
||||||
background: linear-gradient(135deg, #ECFDF5 0%, #F0F9FF 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-trend-chart {
|
|
||||||
height: 176rpx;
|
|
||||||
margin-top: 22rpx;
|
|
||||||
padding: 0 4rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1rpx solid rgba(100, 116, 139, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-trend-col {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
@include flex-center;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-trend-line {
|
|
||||||
position: relative;
|
|
||||||
width: 4rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
background: linear-gradient(180deg, rgba(251, 191, 36, 0.86), rgba(103, 232, 249, 0.5));
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-trend-dot {
|
|
||||||
position: absolute;
|
|
||||||
top: -8rpx;
|
|
||||||
left: 50%;
|
|
||||||
width: 16rpx;
|
|
||||||
height: 16rpx;
|
|
||||||
margin-left: -8rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #67E8F9;
|
|
||||||
box-shadow: 0 0 0 5rpx rgba(103, 232, 249, 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-trend-dot-active {
|
|
||||||
background: var(--record-gold);
|
|
||||||
box-shadow: 0 0 0 6rpx rgba(251, 191, 36, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-trend-label {
|
|
||||||
font-size: 18rpx;
|
|
||||||
color: #64748B;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-bio-achievement-card {
|
.record-bio-achievement-card {
|
||||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.78), rgba(250, 245, 255, 0.5));
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.78), rgba(250, 245, 255, 0.5));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
import { defineStore } from 'pinia'
|
|
||||||
import { getDashboard, getNextSmokeTime } from '@/api/smoke'
|
|
||||||
|
|
||||||
export const useDashboardStore = defineStore('dashboard', {
|
|
||||||
state: () => ({
|
|
||||||
todayCount: 0,
|
|
||||||
minutesSinceLast: 0,
|
|
||||||
weekly: [],
|
|
||||||
nextSmokeTime: null,
|
|
||||||
lastFetchTime: 0,
|
|
||||||
cacheExpiry: 30 * 1000,
|
|
||||||
loading: false
|
|
||||||
}),
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
isCacheValid: (state) => {
|
|
||||||
return Date.now() - state.lastFetchTime < state.cacheExpiry
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
async fetchDashboard(forceRefresh = false) {
|
|
||||||
if (!forceRefresh && this.isCacheValid) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = true
|
|
||||||
try {
|
|
||||||
const res = await getDashboard()
|
|
||||||
this.todayCount = res.data.today_count || 0
|
|
||||||
this.minutesSinceLast = res.data.minutes_since_last || 0
|
|
||||||
this.weekly = res.data.weekly || []
|
|
||||||
this.lastFetchTime = Date.now()
|
|
||||||
} catch (e) {
|
|
||||||
console.error('fetchDashboard error:', e)
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchNextSmokeTime() {
|
|
||||||
try {
|
|
||||||
const res = await getNextSmokeTime()
|
|
||||||
this.nextSmokeTime = res.data
|
|
||||||
return res.data
|
|
||||||
} catch (e) {
|
|
||||||
console.error('fetchNextSmokeTime error:', e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setDashboard(data) {
|
|
||||||
this.todayCount = data.today_count || 0
|
|
||||||
this.minutesSinceLast = data.minutes_since_last || 0
|
|
||||||
this.weekly = data.weekly || []
|
|
||||||
this.lastFetchTime = Date.now()
|
|
||||||
},
|
|
||||||
|
|
||||||
setNextSmokeTime(data) {
|
|
||||||
this.nextSmokeTime = data
|
|
||||||
},
|
|
||||||
|
|
||||||
incrementTodayCount() {
|
|
||||||
this.todayCount++
|
|
||||||
},
|
|
||||||
|
|
||||||
resetTimer() {
|
|
||||||
this.minutesSinceLast = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -5,6 +5,5 @@ const pinia = createPinia()
|
|||||||
export default pinia
|
export default pinia
|
||||||
|
|
||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './dashboard'
|
|
||||||
export * from './profile'
|
export * from './profile'
|
||||||
export * from './logs'
|
export * from './logs'
|
||||||
|
|||||||
Reference in New Issue
Block a user