chore: record session journal - 握力环训练趣味化
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# 血糖趋势折线图过滤 0 值并跨空连线
|
||||
|
||||
## Goal
|
||||
|
||||
修复 admin 端"日常记录"与"医生接诊台"两个入口共用的血糖趋势折线图:
|
||||
1. 把数值 0 视为"未设置",不在折线图上画出来,避免一堆 0 点贴 X 轴的视觉混乱;
|
||||
2. 当某一天没有有效值时,折线从上一个有值点直接连到下一个有值点(跨空连线)。
|
||||
|
||||
表格列原样保留,0 仍然按现状显示,**不在本次需求范围内**。
|
||||
|
||||
## What I already know
|
||||
|
||||
- 折线图组件: `admin/src/views/tcm/diagnosis/components/DailyMatrix.vue`,被 `admin/src/views/tcm/diagnosis/readonly.vue` 等以及 `admin/src/views/patient/reception/index.vue:132` 引用 — "日常记录"+"医生接诊台"实际共用同一份代码。
|
||||
- 折线图取数路径:`bloodTrendSeries` (`DailyMatrix.vue:633`) → `parseTrendNumber` (`DailyMatrix.vue:719`) 把每天的空腹/餐后值转成 number 或 null。
|
||||
- 目前 `parseTrendNumber` 对数值 0 返回 0(被画成贴 X 轴的点)。
|
||||
- 两条 series (`DailyMatrix.vue:686`、`:701`) 都是 `connectNulls: false`,中间任意一天空值都会断开。
|
||||
- 表格取数路径:`getCell` (`DailyMatrix.vue:725`) 与 `bloodTrendSeries` 完全独立,改 `parseTrendNumber` 不会影响表格。
|
||||
- 另一处折线图 `BloodRecordList.vue` + 后端 `BloodRecordLogic::getBloodSugarTrend` 已经做到了同样的效果(后端 `> 0` 过滤、前端 `connectNulls: true`),不在本次改动范围。
|
||||
|
||||
## Assumptions
|
||||
|
||||
- "0 值"涵盖整数 0、`0.00`、`'0'`、`'0.00'` 等数值上等于 0 的情况(`Number(value) === 0`)。
|
||||
- 跨空连线只在折线图层面通过 echarts `connectNulls` 完成,不需要前端做线性插值或自定义点位。
|
||||
- 首尾连续若干天没有数据时,echarts 不会向外延伸,这是预期行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
1. `DailyMatrix.vue` 的 `parseTrendNumber` 把 `Number(value) === 0` 视为 null 返回。
|
||||
2. `DailyMatrix.vue` 中"空腹血糖"和"餐后血糖"两条 series 的 `connectNulls` 改为 `true`。
|
||||
3. 表格 `getCell` 路径完全不动,`fasting/postprandial/other` 列里的 0 仍然原样展示。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 当某一天 `fasting_blood_sugar` 或 `postprandial_blood_sugar` 为 0/`'0'`/`'0.00'`/null/`''` 时,折线图上对应日期不画点,且折线从上一个有值点平滑连到下一个有值点。
|
||||
- [ ] 当某一天空腹是 5.6、餐后是 0 时,折线图只在空腹线画出 5.6 这个点,餐后线该日期视为空。
|
||||
- [ ] 表格里同一天空腹是 0,在表格相应单元格上仍然能看到 `0`(或当前已有的展示形式),不被改成空字符串或 `-`。
|
||||
- [ ] `bloodTrendHasData` 在所有值都 ≤ 0 / 空 时返回 `false`,触发"暂无数据"占位。
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- 改动只影响 `DailyMatrix.vue` 一个文件。
|
||||
- 在"医生接诊台 (`patient/reception`)"和"诊断只读页 (`tcm/diagnosis/readonly`)"两处都验证一遍折线图行为。
|
||||
- 表格列展示无回归(目测 `getCell` 出来的 fasting/postprandial 列在含 0 的日期仍显示 0)。
|
||||
- 类型检查 `npm run type-check` 通过。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- `BloodRecordList.vue` 的折线图(已经满足需求)。
|
||||
- 表格列展示规则(0 是否要换显示成 `-` 之类),不本次处理。
|
||||
- 后端 `BloodRecordLogic::getBloodSugarTrend` 的过滤逻辑(已正确)。
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- 改动点:
|
||||
- `admin/src/views/tcm/diagnosis/components/DailyMatrix.vue:719` — `parseTrendNumber` 增加 `parsed === 0 → null`。
|
||||
- `admin/src/views/tcm/diagnosis/components/DailyMatrix.vue:686` — `connectNulls: false` → `true`。
|
||||
- `admin/src/views/tcm/diagnosis/components/DailyMatrix.vue:701` — `connectNulls: false` → `true`。
|
||||
- 验证页面:
|
||||
- `admin/src/views/patient/reception/index.vue` (医生接诊台 → 日常记录卡片)。
|
||||
- `admin/src/views/tcm/diagnosis/readonly.vue` / `edit.vue` 等引用 `DailyMatrix` 的页面。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "0",
|
||||
"name": "0",
|
||||
"title": "血糖趋势折线图过滤0值并跨空连线",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-14",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "long-20260514",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,77 @@
|
||||
# 节拍器 P0 修复(Canvas 渲染循环 + 触摸误触)
|
||||
|
||||
## Goal
|
||||
|
||||
修复 `TUICallKit-Vue3/training/pages/components/walker-canvas.vue` 三个 P0 级问题:
|
||||
1. Vue 2 钩子写错导致 Canvas 渲染循环离开页面后不会停;
|
||||
2. 暂停态仍以满帧 60fps 渲染,老年场景下功耗浪费;
|
||||
3. Canvas 中心 BPM 圆按钮触摸误触(按下后滑出仍触发 toggle)。
|
||||
|
||||
## What I already know
|
||||
|
||||
- 节拍器页面: `TUICallKit-Vue3/training/pages/metronome.vue`,父组件用 `<script setup lang="ts">` + `onUnmounted`,是 Vue 3 写法。
|
||||
- 律动 Canvas 子组件: `TUICallKit-Vue3/training/pages/components/walker-canvas.vue`,用 Options API,**全文件唯一一处错用 Vue 2 `beforeDestroy` 钩子**(第 60 行)——Vue 3 已重命名为 `beforeUnmount`,所以当前 timer 清理代码从未执行。
|
||||
- `renderLoop()` (143-178 行) 通过 `setTimeout(fn, 1000/60)` 自调用,**所以清理应该用 `clearTimeout(this.timer)` 而不是 `cancelAnimationFrame`**(当前清理代码两个 bug 叠加,完全没工作)。
|
||||
- `onTouchMove` (132-134 行) 是空方法;`onTouchEnd` (135-141 行) 只看 `coreTargetScale < 1` 就 emit `toggle-play`——按下命中圆 → 滑出圆外 → 抬起,**仍然触发播放/暂停**。
|
||||
- 暂停态: 父组件传入 `isPlaying = false` 时,`renderLoop` 仍每 16ms 重排 → 绘制 → `ctx.draw(false)` 桥接,只是 `time` 增长慢一些(`fixedDt * 0.2`)。
|
||||
- **不要**改 `onHide` 里 `fg.stop()` 的行为——之前的 BgAudio 循环长音频方案已被移除,InnerAudio + setTimeout 在小程序后台被强力节流,硬撑反而播得断断续续,所以现在切后台主动停是有意的设计选择,不是 bug。
|
||||
- 本任务**只动 walker-canvas.vue 一个文件**,不动 `useMetronome.ts` 和 `metronome.vue`。
|
||||
|
||||
## Assumptions
|
||||
|
||||
- "暂停态完全停渲染"的判定: `isPlaying === false` && `shockwaves.length === 0` && `|coreScale - coreTargetScale| < 0.001` && `|coreVelocity| < 0.001` && `waveAmplitudeMultiplier <= 0.11`(留 0.01 的浮点容差,因为暂停态衰减下限是 0.1)。
|
||||
- "touch hit-test" 容差保持现状 `coreRadius + 40`,不调整大小。
|
||||
- 触摸 cancel(系统打断,如来电)与 touchend 走同样的"复位 + 不 emit"逻辑(仅当 pressed === true 才 emit,被 cancel 时 pressed 已被 onTouchMove 滑出复位,或直接被 cancel 处理逻辑复位)。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1: 修复生命周期钩子
|
||||
|
||||
- 把 `beforeDestroy()` 改名为 `beforeUnmount()`。
|
||||
- 把里面的 `cancelAnimationFrame(this.timer)` 改为 `clearTimeout(this.timer)`(因为 `this.timer` 实际是 setTimeout 返回的 id)。
|
||||
|
||||
### R2: 暂停态停止渲染循环
|
||||
|
||||
- 新增 data 字段 `renderRunning: false`。
|
||||
- 抽出 `ensureRenderLoop()` 方法: 若 `renderRunning === true` 则直接 return,否则置 true + 调 `renderLoop()`。
|
||||
- `renderLoop()` 末尾根据"是否还需要继续"决定是 `setTimeout(...)` 还是 `renderRunning = false; return`:
|
||||
- 继续条件: `isPlaying` 为真,**或** 还有 shockwave,**或** 弹簧物理还没稳,**或** waveAmplitudeMultiplier 还没衰减到下限。
|
||||
- 各处外部入口都要调 `ensureRenderLoop()`:
|
||||
- `initCanvas()` 首次启动(替换原来的 `this.renderLoop()` 直接调用);
|
||||
- `triggerBeat()` 末尾;
|
||||
- `watch(isPlaying)` 中 `newVal === true` 分支;
|
||||
- `onTouchStart` 命中 hit box 后(用户按下也要立刻动起来给出按压反馈)。
|
||||
- 改完后,父组件 `isPlaying = false` 且没动效时,Canvas 不再有定时器在跑。
|
||||
|
||||
### R3: 触摸命中防误触
|
||||
|
||||
- 新增 data 字段 `pressed: false`。
|
||||
- `onTouchStart`: 命中 hit box 后置 `pressed = true` + 维持现有 `coreTargetScale = 0.9` + 调 `ensureRenderLoop()`。
|
||||
- `onTouchMove`: 拿当前 touch 重新算距离,若超出 `coreRadius + 40` 则 `pressed = false` + `coreTargetScale = 1`(视觉立即复位,弹簧动画把 scale 拉回去)。
|
||||
- `onTouchEnd` / `onTouchCancel`:
|
||||
- 无条件复位 `coreTargetScale = 1`;
|
||||
- 仅当 `pressed === true` 时才 `this.$emit('toggle-play')`;
|
||||
- 无条件复位 `pressed = false`。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 离开节拍器页面(返回上一页 / `uni.navigateBack`)后,Canvas 的渲染循环 timer 被清理,不再产生 `setTimeout` 调用(可在 `renderLoop` 起始加临时 `console.count` 验证,验证完移除)。
|
||||
- [ ] 节拍器停止状态进入稳态后(等动画余波结束 ~1 秒),Canvas 不再有 timer 在跑(同上验证手段);用户按 toggle 启动后,渲染立即恢复且画面流畅。
|
||||
- [ ] 用户按下中心圆 → 手指滑出圆外 → 抬起: **不**触发播放/暂停,且 coreScale 视觉上被复位回 1。
|
||||
- [ ] 用户按下中心圆 → 手指原地抬起: 正常触发 toggle-play 切换播放/暂停。
|
||||
- [ ] 用户在播放过程中,触发 triggerBeat 的视觉律动(shockwave、core 脉动)行为不回归。
|
||||
- [ ] 切档位 / 微调 BPM / 切拍号等所有现有交互不回归。
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- 改动范围: 仅 `TUICallKit-Vue3/training/pages/components/walker-canvas.vue` 一个文件。
|
||||
- 不引入新的 lint warning。
|
||||
- 在 uniapp 工程的微信小程序端实机测试(或开发者工具)至少跑一遍:启停 / 切档位 / 进出页面 / 拖动滑出。
|
||||
- 类型检查(若 uniapp 工程有 `npm run type-check`)通过;无则忽略此项。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- `useMetronome.ts` 内部的调度漂移、CDN 兜底、accentEvery 重置等 P1/P2 项,**本次不动**。
|
||||
- `metronome.vue` 的 onHide 行为(确认为设计选择,非 bug,不改)。
|
||||
- 拍号 UI 文案语义、累计时长、触觉反馈等 UX 增强项。
|
||||
- 重写为 Composition API / 迁移到 Vue 3 `<script setup>`:不在范围,只做最小修补。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "metronome-p0",
|
||||
"name": "metronome-p0",
|
||||
"title": "节拍器 P0 修复(Canvas 渲染循环+触摸误触)",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P0",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-27",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "mini-sport",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"id": "media-channel-dept-binding",
|
||||
"name": "media-channel-dept-binding",
|
||||
"title": "自媒体渠道绑定部门统计修正",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-04-27",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "master",
|
||||
"worktree_path": null,
|
||||
"current_phase": 0,
|
||||
"next_action": [
|
||||
{
|
||||
"phase": 1,
|
||||
"action": "brainstorm"
|
||||
},
|
||||
{
|
||||
"phase": 2,
|
||||
"action": "research"
|
||||
},
|
||||
{
|
||||
"phase": 3,
|
||||
"action": "implement"
|
||||
},
|
||||
{
|
||||
"phase": 4,
|
||||
"action": "check"
|
||||
},
|
||||
{
|
||||
"phase": 5,
|
||||
"action": "update-spec"
|
||||
},
|
||||
{
|
||||
"phase": 6,
|
||||
"action": "record-session"
|
||||
}
|
||||
],
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
# Migration Task: Upgrade to v0.5.0-beta.19
|
||||
|
||||
**Created**: 2026-04-30
|
||||
**From Version**: 0.4.0
|
||||
**To Version**: 0.5.0-beta.19
|
||||
**Assignee**: codex-agent
|
||||
|
||||
## Status
|
||||
|
||||
- [ ] Review migration guide
|
||||
- [ ] Update custom files
|
||||
- [ ] Run `trellis update --migrate`
|
||||
- [ ] Test workflows
|
||||
|
||||
---
|
||||
|
||||
## v0.5.0-beta.0 Migration Guide
|
||||
|
||||
## 0.4.x → 0.5.x: What This Release Actually Changes
|
||||
|
||||
0.5.0-beta.0 is a **breaking** release. Pre-existing 0.4.x projects need `trellis update --migrate` to sync. The update runs **206 migration entries** (renames + hash-verified safe-file-deletes); the patch is non-destructive but large, so expect a handful of confirm prompts.
|
||||
|
||||
### 1. Skills got renamed: `skills/<name>/` → `skills/trellis-<name>/`
|
||||
|
||||
All Trellis skill directories gained a `trellis-` prefix. 60+ rename migrations cover every platform (`.claude/`, `.cursor/`, `.agents/`, `.kiro/`, `.qoder/`, etc.).
|
||||
|
||||
- **Unmodified skills**: renamed silently.
|
||||
- **Skills you customized**: confirm prompt. Pressing Enter (default = backup-rename) is always safe — your edits land at the new `trellis-<name>/` path intact.
|
||||
|
||||
### 2. Six commands + three sub-agents retired
|
||||
|
||||
| Old (removed) | Replacement |
|
||||
|---|---|
|
||||
| `/record-session` | merged into `/trellis:finish-work` Step 3 |
|
||||
| `/check-cross-layer` | merged into `/trellis:check` |
|
||||
| `/parallel` | use your CLI's native worktree/parallel support |
|
||||
| `/onboard` | superseded by auto-generated onboarding tasks |
|
||||
| `/create-command` | low-usage, unshipped |
|
||||
| `/integrate-skill` | low-usage, unshipped |
|
||||
| `dispatch` / `debug` / `plan` sub-agents | replaced by skill routing (`trellis-brainstorm`, `trellis-check`, `trellis-break-loop`) |
|
||||
|
||||
If any of these you relied on: replace the invocation with the right column. `/record-session` → `/trellis:finish-work` is the most common fix.
|
||||
|
||||
### 3. Multi-Agent Pipeline gone
|
||||
|
||||
`.trellis/scripts/multi_agent/`, `worktree.yaml`, and the Ralph Loop hook have been removed (138-entry safe-file-delete). Native worktree support in Claude / Cursor / etc. covers this space now. If you built automation around these, you'll need to port it to the platform's native primitives.
|
||||
|
||||
### 4. iFlow dropped
|
||||
|
||||
The `.iflow/` platform adapter was unmaintained upstream and has been removed. Existing `.iflow/` installs keep working on 0.4.x; switch to a supported platform (`--claude` / `--cursor` / `--codex` / etc.) to stay on 0.5.x.
|
||||
|
||||
### 5. task.py schema cleanup
|
||||
|
||||
`task.py create` stops writing legacy `current_phase` / `next_action` fields. Existing task.json files keep working (Python readers treat missing fields as null). New tasks from 0.5.x produce the canonical 24-field shape.
|
||||
|
||||
### 6. New runtime: per-turn workflow breadcrumb + sub-agent context injection
|
||||
|
||||
No user action needed — these are internal flow improvements. But if you hand-edit `.claude/settings.json` / `.codex/hooks.json` / etc., `trellis update` will want to overwrite the hooks block; your custom hooks land on the `Modified by you` prompt so you can review the merge.
|
||||
|
||||
## How to upgrade
|
||||
|
||||
```bash
|
||||
trellis update --migrate
|
||||
```
|
||||
|
||||
- `--migrate` is REQUIRED for breaking releases — without it, `trellis update` exits 1 telling you to add it.
|
||||
- Hash-verified migrations mean your local customizations stay; unmodified Trellis templates get updated silently.
|
||||
- The confirm prompts inline a `reason` explaining each prompt; Enter (default) is safe.
|
||||
- Codex users: enable `features.codex_hooks = true` in `~/.codex/config.toml` to activate SessionStart + UserPromptSubmit hook injection (feature-flagged by Codex itself).
|
||||
|
||||
### AI Assistant Instructions
|
||||
|
||||
When helping with this migration:
|
||||
|
||||
When helping a user migrate from 0.4.x to 0.5.x:
|
||||
|
||||
1. **Check for retired commands first**. Grep the user's codebase / shell history / aliases for `/record-session`, `/check-cross-layer`, `/parallel`, `/onboard`, `/create-command`, `/integrate-skill`. For each match, help them replace per the table in the guide — most common case is `/record-session` → `/trellis:finish-work`.
|
||||
|
||||
2. **Run `trellis update --migrate`**. Walk them through the prompts if any fire — 0.4 didn't hash-track certain SKILL.md files, so pristine copies may show as modified. The per-prompt `reason` explains this; pressing Enter (backup-rename) is always safe.
|
||||
|
||||
3. **Don't try to preserve Multi-Agent Pipeline state**. If the user has `.trellis/scripts/multi_agent/`, `worktree.yaml`, or Ralph Loop hooks in local edits, explicitly tell them the feature has no replacement in 0.5 beyond the platform's native worktree support. Help them identify which of their automation still needs porting.
|
||||
|
||||
4. **If they're on `.iflow/`**: the platform is gone. Help them pick a supported platform (`--claude` / `--cursor` / `--codex` / etc.) and run `trellis init --<platform>` on the project to add it. `.iflow/` can be manually deleted after.
|
||||
|
||||
5. **After migrate**, run `trellis update` a second time and confirm it says "Already up to date!" — any remaining diff indicates an incomplete migration (usually a skill that the user customized and that needs a manual decision).
|
||||
|
||||
6. **Skill paths in existing task jsonl files**: if the user has active tasks with `implement.jsonl` / `check.jsonl` referencing old paths (e.g. `.kiro/skills/check/SKILL.md` without `trellis-`), those paths now 404. Re-run `task.py init-context <task-dir> <type> --platform <platform>` to regenerate with correct paths.
|
||||
|
||||
7. **Codex feature flag**: remind them to set `features.codex_hooks = true` in `~/.codex/config.toml`, otherwise SessionStart injection is silently skipped and they'll wonder why workflow breadcrumbs don't show up.
|
||||
|
||||
---
|
||||
|
||||
## v0.5.0-beta.5 Migration Guide
|
||||
|
||||
## Sub-Agent Rename: `implement` / `check` / `research` → `trellis-*`
|
||||
|
||||
beta.5 renames the three core sub-agents to add a `trellis-` prefix across all 10 agent-capable platforms (Claude, Cursor, OpenCode, Codex, Kiro, Gemini, Qoder, CodeBuddy, Copilot, Droid). The generic names (`implement`, `check`, `research`) were colliding with user-defined agents in some setups.
|
||||
|
||||
### What changes
|
||||
|
||||
| Old name | New name |
|
||||
|---|---|
|
||||
| `implement` | `trellis-implement` |
|
||||
| `check` | `trellis-check` |
|
||||
| `research` | `trellis-research` |
|
||||
|
||||
Paths per platform:
|
||||
|
||||
- **Claude / OpenCode / Cursor / etc.**: `.<platform>/agents/implement.md` → `.<platform>/agents/trellis-implement.md`
|
||||
- **Codex**: `.codex/agents/implement.toml` → `.codex/agents/trellis-implement.toml`
|
||||
- **Kiro**: `.kiro/agents/implement.json` → `.kiro/agents/trellis-implement.json`
|
||||
|
||||
### Side-effect: removed `model: opus` from agent templates
|
||||
|
||||
Agent YAML frontmatter no longer hardcodes a model. Falls back to whatever the platform default is. If you pinned opus explicitly for a reason (budget, performance), re-add `model: opus` to your local agent files after migrate.
|
||||
|
||||
### How to upgrade
|
||||
|
||||
```bash
|
||||
trellis update --migrate
|
||||
```
|
||||
|
||||
30 rename entries run. Unmodified agent files are renamed silently; customized ones (e.g. you tweaked the agent prompt) land on the standard `Modified by you` confirm prompt — pressing Enter (backup-rename) is safe.
|
||||
|
||||
### What to check after migrate
|
||||
|
||||
1. **Any code that spawns agents by name** — shell scripts, CI jobs, or other automation calling `Task(subagent_type="implement")` must update to `Task(subagent_type="trellis-implement")`.
|
||||
2. **Aliases / hot keys** — if you had a keyboard shortcut or prompt alias that referenced `implement` / `check` / `research`, update it.
|
||||
3. **Model pin** — if you customized `model: opus` in agent frontmatter, re-add it. Trellis no longer pins a model by default.
|
||||
|
||||
### AI Assistant Instructions
|
||||
|
||||
When helping with this migration:
|
||||
|
||||
When helping a user migrate to 0.5.0-beta.5:
|
||||
|
||||
1. **Grep for bare agent names in user code**: look for `Task(subagent_type="implement"|"check"|"research")` in any .md / .ts / .py / shell files. Rename each to the `trellis-` prefixed version.
|
||||
|
||||
2. **Check agent customizations**: diff the user's `.<platform>/agents/{implement,check,research}.*` against the new `trellis-*` templates. If they had custom content, help merge it into the new file.
|
||||
|
||||
3. **Model pinning**: if their agent frontmatter had `model: opus` and they still want it pinned, re-add it after migrate. beta.5 removes the default pin in favor of platform-default.
|
||||
|
||||
4. **Run migrate**: `trellis update --migrate`. Hash-verified renames — pristine files renamed silently, customized files land on the confirm prompt (Enter = backup-rename is safe).
|
||||
|
||||
5. **Verify clean second run**: after migrate, running `trellis update` again should report "Already up to date!". Any diff indicates a rename that didn't complete (user chose skip on a modified file).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "migrate-to-0.5.0-beta.19",
|
||||
"name": "migrate-to-0.5.0-beta.19",
|
||||
"title": "Migrate to v0.5.0-beta.19",
|
||||
"description": "Breaking change migration from v0.4.0 to v0.5.0-beta.19",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": "migration",
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "trellis-update",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-04-30",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": null,
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,113 @@
|
||||
# 诊单编辑对话框:新增「患者业务订单」Tab
|
||||
|
||||
## Goal
|
||||
|
||||
在「编辑诊单」抽屉对话框(`admin/src/views/tcm/diagnosis/edit.vue`)中新增一个 tab,一站式查看该患者的**处方业务订单**(含订单编号、金额、医生、医助、创建人、时间)。直接复用 `admin/src/views/consumer/prescription/order_list.vue` 所用接口 `/tcm.prescriptionOrder/lists`,仅以患者维度过滤。tab 受 `tcm.diagnosis/chufang` 权限控制。
|
||||
|
||||
## Decision Log
|
||||
|
||||
- **D1(数据源)**:处方业务订单 `tcm_prescription_order`(不是支付单 `zyt_order`)。用户明确:「订单信息可以参考这个页面,这个页面接口获取的是所有订单 关联到患者就好了」
|
||||
- **D2(订单范围 / Q1)**:默认跨诊单聚合 + tab 顶部放切换开关,可一键切到"仅本诊单"
|
||||
- **D3(接口)**:复用 `prescriptionOrderLists` (`@/api/tcm.ts:346`),不另起新接口
|
||||
- **D4(列映射 / Q2)**:5 列:订单编号 / 金额 / 医生(`doctor_name`) / 创建人·医助(`creator_name`) / 创建时间。**不**单独显示 `assistant_name`,与创建人合并 → 后端无需新增字段注入
|
||||
- **D5(行操作 / Q3)**:纯只读,不点击跳转、不弹详情、不挂任何按钮
|
||||
|
||||
## What I already know(已查证的事实)
|
||||
|
||||
### 前端
|
||||
- 「编辑诊单」对话框:`admin/src/views/tcm/diagnosis/edit.vue:55` `<el-tabs>`,已含 12 个 tab,规范统一为 `:disabled="!formData.id" lazy` + `v-if="hasPermission([...])"`,子组件用 props 接 `diagnosisId / patientId`
|
||||
- 处方 tab 现用 `tcm.diagnosis/chufang` 权限:`edit.vue:627`
|
||||
- 参考页面:`admin/src/views/consumer/prescription/order_list.vue`(5890 行)
|
||||
- 现有 API:`prescriptionOrderLists` → `/tcm.prescriptionOrder/lists`(`admin/src/api/tcm.ts:346`),已支持 `patient_keyword / order_no / prescription_id / fulfillment_status / prescription_audit_status / payment_slip_audit_status / start_time / end_time / doctor_id / assistant_id / express_keyword`
|
||||
- 列表字段(前端列):订单号、处方ID、诊单ID、收货人、费用类别、金额、内部成本、处方审核、支付审核、履约、关联支付单、创建时间、**开方人 `doctor_name`**、**创建人 `creator_name`**
|
||||
|
||||
### 后端 `server/app/adminapi/lists/tcm/PrescriptionOrderLists.php`
|
||||
- `setSearch()`(line 29-36):当前 `=` 等值过滤仅含 `prescription_id / fulfillment_status / prescription_audit_status / payment_slip_audit_status`,**未含 `diagnosis_id`**
|
||||
- `applyPatientKeywordFilter()`(line 768-785):用 `whereExists` 关联 `tcm_diagnosis`,按 `patient_name LIKE '%kw%' OR phone LIKE '%kw%'` 模糊聚合,**天然跨诊单**,正符合 D2 默认模式
|
||||
- 响应注入字段(line 436-461):`creator_name`、`doctor_name`、`prescription_phone` 已写入;`assistant_id` 已查出(`$assistantByDiag` line 432-434)但**未写入 item**,需要补
|
||||
- 返回 `assistant_name` 还需要再 `Admin::whereIn('id', assistant_ids)->column('name','id')` 一次
|
||||
|
||||
## Assumptions(待确认)
|
||||
|
||||
1. **「患者」匹配键**:用 `phone`(手机号)作 `patient_keyword`。如手机号脱敏不可用,回退用 `patient_name`。需要避免空 phone 误匹配
|
||||
2. **「仅本诊单」开关 = 传 diagnosis_id**:后端补 `setSearch` `'=' => ['diagnosis_id']` 即可
|
||||
3. **三列字段映射**:
|
||||
- 订单编号 → `order_no`
|
||||
- 金额 → `amount`
|
||||
- 医生 → `doctor_name`(开方人,已存在)
|
||||
- 医助 → `assistant_name`(需后端新增注入)
|
||||
- 创建人 → `creator_name`(已存在)
|
||||
- 时间 → `create_time`
|
||||
4. **只读**:tab 内只展示,不嵌业务操作(详情/取消/审核 等都不放)
|
||||
|
||||
## Open Questions
|
||||
|
||||
(已无阻塞问题,三个 Decision 已落定 D2 / D4 / D5)
|
||||
|
||||
## Requirements
|
||||
|
||||
- [ ] **后端** `server/app/adminapi/lists/tcm/PrescriptionOrderLists.php`:
|
||||
- `setSearch()` 在 `'='` 数组里追加 `'diagnosis_id'`,使前端可传 `diagnosis_id` 做精确等值过滤
|
||||
- 不动其他过滤、不动响应字段
|
||||
- [ ] **前端 子组件** `admin/src/views/tcm/diagnosis/components/PatientOrderList.vue`:
|
||||
- props:`diagnosisId: number`、`patientPhone?: string`、`patientName?: string`
|
||||
- 顶部 `el-radio-group`/`el-segmented`:`全部历史(默认) | 仅本诊单` 二选一
|
||||
- 全部历史 → 调 `prescriptionOrderLists({ patient_keyword: phone || patient_name, page_no, page_size })`
|
||||
- 仅本诊单 → 调 `prescriptionOrderLists({ diagnosis_id, page_no, page_size })`
|
||||
- 5 列:订单编号(`order_no`) / 金额(`amount`) / 医生(`doctor_name`) / 创建人(`creator_name`) / 创建时间(`create_time`)
|
||||
- 分页:默认 size=10,可切 10/20/50
|
||||
- 空态:`<el-empty>`
|
||||
- [ ] **前端 入口** `admin/src/views/tcm/diagnosis/edit.vue`:
|
||||
- 在 `<el-tabs>` 内追加 `<el-tab-pane label="业务订单" name="patientOrders" :disabled="!formData.id" lazy v-if="hasPermission(['tcm.diagnosis/chufang'])">`
|
||||
- 内部嵌 `<PatientOrderList v-if="formData.id" :diagnosisId="Number(formData.id)" :patientPhone="originalPhone || formData.phone" :patientName="formData.patient_name" />`,否则 `<el-empty>`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 有 `tcm.diagnosis/chufang` 权限的角色打开任一已保存诊单 → 看到"业务订单"tab
|
||||
- [ ] 默认聚合该患者所有诊单的处方业务订单(按 phone 优先,回退 patient_name 模糊匹配)
|
||||
- [ ] 顶部切换到"仅本诊单"后,列表使用新加的 `diagnosis_id` 等值过滤,仅展示当前诊单的订单
|
||||
- [ ] 5 列正确:`order_no` / `amount` / `doctor_name` / `creator_name` / `create_time`
|
||||
- [ ] 无该权限的用户看不见此 tab
|
||||
- [ ] 新建(未保存)诊单 → tab 不可点
|
||||
- [ ] 既有「处方业务订单」列表页(`consumer/prescription/order_list.vue`)行为完全不变(只新增过滤参数,不改既有过滤)
|
||||
- [ ] 行无任何点击/操作行为
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
**PR1(小步合并,约 30 行后端 + 200 行前端)**
|
||||
|
||||
1. 后端:`PrescriptionOrderLists.php:32` `setSearch()` 在 `'='` 数组追加 `'diagnosis_id'`
|
||||
2. 前端:新增 `admin/src/views/tcm/diagnosis/components/PatientOrderList.vue`
|
||||
3. 前端:`edit.vue` 注册 tab + import + 占位
|
||||
|
||||
**手动验证(无单测框架)**
|
||||
|
||||
- 角色:医生(有 chufang)/ 医助(无 chufang)/ 超管 — 看 tab 显隐与数据范围
|
||||
- 数据:跨诊单同患者、单诊单单订单、无订单 三种场景
|
||||
- 过滤:聚合开关切换两次响应应不同
|
||||
- 回归:旧的 `consumer/prescription/order_list.vue` 列表所有过滤项均正常工作
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- ESLint / vue-tsc 通过;新增字段 TS 类型完整
|
||||
- 后端无 SQL 注入风险(`diagnosis_id` 走 `setSearch` 自动转 int)
|
||||
- 不引入 `console.log`
|
||||
- 既有 `consumer/prescription/order_list.vue` 视觉/交互无回归
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 不展示支付单(`zyt_order`),不在该 tab 内做收款关联
|
||||
- 不在 tab 内做任何写操作(无审核、改价、取消、退款、发货)
|
||||
- 不为本 tab 单独建菜单/权限位(沿用 `tcm.diagnosis/chufang`)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- 关键文件:
|
||||
- `admin/src/views/tcm/diagnosis/edit.vue:55-697`(el-tabs 区)
|
||||
- `admin/src/views/consumer/prescription/order_list.vue`(参考)
|
||||
- `admin/src/api/tcm.ts:346` `prescriptionOrderLists`
|
||||
- `server/app/adminapi/lists/tcm/PrescriptionOrderLists.php:29-36, 430-465, 768-785`
|
||||
- 风险:
|
||||
- `patient_keyword` 是 LIKE 模糊匹配,对短手机号 / 同名同号场景可能误聚合 → 建议优先用完整 11 位手机号
|
||||
- `assistant_name` 注入需要再发一次 `Admin` 查询(很轻量,已有 admin id 集合)
|
||||
- 测试:手动验证四种角色(医生/医助/超管/无权限)+ 三种诊单(无订单/有 1 条/有 N 条跨诊单)
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "tcm-diagnosis-patient-orders-tab",
|
||||
"name": "tcm-diagnosis-patient-orders-tab",
|
||||
"title": "诊单编辑对话框:新增患者历史订单 tab",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-05",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "history-order",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,115 @@
|
||||
# T1+T2 · 二诊只读病例页 + 跟踪矩阵
|
||||
|
||||
> 来源:~/.claude/plans/6-7-fluffy-sunset.md(T1 + T2 节)
|
||||
> 依赖:T4(已完成 — 提供 PatientInfoCard / PatientCaseCard / blood-thresholds.ts);T3(已完成 — 顺手把 unserved_days 用到 readonly 页头部 badge)
|
||||
> 范围:单 PR,前端 + 后端 + 菜单 SQL,整合进入 fix-diagnosis-20250505 分支
|
||||
|
||||
## 用户故事
|
||||
|
||||
二中心医助每天进诊单列表,**双击行**或点**「查看」**按钮 → 直达「只读病例页」:
|
||||
- 患者基本信息 + 完整病例(PatientInfoCard / PatientCaseCard,与接诊台像素级一致)
|
||||
- 跟踪矩阵:日期为列、6 项指标为行(血糖|血压|西药|胰岛素|饮食|运动),同日血糖三段挤一格,超阈值红字 + ↑
|
||||
- 顶部 hero:未服务 X 天的红/橙/绿 badge
|
||||
|
||||
## 后端
|
||||
|
||||
### 接口 `GET /tcm.diagnosis/readonlyDetail?id=:diagnosisId`
|
||||
|
||||
**Validate**:`DiagnosisValidate::sceneReadonlyDetail` → `only(['id'])` + `id` 必填且 `checkDiagnosis`
|
||||
|
||||
**Logic** `DiagnosisLogic::readonlyDetail($params, $adminId, $adminInfo)`:
|
||||
1. 校验诊单存在
|
||||
2. 数据权限:医助 (role_id=2) 仅看 `assistant_id == self`;DataScope 启用时 `assistant_id IN visibleAdminIds`
|
||||
3. 拼装 `diagnosis`(复用 `self::detail` + `AppointmentLogic::enrichDiagnosisLabels`)
|
||||
4. 取最近一条挂号(用于 PatientInfoCard 显示挂号信息);没有就给个默认空对象
|
||||
5. 取最近 30 天的 blood / diet / exercise 三类记录
|
||||
6. 取医生备注(DoctorNoteLogic)
|
||||
7. 计算 unserved_days(= today − MAX(blood.record_date) 天)
|
||||
8. 返回:`{ appointment, diagnosis, blood_records, diet_records, exercise_records, doctor_notes, unserved_days, last_blood_record_at }`
|
||||
|
||||
**Controller**:`DiagnosisController::readonlyDetail()` thin wrapper
|
||||
|
||||
**AppointmentLogic 小改**:`enrichDiagnosisLabels` 由 `private` 改 `public static`,便于 DiagnosisLogic 复用。其它行为不变。
|
||||
|
||||
**菜单 SQL**:`server/sql/1.9.20260420/add_diagnosis_readonly_menu.sql` —— 注册 `tcm.diagnosis/readonlyDetail` 权限节点。
|
||||
|
||||
## 前端
|
||||
|
||||
### 路由
|
||||
`/tcm/diagnosis-readonly?id=:diagnosisId` — 注册在 `admin/src/router/routes.ts` 的 **constantRoutes**(不依赖菜单驱动),用 `LAYOUT` 包裹保留侧边栏;`meta.hidden=true` 不显示在导航;`meta.activeMenu='/tcm/diagnosis'` 高亮诊单父菜单。
|
||||
|
||||
> 实践教训:admin 的动态路由是后端菜单表驱动的(`createRouteRecord` 只看 menu API);因此「隐藏路由」**不能仅靠不加菜单 SQL** 解决,否则路径根本没注册到 vue-router。最稳的做法是写 constantRoutes 静态路由 + 用与父菜单不同的路径前缀(这里用连字符 `-readonly` 而非 `/readonly` 子路径,避免和动态菜单同前缀互相影响)。
|
||||
|
||||
### 入口(在 diagnosis/index.vue 上)
|
||||
- 操作列**最前**新增 `<el-button link>查看</el-button>` → `router.push({ path: '/tcm/diagnosis/readonly', query: { id: row.id } })`
|
||||
- `<el-table @row-dblclick="goReadonly">` 双击同样跳转
|
||||
- 操作列原有按钮加 `@click.stop`(避免冒泡触发双击)
|
||||
|
||||
### 新建 `admin/src/views/tcm/diagnosis/readonly.vue`
|
||||
```
|
||||
┌─ Hero:返回 + 患者姓名 + 「未服务 X 天」 badge(color 同 T3)
|
||||
├─ <PatientInfoCard :apt :diag />
|
||||
├─ <PatientCaseCard :apt :diag />
|
||||
├─ <TrackingMatrix>
|
||||
└─ <NoteTimeline>(可选;先复用 reception 的样式)
|
||||
```
|
||||
|
||||
### 新建 `admin/src/views/tcm/diagnosis/components/TrackingMatrix.vue`
|
||||
|
||||
Props:
|
||||
```ts
|
||||
{
|
||||
diagnosisId: number
|
||||
patientId?: number
|
||||
age?: number | null // 阈值依赖
|
||||
bloodRecords: BloodRecord[]
|
||||
dietRecords: DietRecord[]
|
||||
exerciseRecords: ExerciseRecord[]
|
||||
}
|
||||
```
|
||||
|
||||
渲染规则:
|
||||
| 列 | 来源 | cell |
|
||||
|---|---|---|
|
||||
| 日期 (fixed left) | union(blood/diet/exercise.record_date) desc 去重 | `MM-DD(周X)` |
|
||||
| 血糖 | bloodRecords by date | `空 6.5 餐 8.9 其他 7.2`,缺值 `—`;任一段超阈值 → 红字 + ↑ |
|
||||
| 血压 | bloodRecords by date | `135/90`;超 → 红 + ↑ |
|
||||
| 西药 | bloodRecords.western_medicine | tooltip 全文,cell 截断 |
|
||||
| 胰岛素 | bloodRecords.insulin | 同上 |
|
||||
| 饮食 | dietRecords by date | `已打卡` / `—`;点 cell 弹出当日早/午/晚 |
|
||||
| 运动 | exerciseRecords by date | `已打卡 · 30min · 中强度` / `—`;点弹详情 |
|
||||
|
||||
时间窗:`7 / 30 / 90 / 365 / 自定义`(默认 30);切换时由父组件重新拉数据或客户端过滤。**本期实现简化**:父组件一次拉 30 天,组件本地过滤切换的窗口。
|
||||
|
||||
CSS 类:`.matrix-table` `.cell-blood` `.cell-bp` `.cell-empty` `.cell-high` `.cell-up`(红字 + ↑),文字截断/tooltip 走 el-table 的 `show-overflow-tooltip`。
|
||||
|
||||
阈值判定:`@/utils/blood-thresholds` 的 isHigh*(T4 提供)。
|
||||
|
||||
### API
|
||||
`admin/src/api/tcm.ts` 新增:
|
||||
```ts
|
||||
export function diagnosisReadonlyDetail(params: { id: number }) {
|
||||
return request.get({ url: '/tcm.diagnosis/readonlyDetail', params })
|
||||
}
|
||||
```
|
||||
|
||||
## 不做(边界)
|
||||
|
||||
- 不做评论 / 编辑 / 删除按钮 — 整页**严格只读**
|
||||
- 不引入 `useRouter` 之外的全局状态(页面无状态)
|
||||
- 不做日期窗口的服务端拉取(窗口切换走前端过滤),后续真有量再做
|
||||
- 不做表头排序(矩阵本身是日期 desc,无意义)
|
||||
- 不在菜单里展示路由(不配 system_menu type='C')
|
||||
- DoctorNote 区块复用 reception 的 NoteTimeline 但不绑 refresh 事件(只读)
|
||||
- 跟踪矩阵不展示「备注」字段(跟踪场景看血糖/血压数值即可)
|
||||
|
||||
## 验收
|
||||
|
||||
1. 在诊单列表点「查看」 / 双击行 → 跳到 readonly 页
|
||||
2. PatientInfoCard / PatientCaseCard 视觉与接诊台一致(T4 已确保)
|
||||
3. 跟踪矩阵:同一天血糖三段挤一格,红色 + ↑ 与 reception 视觉一致
|
||||
4. 没有任何打卡时矩阵显示空状态(`el-empty`)
|
||||
5. 编辑/删除按钮全程不存在 — **页面纯只读**
|
||||
6. 越权访问 → 提示无权限,无 500
|
||||
7. 时间窗 7/30/90/365 切换有效
|
||||
8. 操作列其它按钮(编辑/删除/指派)的点击不会触发跳转(`@click.stop`)
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "tcm-diagnosis-readonly-page",
|
||||
"name": "tcm-diagnosis-readonly-page",
|
||||
"title": "二诊只读病例页 + 跟踪矩阵(T1+T2)",
|
||||
"description": "T1+T2:新建只读病例页(路由 tcm/diagnosis/readonly),复用 PatientInfoCard / PatientCaseCard;新建 TrackingMatrix 组件(日期列 + 6 行指标 + 同日血糖三段挤一格);后端 readonlyDetail 接口(复用 reception 拼装 + 30 天 blood/diet/exercise);diagnosis/index.vue 加「查看」按钮 + 双击。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-05",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "fix-diagnosis-20250505",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,106 @@
|
||||
# T4 · 阈值工具 + 病例公共组件抽离
|
||||
|
||||
> 来源:~/.claude/plans/6-7-fluffy-sunset.md(T4 节)
|
||||
> 目标:把 reception/index.vue 中的工具函数和两张「患者信息 / 患者病例」卡片抽成共用模块,为 T1 只读病例页面铺路。
|
||||
> 验收硬指标:reception/index.vue **视觉与功能 0 回归**。
|
||||
|
||||
## 背景
|
||||
|
||||
- T1 二诊只读病例页要复用 reception/index.vue 的「患者信息」「患者病例」两张卡,且 isHigh* 阈值规则要保持一致。
|
||||
- T2 跟踪矩阵也用同一套 isHigh* 函数标红。
|
||||
- 当前两套规则只活在 `reception/index.vue` 内联 → 任何 T1/T2 直接 import 都办不到。
|
||||
|
||||
## 拆分
|
||||
|
||||
### 1) `admin/src/utils/blood-thresholds.ts`(新建)
|
||||
|
||||
从 `reception/index.vue:540-599` 搬出:
|
||||
```ts
|
||||
export function toNumeric(value: unknown): number | null
|
||||
export function getBloodSugarThresholds(age: unknown): { fasting: number; postprandial: number } | null
|
||||
export function isHighFastingBloodSugar(value: unknown, age: unknown): boolean
|
||||
export function isHighPostprandialBloodSugar(value: unknown, age: unknown): boolean
|
||||
export function isHighOtherBloodSugar(value: unknown, age: unknown): boolean
|
||||
export function isHighSystolicPressure(value: unknown): boolean
|
||||
export function isHighDiastolicPressure(value: unknown): boolean
|
||||
export function isHighBloodPressure(row: { systolic_pressure?: unknown; diastolic_pressure?: unknown }): boolean
|
||||
export function formatBp(row: { systolic_pressure?: unknown; diastolic_pressure?: unknown }): string
|
||||
```
|
||||
|
||||
### 2) `admin/src/utils/diag-display.ts`(新建)
|
||||
|
||||
```ts
|
||||
export function maskPhone(phone?: string): string
|
||||
export function formatGender(g?: number): string
|
||||
export function formatPeriod(period?: string): string
|
||||
export function joinArray(arr: unknown): string
|
||||
export function makeTextOf(diagRef: { value: any } | (() => any)): (field: string) => string
|
||||
```
|
||||
|
||||
> `textOf` 在原文件里闭包了 `diag.value`,抽离后做成工厂,由调用方传 ref/getter,避免把 diag 全局化。
|
||||
|
||||
### 3) `admin/src/views/tcm/diagnosis/components/PatientInfoCard.vue`(新建)
|
||||
|
||||
复用 `reception/index.vue:113-127`。
|
||||
|
||||
Props:
|
||||
```ts
|
||||
interface Props {
|
||||
apt: Record<string, any>
|
||||
diag: Record<string, any>
|
||||
}
|
||||
```
|
||||
|
||||
内部组件:函数式 `KVItem` 不需要(这张卡里没用 KVItem)。
|
||||
|
||||
### 4) `admin/src/views/tcm/diagnosis/components/PatientCaseCard.vue`(新建)
|
||||
|
||||
复用 `reception/index.vue:129-233`。
|
||||
|
||||
Props:
|
||||
```ts
|
||||
interface Props {
|
||||
apt: Record<string, any>
|
||||
diag: Record<string, any>
|
||||
}
|
||||
```
|
||||
|
||||
内部组件:把 `KVItem` / `MetricKVItem`(reception 376-420 行的函数式组件)一起搬过来。
|
||||
内部 computed:`caseTypeLabel`。
|
||||
工具函数:从 utils 引入。
|
||||
|
||||
### 5) `reception/index.vue` 修改
|
||||
|
||||
- import 新工具:`@/utils/blood-thresholds`、`@/utils/diag-display`
|
||||
- import 新组件:`PatientInfoCard` / `PatientCaseCard`
|
||||
- 删除:540-599 行的 isHigh* / toNumeric / formatBp / joinArray / getBloodSugarThresholds(共 60 行)
|
||||
- 删除:376-420 行的内联 KVItem / MetricKVItem(共 ~45 行)
|
||||
- 删除:maskPhone / formatGender / formatPeriod(utils 通用化后 reception 也用 import)
|
||||
- 删除:caseTypeLabel computed(搬到 PatientCaseCard 内)
|
||||
- 替换:113-233 行两个 card div → `<PatientInfoCard>` `<PatientCaseCard>`
|
||||
- 保留:`formatGenderAge`(队列里的列表用,跟 card 无关)、`textOf`(仍要在 reception 里被用到 case card 之外的地方)
|
||||
|
||||
> 实际只要 reception/index.vue 现存的 isHigh* 用法 / KVItem 用法 / 工具函数引用 全部消失,剩下只用到 utils 和子组件即可。
|
||||
|
||||
### 6) 不修改
|
||||
|
||||
- `BloodRecordList.vue` / `DietRecordList.vue` 等已有列表组件(它们用各自的内联实现,T4 不动它们 —— 后续可以渐进迁移)。
|
||||
- 对外 API、模板布局、CSS 类名 全部保持。
|
||||
|
||||
## 关键风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| 两张 card 视觉回归 | reception 用与抽离前完全一致的 props 调用;CSS 类名(`.card`/`.case-card`/`.patient-card`/`.kv-grid`/`.grid-block` 等)原样搬到子组件里 `<style scoped>` 中 |
|
||||
| KVItem / MetricKVItem 函数式组件 type 漂移 | 子组件内部继续用 `h()` 函数式定义,签名不变 |
|
||||
| textOf 在 reception 顶部还有别的引用? | 抽离前 grep 全文,凡是 reception 模板里非 case-card 区域用到的,由 reception 自己保留 textOf 闭包 |
|
||||
| caseTypeLabel 移走后影响外部? | reception 模板里只在 case-card 里使用,搬走安全 |
|
||||
| 类型声明完整性 | 6 个 isHigh* + formatBp / joinArray 都加 `unknown` 入参 + 严格 narrow,避免 any 漏判 |
|
||||
|
||||
## 验收
|
||||
|
||||
1. **视觉**:reception/index.vue 接诊页 patient card / case card 像素级一致(手动 vs git diff 对比)
|
||||
2. **类型**:`npx vue-tsc --noEmit` 无新增错误(T4 文件 0 报错)
|
||||
3. **功能**:reception 一遍走通:选患者、看 case card、看血糖记录红字标记、点完成接诊
|
||||
4. **可复用性**:T1 只读病例页能直接 `import { PatientInfoCard, PatientCaseCard } from '...'` 使用
|
||||
5. **回归 grep**:`grep -n "isHighFastingBloodSugar\|KVItem\b" reception/index.vue` 应该只剩 `import` 语句
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "tcm-diagnosis-shared-extract",
|
||||
"name": "tcm-diagnosis-shared-extract",
|
||||
"title": "阈值工具与病例公共组件抽离(T4)",
|
||||
"description": "T4:把 reception/index.vue 的 isHigh* 6 函数抽到 admin/src/utils/blood-thresholds.ts;把「患者信息」「患者病例」两张 card 抽到 components/{PatientInfoCard,PatientCaseCard}.vue,reception/index.vue 改为引用新文件。视觉/功能 0 回归。为 T1+T2 铺路。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-05",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "fix-diagnosis-20250505",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,112 @@
|
||||
# T5 · 诊单待办事项 + 企微定时推送
|
||||
|
||||
> 来源:~/.claude/plans/6-7-fluffy-sunset.md(T5 节)
|
||||
> 范围:单个 PR、全栈,独立于 T1–T4,可单独并行上线。
|
||||
|
||||
## 用户故事
|
||||
|
||||
二中心医助希望在诊单上挂一些「将来要做的事」(例:3 天后回访、2 小时后提醒患者复测血糖),到点由企业微信推送给**创建人本人**(不是患者、不是当前指派医助)。失败一次即终态,由人工取消重建,不做自动重试。
|
||||
|
||||
## 范围
|
||||
|
||||
- 「待办事项」tab 出现在诊单编辑页 (`tcm/diagnosis/edit.vue`) — 在「运动打卡记录」之后。
|
||||
- 待办列表(历史 + 当前一锅端,按 `remind_time desc`,分页)。
|
||||
- 新建:选时间 + 输入内容;只允许 `remind_time > now`。
|
||||
- 取消:仅 `status=0` 可取消;只允许创建人本人或超管,按已有 role 边界判定。
|
||||
- 不实现编辑(要改 = 取消重建)。
|
||||
- 不实现提前 N 分钟提醒、不实现自动重试。
|
||||
- 列表本身不做轮询,进 tab 拉一次。
|
||||
|
||||
## 数据模型
|
||||
|
||||
新建表 `zyt_tcm_diagnosis_todo`:
|
||||
|
||||
| 字段 | 类型 | 备注 |
|
||||
|---|---|---|
|
||||
| `id` | int unsigned PK auto_increment | |
|
||||
| `diagnosis_id` | int unsigned | 所属诊单 |
|
||||
| `patient_id` | int unsigned | 冗余便于反查 |
|
||||
| `content` | varchar(500) | 通知内容 |
|
||||
| `remind_time` | int(10) unsigned | 提醒时间戳(秒) |
|
||||
| `status` | tinyint | 0=待执行 / 1=已发送 / 2=已取消 / 3=发送失败 |
|
||||
| `creator_id` | int unsigned | admin.id |
|
||||
| `creator_name` | varchar(64) | 冗余,免 join |
|
||||
| `notified_at` | int(10) unsigned NULL | 实际推送时间(成功时写) |
|
||||
| `error` | varchar(500) NULL | 失败原因 |
|
||||
| `cancelled_at` | int(10) unsigned NULL | |
|
||||
| `cancelled_by` | int unsigned NULL | |
|
||||
| `create_time` / `update_time` / `delete_time` | int(10) unsigned | 软删 + 整型时间戳 |
|
||||
|
||||
索引:
|
||||
- `idx_diagnosis_id (diagnosis_id)`
|
||||
- `idx_creator_id (creator_id)`
|
||||
- `idx_status_remind_time (status, remind_time)` —— cron 扫表核心索引
|
||||
|
||||
迁移文件:`server/sql/1.9.20260420/add_tcm_diagnosis_todo.sql`
|
||||
|
||||
## API 路径
|
||||
|
||||
```
|
||||
GET /tcm.diagnosisTodo/lists?diagnosis_id=:id 分页列表
|
||||
POST /tcm.diagnosisTodo/add { diagnosis_id, content, remind_time }
|
||||
POST /tcm.diagnosisTodo/cancel { id }
|
||||
GET /tcm.diagnosisTodo/detail?id=:id
|
||||
```
|
||||
|
||||
权限节点(`add_diagnosis_todo_menu.sql`,按钮型菜单挂在「诊单管理」下):
|
||||
- `tcm.diagnosisTodo/lists`
|
||||
- `tcm.diagnosisTodo/add`
|
||||
- `tcm.diagnosisTodo/cancel`
|
||||
|
||||
## 后端文件
|
||||
|
||||
- `server/app/common/model/tcm/DiagnosisTodo.php`
|
||||
- `server/app/adminapi/controller/tcm/DiagnosisTodoController.php`
|
||||
- `server/app/adminapi/logic/tcm/DiagnosisTodoLogic.php`
|
||||
- `server/app/adminapi/lists/tcm/DiagnosisTodoLists.php`
|
||||
- `server/app/adminapi/validate/tcm/DiagnosisTodoValidate.php`
|
||||
- `server/app/command/DiagnosisTodoNotify.php`
|
||||
|
||||
## Command 行为(`tcm:diagnosis-todo-notify`)
|
||||
|
||||
每次执行:
|
||||
1. `select * from zyt_tcm_diagnosis_todo where status=0 AND remind_time<=now AND delete_time IS NULL order by remind_time asc limit 200`
|
||||
2. 对每条:
|
||||
- 乐观锁更新:`update set status=1, notified_at=now where id=? and status=0` —— 如果影响行 0,说明被其它进程抢走,跳过。
|
||||
- 取 `creator_id` → `Admin::find($creator_id)->work_wechat_userid`,没绑定时记录 `error='创建人未绑定企业微信'` 并置 `status=3`。
|
||||
- 推文本:`【患者跟踪提醒】\n患者:{patient_name}\n时间:{remind_time_text}\n内容:{content}\n— 二中心跟踪系统`
|
||||
- 调 `WechatWorkAppMessageService::sendTextToUser($wxId, $text)`;失败 → `status=3, error=<msg>`。
|
||||
3. 每条 try-catch,整批不中断;输出 `处理 N / 成功 X / 失败 Y / 跳过未绑定 Z`。
|
||||
4. 注册到 `server/config/console.php`:`'tcm:diagnosis-todo-notify' => 'app\\command\\DiagnosisTodoNotify'`
|
||||
5. 用户在服务器 crontab 自挂:`* * * * * cd /path && php think tcm:diagnosis-todo-notify >> /var/log/zyt-todo.log 2>&1`
|
||||
|
||||
## 前端文件
|
||||
|
||||
- 新建:`admin/src/views/tcm/diagnosis/components/DiagnosisTodoList.vue`
|
||||
- 修改:`admin/src/views/tcm/diagnosis/edit.vue`(在 `name="exercise"` 之后追加 tab)
|
||||
- 修改:`admin/src/api/tcm.ts`(加 4 个函数)
|
||||
|
||||
UI:
|
||||
- 顶部「+ 新增待办」按钮 → `el-dialog` (`el-date-picker type=datetime` + `el-input type=textarea`)。
|
||||
- 表格列:提醒时间 / 内容 / 状态 chip / 创建人 / 推送时间 / 失败原因 / 操作。
|
||||
- 状态 chip:`info` (待执行) / `success` (已发送) / `warning` (已取消) / `danger` (失败)。
|
||||
- 操作列:`status===0` 显「取消」二次确认;其它隐藏。
|
||||
- 排序:`remind_time desc`;分页。
|
||||
|
||||
## 验收清单(对应计划文件 T5 验收)
|
||||
|
||||
1. [x] 在某诊单编辑页 → 待办事项 tab → 新建一条「2 分钟后,内容=测试」→ 表格出现「待执行」。
|
||||
2. [x] 等 2 分钟,手动跑 `php think tcm:diagnosis-todo-notify`,企微收到文本;状态变「已发送」、推送时间已写。
|
||||
3. [x] 把 admin `work_wechat_userid` 清空,再创建一条,跑命令 → 状态「失败」+ `error` 列显示原因;不会再次重试。
|
||||
4. [x] 创建一条 1 小时后的 → 立即点「取消」→ 状态变「已取消」、操作列消失;再跑命令也不会推。
|
||||
5. [x] 切换到该诊单的另一名 admin(非创建人)→ 看见这条但**没有取消按钮**。
|
||||
6. [x] 历史 / 当前同表,按时间倒序,分页正常。
|
||||
7. [x] `tcm:diagnosis-todo-notify` 二次执行不重复推送同一条(乐观锁验证)。
|
||||
|
||||
## 不做(边界)
|
||||
|
||||
- 不实现「提前 N 分钟提醒」逻辑("时间到了"做精准触发即可)。
|
||||
- 不实现重试退避 —— 失败即终态。
|
||||
- 不引入 SSE / WebSocket(待办列表不实时刷新,分钟级 cron 已够用)。
|
||||
- 不修改已有的 `BloodRecordList.vue / DietRecordList.vue / ExerciseRecordList.vue`。
|
||||
- 不与 T1–T4 共用任何文件之外的逻辑(这些子任务是独立 PR)。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "tcm-diagnosis-todo",
|
||||
"name": "tcm-diagnosis-todo",
|
||||
"title": "诊单待办事项 + 企微定时推送(T5)",
|
||||
"description": "T5: 在 tcm/diagnosis/edit.vue 新增「待办事项」tab,新建 zyt_tcm_diagnosis_todo 表 + 后端增删查改 + PHP command 扫表企微推送。具体方案见 ~/.claude/plans/6-7-fluffy-sunset.md T5 节。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-05",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "fix-diagnosis-20250505",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,87 @@
|
||||
# T3 · 诊单列表「未服务天数」
|
||||
|
||||
> 来源:~/.claude/plans/6-7-fluffy-sunset.md(T3 节)— 拆分版
|
||||
> 范围:本 PR 仅完成 **后端字段 + 前端列**;「查看」按钮 + 行双击留到 T1+T2 上线时一并加(避免在 readonly 路由不存在的窗口期产生 404)。
|
||||
|
||||
## 用户故事
|
||||
|
||||
二中心医助每天开诊单列表时,要一眼看出「这个患者多少天没打卡血糖」,便于优先回访。**口径**:未服务天数 = 当天 − 该患者最近一条血糖记录日期(天,向下取整)。以血糖(`zyt_tcm_blood_record.record_date` MAX)为唯一锚点,不混合其它打卡表。
|
||||
|
||||
## 后端改动
|
||||
|
||||
文件:`server/app/adminapi/lists/tcm/DiagnosisLists.php`
|
||||
|
||||
在 `lists()` 末尾(拼装完所有其它字段之后):
|
||||
|
||||
```php
|
||||
$diagIds = array_filter(array_unique(array_column($lists, 'id')));
|
||||
$maxRows = [];
|
||||
if (!empty($diagIds)) {
|
||||
$maxRows = BloodRecord::whereIn('diagnosis_id', $diagIds)
|
||||
->whereNull('delete_time')
|
||||
->group('diagnosis_id')
|
||||
->column('MAX(record_date)', 'diagnosis_id');
|
||||
}
|
||||
$now = time();
|
||||
foreach ($lists as &$item) {
|
||||
$last = (int) ($maxRows[(int) $item['id']] ?? 0);
|
||||
$item['last_blood_record_at'] = $last > 0 ? date('Y-m-d', $last) : null;
|
||||
$item['unserved_days'] = $last > 0 ? max(0, (int) floor(($now - $last) / 86400)) : null;
|
||||
}
|
||||
```
|
||||
|
||||
字段语义:
|
||||
- `last_blood_record_at`:`Y-m-d` 字符串,无打卡 → `null`
|
||||
- `unserved_days`:整数,无打卡 → `null`
|
||||
|
||||
## 前端改动
|
||||
|
||||
文件:`admin/src/views/tcm/diagnosis/index.vue`
|
||||
|
||||
在「开方」列后、「视频旁观」列前,新增列:
|
||||
|
||||
```html
|
||||
<el-table-column label="未服务天数" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="unservedDaysClass(row.unserved_days)">
|
||||
{{ row.unserved_days ?? '—' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
```
|
||||
|
||||
颜色规则(计划原文):
|
||||
- `null/undefined` → 灰
|
||||
- `>=7` → 红
|
||||
- `3-6` → 橙
|
||||
- `<=2` → 绿
|
||||
|
||||
```ts
|
||||
const unservedDaysClass = (n: unknown) => {
|
||||
if (n === null || n === undefined) return 'unserved-muted'
|
||||
const num = Number(n)
|
||||
if (!Number.isFinite(num)) return 'unserved-muted'
|
||||
if (num >= 7) return 'unserved-red'
|
||||
if (num >= 3) return 'unserved-orange'
|
||||
return 'unserved-green'
|
||||
}
|
||||
```
|
||||
|
||||
CSS 加 4 个 class(红 #dc2626 / 橙 #ea580c / 绿 #16a34a / 灰 #94a3b8)。
|
||||
|
||||
## 不做(边界)
|
||||
|
||||
- 不加「查看」按钮(依赖 T1 readonly 页)
|
||||
- 不加 `@row-dblclick`(同上)
|
||||
- 不加 `?sort=unserved_days&order=desc` 排序白名单(这是计划原文的可选项;当前没有用户反馈需要排序,先简化)
|
||||
- 不动 `count()` 函数(仅展示字段,不影响过滤/分页)
|
||||
|
||||
## 验收
|
||||
|
||||
1. 在诊单列表里能看到「未服务天数」列
|
||||
2. 没有任何血糖打卡的诊单 → 显示 `—`(灰)
|
||||
3. 7 天前打过卡 → 红色(如 9)
|
||||
4. 3-6 天前打过卡 → 橙色
|
||||
5. ≤ 2 天打过卡 → 绿色
|
||||
6. 列表分页 / 搜索 / Tab 切换不受影响
|
||||
7. 性能:单页面查询多一次 `WHERE IN + GROUP BY` 走 `idx_diagnosis_id`,N+1 不会出现
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "tcm-diagnosis-unserved-days",
|
||||
"name": "tcm-diagnosis-unserved-days",
|
||||
"title": "诊单列表「未服务天数」(T3)",
|
||||
"description": "T3:DiagnosisLists.php 给每行追加 last_blood_record_at + unserved_days;admin 诊单列表加列。说明:「查看」按钮 + 双击 等 T1+T2 上线时一起加(避免链向不存在的路由)。",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-05",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "fix-diagnosis-20250505",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-conversion-overview-mysql8/research/current-findings.md","reason":"Use the documented MySQL 8 compatibility risks and log evidence to review only the relevant SQL paths in ConversionLogic.php."}
|
||||
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-conversion-overview-mysql8/research/current-findings.md","reason":"Summarizes the actual MySQL 8 log error and the current risk points inside ConversionLogic.php so the implement agent can focus on the likely incompatible SQL paths."}
|
||||
@@ -0,0 +1,57 @@
|
||||
# conversion overview mysql8 compatibility
|
||||
|
||||
## Goal
|
||||
|
||||
修复 `adminapi/stats.conversion/overview` 在数据库切换到 MySQL 8.0 后的 SQL 不兼容问题,并在不引入缓存的前提下优化该方法中的慢查询,保证 `dimension=dept` 与 `include_filters=1` 场景可正常且更快地查询。
|
||||
|
||||
## What I already know
|
||||
|
||||
* 主逻辑在 `server/app/adminapi/logic/stats/ConversionLogic.php`
|
||||
* 运行日志里已有一条 MySQL 语法错误记录:
|
||||
`SQLSTATE[42000]: Syntax error or access violation: 1064 ... near '( q.follow_admin_ids, '$[*]' COLUMNS (...)'`
|
||||
* 当前 `ConversionLogic.php` 已不再包含 `JSON_TABLE`,说明兼容修复可能做过一半,仍需核对当前文件中的 MySQL 8 风险点
|
||||
* 当前文件仍有多处 `fieldRaw + group` 聚合查询,MySQL 8 默认 `ONLY_FULL_GROUP_BY` 更严格,需要重点检查
|
||||
* 当前文件使用了表名 `order`、`whereRaw`、多处左连接与聚合,均可能触发 MySQL 8 兼容性问题
|
||||
* 历史运行日志显示慢点主要集中在:
|
||||
* `hydrateFanStats`,多次达到 `0.7s ~ 2.4s`
|
||||
* `include_filters=1` 时的 `buildFilterOptions -> DiagnosisLogic::getAssistants`,曾到 `18s+`
|
||||
|
||||
## Assumptions (temporary)
|
||||
|
||||
* 当前用户提到的“不兼容”针对的是 `stats.conversion/overview` 当前版本,不要求顺带修复其他统计模块
|
||||
* 优先修复服务端 SQL 兼容性,并优化 SQL 结构,不改接口出参结构
|
||||
* 若发现旧版遗留的 JSON_TABLE 逻辑已被替换,则以当前代码中的聚合、NULL 比较、保留字、原生 SQL 片段为修复重点
|
||||
|
||||
## Open Questions
|
||||
|
||||
* 无。已有日志和代码足够开始修复。
|
||||
|
||||
## Requirements
|
||||
|
||||
* 审查并修复 `ConversionLogic.php` 中 MySQL 8.0 不兼容 SQL
|
||||
* 保证 `overview?page_no=1&page_size=15&dimension=dept&media_channel_code=&time_type=today&include_filters=1` 可执行
|
||||
* 优化该接口中的主要慢 SQL,重点关注 `hydrateFanStats` 和 `include_filters=1` 时的筛选项查询
|
||||
* 避免引入新的统计口径变化
|
||||
* 尽量将原生 SQL 改为更稳定的 Builder 写法,或改成 MySQL 8 兼容表达式
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [ ] `ConversionLogic` 中不再包含已知 MySQL 8 不兼容写法
|
||||
* [ ] 关键聚合查询在 MySQL 8 的 `ONLY_FULL_GROUP_BY` 下可通过
|
||||
* [ ] 至少一个已知慢点的 SQL 结构得到收敛优化
|
||||
* [ ] 不改变现有接口结构和主要业务语义
|
||||
* [ ] 至少完成 PHP 语法检查
|
||||
|
||||
## Out of Scope
|
||||
|
||||
* 其他统计模块(如 `YejiStatsLogic`、`DoctorDailyStatsLogic`)的整体兼容性修复
|
||||
* 前端页面样式或交互修改
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* 目标文件:`server/app/adminapi/logic/stats/ConversionLogic.php`
|
||||
* 参考日志:`server/runtime/adminapi/log/202604/22.log`
|
||||
|
||||
## Research References
|
||||
|
||||
* [`research/current-findings.md`](research/current-findings.md) — 当前代码与历史报错线索
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# 当前排查结论
|
||||
|
||||
## 已确认事实
|
||||
|
||||
* `stats.conversion/overview` 的主实现位于 `server/app/adminapi/logic/stats/ConversionLogic.php`
|
||||
* 历史日志 `server/runtime/adminapi/log/202604/22.log` 中存在 MySQL 1064:
|
||||
`near '( q.follow_admin_ids, '$[*]' COLUMNS (...)'`
|
||||
* 当前 `ConversionLogic.php` 中已经搜索不到 `JSON_TABLE`,说明旧问题可能已被部分替换
|
||||
|
||||
## 当前文件中的 MySQL 8 风险点
|
||||
|
||||
* 多处 `fieldRaw(...) + group(...)` 聚合
|
||||
* 多处 `whereRaw(...)`
|
||||
* 使用保留字表名 `order`
|
||||
* `include_filters=1` 会额外触发筛选项构建,可能带出更多 SQL 路径
|
||||
|
||||
## 已观察到的性能热点
|
||||
|
||||
* `server/runtime/adminapi/log/202604/22.log` 中:
|
||||
* `hydrateFanStats cost` 多次达到 `0.7s ~ 2.4s`
|
||||
* `hydrateAppointmentStats` / `hydrateOrderAndAmountStats` 通常约 `0.08s ~ 0.13s`
|
||||
* `include_filters=1` 时 `buildFilterOptions -> DiagnosisLogic::getAssistants` 曾达到 `18s+`
|
||||
* 因此本次 SQL 优化应优先关注:
|
||||
* `ConversionLogic::hydrateFanStats()`
|
||||
* `ConversionLogic::buildFilterOptions()` 触发的医助筛选项路径
|
||||
|
||||
## 本次修复策略
|
||||
|
||||
* 以 `ConversionLogic.php` 为主;若 `include_filters=1` 的真实瓶颈仍在 `DiagnosisLogic::getAssistants()`,允许一并做局部 SQL 优化
|
||||
* 优先处理:
|
||||
* `ONLY_FULL_GROUP_BY` 风险
|
||||
* 原生 SQL / NULL 比较兼容性
|
||||
* Builder 可替换的 `whereRaw`
|
||||
* 与 `order` 表相关的潜在保留字问题
|
||||
* 明确慢点 SQL 的结构优化,避免全表扫描/不必要 join/重复聚合
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "conversion-overview-mysql8",
|
||||
"name": "conversion-overview-mysql8",
|
||||
"title": "conversion overview mysql8 compatibility",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-07",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "long-0507",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-diagnosis-business-order-no-data-scope/research/prescription-order-permission-scope.md","reason":"Check agent should verify the scoped bypass only affects diagnosis-edit patient-order queries."}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-diagnosis-business-order-no-data-scope/research/prescription-order-permission-scope.md","reason":"Implementation needs the verified permission-filter locations and the agreed scoped bypass approach."}
|
||||
@@ -0,0 +1,41 @@
|
||||
# 诊单编辑业务订单 tab 去除数据权限
|
||||
|
||||
## Goal
|
||||
|
||||
修正「编辑诊单」对话框中的“业务订单”tab:获取订单数据时不再受业务订单列表页的数据权限限制,只要订单与当前诊单患者相关,就应展示出来。
|
||||
|
||||
## What I verified
|
||||
|
||||
- 前端组件是 [admin/src/views/tcm/diagnosis/components/PatientOrderList.vue](/Users/long/Work/zyt/admin/src/views/tcm/diagnosis/components/PatientOrderList.vue),直接调用 `prescriptionOrderLists`。
|
||||
- 路由仍是 `GET /tcm.prescriptionOrder/lists`,控制器 [server/app/adminapi/controller/tcm/PrescriptionOrderController.php](/Users/long/Work/zyt/server/app/adminapi/controller/tcm/PrescriptionOrderController.php) 只是透传给 `PrescriptionOrderLists`。
|
||||
- 真正的权限过滤在 [server/app/adminapi/lists/tcm/PrescriptionOrderLists.php](/Users/long/Work/zyt/server/app/adminapi/lists/tcm/PrescriptionOrderLists.php):
|
||||
- `applyCreatorOrOwnPrescriptionVisibility()`
|
||||
- `applyDataScopeForPrescriptionOrder()`
|
||||
- 当前诊单 tab 已经按 `patient_id` 过滤,且支持 `diagnosis_id` 精确过滤,因此即使跳过列表权限,也仍然只会落在“当前患者/当前诊单关联订单”的安全范围内。
|
||||
|
||||
## Decision
|
||||
|
||||
- 不新增独立接口,继续复用 `/tcm.prescriptionOrder/lists`。
|
||||
- 仅为“诊单编辑业务订单 tab”增加一个显式场景参数,例如 `scene=diagnosis_edit`。
|
||||
- 只有在该场景下,`PrescriptionOrderLists` 才跳过业务订单列表页的两层权限过滤。
|
||||
- 其它页面继续保持原有权限逻辑,不改行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 前端业务订单 tab 请求列表时附带场景参数,标记这是诊单编辑页内的患者订单查询。
|
||||
- 后端列表类识别该场景后,不执行:
|
||||
- 创建人/开方人可见性过滤
|
||||
- 数据范围过滤
|
||||
- 后端仍保留患者/诊单过滤逻辑,不允许无条件返回全量订单。
|
||||
- 原订单管理页、统计页、其它依赖 `/tcm.prescriptionOrder/lists` 的页面行为不变。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- 打开任意诊单编辑页,“业务订单”tab 可以看到该患者实际关联的业务订单,不因当前登录人不是订单创建人或不在数据范围内而丢失数据。
|
||||
- 切换“仅本诊单”时,只显示当前诊单关联的业务订单。
|
||||
- 业务订单主列表页权限行为不变。
|
||||
- 不新增新的权限点,不修改现有 tab 显隐权限。
|
||||
|
||||
## Risks
|
||||
|
||||
- 如果跳过权限的判断条件不够收敛,可能误影响订单管理页;因此必须使用显式场景参数,不要靠 `patient_id`/`diagnosis_id` 猜测场景。
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Findings
|
||||
|
||||
- Diagnosis edit dialog business-order tab uses `prescriptionOrderLists` from `admin/src/api/tcm.ts`.
|
||||
- The request is rendered by `admin/src/views/tcm/diagnosis/components/PatientOrderList.vue`.
|
||||
- Backend controller `server/app/adminapi/controller/tcm/PrescriptionOrderController.php` forwards to `PrescriptionOrderLists`.
|
||||
- Visibility restrictions are applied in `server/app/adminapi/lists/tcm/PrescriptionOrderLists.php` by:
|
||||
- `applyCreatorOrOwnPrescriptionVisibility()`
|
||||
- `applyDataScopeForPrescriptionOrder()`
|
||||
- Patient scoping is already constrained through:
|
||||
- `applyPatientIdFilter()`
|
||||
- built-in `diagnosis_id` equality search
|
||||
|
||||
# Recommended change
|
||||
|
||||
Introduce an explicit scene flag for diagnosis-edit patient-order queries and bypass only the list visibility filters in that scene, while keeping patient/diagnosis filters intact.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "diagnosis-business-order-no-data-scope",
|
||||
"name": "diagnosis-business-order-no-data-scope",
|
||||
"title": "诊单编辑业务订单去除数据权限",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-07",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "long-0507",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-diagnosis-daily-matrix-tab/research/implementation-plan.md","reason":"Use the repo-local plan summary to review whether the new DailyMatrix tab fully replaces the four old tabs and preserves the agreed interaction behavior."}
|
||||
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-diagnosis-daily-matrix-tab/research/implementation-plan.md","reason":"Summarizes the external implementation plan into repo-local task context for the implement agent, including component boundaries, interaction rules, and file targets."}
|
||||
@@ -0,0 +1,73 @@
|
||||
# 诊单编辑:日历矩阵汇总 Tab
|
||||
|
||||
## Goal
|
||||
|
||||
在 `admin/src/views/tcm/diagnosis/edit.vue` 中,把现有“血糖血压记录 / 饮食记录 / 运动打卡记录 / 待办事项”4 个独立 tab,替换为 1 个“日常记录”汇总 tab,使用日期矩阵视图统一展示并支持点击新增/编辑。
|
||||
|
||||
## What I already know
|
||||
|
||||
* 当前相关组件位于:
|
||||
* `admin/src/views/tcm/diagnosis/components/BloodRecordList.vue`
|
||||
* `admin/src/views/tcm/diagnosis/components/DietRecordList.vue`
|
||||
* `admin/src/views/tcm/diagnosis/components/ExerciseRecordList.vue`
|
||||
* `admin/src/views/tcm/diagnosis/components/DiagnosisTodoList.vue`
|
||||
* 现有只读矩阵参考实现位于:
|
||||
* `admin/src/views/tcm/diagnosis/components/TrackingMatrix.vue`
|
||||
* `admin/src/views/tcm/diagnosis/readonly.vue`
|
||||
* 现有接口已具备:
|
||||
* `diagnosisTrackingWindow`
|
||||
* `bloodRecordAdd / bloodRecordEdit / bloodRecordDetail`
|
||||
* `dietRecordAdd / dietRecordEdit`
|
||||
* `exerciseRecordAdd / exerciseRecordEdit`
|
||||
* 现有阈值工具可复用:`admin/src/utils/blood-thresholds.ts`
|
||||
|
||||
## User Decisions
|
||||
|
||||
* 完全替换原 4 个 tab,不并存
|
||||
* 默认最近 7 天
|
||||
* 待办事项不进矩阵,作为独立列表放矩阵下方
|
||||
* 点击单元格后用对话框编辑/新增,不做内联编辑
|
||||
|
||||
## Requirements
|
||||
|
||||
* 新建 `admin/src/views/tcm/diagnosis/components/DailyMatrix.vue`
|
||||
* `edit.vue` 中删除原 4 个 tab,引入 1 个“日常记录”tab
|
||||
* 矩阵横向按天,纵向按指标,空单元格也可点击新增
|
||||
* 最近 7 天场景下,表格应尽量填满可用宽度,避免右侧大块空白
|
||||
* 默认选择最近 7 天
|
||||
* 在待办事项上方新增折线图区域,时间范围与上方矩阵联动
|
||||
* 折线图先展示两条曲线:空腹血糖、餐后血糖
|
||||
* 权限统一收口:删除原血糖/饮食/运动相关按钮权限,新增 1 个“日常记录”权限,待办查看/操作权限也归入该权限域
|
||||
* 待办事项沿用 `DiagnosisTodoList`,放在矩阵下方
|
||||
* 支持 7 天 / 30 天 / 自定义日期范围
|
||||
* 支持 `viewOnly` 模式下禁用点击和新增
|
||||
* 不删除旧组件文件,仅停止在 `edit.vue` 中使用
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [ ] `edit.vue` 中原 4 个 tab 不再显示
|
||||
* [ ] 新 tab “日常记录”可在诊单保存后显示矩阵
|
||||
* [ ] 默认最近 7 天,可切换 30 天和自定义范围
|
||||
* [ ] 最近 7 天时矩阵不会在右侧留下明显大块空白
|
||||
* [ ] 待办事项上方折线图可随时间范围联动刷新
|
||||
* [ ] 日常记录相关前端权限判断与菜单权限已统一,不再散落在旧血糖/饮食/运动/待办权限上
|
||||
* [ ] 点击空单元格可新增,点击已有单元格可编辑
|
||||
* [ ] 待办事项在矩阵下方正常显示
|
||||
* [ ] `viewOnly` 模式下矩阵不可编辑
|
||||
|
||||
## Out of Scope
|
||||
|
||||
* 不修改后端 trackingWindow 接口
|
||||
* 不删除旧组件文件
|
||||
* 不重做只读页 `readonly.vue` 的整体结构
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* 新建主组件:`admin/src/views/tcm/diagnosis/components/DailyMatrix.vue`
|
||||
* 修改入口页:`admin/src/views/tcm/diagnosis/edit.vue`
|
||||
* 参考实现:`admin/src/views/tcm/diagnosis/components/TrackingMatrix.vue`
|
||||
* 复用阈值工具:`admin/src/utils/blood-thresholds.ts`
|
||||
|
||||
## Research References
|
||||
|
||||
* [`research/implementation-plan.md`](research/implementation-plan.md) — 从外部计划整理出的实施要点
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# 实施要点
|
||||
|
||||
## 新组件
|
||||
|
||||
新建 `admin/src/views/tcm/diagnosis/components/DailyMatrix.vue`,负责:
|
||||
|
||||
* 顶部工具栏:7 天 / 30 天 / 自定义 / 新增按钮 / 刷新
|
||||
* 矩阵表格:左侧指标固定列,右侧日期列动态生成
|
||||
* 3 个 dialog:血糖、饮食、运动
|
||||
* 底部待办:复用 `DiagnosisTodoList`
|
||||
|
||||
## edit.vue 改动
|
||||
|
||||
* 删除 4 个旧 tab 的挂载
|
||||
* 删除对应 import
|
||||
* 插入 1 个“日常记录”tab,空诊单时显示 `el-empty`
|
||||
* 新增 `DailyMatrix` import
|
||||
|
||||
## 数据来源
|
||||
|
||||
* `diagnosisTrackingWindow` 一次拉取血糖/饮食/运动数据
|
||||
* 前端按日期归并成 `bloodByDate / dietByDate / exerciseByDate`
|
||||
* 空日期也要生成列
|
||||
* 折线图可直接复用同一批 `blood_records` 数据,无需新增接口
|
||||
|
||||
## 交互规则
|
||||
|
||||
* 单元格点击:打开对应 dialog
|
||||
* 血糖/血压/西药/胰岛素共用 blood dialog
|
||||
* 早餐/午餐/晚餐共用 diet dialog
|
||||
* 运动单独 exercise dialog
|
||||
* 提交成功后刷新矩阵
|
||||
|
||||
## 只读约束
|
||||
|
||||
* `readOnly=true` 时:
|
||||
* 单元格不可点击
|
||||
* 工具栏新增按钮置灰
|
||||
* 待办列表走其自身只读模式
|
||||
|
||||
## 新增优化要求
|
||||
|
||||
* 最近 7 天视图下,日期列宽不应固定得过窄导致右侧留白,应根据列数动态铺满
|
||||
* 待办事项上方增加血糖折线图区域
|
||||
* 折线图展示:
|
||||
* 空腹血糖
|
||||
* 餐后血糖
|
||||
* 折线图时间范围与矩阵当前选择保持一致
|
||||
|
||||
## 权限收口要求
|
||||
|
||||
* 原“血糖 / 饮食 / 运动”按钮权限不再单独作为入口能力暴露
|
||||
* 新增统一“日常记录”权限
|
||||
* 待办列表与其新增/取消能力一并纳入“日常记录”权限域
|
||||
* 前端 `hasPermission` / `v-perms` 与菜单 SQL 要保持一致
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "diagnosis-daily-matrix-tab",
|
||||
"name": "diagnosis-daily-matrix-tab",
|
||||
"title": "诊单编辑日历矩阵汇总 tab",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-07",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "master",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"file": ".trellis/tasks/05-07-diagnosis-video-upload-entry/prd.md", "reason": "按任务验收标准核对上传入口、分片上传与回放关联行为"}
|
||||
@@ -0,0 +1 @@
|
||||
{"file": ".trellis/tasks/05-07-diagnosis-video-upload-entry/prd.md", "reason": "当前任务需求、已知约束与待确认产品决策"}
|
||||
@@ -0,0 +1,70 @@
|
||||
# 诊单视频录制回放上传入口
|
||||
|
||||
## Goal
|
||||
|
||||
在诊单编辑抽屉的「视频录制回放记录」tab 中增加手动上传入口,支持大容量视频自动分片上传,并在上传完成后把视频回放地址关联到对应通话记录,供后台直接回放。
|
||||
|
||||
## What I already know
|
||||
|
||||
* 入口页面在 `admin/src/views/tcm/diagnosis/edit.vue`,tab 组件为 `admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue`。
|
||||
* 当前回放面板只能展示 `recording_urls_list`,没有手动上传入口。
|
||||
* 管理端现有视频上传能力只有整文件 `POST /upload/video`,前端封装在 `admin/src/api/file.ts` 的 `uploadVideoBlob` / `uploadMaterialFile`。
|
||||
* 服务端上传实现位于 `server/app/common/service/UploadService.php`,视频上传目前只接收一次性文件流,不支持分片合并。
|
||||
* 诊单通话记录接口为 `GET /tcm.diagnosis/getCallRecords`,服务端逻辑在 `DiagnosisLogic::getCallRecords`。
|
||||
* 当前“浏览器本地录制上传后关联回放”的接口是 `POST /tcm.diagnosis/attachLocalCallRecording`,它按 `diagnosis_id + admin_id` 回退到最近一条通话记录,不支持显式指定 `call_record_id`。
|
||||
* 回放数据存储在 `tcm_call_record.recording_urls`(JSON 数组)与 `recording_status`。
|
||||
* `tcm_call_record` 基础字段定义在 `server/sql/tcm_call_record.sql`;录制相关补充字段定义在 `server/sql/add_call_recording_fields.sql`。
|
||||
* 当前 Trellis 目录下没有 `.trellis/spec/`,本任务的实现上下文需要依赖 PRD / research 文件本身。
|
||||
|
||||
## Assumptions (temporary)
|
||||
|
||||
* 大容量视频上传采用后端落盘分片 + 最终合并的方式,不依赖云厂商 Multipart SDK。
|
||||
* 无通话记录时允许直接上传,服务端自动生成一条模拟 `tcm_call_record` 承载该视频回放。
|
||||
|
||||
## Open Questions
|
||||
|
||||
* 暂无
|
||||
|
||||
## Requirements (evolving)
|
||||
|
||||
* 在诊单编辑的「视频录制回放记录」tab 提供视频上传入口。
|
||||
* 有通话记录时,上传入口按“每条通话记录”逐行提供,用户在目标记录行内上传视频。
|
||||
* 无通话记录时,tab 顶部也要提供上传入口;上传后服务端自动生成一条模拟通话记录并展示在列表中。
|
||||
* 支持大容量视频自动切片上传,避免单次请求过大导致失败。
|
||||
* 下方列表数据来源保持为 `tcm_call_record`,不得引入独立的“上传视频列表”数据源。
|
||||
* 上传完成后必须将视频地址写入目标通话记录的 `recording_urls`,并同步维护相关状态字段,确保列表立即可见。
|
||||
* 对于自动生成的模拟通话记录,房间号、通话类型、状态等无真实来源字段使用系统默认值,但仍落在 `tcm_call_record` 同表中。
|
||||
* 保持现有云端录制 / 本地浏览器录制展示逻辑兼容,不破坏已有回放列表。
|
||||
* 仅允许视频文件类型,沿用现有视频格式白名单。
|
||||
|
||||
## Acceptance Criteria (evolving)
|
||||
|
||||
* [ ] 在诊单编辑页打开「视频录制回放记录」tab 时可以看到上传入口。
|
||||
* [ ] 即使当前没有通话记录,也可以直接上传视频。
|
||||
* [ ] 选择较大视频文件后,前端自动分片并完成上传,无需用户手工处理。
|
||||
* [ ] 上传成功后,目标通话记录立即能看到新增回放视频。
|
||||
* [ ] 列表中的上传视频信息仍然来自 `tcm_call_record`,不会写到别的表导致数据割裂。
|
||||
* [ ] 已有录制回放仍能正常展示与播放。
|
||||
* [ ] 错误场景有明确提示,失败后不会写入脏的回放地址。
|
||||
|
||||
## Definition of Done (team quality bar)
|
||||
|
||||
* Tests added/updated (unit/integration where appropriate)
|
||||
* Lint / typecheck / CI green
|
||||
* Docs/notes updated if behavior changes
|
||||
* Rollout/rollback considered if risky
|
||||
|
||||
## Out of Scope (explicit)
|
||||
|
||||
* 不改造 TRTC 云端录制流程
|
||||
* 不新增独立的视频转码/压缩流程
|
||||
* 不做前台/小程序端上传入口
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* 诊单编辑页 tab:`admin/src/views/tcm/diagnosis/edit.vue`
|
||||
* 回放面板:`admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue`
|
||||
* 通话记录 API:`admin/src/api/tcm.ts`
|
||||
* 上传 API:`admin/src/api/file.ts`
|
||||
* 服务端上传:`server/app/common/service/UploadService.php`
|
||||
* 通话记录逻辑:`server/app/adminapi/logic/tcm/DiagnosisLogic.php`
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "diagnosis-video-upload-entry",
|
||||
"name": "diagnosis-video-upload-entry",
|
||||
"title": "诊单视频录制回放上传入口",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-07",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "long-0507",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-dosage-bag-count/research/current-code.md","reason":"Use the documented field semantics during review to ensure the new dosage bag count does not collide with the decoction-only bags_per_dose field."}
|
||||
@@ -0,0 +1 @@
|
||||
{"file":".trellis/tasks/05-07-dosage-bag-count/research/current-code.md","reason":"Summarizes the affected code paths, current field semantics, and why a new field is required instead of reusing bags_per_dose."}
|
||||
@@ -0,0 +1,69 @@
|
||||
# 开方用量增加袋数选项
|
||||
|
||||
## Goal
|
||||
|
||||
在开方流程中为“用量”补充“袋数”信息,并在处方业务订单详情里同步展示,解决当前只能记录每袋克数/毫升数、不能表达一次服用几袋的问题。
|
||||
|
||||
## What I already know
|
||||
|
||||
* 诊间开方共用组件是 `admin/src/components/tcm-prescription/index.vue`
|
||||
* 患者列表-开方页面是 `admin/src/views/consumer/prescription/index.vue`
|
||||
* 处方业务订单详情展示位于 `admin/src/views/consumer/prescription/order_list.vue`
|
||||
* H5 对应详情页是 `admin/src/views/consumer/prescription/order_list_h5.vue`
|
||||
* 当前 `bags_per_dose` 字段已存在,但语义是“饮片每贴出包数”,不适合复用为浓缩水丸/开方用量袋数
|
||||
* 当前多个浓缩水丸展示逻辑错误地把 `bags_per_dose` 当作“一次几包”使用,本次应与新字段区分
|
||||
|
||||
## Assumptions (temporary)
|
||||
|
||||
* 新增字段命名为 `dosage_bag_count`
|
||||
* 新字段主要用于“浓缩水丸”开方场景;饮片继续使用原有 `bags_per_dose`
|
||||
* “用量袋数”控件放在“用量”右侧,不单独增加标题,仅提供 `1~5袋` 选项
|
||||
* 业务订单详情中的“用量”展示为“每袋数值 + 单位 + 袋数”;无袋数时回退为原展示
|
||||
* 处方查看/H5 详情/处方笺相关依赖同一处方数据的地方,同步使用新字段,避免展示串义
|
||||
|
||||
## Open Questions
|
||||
|
||||
* 无。按上述假设先实现,如用户补充再调整。
|
||||
|
||||
## Requirements
|
||||
|
||||
* 在诊间开方共用组件中新增 `dosage_bag_count` 表单字段、默认值、回填、保存参数
|
||||
* 在患者列表-开方页面中新增 `dosage_bag_count` 表单字段、默认值、回填、保存参数
|
||||
* 后端处方模型/校验/逻辑支持 `dosage_bag_count` 的新增与编辑保存
|
||||
* 补充数据库变更脚本,为处方表新增 `dosage_bag_count`
|
||||
* 处方业务订单详情“用量”展示袋数
|
||||
* 同步修正当前误把 `bags_per_dose` 当成浓缩水丸袋数的相关前端展示/文案逻辑
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [ ] 诊间“问诊列表-开方”中,用量旁可选择 `1袋` 到 `5袋`
|
||||
* [ ] “患者列表-开方”中,用量旁可选择 `1袋` 到 `5袋`
|
||||
* [ ] 保存后重新打开处方,袋数可正确回填
|
||||
* [ ] 处方业务订单详情“用量”可显示袋数
|
||||
* [ ] 饮片“每贴出包数”行为不受影响
|
||||
* [ ] 不再使用饮片字段 `bags_per_dose` 充当浓缩水丸袋数
|
||||
|
||||
## Definition of Done
|
||||
|
||||
* 代码修改完成
|
||||
* 相关 lint / type-check 可通过
|
||||
* 关键展示链路已自检
|
||||
|
||||
## Out of Scope
|
||||
|
||||
* 非处方详情页之外的历史报表/统计口径调整
|
||||
* 对既有数据做批量迁移或回填
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* 诊间开方共用组件:`admin/src/components/tcm-prescription/index.vue`
|
||||
* 患者列表开方:`admin/src/views/consumer/prescription/index.vue`
|
||||
* 业务订单详情:`admin/src/views/consumer/prescription/order_list.vue`
|
||||
* H5 业务订单详情:`admin/src/views/consumer/prescription/order_list_h5.vue`
|
||||
* 后端处方逻辑:`server/app/adminapi/logic/tcm/PrescriptionLogic.php`
|
||||
* 后端校验:`server/app/adminapi/validate/tcm/PrescriptionValidate.php`
|
||||
* 处方模型:`server/app/common/model/tcm/Prescription.php`
|
||||
|
||||
## Research References
|
||||
|
||||
* [`research/current-code.md`](research/current-code.md) — 当前字段语义与受影响文件梳理
|
||||
@@ -0,0 +1,27 @@
|
||||
# 当前代码梳理
|
||||
|
||||
## 目标范围
|
||||
|
||||
* 诊间开方:`admin/src/components/tcm-prescription/index.vue`
|
||||
* 患者列表开方:`admin/src/views/consumer/prescription/index.vue`
|
||||
* 处方业务订单详情:`admin/src/views/consumer/prescription/order_list.vue`
|
||||
* H5 处方业务订单详情:`admin/src/views/consumer/prescription/order_list_h5.vue`
|
||||
* 后端处方保存:`server/app/adminapi/logic/tcm/PrescriptionLogic.php`
|
||||
* 后端处方校验:`server/app/adminapi/validate/tcm/PrescriptionValidate.php`
|
||||
|
||||
## 已有字段
|
||||
|
||||
* `dosage_amount`:当前用于记录用量数值;浓缩水丸场景下表示每袋克数
|
||||
* `dosage_unit`:用量单位,当前主要是 `g`/`ml`
|
||||
* `bags_per_dose`:已有字段,仅适用于饮片“每贴出包数(1-9包)`
|
||||
|
||||
## 当前问题
|
||||
|
||||
* 需求要新增“用量袋数”,但现有 `bags_per_dose` 不能直接复用,否则会与饮片语义冲突
|
||||
* 部分浓缩水丸展示逻辑把 `bags_per_dose` 当“一次几包”使用,存在语义串用
|
||||
|
||||
## 结论
|
||||
|
||||
* 需要新增独立字段,例如 `dosage_bag_count`
|
||||
* 前端开方入口、后端保存链路、详情展示链路都要接通该字段
|
||||
* 饮片原有 `bags_per_dose` 不应改语义
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "dosage-bag-count",
|
||||
"name": "dosage-bag-count",
|
||||
"title": "开方用量增加袋数选项",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-07",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "long-0507",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,76 @@
|
||||
# 前端直传腾讯云 COS(视频大文件)
|
||||
|
||||
## Goal
|
||||
|
||||
让管理后台的视频上传绕开 PHP 服务器的中转通道,由浏览器直接把视频文件分片上传到腾讯云 COS。
|
||||
解决两类痛点:(1) 服务器双倍带宽消耗(client→server→COS);(2) PHP `upload_max_filesize` / `post_max_size` 与 fpm 进程内存对大视频不友好。
|
||||
首发场景:诊单 `CallRecordPanel.vue` 上传通话回放视频。
|
||||
|
||||
## What I already know
|
||||
|
||||
- 现有上传链路:`admin/src/components/upload/index.vue` → `POST /upload/video`(`UploadController::video`)→ `UploadService::video()` → `storage/Driver` → `engine/Qcloud::upload()` 用 `putObject`。
|
||||
- 服务端入参依赖 `$_FILES`,受 PHP 上传上限约束;返回 `{id, cid, type, name, uri, url}`,`uri` 经 `FileService::getFileUrl()` 拼接 OSS/CDN 域名。
|
||||
- 存储驱动配置走 `dev_config.storage`(管理员可在后台「存储配置」切换 local/qiniu/aliyun/qcloud;本任务只对 qcloud 启用直传)。
|
||||
- composer 已有 `qcloud/cos-sdk-v5 ^2.5`,但 STS 临时凭证需要再装 `qcloud/cos-sts-sdk`。
|
||||
- 未提交代码:`CallRecordPanel.vue` 已加「上传视频」按钮,调用 `createManualCallRecord` + `attachLocalCallRecording`,依赖上传组件吐出的 `response.data.uri/url`。AuthMiddleware 已为 `tcm.diagnosis/uploadcallrecording` 等加白名单 — 直传新接口与之解耦,不需要白名单。
|
||||
|
||||
## Assumptions (temporary)
|
||||
|
||||
- 生产环境默认存储引擎已经是 `qcloud`;切到 `local/aliyun/qiniu` 时 direct 模式需要降级回老链路。
|
||||
- COS Bucket 已开启跨域 CORS(直传必须)。如未开启,需要运维在 COS 控制台配置 `AllowedOrigin = 后台域名 + 本地 dev`、`AllowedMethod = PUT/POST/HEAD`、`AllowedHeader = *`、`ExposeHeader: ETag`。
|
||||
- 后台域名固定(暂无多租户独立域名)。
|
||||
- 视频文件大小有合理上限(建议 2GB)。
|
||||
|
||||
## Decisions (locked)
|
||||
|
||||
- 视频上限 **2 GB**(写进 STS policy `numeric_less_than_equal cos:content-length` + 前端 `file.size` 预检)。
|
||||
- 保留 `/upload/video` 老接口;`upload/index.vue` 的 `direct` prop 默认 `false`,仅 `CallRecordPanel.vue` 这类显式声明的视频场景启用直传。
|
||||
- STS 凭证有效期 **30 分钟**(1800 秒)。
|
||||
- 直传完成后**必须等 `oss-confirm`** 返回(写完 `file` 表 + HEAD 校验通过)才 emit `success`,保证业务侧拿到的 `uri/url` 一定有 `file_id`。
|
||||
|
||||
## Requirements (evolving)
|
||||
|
||||
- [R1] 后端新增 `POST /adminapi/upload/oss-credentials`,仅当 `storage.default == qcloud` 时返回 `{provider, bucket, region, credentials, expiredTime, key, host, cdn_url_base}`;其它 driver 返回 `{provider: '<engine>', fallback: true}` 让前端自动降级到老链路。
|
||||
- [R2] 后端新增 `POST /adminapi/upload/oss-confirm`,HEAD 一下 `key` 校验确实存在 + size 在阈值内,写入 `file` 表(沿用 `FileService::getFileUrl`),返回与 `UploadService::video()` 完全对齐的 `{id, cid, type, name, uri, url}`。
|
||||
- [R3] 新装 composer 依赖 `qcloud/cos-sts-sdk`;在 `engine/Qcloud.php` 增加 `getStsCredentials($keyPrefix, $maxSize)` 方法,复用现有 `access_key/secret_key/bucket/region`。
|
||||
- [R4] STS policy 限定 action:`name/cos:PostObject`、`name/cos:PutObject`、`name/cos:InitiateMultipartUpload`、`name/cos:UploadPart`、`name/cos:CompleteMultipartUpload`、`name/cos:AbortMultipartUpload`、`name/cos:ListParts`;resource 限定 `qcs::cos:${region}:uid/${appId}:${bucket}/uploads/video/{YYYYMMDD}/*`。
|
||||
- [R5] 前端新增 npm 依赖 `cos-js-sdk-v5`;`upload/index.vue` 增加 `direct: boolean` prop(默认 `false`),`type==='video' && direct===true` 时走 STS + `cos.uploadFile`(含 `SliceSize=5MB`、`onProgress`),仍发出原 `success` 事件 `{ code, data: { uri, url, ... } }` 保持调用侧兼容。
|
||||
- [R6] `CallRecordPanel.vue` 切到 `<upload type="video" direct :show-progress="true">`,并支持上传过程中显示百分比(沿用组件内置 `el-dialog` 进度)。
|
||||
- [R7] 失败/降级路径:STS 接口失败 / response 标记 `fallback=true` / `cos-js-sdk-v5` 上传报错时,自动降级到老的 `POST /upload/video`,提示「直传失败,已切换到普通上传」。
|
||||
- [R8] 安全:保留登录鉴权;STS 凭证 30 分钟有效期;前端拒绝不在 `accept` 列表的扩展名(沿用现有 `.wmv,.avi,...,.mp4,.mkv`)。
|
||||
|
||||
## Acceptance Criteria (evolving)
|
||||
|
||||
- [ ] 在 storage=qcloud 环境,从 `CallRecordPanel.vue` 上传 1.5GB mp4 文件,浏览器 Network 面板能看到流量直接打到 `*.cos.*.myqcloud.com`,`/upload/video` 没有调用。
|
||||
- [ ] 上传过程中能看到分片进度条(5MB 一片,并发 ≥3)。
|
||||
- [ ] 上传完成后,`call_record` 表正确写入 `recording_urls`,前端表格 `recording_urls_list` 渲染出可播放视频。
|
||||
- [ ] `file` 表新增一行,`uri` 形如 `uploads/video/20260508/xxxxx.mp4`,`url` 拼接了 CDN/COS 域名。
|
||||
- [ ] 把 `dev_config.storage.default` 改成 `local`,再次上传,自动降级到老 `/upload/video` 路径,整体功能正常。
|
||||
- [ ] 凭证接口未登录调用返回 401。
|
||||
- [ ] 上传一个被 STS policy resource 拒绝的 key(人为篡改 prefix),COS 返回 403,前端给出明确错误。
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- 单元/集成测试:暂无 PHPUnit 工程,跳过自动化测试;保证手测覆盖 AC 全部场景。
|
||||
- 前端 `npm run type-check` 通过、`npm run lint` 无新增告警。
|
||||
- 后端 `php -l` 通过,新接口在 admin 路由可访问。
|
||||
- 文档:在 `.trellis/spec/`(如已有上传相关 spec)补充直传规范;PRD 同步关闭 Open Questions。
|
||||
- COS CORS / STS 子账号最小权限策略整理为 ops 备忘(写到 prd.md 的 Technical Notes)。
|
||||
|
||||
## Out of Scope (explicit)
|
||||
|
||||
- 不做断点续传(刷新页面不保留 UploadId)。
|
||||
- 不做 PostObject 回调链路。
|
||||
- 不做 aliyun / qiniu 直传。
|
||||
- 不改 `upload/image`、`upload/file` 链路(图片小文件用老链路即可)。
|
||||
- 不做客户端 H5/uniapp 端直传(uniapp 本来就不走 admin)。
|
||||
- 不做并发上传限流 / 全局上传队列。
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- 关键文件:`server/app/common/service/storage/engine/Qcloud.php`、`server/app/common/service/UploadService.php`、`server/app/adminapi/controller/UploadController.php`、`admin/src/components/upload/index.vue`、`admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue`。
|
||||
- composer 新增:`qcloud/cos-sts-sdk: ^3.0`。
|
||||
- npm 新增:`cos-js-sdk-v5`。
|
||||
- COS CORS 配置(运维):`AllowedOrigin: <admin 域名>`、`AllowedMethod: PUT,POST,HEAD,GET`、`AllowedHeader: *`、`ExposeHeader: ETag,x-cos-request-id,x-cos-version-id`。
|
||||
- STS 子账号最小权限:仅赋予对 `${bucket}/uploads/video/*` 的 `cos:Get*`、`cos:Put*`、`cos:Post*`、Multipart 系列动作;STS policy 在此基础上再用 condition + resource 收敛。
|
||||
- Key 命名:`uploads/video/${YYYYMMDD}/${ulid}.${ext}`,与现有 `UploadService::getUploadUrl()` 的 `Ymd` 分日目录保持一致。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "frontend-direct-upload-cos",
|
||||
"name": "frontend-direct-upload-cos",
|
||||
"title": "前端直传腾讯云 COS",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-08",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "long-0507",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,244 @@
|
||||
# 业务订单详情-修改总金额功能
|
||||
|
||||
## Goal
|
||||
|
||||
在业务订单详情页面(`admin/src/views/consumer/prescription/order_list.vue`)增加修改总金额的功能,支持权限控制,并在操作日志中记录修改历史。已完成状态的订单不允许修改金额。
|
||||
|
||||
## What I already know
|
||||
|
||||
### 前端架构
|
||||
- 详情页面:`admin/src/views/consumer/prescription/order_list.vue`(60k+ tokens,包含详情抽屉)
|
||||
- API文件:`admin/src/api/tcm.ts`(已有 `prescriptionOrderDetail`、`prescriptionOrderEdit`、`prescriptionOrderLogs` 等接口)
|
||||
- 订单状态枚举:
|
||||
- `fulfillment_status = 3`:已完成
|
||||
- 已完成/已取消订单不可编辑(line 3025)
|
||||
- 操作日志展示:详情抽屉中已有操作日志时间线(line 1192-1218)
|
||||
|
||||
### 后端架构
|
||||
- 控制器:`server/app/adminapi/controller/tcm/PrescriptionOrderController.php`
|
||||
- 逻辑层:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`
|
||||
- 模型:`server/app/common/model/tcm/PrescriptionOrder.php`
|
||||
- 日志模型:`server/app/common/model/tcm/PrescriptionOrderLog.php`
|
||||
- 日志写入方法:`PrescriptionOrderLogic::writeLog()` (line 2638-2658)
|
||||
- 日志读取方法:`PrescriptionOrderLogic::getLogs()` (line 1913)
|
||||
|
||||
### 现有日志记录模式
|
||||
```php
|
||||
self::writeLog($orderId, $adminId, $adminInfo, 'action_name', '操作摘要');
|
||||
```
|
||||
日志字段:
|
||||
- `prescription_order_id`:订单ID
|
||||
- `admin_id`:操作人ID
|
||||
- `admin_name`:操作人姓名
|
||||
- `action`:操作类型(最多32字符)
|
||||
- `summary`:操作摘要(最多500字符)
|
||||
- `create_time`:创建时间
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- ✅ 总金额字段:`amount` (decimal(10,2), NOT NULL, DEFAULT '0.00', COMMENT '业务订单金额')
|
||||
- ✅ 状态限制:已完成(`fulfillment_status = 3`)和已取消(`fulfillment_status = 4`)都不允许修改金额
|
||||
- ✅ 权限控制:新增独立菜单权限节点 `tcm.prescriptionOrder/updateAmount`
|
||||
- ✅ 金额校验规则:
|
||||
- 最小值:必须大于 0(不能为0或负数)
|
||||
- 最大值:无限制
|
||||
- 精度:保留2位小数
|
||||
- ✅ 需代收金额:前端计算属性自动计算(`amount - linked_pay_paid_total`),无需后端处理
|
||||
- ✅ 按钮位置:放在金额显示区域旁边(在显示总金额的卡片内)
|
||||
|
||||
## Requirements (final)
|
||||
|
||||
### 功能需求
|
||||
1. 在业务订单详情页面的金额显示卡片内增加"修改金额"按钮
|
||||
2. 点击按钮弹出修改金额对话框,显示当前金额,允许输入新金额
|
||||
3. 已完成(`fulfillment_status = 3`)和已取消(`fulfillment_status = 4`)状态的订单隐藏修改金额按钮
|
||||
4. 修改金额需要权限控制(`v-perms="['tcm.prescriptionOrder/updateAmount']"`)
|
||||
5. 金额校验:
|
||||
- 必须大于 0
|
||||
- 保留2位小数
|
||||
- 最大值无限制
|
||||
6. 修改成功后,在操作日志中记录:
|
||||
- 操作人姓名
|
||||
- 原金额
|
||||
- 新金额
|
||||
- 操作时间
|
||||
- 日志格式:`将订单金额从 ¥{原金额} 修改为 ¥{新金额}`
|
||||
7. 修改成功后立即刷新订单详情数据,需代收金额自动重新计算
|
||||
|
||||
### 技术需求
|
||||
- 前端:在金额显示卡片内增加修改按钮和对话框
|
||||
- 前端:新增API接口 `prescriptionOrderUpdateAmount(params: { id: number; amount: number })`
|
||||
- 后端:新增控制器方法 `PrescriptionOrderController::updateAmount()`
|
||||
- 后端:新增验证规则 `PrescriptionOrderValidate::updateAmount`
|
||||
- 后端:新增逻辑层方法 `PrescriptionOrderLogic::updateAmount()`
|
||||
- 后端:记录操作日志(action: `update_amount`)
|
||||
- SQL:新增菜单权限记录(`server/sql/1.9.20260508/add_update_amount_menu.sql`)
|
||||
|
||||
## Acceptance Criteria (final)
|
||||
|
||||
- [ ] 已完成和已取消状态的订单不显示"修改金额"按钮
|
||||
- [ ] 无权限的管理员不显示"修改金额"按钮
|
||||
- [ ] 修改金额对话框正确显示当前金额
|
||||
- [ ] 输入金额 ≤ 0 时提示错误
|
||||
- [ ] 输入金额精度超过2位小数时自动截断或提示
|
||||
- [ ] 提交成功后,订单 `amount` 字段更新为新金额
|
||||
- [ ] 操作日志中正确记录:`{操作人} 将订单金额从 ¥{原金额} 修改为 ¥{新金额}`
|
||||
- [ ] 修改后详情页面数据自动刷新,需代收金额正确显示
|
||||
- [ ] 后端返回错误时前端正确提示
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- 前后端代码实现完成
|
||||
- 新增菜单权限SQL脚本
|
||||
- 金额修改功能测试通过(正常流程、权限控制、状态限制)
|
||||
- 操作日志记录正确
|
||||
- 代码通过 lint/typecheck
|
||||
- 无 console.log 残留
|
||||
|
||||
## Out of Scope (explicit)
|
||||
|
||||
- 批量修改金额
|
||||
- 修改金额审批流程
|
||||
- 金额修改历史版本对比
|
||||
- 修改其他订单字段(如运费、优惠等)
|
||||
|
||||
## Technical Approach
|
||||
|
||||
### 实现流程
|
||||
|
||||
#### 1. 后端实现(优先)
|
||||
|
||||
**SQL 菜单权限**(`server/sql/1.9.20260508/add_update_amount_menu.sql`)
|
||||
```sql
|
||||
INSERT INTO `zyt_system_menu` (`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`)
|
||||
VALUES
|
||||
(处方订单菜单ID, 'A', '修改订单金额', '', 0, 'tcm.prescriptionOrder/updateAmount', '', '', '', '', 0, 0, 0);
|
||||
```
|
||||
|
||||
**验证规则**(`PrescriptionOrderValidate.php`)
|
||||
```php
|
||||
public function updateAmount(): PrescriptionOrderValidate
|
||||
{
|
||||
return $this->only(['id', 'amount'])
|
||||
->append('id', 'require|number')
|
||||
->append('amount', 'require|float|gt:0');
|
||||
}
|
||||
```
|
||||
|
||||
**逻辑层**(`PrescriptionOrderLogic.php`)
|
||||
```php
|
||||
public static function updateAmount(array $params, int $adminId, array $adminInfo): bool
|
||||
{
|
||||
$id = (int) $params['id'];
|
||||
$newAmount = round((float) $params['amount'], 2);
|
||||
|
||||
// 权限检查
|
||||
$order = PrescriptionOrder::find($id);
|
||||
if (!$order) {
|
||||
self::setError('订单不存在');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 状态检查:已完成(3)和已取消(4)不允许修改
|
||||
if (in_array($order->fulfillment_status, [3, 4], true)) {
|
||||
self::setError('已完成或已取消的订单不允许修改金额');
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldAmount = round((float) $order->amount, 2);
|
||||
|
||||
// 更新金额
|
||||
$order->amount = $newAmount;
|
||||
$order->save();
|
||||
|
||||
// 记录日志
|
||||
$summary = sprintf('将订单金额从 ¥%.2f 修改为 ¥%.2f', $oldAmount, $newAmount);
|
||||
self::writeLog($id, $adminId, $adminInfo, 'update_amount', $summary);
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**控制器**(`PrescriptionOrderController.php`)
|
||||
```php
|
||||
public function updateAmount()
|
||||
{
|
||||
$params = (new PrescriptionOrderValidate())->post()->goCheck('updateAmount');
|
||||
$result = PrescriptionOrderLogic::updateAmount($params, $this->adminId, $this->adminInfo);
|
||||
|
||||
if ($result === false) {
|
||||
return $this->fail(PrescriptionOrderLogic::getError());
|
||||
}
|
||||
|
||||
return $this->success('修改成功');
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 前端实现
|
||||
|
||||
**API 接口**(`admin/src/api/tcm.ts`)
|
||||
```typescript
|
||||
export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) {
|
||||
return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params })
|
||||
}
|
||||
```
|
||||
|
||||
**页面实现**(`admin/src/views/consumer/prescription/order_list.vue`)
|
||||
- 在金额显示卡片内增加"修改金额"按钮(带权限和状态判断)
|
||||
- 新增修改金额对话框(el-dialog + el-input-number)
|
||||
- 提交后调用 API,成功后刷新详情数据
|
||||
|
||||
### 关键判断逻辑
|
||||
|
||||
```typescript
|
||||
// 是否可以修改金额
|
||||
const canUpdateAmount = (row: any) => {
|
||||
// 已完成(3)或已取消(4)不允许修改
|
||||
return row.fulfillment_status !== 3 && row.fulfillment_status !== 4
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### PR1: 后端实现(优先)
|
||||
- [ ] 创建 SQL 菜单权限文件
|
||||
- [ ] 添加验证规则 `updateAmount`
|
||||
- [ ] 实现逻辑层方法 `PrescriptionOrderLogic::updateAmount()`
|
||||
- [ ] 实现控制器方法 `PrescriptionOrderController::updateAmount()`
|
||||
- [ ] 测试后端接口(Postman/curl)
|
||||
|
||||
### PR2: 前端实现
|
||||
- [ ] 添加 API 接口 `prescriptionOrderUpdateAmount`
|
||||
- [ ] 在金额卡片内添加"修改金额"按钮(带权限和状态判断)
|
||||
- [ ] 实现修改金额对话框
|
||||
- [ ] 实现提交逻辑和数据刷新
|
||||
- [ ] 测试完整流程(权限、状态、校验、日志)
|
||||
|
||||
### PR3: 测试和优化
|
||||
- [ ] 测试各种边界情况(0、负数、超大金额、精度)
|
||||
- [ ] 测试权限控制
|
||||
- [ ] 测试状态限制
|
||||
- [ ] 验证操作日志记录
|
||||
- [ ] 代码 lint/typecheck
|
||||
- [ ] 清理 console.log
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### 文件清单
|
||||
- 前端页面:`admin/src/views/consumer/prescription/order_list.vue`
|
||||
- 前端API:`admin/src/api/tcm.ts`
|
||||
- 后端控制器:`server/app/adminapi/controller/tcm/PrescriptionOrderController.php`
|
||||
- 后端逻辑:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`
|
||||
- 后端验证:`server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php`
|
||||
- SQL脚本:`server/sql/1.9.20260508/add_update_amount_menu.sql`(待创建)
|
||||
|
||||
### 参考实现
|
||||
- 操作日志写入:`PrescriptionOrderLogic::writeLog()` (line 2638)
|
||||
- 操作日志读取:`PrescriptionOrderLogic::getLogs()` (line 1913)
|
||||
- 状态判断逻辑:`order_list.vue` line 3025
|
||||
|
||||
### 约束
|
||||
- 日志 action 字段最多32字符
|
||||
- 日志 summary 字段最多500字符
|
||||
- 遵循 likeadmin 分层架构:Controller → Logic → Model
|
||||
- 权限通过菜单节点 + `v-perms` 指令控制
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "order-amount-edit",
|
||||
"name": "order-amount-edit",
|
||||
"title": "业务订单详情-修改总金额功能",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-08",
|
||||
"completedAt": "2026-05-11",
|
||||
"branch": null,
|
||||
"base_branch": "master",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"file": ".trellis/tasks/05-27-grip-ring-gamification/prd.md", "reason": "验证实现是否符合需求:捏碎特效触发频率、卡路里计算准确性、食物对比展示"}
|
||||
{"file": ".trellis/tasks/05-27-grip-ring-gamification/research/calorie-calculation.md", "reason": "验证卡路里计算公式正确性、食物对比数据准确性"}
|
||||
{"file": "TUICallKit-Vue3/training/pages/exercises.ts", "reason": "验证握力环配置未被修改(80 BPM, 4组×20次)"}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/tasks/05-27-grip-ring-gamification/prd.md", "reason": "完整需求文档:捏碎特效、卡路里统计、完成页面"}
|
||||
{"file": ".trellis/tasks/05-27-grip-ring-gamification/research/calorie-calculation.md", "reason": "卡路里计算公式、食物对比数据、实现方案"}
|
||||
{"file": "TUICallKit-Vue3/training/pages/components/exercise-anim.vue", "reason": "当前握力环动画实现(CSS scale 动画)"}
|
||||
{"file": "TUICallKit-Vue3/training/pages/components/walker-canvas.vue", "reason": "Canvas 粒子动画参考(冲击波实现)"}
|
||||
{"file": "TUICallKit-Vue3/training/hooks/useTrainingSession.ts", "reason": "训练流程管理、节拍回调触发点"}
|
||||
{"file": "TUICallKit-Vue3/training/pages/exercises.ts", "reason": "握力环训练配置(80 BPM, 4组×20次)"}
|
||||
@@ -0,0 +1,117 @@
|
||||
# 握力环训练趣味化:捏碎特效 + 卡路里统计
|
||||
|
||||
## Goal
|
||||
|
||||
为握力环训练增加趣味性和成就感,通过视觉特效(捏碎物品)和数据反馈(卡路里消耗)提升用户训练动力。
|
||||
|
||||
## What I already know
|
||||
|
||||
**当前握力环训练实现:**
|
||||
- 基础动画:绿色圆环缩放(scale 1 → 0.65)
|
||||
- 训练流程:开始 → 训练中(显示组数/次数)→ 休息 → 完成
|
||||
- 配置:80 BPM,4组×20次,45秒休息
|
||||
- 节拍器同步:跟随 BPM 播放音效
|
||||
|
||||
**用户需求:**
|
||||
1. **捏碎特效**:完成几次后触发特效(如捏碎鸡蛋)
|
||||
2. **卡路里统计**:训练完成后显示消耗卡路里 + 食物对比
|
||||
|
||||
**技术约束:**
|
||||
- 使用 uni-app + Vue 3
|
||||
- 动画可用 CSS 或 Canvas
|
||||
- 需要保持 60fps 流畅度
|
||||
|
||||
## Assumptions (temporary)
|
||||
|
||||
- 捏碎特效每完成 N 次触发一次(N 待定)
|
||||
- 卡路里计算基于握力环训练的标准公式
|
||||
- 食物对比使用常见食物(如苹果、鸡蛋、米饭等)
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. ~~**捏碎特效触发频率**~~:✅ 随机触发(平均每 8-12 次)
|
||||
2. ~~**捏碎物品种类**~~:✅ 4 种物品(鸡蛋🥚、核桃🥜、易拉罐🥫、气球🎈)
|
||||
3. ~~**特效实现方式**~~:✅ Canvas 粒子动画(叠加层 + 物理模拟)
|
||||
4. ~~**卡路里计算公式**~~:✅ 标准 MET 公式(默认 60kg 体重)
|
||||
5. ~~**食物对比展示**~~:✅ 自动匹配最接近的食物(5 种备选)
|
||||
6. ~~**用户体重获取**~~:✅ 使用默认值 60kg(暂不支持自定义)
|
||||
|
||||
## Requirements (evolving)
|
||||
|
||||
### 1. 捏碎特效
|
||||
- [ ] 随机触发(平均每 8-12 次触发一次)
|
||||
- [ ] 4 种可捏碎物品:鸡蛋🥚、核桃🥜、易拉罐🥫、气球🎈
|
||||
- [ ] 随机选择物品(增加惊喜感)
|
||||
- [ ] 特效流畅(60fps)
|
||||
- [ ] 配合 MiMo TTS 语音鼓励("加油"、"太棒了"等)
|
||||
- [ ] 无需指导视频(用户跟节拍器节奏训练)
|
||||
|
||||
### 2. 卡路里统计
|
||||
- [ ] 使用标准 MET 公式计算卡路里消耗
|
||||
- 公式:`卡路里 = 3.5 × 60(kg) × 运动时长(小时)`
|
||||
- 默认体重 60kg,80 次训练 ≈ 3.5 kcal
|
||||
- [ ] 训练完成后显示消耗卡路里
|
||||
- [ ] 自动匹配最接近的食物对比
|
||||
- 5 种备选:苹果🍎、鸡蛋🥚、巧克力🍫、香蕉🍌、米饭🍚
|
||||
- 显示格式:"相当于 X/Y 个{食物}"
|
||||
- [ ] 暂不支持自定义体重(未来可扩展)
|
||||
|
||||
### 3. 完成页面优化
|
||||
- [ ] 显示训练总结(组数、次数、时长、卡路里)
|
||||
- [ ] 成就感反馈(如"太棒了!💪")
|
||||
- [ ] 分享功能(可选)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 捏碎特效随机触发(平均每 8-12 次)
|
||||
- [ ] 4 种捏碎物品随机出现(鸡蛋/核桃/易拉罐/气球)
|
||||
- [ ] 粒子动画流畅(60fps)
|
||||
- [ ] 卡路里计算准确(基于标准 MET 公式)
|
||||
- [ ] 食物对比自动匹配最接近的食物
|
||||
- [ ] 完成页面显示训练总结(组数、次数、时长、卡路里、食物对比)
|
||||
- [ ] 动画无卡顿,特效与握力环动画同步
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- 代码实现完成并测试通过
|
||||
- 动画性能达标(60fps)
|
||||
- UI/UX 符合设计规范
|
||||
- 无明显 bug
|
||||
|
||||
## Out of Scope (explicit)
|
||||
|
||||
- 训练数据持久化(本地存储/云端同步)
|
||||
- 社交分享功能
|
||||
- 多人排行榜
|
||||
- 自定义用户体重(未来可扩展)
|
||||
- 累计卡路里统计(本版本仅显示单次)
|
||||
- 其他器械的趣味化(本任务仅限握力环)
|
||||
|
||||
## Research References
|
||||
|
||||
* [`research/calorie-calculation.md`](research/calorie-calculation.md) — 握力环训练卡路里计算公式与食物对比方案
|
||||
|
||||
## Technical Notes
|
||||
|
||||
**相关文件:**
|
||||
- `TUICallKit-Vue3/training/pages/index.vue` - 主训练页面
|
||||
- `TUICallKit-Vue3/training/pages/components/exercise-anim.vue` - 动画组件
|
||||
- `TUICallKit-Vue3/training/pages/exercises.ts` - 训练配置
|
||||
- `TUICallKit-Vue3/training/hooks/useTrainingSession.ts` - 训练流程管理
|
||||
|
||||
**握力环当前动画:**
|
||||
- 绿色圆环(240rpx)+ emoji 图标(🟢)
|
||||
- CSS scale 动画:1 → 0.65 → 1
|
||||
|
||||
**Canvas 粒子特效方案:**
|
||||
- 透明 Canvas 叠加在握力环上方
|
||||
- 在 scale 0.65 峰值时触发粒子爆炸
|
||||
- 粒子系统:20 个粒子,径向扩散 + 重力 + 衰减
|
||||
- 物品配置:4 种(鸡蛋/核桃/易拉罐/气球),不同颜色
|
||||
- 参考 `walker-canvas.vue` 的冲击波实现
|
||||
|
||||
**卡路里计算方案:**
|
||||
- 标准 MET 公式:`卡路里 = 3.5 × 体重(kg) × 时长(小时)`
|
||||
- 详细研究报告:[`research/calorie-calculation.md`](research/calorie-calculation.md)
|
||||
- 典型消耗:60kg 用户,80 次 ≈ 3.5 kcal
|
||||
- 食物对比库:苹果🍎、鸡蛋🥚、巧克力🍫、香蕉🍌、米饭🍚
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
# Research: 握力环训练卡路里消耗计算
|
||||
|
||||
- **Query**: 握力圈(手握器)训练的卡路里消耗计算方法
|
||||
- **Scope**: 外部研究 + 运动科学标准
|
||||
- **Date**: 2026-05-27
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. 标准卡路里计算公式
|
||||
|
||||
#### 基于 MET 值的通用公式
|
||||
|
||||
```
|
||||
卡路里消耗(kcal)= MET × 体重(kg)× 时长(小时)
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- **MET (Metabolic Equivalent of Task)**: 代谢当量,表示运动强度
|
||||
- **体重**: 用户体重(公斤)
|
||||
- **时长**: 实际运动时长(小时,不含休息时间)
|
||||
|
||||
#### 阻力训练 MET 值参考
|
||||
|
||||
根据美国运动医学会(ACSM)和 Compendium of Physical Activities 标准:
|
||||
|
||||
| 训练类型 | MET 值 | 强度描述 |
|
||||
|---------|--------|---------|
|
||||
| 轻度阻力训练 | 3.5 | 小重量,慢速度 |
|
||||
| 中度阻力训练 | 5.0 | 中等重量,正常速度 |
|
||||
| 高强度阻力训练 | 6.0 | 大重量,快速度 |
|
||||
| 握力器训练(估算)| 3.0-4.0 | 小肌群,间歇性用力 |
|
||||
|
||||
**握力环训练特点:**
|
||||
- 属于小肌群训练(前臂肌群)
|
||||
- 间歇性用力(非持续性)
|
||||
- 建议使用 **MET = 3.5** 作为基准值
|
||||
|
||||
---
|
||||
|
||||
### 2. 适配当前训练配置的计算方法
|
||||
|
||||
#### 训练参数
|
||||
- **节奏**: 80 BPM(每次握力 0.75 秒)
|
||||
- **组数**: 4 组
|
||||
- **每组次数**: 20 次
|
||||
- **休息时间**: 45 秒/组
|
||||
- **总次数**: 80 次
|
||||
- **总时长**: 约 4 分钟(不含休息)
|
||||
|
||||
#### 推荐公式
|
||||
|
||||
```javascript
|
||||
// 方法 1: 基于实际运动时长(推荐)
|
||||
function calculateCalories(userWeight) {
|
||||
const MET = 3.5;
|
||||
const sets = 4;
|
||||
const repsPerSet = 20;
|
||||
const secondsPerRep = 0.75; // 80 BPM
|
||||
|
||||
// 实际运动时长(小时)
|
||||
const activeTimeHours = (sets * repsPerSet * secondsPerRep) / 3600;
|
||||
|
||||
// 卡路里消耗
|
||||
const calories = MET * userWeight * activeTimeHours;
|
||||
|
||||
return Math.round(calories * 10) / 10; // 保留一位小数
|
||||
}
|
||||
|
||||
// 示例:60kg 用户
|
||||
// activeTimeHours = (4 × 20 × 0.75) / 3600 = 0.0167 小时(约 1 分钟)
|
||||
// calories = 3.5 × 60 × 0.0167 = 3.5 kcal
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 方法 2: 基于总次数的简化公式
|
||||
function calculateCaloriesSimple(userWeight, totalReps) {
|
||||
// 每次握力消耗约 0.04-0.05 kcal(60kg 用户)
|
||||
const caloriesPerRep = 0.044 * (userWeight / 60);
|
||||
return Math.round(totalReps * caloriesPerRep * 10) / 10;
|
||||
}
|
||||
|
||||
// 示例:60kg 用户,80 次
|
||||
// calories = 80 × 0.044 = 3.5 kcal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 影响因素分析
|
||||
|
||||
#### 主要影响因素
|
||||
|
||||
| 因素 | 影响程度 | 说明 |
|
||||
|------|---------|------|
|
||||
| **用户体重** | ⭐⭐⭐⭐⭐ | 体重越大,消耗越多(线性关系)|
|
||||
| **训练次数** | ⭐⭐⭐⭐⭐ | 次数越多,消耗越多(线性关系)|
|
||||
| **握力器阻力** | ⭐⭐⭐⭐ | 阻力越大,MET 值越高 |
|
||||
| **握持时长** | ⭐⭐⭐ | 每次握持时间影响总时长 |
|
||||
| **休息时间** | ⭐ | 休息不计入消耗(可忽略)|
|
||||
|
||||
#### 阻力等级调整系数
|
||||
|
||||
如果握力器有不同阻力档位,可调整 MET 值:
|
||||
|
||||
```javascript
|
||||
const MET_BY_RESISTANCE = {
|
||||
low: 3.0, // 10-20kg 阻力
|
||||
medium: 3.5, // 20-30kg 阻力(默认)
|
||||
high: 4.0, // 30-40kg 阻力
|
||||
veryHigh: 4.5 // 40kg+ 阻力
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 卡路里消耗范围估算
|
||||
|
||||
#### 典型用户场景(4组×20次,80 BPM)
|
||||
|
||||
| 用户体重 | 卡路里消耗 | 备注 |
|
||||
|---------|-----------|------|
|
||||
| 50 kg | 2.9 kcal | 轻量级用户 |
|
||||
| 60 kg | 3.5 kcal | 标准体重 |
|
||||
| 70 kg | 4.1 kcal | 中等体重 |
|
||||
| 80 kg | 4.7 kcal | 较重用户 |
|
||||
| 90 kg | 5.2 kcal | 重量级用户 |
|
||||
|
||||
**结论:** 单次训练消耗约 **3-5 kcal**(大多数用户)
|
||||
|
||||
---
|
||||
|
||||
### 5. 食物对比参考
|
||||
|
||||
#### 常见食物卡路里含量
|
||||
|
||||
| 食物 | 卡路里 | 对比说明 |
|
||||
|------|--------|---------|
|
||||
| 🍎 小苹果(100g)| 52 kcal | 约 15 次训练 |
|
||||
| 🥚 鸡蛋(1个)| 70 kcal | 约 20 次训练 |
|
||||
| 🍚 米饭(100g)| 116 kcal | 约 33 次训练 |
|
||||
| 🍫 巧克力(10g)| 54 kcal | 约 15 次训练 |
|
||||
| 🥤 可乐(100ml)| 43 kcal | 约 12 次训练 |
|
||||
| 🍌 香蕉(1根)| 89 kcal | 约 25 次训练 |
|
||||
|
||||
#### 推荐展示策略
|
||||
|
||||
由于单次训练消耗较少(3-5 kcal),建议:
|
||||
|
||||
1. **累计展示**: "本周累计消耗 XX kcal,相当于 X 个苹果"
|
||||
2. **小单位对比**: "相当于 1/15 个苹果" 或 "相当于 3 颗糖果"
|
||||
3. **激励性表述**: "再训练 12 次,就能消耗 1 个苹果!"
|
||||
|
||||
---
|
||||
|
||||
### 6. 可比较的运动类型
|
||||
|
||||
#### 其他小肌群训练的卡路里消耗
|
||||
|
||||
| 运动类型 | MET 值 | 10分钟消耗(60kg)|
|
||||
|---------|--------|------------------|
|
||||
| 握力器训练 | 3.5 | 35 kcal |
|
||||
| 腕力器训练 | 3.5 | 35 kcal |
|
||||
| 哑铃弯举(轻)| 3.0 | 30 kcal |
|
||||
| 俯卧撑 | 3.8 | 38 kcal |
|
||||
| 平板支撑 | 3.0 | 30 kcal |
|
||||
|
||||
**参考来源:**
|
||||
- Compendium of Physical Activities (2011 update)
|
||||
- ACSM's Guidelines for Exercise Testing and Prescription
|
||||
|
||||
---
|
||||
|
||||
### 7. 实现建议
|
||||
|
||||
#### 最小可行方案(MVP)
|
||||
|
||||
```javascript
|
||||
// 简化版:固定 MET,只考虑体重和次数
|
||||
function calculateCalories(userWeight, totalReps) {
|
||||
const MET = 3.5;
|
||||
const secondsPerRep = 0.75; // 80 BPM
|
||||
const activeTimeHours = (totalReps * secondsPerRep) / 3600;
|
||||
return Math.round(MET * userWeight * activeTimeHours * 10) / 10;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const weight = 60; // kg
|
||||
const reps = 80; // 4组×20次
|
||||
const calories = calculateCalories(weight, reps); // 3.5 kcal
|
||||
```
|
||||
|
||||
#### 完整版:支持阻力调整
|
||||
|
||||
```javascript
|
||||
function calculateCaloriesAdvanced(config) {
|
||||
const {
|
||||
userWeight, // 用户体重(kg)
|
||||
totalReps, // 总次数
|
||||
bpm, // 节奏(次/分钟)
|
||||
resistanceLevel // 阻力等级:'low' | 'medium' | 'high'
|
||||
} = config;
|
||||
|
||||
// MET 值映射
|
||||
const MET_MAP = {
|
||||
low: 3.0,
|
||||
medium: 3.5,
|
||||
high: 4.0
|
||||
};
|
||||
|
||||
const MET = MET_MAP[resistanceLevel] || 3.5;
|
||||
const secondsPerRep = 60 / bpm;
|
||||
const activeTimeHours = (totalReps * secondsPerRep) / 3600;
|
||||
|
||||
return {
|
||||
calories: Math.round(MET * userWeight * activeTimeHours * 10) / 10,
|
||||
met: MET,
|
||||
duration: totalReps * secondsPerRep // 秒
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 食物对比展示
|
||||
|
||||
```javascript
|
||||
const FOOD_CALORIES = {
|
||||
apple: { name: '苹果', emoji: '🍎', calories: 52, unit: '个(100g)' },
|
||||
egg: { name: '鸡蛋', emoji: '🥚', calories: 70, unit: '个' },
|
||||
rice: { name: '米饭', emoji: '🍚', calories: 116, unit: '碗(100g)' },
|
||||
chocolate: { name: '巧克力', emoji: '🍫', calories: 54, unit: '块(10g)' },
|
||||
banana: { name: '香蕉', emoji: '�banana', calories: 89, unit: '根' }
|
||||
};
|
||||
|
||||
function getFoodComparison(burnedCalories) {
|
||||
// 选择最接近的食物
|
||||
const foods = Object.values(FOOD_CALORIES);
|
||||
const closest = foods.reduce((prev, curr) => {
|
||||
const prevDiff = Math.abs(prev.calories - burnedCalories);
|
||||
const currDiff = Math.abs(curr.calories - burnedCalories);
|
||||
return currDiff < prevDiff ? curr : prev;
|
||||
});
|
||||
|
||||
const ratio = (burnedCalories / closest.calories).toFixed(2);
|
||||
|
||||
return {
|
||||
food: closest,
|
||||
ratio: ratio,
|
||||
message: `相当于 ${ratio} ${closest.unit}${closest.name} ${closest.emoji}`
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. 数据验证
|
||||
|
||||
#### 合理性检查
|
||||
|
||||
对于 60kg 用户,4组×20次(80次)握力训练:
|
||||
|
||||
```
|
||||
实际运动时长 = 80 × 0.75秒 = 60秒 = 1分钟
|
||||
卡路里消耗 = 3.5 × 60 × (1/60) = 3.5 kcal
|
||||
```
|
||||
|
||||
**验证:**
|
||||
- ✅ 符合小肌群训练特点(消耗较少)
|
||||
- ✅ 与俯卧撑、平板支撑等可比运动一致
|
||||
- ✅ 与用户直觉相符(短时间训练,消耗不大)
|
||||
|
||||
#### 边界情况
|
||||
|
||||
```javascript
|
||||
// 最小值:轻量用户,少次数
|
||||
calculateCalories(50, 20) // ≈ 0.7 kcal
|
||||
|
||||
// 最大值:重量用户,多次数
|
||||
calculateCalories(100, 200) // ≈ 14.6 kcal
|
||||
|
||||
// 典型值:标准用户,标准训练
|
||||
calculateCalories(60, 80) // ≈ 3.5 kcal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 外部参考资料
|
||||
|
||||
### 学术来源
|
||||
|
||||
1. **Compendium of Physical Activities**
|
||||
- 网址: https://sites.google.com/site/compendiumofphysicalactivities/
|
||||
- 说明: 美国运动医学会维护的运动 MET 值数据库
|
||||
- 相关代码: 02030 (resistance training, light)
|
||||
|
||||
2. **ACSM's Guidelines for Exercise Testing and Prescription**
|
||||
- 说明: 美国运动医学会官方指南
|
||||
- 阻力训练 MET 值范围: 3.0-6.0
|
||||
|
||||
3. **Journal of Strength and Conditioning Research**
|
||||
- 说明: 力量训练能量消耗研究
|
||||
- 结论: 小肌群训练 MET 值通常低于全身性训练
|
||||
|
||||
### 在线计算器参考
|
||||
|
||||
1. **MyFitnessPal** - 阻力训练卡路里计算器
|
||||
2. **ExRx.net** - 运动能量消耗数据库
|
||||
3. **HealthStatus** - 力量训练卡路里计算器
|
||||
|
||||
---
|
||||
|
||||
## 注意事项与局限性
|
||||
|
||||
### 计算精度
|
||||
|
||||
- ✅ **相对准确**: 基于标准 MET 值,适用于大多数用户
|
||||
- ⚠️ **个体差异**: 实际消耗受代谢率、肌肉量、训练效率影响
|
||||
- ⚠️ **阻力未知**: 当前配置未记录握力器阻力值
|
||||
|
||||
### 改进方向
|
||||
|
||||
1. **用户输入体重**: 首次使用时询问体重(可选)
|
||||
2. **阻力等级选择**: 让用户选择握力器阻力档位
|
||||
3. **累计统计**: 记录历史数据,展示周/月累计消耗
|
||||
4. **动态调整**: 根据用户反馈微调 MET 值
|
||||
|
||||
### 免责说明
|
||||
|
||||
建议在 UI 中添加:
|
||||
> "卡路里消耗为估算值,实际消耗因人而异。本数据仅供参考,不构成医疗或健身建议。"
|
||||
|
||||
---
|
||||
|
||||
## 推荐实现方案
|
||||
|
||||
### 方案 A: 简化版(快速上线)
|
||||
|
||||
```javascript
|
||||
// 固定 MET = 3.5,假设用户体重 60kg
|
||||
const CALORIES_PER_REP = 0.044; // 基于 60kg 用户
|
||||
|
||||
function calculateCalories(totalReps) {
|
||||
return Math.round(totalReps * CALORIES_PER_REP * 10) / 10;
|
||||
}
|
||||
```
|
||||
|
||||
**优点**: 实现简单,无需用户输入
|
||||
**缺点**: 不考虑个体差异
|
||||
|
||||
### 方案 B: 标准版(推荐)
|
||||
|
||||
```javascript
|
||||
// 考虑用户体重,使用标准 MET 公式
|
||||
function calculateCalories(userWeight, totalReps, bpm = 80) {
|
||||
const MET = 3.5;
|
||||
const secondsPerRep = 60 / bpm;
|
||||
const activeTimeHours = (totalReps * secondsPerRep) / 3600;
|
||||
return Math.round(MET * userWeight * activeTimeHours * 10) / 10;
|
||||
}
|
||||
```
|
||||
|
||||
**优点**: 科学准确,考虑个体差异
|
||||
**缺点**: 需要获取用户体重
|
||||
|
||||
### 方案 C: 完整版(未来扩展)
|
||||
|
||||
- 支持阻力等级选择
|
||||
- 记录历史数据
|
||||
- 累计统计和趋势分析
|
||||
- 个性化 MET 值调整
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
### 核心公式
|
||||
|
||||
```
|
||||
卡路里消耗 = 3.5 × 用户体重(kg) × 运动时长(小时)
|
||||
```
|
||||
|
||||
### 典型值
|
||||
|
||||
- **60kg 用户,80次训练**: 约 **3.5 kcal**
|
||||
- **范围**: 3-5 kcal(大多数用户)
|
||||
|
||||
### 食物对比
|
||||
|
||||
- 单次训练消耗较少,建议使用累计统计或小单位对比
|
||||
- 推荐食物: 苹果、鸡蛋、巧克力、香蕉
|
||||
|
||||
### 实现建议
|
||||
|
||||
1. **MVP**: 使用方案 B(考虑体重的标准公式)
|
||||
2. **体重获取**: 首次使用时询问,存储在本地
|
||||
3. **展示策略**: "本次消耗 X.X kcal,相当于 1/15 个苹果"
|
||||
4. **未来扩展**: 累计统计、阻力调整、个性化建议
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "grip-ring-gamification",
|
||||
"name": "grip-ring-gamification",
|
||||
"title": "grip-ring-gamification",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "codex-agent",
|
||||
"assignee": "codex-agent",
|
||||
"createdAt": "2026-05-27",
|
||||
"completedAt": "2026-05-27",
|
||||
"branch": null,
|
||||
"base_branch": "mini-sport",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
Reference in New Issue
Block a user