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'