diff --git a/docs/ALGORITHM.md b/docs/ALGORITHM.md
index 901500b..1169bfd 100644
--- a/docs/ALGORITHM.md
+++ b/docs/ALGORITHM.md
@@ -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` 字段。
根据用户状态生成不同的激励语:
diff --git a/docs/TECH.md b/docs/TECH.md
index 1d00944..b39c14c 100644
--- a/docs/TECH.md
+++ b/docs/TECH.md
@@ -60,18 +60,14 @@
├─────────────────────────────────────────────────────────┤
│ 0ms ─ 页面骨架屏渲染 │
│ │ │
-│ ├──── 并行请求 ──────────────────────────────────── │
+│ ├──── 串行守卫 + 单接口数据 ──────────────────────── │
│ │ ├── /profile (检查用户状态) │
-│ │ ├── /dashboard (核心数据) │
-│ │ └── /next_smoke_time (建议时间) │
+│ │ └── /home (首页核心数据) │
│ │ │
│ 200ms ─ 核心数据返回,渲染计时器+统计卡片 │
│ │ │
│ 300ms ─ 首屏渲染完成 │
│ │ │
-│ │ ┌── 延迟加载 ────────────────────────────── │
-│ │ └── /ai/advice (AI提示卡片) │
-│ │ │
│ 500ms ─ 完整页面渲染 │
└─────────────────────────────────────────────────────────┘
```
@@ -79,29 +75,14 @@
### 3.2 缓存策略
```javascript
-// stores/dashboard.js
-import { defineStore } from 'pinia'
+// 首页使用 /smoke/home 单接口返回当前屏所需字段。
+// 页面级刷新由 onShow 触发,避免维护额外 dashboard store 和重复请求。
+const homeData = ref(null)
-export const useDashboardStore = defineStore('dashboard', {
- state: () => ({
- todayCount: 0,
- 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
- }
- // 发起请求...
- }
- }
-})
+async function fetchRecordHomeData() {
+ const res = await api.getHome()
+ homeData.value = res.data || {}
+}
```
### 3.3 计时器优化
@@ -240,35 +221,27 @@ export const request = {
```javascript
// pages/index/index.vue
import { ref, onMounted } from 'vue'
-import { useDashboardStore } from '@/stores/dashboard'
-import * as api from '@/api/smoke'
+import * as api from '@/api'
const loading = ref(true)
-const dashboardStore = useDashboardStore()
+const homeData = ref(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) {
+ const profileRes = await api.getSmokeProfile()
+ if (!profileRes.exists || !profileRes.is_completed) {
uni.redirectTo({ url: '/pages/onboarding/index' })
return
}
-
- dashboardStore.setDashboard(dashboardRes.data)
- dashboardStore.setNextSmokeTime(nextTimeRes.data)
+
+ const homeRes = await api.getHome()
+ homeData.value = homeRes.data || {}
} finally {
loading.value = false
}
-
- setTimeout(loadAiAdvice, 300)
}
onMounted(initPage)
diff --git a/docs/api.md b/docs/api.md
index 3372cbd..3afe6d8 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -54,18 +54,7 @@ curl -X POST 'http://127.0.0.1:8080/api/v1/smoke/logs' \
}
```
-## 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) 列表查询(分页)
+## 2) 列表查询(分页)
`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) 获取看板概览
-
-`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) 更新记录
+## 3) 更新记录
`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":""}`。
- 如果传 `null` 或者不传 `smoke_time`,后端会认为你没有修改该字段。
-## 7) 删除记录(软删除)
+## 4) 删除记录(软删除)
`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 戒烟建议(会员 + 广告解锁并行)
-
-`GET /api/v1/smoke/ai/advice?date=2026-01-02`
-
-说明:
-- `date` 可选,默认“昨天”(建议针对哪一天的数据)。
-- 权限:会员用户直接可用;非会员需要先对该 `date` 完成“看广告解锁”(见下一个接口)。
-- 建议结果会按 `uid + date + prompt_version` 缓存(表:`fa_smoke_ai_advice`)。
-
-## 9) 首页整合接口(Home Dashboard)
+## 5) 首页整合接口(Home)
`GET /api/v1/smoke/home`
-此接口把首页 UI 所需核心模块一次返回,避免前端串行请求多个接口。返回示例:
+此接口只返回首页、AI 时间页和 AI 日总结页正在消费的核心字段,避免生成或传递无用模块。返回示例:
```json
{
"code": 200,
"message": "success",
"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": {
- "label": "距上次抽烟",
- "last_smoke_at": "2026-01-05T07:42:00+08:00",
"seconds_since_last": 9900,
"next_suggested_at": "2026-01-05T10:30:00+08:00",
"next_suggested_clock": "10:30",
- "not_before_at": "2026-01-05T10:30:00+08:00",
- "suggestion_source": "default",
- "suggestion_algorithm": "staircase_delay_v1"
+ "suggestion_source": "default"
},
"summary": {
"today_count": 3,
"daily_target": 10,
"resisted_count": 1,
"reduced_from_yesterday": 2,
- "exceeded_yesterday": false,
- "profile_completed": true
+ "exceeded_yesterday": false
},
"motivation": {
"message": "太棒了!你刚刚成功抵抗了一次烟瘾",
"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.next_suggested_at`:建议下次抽烟时间(RFC3339)。
- `timer.next_suggested_clock`:仅时分显示(如“16:30”)。
-- `timer.not_before_at`:不早于的时间点(当前与 `next_suggested_at` 一致)。
- `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.daily_target`:每日目标(线性递减:以 `onboarding_completed_at` 为起点,按 `quit_date` 线性下降到 0)。
+- `summary.daily_target`:每日目标。
- `summary.resisted_count`:今日忍住次数。
- `summary.reduced_from_yesterday`:与昨日的绝对差值(非负)。
- `summary.exceeded_yesterday`:是否比昨天多。
-- `summary.profile_completed`:是否已完成基础信息。
+- `daily_summary`:当天已缓存的 AI 日总结;无缓存时为 `null`。
- `motivation.message`:激励语文案。
- `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 生成成本。
-
-未满足权限时的建议响应(示例):
-```json
-{
- "code": 403,
- "message": "需要会员或观看广告解锁后才可生成建议",
- "data": {
- "date": "2026-01-02",
- "need": "vip_or_ad"
- }
-}
-```
-
-成功响应(示例):
-```json
-{
- "code": 200,
- "message": "success",
- "data": {
- "date": "2026-01-02",
- "advice": "..."
- }
-}
-```
+如需生成 AI 时间节点,请调用 `GET /api/v1/smoke/ai/next_smoke_time`;首页接口只读取缓存,不主动生成 AI 建议,避免额外性能成本。
## 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`)。
-## 13) 想抽但忍住了(写入一条 level=0,num=0 的记录)
+## 13) 获取 AI 下次抽烟建议
-`POST /api/v1/smoke/logs/resisted`
-
-请求体(示例):
-```json
-{
- "smoke_time": "2026-01-05",
- "smoke_at": "2026-01-05 10:20:00",
- "remark": "压力大,想抽但忍住了",
- "level": 0,
- "num": 0
-}
-```
+`GET /api/v1/smoke/ai/next_smoke_time`
说明:
-- 该接口会在 `fa_smoke_log` 中新增一条记录:`level=0` 且 `num=0`,用于更直观记录“想抽/忍住”的过程。
-- 这类记录不会影响 `today_count/weekly.count` 的支数统计(因为 `num=0`)。
-
-## 14) 获取“下次抽烟记录时间”(默认 + AI 自动切换)
-
-`GET /api/v1/smoke/next_smoke_time`
-
-说明:
-- 用于首页展示“建议的下次记录时间”。
-- 已整合首页所需汇总字段(上次抽烟时间/今日抽烟支数/今日克制次数/较昨日减少支数)。
-- 如果指定日期存在 AI 给出的时间节点(`time_nodes` 不为空),则优先使用 AI 的建议;否则使用默认策略。
+- 用于 AI 建议页生成当天时间节点。
+- 首页只通过 `GET /smoke/home` 读取已缓存的 AI 结果,不主动生成 AI,避免首页加载时产生额外性能成本。
- 可选参数:
- `date`:计划日期(默认今天),支持 `YYYY-MM-DD` 或 `today/tomorrow`。
- `mode`(默认 `auto`)
@@ -495,74 +296,21 @@ AI 生成说明:
- 当 `mode=ai` 时,会把最近 3 天的抽烟数据(含“忍住记录”)作为输入提供给 AI,用于更贴合近期模式生成时间节点。
- 未解锁时会返回 `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
{
"code": 200,
"message": "success",
"data": {
"source": "ai",
- "not_before_at": "2026-01-05T10:18: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"],
- "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"
- }
+ "advice": "先把这次冲动延后到10:28,期间做一次5分钟快走+喝水,压力场景用深呼吸替代。"
}
}
```
-字段说明(新增首页字段):
-- `last_smoke_at`:上次“实际抽烟”时间(忽略忍住记录),格式 `RFC3339`(含时区)。
-- `today_count`:今日抽烟支数(累加 `num`)。
-- `resisted_count`:今日克制次数(`num=0`)。
-- `reduced_from_yesterday`:较昨日减少的支数(允许为负数;为负时表示“今天超出昨日”)。
-- `exceeded_yesterday`:是否超出昨日(`true` 表示今天超出昨日,前端可用作单独标识)。
-
-## 15) 数据统计分析(趋势 + 健康 + 省钱)
+## 14) 数据统计分析(趋势 + 健康 + 省钱)
`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.saved_cent`:按 `max(expected_total - actual_total, 0)` 计算,避免出现负值。
- `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"
- }
-}
-```
diff --git a/src/api/smoke.js b/src/api/smoke.js
index e8502c3..f98815c 100644
--- a/src/api/smoke.js
+++ b/src/api/smoke.js
@@ -3,30 +3,14 @@ import { BASE_URL } from '@/config'
const BASE_URL_V2 = BASE_URL.replace('/v1', '/v2')
-export function getDashboard(params = {}) {
- return request.get('/smoke/dashboard', params)
-}
-
export function getHome(params = {}) {
return request.get('/smoke/home', params)
}
-export function getNextSmokeTime(params = {}) {
- return request.get('/smoke/next_smoke_time', params)
-}
-
export function getLogs(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) {
return request.post('/smoke/logs', data)
}
@@ -39,14 +23,6 @@ export function deleteLog(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) {
return request.post('/smoke/ai/advice_unlocks', data)
}
@@ -71,10 +47,6 @@ export function getShareData(shareToken, params = {}) {
return request.get(`/smoke/share/${shareToken}`, params)
}
-export function revokeShare(shareToken) {
- return request.post(`/smoke/share/${shareToken}/revoke`)
-}
-
// 戒烟计划 API
export function generateQuitPlan() {
return request.post('/smoke/quit-plan/generate')
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
index e846602..ca31e43 100644
--- a/src/pages/index/index.vue
+++ b/src/pages/index/index.vue
@@ -204,26 +204,6 @@
{{ item.name }}
{{ item.value }}
-
-
- +{{ recordLifeSaved }} 小时
- 生命已回收
-
-
-
-
-
-
-
- 7 天节奏
- {{ recordStatusLabel }}
-
-
-
-
-
-
- {{ item.label }}
@@ -330,6 +310,7 @@ const timerSeconds = ref(0) // 页面存活期间累计秒数
const isQuitMode = computed(() => userStore.mode === 'quit')
const homeSummary = computed(() => homeData.value?.summary || {})
const homeTimer = computed(() => homeData.value?.timer || {})
+const homeMotivation = computed(() => homeData.value?.motivation || {})
const quitSummary = computed(() => quitHomeData.value?.summary || {})
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 resistedCount = computed(() => homeSummary.value.resisted_count ?? 0)
const dailyTarget = computed(() => {
const target = homeSummary.value.daily_target
if (target != null) return target
@@ -386,12 +368,6 @@ const todayCountRingStyle = computed(() => {
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(() => {
if (!recordHasData.value) return '先记录第一刻'
if (todayCount.value === 0) return '今天还没抽烟'
@@ -408,25 +384,19 @@ const recordRhythmText = computed(() => {
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(() => {
if (dailyTarget.value <= 0) return 0
return Math.round(Math.max(0, dailyTarget.value - todayCount.value) / dailyTarget.value * 100)
})
const recordHealthTip = computed(() => {
+ if (homeMotivation.value.message) return homeMotivation.value.message
if (todayCount.value === 0) return '今天还没抽烟,继续保持!'
if (todayCount.value < dailyTarget.value) return `今天比目标少抽了${dailyTarget.value - todayCount.value}根,很棒!`
if (todayCount.value === dailyTarget.value) return '今天已达到目标,加油!'
return '今天超标了,明天继续努力'
})
-const recordGoalLeft = computed(() => Math.max(0, dailyTarget.value - todayCount.value))
const recordControlScore = computed(() => {
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
@@ -450,26 +420,20 @@ const recordControlRingStyle = computed(() => {
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(() => [
{ name: '今日目标', value: `${todayCount.value}/${dailyTarget.value || '-'} 根` },
- { name: '剩余额度', value: `${recordGoalLeft.value} 根` },
- { name: '节省金额', value: `¥${recordSavedMoney.value}` }
+ { name: '今日忍住', value: `${resistedCount.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-console-card,
.record-bio-health-card,
-.record-bio-trend-card,
.record-bio-achievement-card,
.record-bio-tip-card {
position: relative;
@@ -3060,7 +3023,6 @@ onShareAppMessage(() => ({
}
.record-bio-health-card,
-.record-bio-trend-card,
.record-bio-achievement-card {
padding: 28rpx;
border-radius: 34rpx;
@@ -3170,82 +3132,6 @@ onShareAppMessage(() => ({
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 {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.78), rgba(250, 245, 255, 0.5));
}
@@ -3296,4 +3182,4 @@ onShareAppMessage(() => ({
line-height: 1.55;
color: #475569;
}
-
\ No newline at end of file
+
diff --git a/src/stores/dashboard.js b/src/stores/dashboard.js
deleted file mode 100644
index e7fbc05..0000000
--- a/src/stores/dashboard.js
+++ /dev/null
@@ -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
- }
- }
-})
diff --git a/src/stores/index.js b/src/stores/index.js
index 73bcaaf..1e06221 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -5,6 +5,5 @@ const pinia = createPinia()
export default pinia
export * from './user'
-export * from './dashboard'
export * from './profile'
export * from './logs'