# 三端联调审计问题修复实施计划 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 修复三端联调审计发现的 15 项问题,恢复系统统一性和可用性。 **Architecture:** 按优先级分 3 个 Phase 执行。Phase 1 快速修复基础设施和统一性问题(#1-#6),Phase 2 补全用户体验(#7-#11),Phase 3 实现功能闭环(#12-#15)。每个 Task 独立可提交。 **Tech Stack:** Rust/Axum/SeaORM (后端) · React 19/Ant Design 6/Zustand (Web 前端) · Taro 4.2 (小程序) · PostgreSQL (数据库) **Spec:** `docs/superpowers/specs/2026-05-01-tri-platform-audit-fix-design.md` --- ## Chunk 1: Phase 1 — 快速修复 (#1-#6) ### Task 1: erp-plugin 测试编译失败修复 **Files:** - Modify: `crates/erp-plugin/src/plugin_validator.rs:227` - [ ] **Step 1: 添加缺失的 import** 在第 227 行 `use super::*;` 之后添加一行: ```rust #[cfg(test)] mod tests { use super::*; use crate::manifest::parse_manifest; ``` - [ ] **Step 2: 验证编译通过** Run: `cargo test -p erp-plugin --no-run` Expected: 编译成功,无错误 - [ ] **Step 3: 运行测试** Run: `cargo test -p erp-plugin` Expected: 所有测试通过 - [ ] **Step 4: 提交** ```bash git add crates/erp-plugin/src/plugin_validator.rs git commit -m "fix(plugin): 修复测试编译失败 — 补充 parse_manifest 导入" ``` --- ### Task 2: 后端 ThemeResp 增加品牌字段 **Files:** - Modify: `crates/erp-config/src/dto.rs:220-227` - Modify: `crates/erp-config/src/handler/theme_handler.rs:15-21` - Modify: `crates/erp-config/src/handler/theme_handler.rs:110-135`(测试) - [ ] **Step 1: 扩展 ThemeResp DTO** 在 `crates/erp-config/src/dto.rs` 第 227 行 `sidebar_style` 字段之后添加 4 个品牌字段: ```rust #[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] pub struct ThemeResp { #[serde(skip_serializing_if = "Option::is_none")] pub primary_color: Option, #[serde(skip_serializing_if = "Option::is_none")] pub logo_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sidebar_style: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_slogan: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_features: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_copyright: Option, } ``` - [ ] **Step 2: 更新 default_theme 函数** 在 `crates/erp-config/src/handler/theme_handler.rs` 第 15-21 行,更新 `default_theme()` 函数: ```rust fn default_theme() -> ThemeResp { ThemeResp { primary_color: None, logo_url: None, sidebar_style: None, brand_name: Some("HMS 健康管理平台".into()), brand_slogan: Some("新一代健康管理平台".into()), brand_features: Some("患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()), brand_copyright: Some("HMS 健康管理平台 · ©汕头市智界科技有限公司".into()), } } ``` - [ ] **Step 3: 更新测试** 在 `crates/erp-config/src/handler/theme_handler.rs` 的测试模块中,更新 `default_theme_all_fields_none` 测试(因为 default_theme 现在品牌字段有值了)和 `theme_resp_serde_roundtrip` 测试: ```rust #[test] fn default_theme_has_brand_defaults() { let theme = default_theme(); assert!(theme.primary_color.is_none()); assert!(theme.logo_url.is_none()); assert!(theme.sidebar_style.is_none()); assert_eq!(theme.brand_name, Some("HMS 健康管理平台".to_string())); assert_eq!(theme.brand_slogan, Some("新一代健康管理平台".to_string())); assert!(theme.brand_features.is_some()); assert!(theme.brand_copyright.is_some()); } #[test] fn theme_resp_serde_roundtrip() { let theme = ThemeResp { primary_color: Some("#1890ff".to_string()), logo_url: None, sidebar_style: Some("dark".to_string()), brand_name: Some("测试平台".to_string()), brand_slogan: None, brand_features: None, brand_copyright: None, }; let json = serde_json::to_string(&theme).unwrap(); let back: ThemeResp = serde_json::from_str(&json).unwrap(); assert_eq!(back.primary_color, Some("#1890ff".to_string())); assert_eq!(back.brand_name, Some("测试平台".to_string())); assert!(back.brand_slogan.is_none()); } ``` - [ ] **Step 4: 添加公开品牌信息端点** 在 `crates/erp-config/src/handler/theme_handler.rs` 中新增公开端点 handler(无需认证): ```rust /// 品牌信息公开响应(不含内部配置) #[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] pub struct PublicBrandResp { pub brand_name: String, pub brand_slogan: String, pub brand_features: String, pub brand_copyright: String, } /// GET /api/v1/public/brand — 公开品牌信息(无需认证) pub async fn get_public_brand() -> JsonResponse> { let defaults = default_theme(); JsonResponse(ApiResponse::ok(PublicBrandResp { brand_name: defaults.brand_name.unwrap_or_else(|| "HMS 健康管理平台".into()), brand_slogan: defaults.brand_slogan.unwrap_or_else(|| "新一代健康管理平台".into()), brand_features: defaults.brand_features.unwrap_or_else(|| "患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()), brand_copyright: defaults.brand_copyright.unwrap_or_else(|| "HMS 健康管理平台 · ©汕头市智界科技有限公司".into()), })) } ``` > **注意**: 此端点需要在 `erp-server` 的公开路由中注册。先完成此 Task 的代码修改,路由注册在单独步骤中处理。公开端点暂时返回默认值,后续可通过 settings 读取租户自定义配置。 - [ ] **Step 5: 验证编译** Run: `cargo check` Expected: 编译通过 - [ ] **Step 6: 注册公开路由** 在 `crates/erp-server/src/main.rs`(或对应的路由注册文件)中,找到公开路由组(通常包含 `/login`、`/register` 等),添加: ```rust // 在公开路由区域添加 .route("/api/v1/public/brand", get(config_handler::get_public_brand)) ``` 需要导入 handler:`use erp_config::handler as config_handler;`(或按项目已有模式导入)。 > 在 `erp-config/src/module.rs` 的 `ErpModule::public_routes()` 中注册此路由。 - [ ] **Step 7: 验证端点可用** Run: `cargo run`(启动后端) Run: `curl http://localhost:3000/api/v1/public/brand` Expected: `{"success":true,"data":{"brand_name":"HMS 健康管理平台",...}}` - [ ] **Step 8: 提交** ```bash git add crates/erp-config/src/dto.rs crates/erp-config/src/handler/theme_handler.rs git commit -m "feat(config): ThemeResp 增加品牌字段 + 公开品牌信息端点" ``` --- ### Task 3: 前端主题设置联动 — API + Store + 主题设置页面 **Files:** - Modify: `apps/web/src/api/themes.ts` - Modify: `apps/web/src/stores/app.ts` - Modify: `apps/web/src/pages/settings/ThemeSettings.tsx` - [ ] **Step 1: 扩展 ThemeConfig 接口** 修改 `apps/web/src/api/themes.ts`: ```typescript import client from './client'; export interface ThemeConfig { primary_color?: string; logo_url?: string; sidebar_style?: 'light' | 'dark'; brand_name?: string; brand_slogan?: string; brand_features?: string; brand_copyright?: string; } export async function getTheme() { const { data } = await client.get<{ success: boolean; data: ThemeConfig }>( '/config/themes', ); return data.data; } export async function updateTheme(theme: ThemeConfig) { const { data } = await client.put<{ success: boolean; data: ThemeConfig }>( '/config/themes', theme, ); return data.data; } export interface BrandConfig { brand_name: string; brand_slogan: string; brand_features: string; brand_copyright: string; } const BRAND_DEFAULTS: BrandConfig = { brand_name: 'HMS 健康管理平台', brand_slogan: '新一代健康管理平台', brand_features: '患者管理 · 健康监测 · 随访管理 · AI 智能分析', brand_copyright: 'HMS 健康管理平台 · ©汕头市智界科技有限公司', }; export async function getPublicBrand(): Promise { try { const res = await fetch('/api/v1/public/brand'); const json = await res.json(); if (json?.success && json?.data) return json.data; } catch {} return BRAND_DEFAULTS; } ``` - [ ] **Step 2: 扩展 app store — 添加 themeConfig 缓存** 修改 `apps/web/src/stores/app.ts`,添加 themeConfig 和 loadThemeConfig: ```typescript import { create } from 'zustand'; import type { ThemeConfig } from '../api/themes'; import { getTheme } from '../api/themes'; export type ThemeName = 'blue' | 'warm' | 'dark' | 'emerald'; // ... (THEME_OPTIONS 保持不变) const STORAGE_KEY = 'hms-theme'; const THEME_CONFIG_KEY = 'hms-theme-config'; function loadTheme(): ThemeName { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved && THEME_OPTIONS.some((t) => t.key === saved)) return saved as ThemeName; } catch {} return 'blue'; } interface AppState { theme: ThemeName; sidebarCollapsed: boolean; themeConfig: ThemeConfig | null; toggleSidebar: () => void; setTheme: (theme: ThemeName) => void; loadThemeConfig: () => Promise; } export const useAppStore = create((set) => ({ theme: loadTheme(), sidebarCollapsed: false, themeConfig: null, toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), setTheme: (theme) => { try { localStorage.setItem(STORAGE_KEY, theme); } catch {} set({ theme }); }, loadThemeConfig: async () => { try { const config = await getTheme(); try { localStorage.setItem(THEME_CONFIG_KEY, JSON.stringify(config)); } catch {} set({ themeConfig: config }); } catch {} }, })); ``` - [ ] **Step 3: 在 MainLayout mount 时加载 themeConfig** 修改 `apps/web/src/layouts/MainLayout.tsx`,在组件中添加 loadThemeConfig 调用: 在组件函数体内,现有 useEffect 附近添加: ```typescript const loadThemeConfig = useAppStore((s) => s.loadThemeConfig); useEffect(() => { loadThemeConfig(); }, [loadThemeConfig]); ``` - [ ] **Step 4: 扩展 ThemeSettings 页面** 修改 `apps/web/src/pages/settings/ThemeSettings.tsx`,在现有表单的保存按钮之前,添加品牌信息区域: ```tsx import { useEffect, useState, useCallback } from 'react'; import { Form, Input, Select, Button, ColorPicker, message, Typography, Divider } from 'antd'; import { getTheme, updateTheme } from '../../api/themes'; export default function ThemeSettings() { const [form] = Form.useForm(); const [, setLoading] = useState(false); const [saving, setSaving] = useState(false); const fetchTheme = useCallback(async () => { setLoading(true); try { const theme = await getTheme(); form.setFieldsValue({ primary_color: theme.primary_color || '#1677ff', logo_url: theme.logo_url || '', sidebar_style: theme.sidebar_style || 'light', brand_name: theme.brand_name || '', brand_slogan: theme.brand_slogan || '', brand_features: theme.brand_features || '', brand_copyright: theme.brand_copyright || '', }); } catch { form.setFieldsValue({ primary_color: '#1677ff', logo_url: '', sidebar_style: 'light', }); } setLoading(false); }, [form]); useEffect(() => { fetchTheme(); }, [fetchTheme]); const handleSave = async (values: Record) => { setSaving(true); try { const primaryColor = typeof values.primary_color === 'string' ? values.primary_color : (values.primary_color as { toHexString?: () => string })?.toHexString?.() ?? String(values.primary_color); await updateTheme({ primary_color: primaryColor, logo_url: values.logo_url as string, sidebar_style: values.sidebar_style as 'light' | 'dark', brand_name: (values.brand_name as string) || undefined, brand_slogan: (values.brand_slogan as string) || undefined, brand_features: (values.brand_features as string) || undefined, brand_copyright: (values.brand_copyright as string) || undefined, }); message.success('主题设置已保存'); } catch (err: unknown) { const errorMsg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || '保存失败'; message.error(errorMsg); } setSaving(false); }; return (
主题设置
); } ``` - [ ] **Step 5: 验证前端编译** Run: `cd apps/web && pnpm build` Expected: 构建成功 - [ ] **Step 6: 提交** ```bash git add apps/web/src/api/themes.ts apps/web/src/stores/app.ts apps/web/src/pages/settings/ThemeSettings.tsx git commit -m "feat(web): 主题设置增加品牌信息字段 + store 缓存 + 公开品牌 API" ``` --- ### Task 4: Login.tsx 和 MainLayout.tsx 从主题配置读取品牌信息 **Files:** - Modify: `apps/web/src/pages/Login.tsx:35-56,105-107` - Modify: `apps/web/src/layouts/MainLayout.tsx:434,526-528` - [ ] **Step 1: 修改 Login.tsx — 从公开端点读取品牌信息** 在 `apps/web/src/pages/Login.tsx` 中: 1. 添加 `useEffect, useState` 导入(如尚未导入) 2. 添加 brandConfig 状态和 useEffect 获取逻辑 3. 替换硬编码品牌文字 在组件函数体开头添加: ```typescript const [brandConfig, setBrandConfig] = useState<{ brand_name?: string; brand_slogan?: string; brand_features?: string; brand_copyright?: string; } | null>(null); useEffect(() => { const cached = localStorage.getItem('hms-theme-config'); if (cached) { try { setBrandConfig(JSON.parse(cached)); } catch {} } fetch('/api/v1/public/brand') .then(res => res.json()) .then(data => { if (data?.success && data?.data) { setBrandConfig(data.data); localStorage.setItem('hms-theme-config', JSON.stringify(data.data)); } }) .catch(() => {}); }, []); ``` 替换第 40-42 行硬编码文字: ```tsx

{brandConfig?.brand_name || 'HMS 健康管理平台'}

{brandConfig?.brand_slogan || '新一代健康管理平台'}

{brandConfig?.brand_features || '患者管理 · 健康监测 · 随访管理 · AI 智能分析'}

``` 替换第 106 行版权文字: ```tsx
{brandConfig?.brand_copyright || 'HMS 健康管理平台 · ©汕头市智界科技有限公司'}
``` - [ ] **Step 2: 修改 MainLayout.tsx — Footer 和 Logo 从配置读取** 在 `apps/web/src/layouts/MainLayout.tsx` 中: 1. 获取 themeConfig:在组件函数体内添加: ```typescript const themeConfig = useAppStore((s) => s.themeConfig); ``` 2. 替换第 434 行侧边栏 Logo 文字: ```tsx {themeConfig?.brand_name || 'HMS 健康'} ``` 3. 替换第 527 行 Footer 文字: ```tsx
{themeConfig?.brand_copyright || 'HMS 健康管理平台'}
``` - [ ] **Step 3: 验证前端编译** Run: `cd apps/web && pnpm build` Expected: 构建成功 - [ ] **Step 4: 浏览器验证** 1. 打开登录页,确认显示 "HMS 健康管理平台"(默认值) 2. 登录后进入系统设置 → 主题设置 3. 修改品牌名称为 "XX 医院健康管理" 4. 保存后刷新页面,确认侧边栏/页脚更新 5. 登出后确认登录页显示新品牌名称 - [ ] **Step 5: 提交** ```bash git add apps/web/src/pages/Login.tsx apps/web/src/layouts/MainLayout.tsx git commit -m "feat(web): Login/MainLayout 从主题配置读取品牌信息" ``` --- ### Task 5: 告警和行动收件箱侧边栏菜单入口 **Files:** - Modify: `apps/web/src/layouts/MainLayout.tsx:1-32,51-75` - [ ] **Step 1: 在 MainLayout.tsx 补充图标导入** 在 `apps/web/src/layouts/MainLayout.tsx` 的 import 区域(第 31 行 `BarChartOutlined` 之后)添加: ```typescript AlertOutlined, BellOutlined, ControlOutlined, InboxOutlined, ApiOutlined, ReadOutlined, ExperimentOutlined, ``` - [ ] **Step 2: 在 iconMap 中补充图标映射** 在 `iconMap` 对象(第 74 行 `BarChartOutlined` 之后)添加: ```typescript AlertOutlined: , BellOutlined: , ControlOutlined: , InboxOutlined: , ApiOutlined: , ReadOutlined: , ExperimentOutlined: , ``` - [ ] **Step 3: 验证前端编译** Run: `cd apps/web && pnpm build` Expected: 构建成功 - [ ] **Step 4: 提交** ```bash git add apps/web/src/layouts/MainLayout.tsx git commit -m "fix(web): 补充告警/行动收件箱/设备等菜单图标映射" ``` --- ### Task 6: 行动收件箱菜单种子迁移 **Files:** - Create: `crates/erp-server/migration/src/m20260501_000100_seed_action_inbox_menu.rs` - Modify: `crates/erp-server/migration/src/lib.rs:207` - [ ] **Step 1: 创建迁移文件** 创建 `crates/erp-server/migration/src/m20260501_000100_seed_action_inbox_menu.rs`: ```rust use sea_orm_migration::prelude::*; #[derive(DeriveMigrationName)] pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); // 插入行动收件箱菜单 db.execute_unprepared( r#" INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, created_at, updated_at, is_active) SELECT 'b0000003-0000-7000-8000-000000000020'::uuid, t.id, (SELECT id FROM menus WHERE path = '/health' AND tenant_id = t.id LIMIT 1), '行动收件箱', '/health/action-inbox', 'InboxOutlined', 36, NOW(), NOW(), true FROM tenants t WHERE NOT EXISTS ( SELECT 1 FROM menus WHERE path = '/health/action-inbox' AND tenant_id = t.id ) "#, ) .await?; // 关联权限码 db.execute_unprepared( r#" INSERT INTO role_permissions (role_id, permission_code, tenant_id) SELECT r.id, 'health.action-inbox.list', t.id FROM tenants t JOIN roles r ON r.tenant_id = t.id AND r.code = 'admin' WHERE NOT EXISTS ( SELECT 1 FROM role_permissions rp WHERE rp.permission_code = 'health.action-inbox.list' AND rp.role_id = r.id ) "#, ) .await?; Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); db.execute_unprepared( "DELETE FROM menus WHERE path = '/health/action-inbox'", ) .await?; Ok(()) } } ``` - [ ] **Step 2: 在 lib.rs 中注册迁移** 在 `crates/erp-server/migration/src/lib.rs` 中: 1. 添加模块声明(在文件末尾 `mod` 声明区域): ```rust mod m20260501_000100_seed_action_inbox_menu; ``` 2. 在 migration list 的最后一项 `m20260501_000099_create_ai_risk_threshold::Migration` 之后添加: ```rust Box::new(m20260501_000100_seed_action_inbox_menu::Migration), ``` - [ ] **Step 3: 验证编译** Run: `cargo check` Expected: 编译通过 - [ ] **Step 4: 提交** ```bash git add crates/erp-server/migration/src/m20260501_000100_seed_action_inbox_menu.rs crates/erp-server/migration/src/lib.rs git commit -m "feat(migration): 行动收件箱菜单种子数据 + 权限关联" ``` --- ### Task 7: 危急值告警端点 500 修复 **Files:** - Modify: `crates/erp-health/src/service/critical_alert_service.rs` - [ ] **Step 1: 添加 tracing 日志** 在 `crates/erp-health/src/service/critical_alert_service.rs` 的 `list_pending_alerts` 函数中,找到查询执行的 `.await`,在错误处理链中添加 tracing: ```rust // 找到类似以下代码,添加 tracing::error! .all(&state.db) .await .map_err(|e| { tracing::error!(error = %e, "查询危急值告警列表失败"); HealthError::DbError(e.to_string()) })?; ``` 对 `get_alert` 函数同理添加。 - [ ] **Step 2: 验证数据库状态并测试** 启动后端 `cargo run`,用 curl 测试端点,查看 tracing 日志确认根因。 根据日志输出决定下一步: - 如果是表不存在 → 确认迁移 m000090 是否执行 - 如果是 RLS 策略缺失 → 创建 Task 7b RLS 补齐迁移 - 如果是其他错误 → 根据日志修复 > **此步骤需要运行时验证,根据实际错误决定是否需要创建 RLS 补齐迁移。** - [ ] **Step 3: 提交** ```bash git add crates/erp-health/src/service/critical_alert_service.rs git commit -m "fix(health): 危急值告警查询添加 tracing 日志" ``` --- ### Task 8: domain_events 堆积清理 **Files:** - Modify: `crates/erp-server/src/tasks.rs:26` - [ ] **Step 1: 修改事件保留期** 在 `crates/erp-server/src/tasks.rs` 第 26 行,将 90 天改为 7 天: ```rust // 修改前 "SELECT cleanup_old_published_events(90, 1000)" // 修改后 "SELECT cleanup_old_published_events(7, 1000)" ``` 同时更新第 6 行注释: ```rust /// - 调用 `cleanup_old_published_events()` 归档 >7 天的已发布事件 ``` - [ ] **Step 2: 手动清理当前堆积** 在数据库中执行 SQL 清理已发布事件: ```sql INSERT INTO domain_events_archive (id, tenant_id, event_type, payload, correlation_id, status, attempts, last_error, created_at, published_at) SELECT id, tenant_id, event_type, payload, correlation_id, status, attempts, last_error, created_at, published_at FROM domain_events WHERE status = 'published'; DELETE FROM domain_events WHERE status = 'published' AND id IN (SELECT id FROM domain_events_archive); ``` > **此步骤需要手动执行 SQL,不在代码中自动化。** - [ ] **Step 3: 验证编译** Run: `cargo check` Expected: 编译通过 - [ ] **Step 4: 提交** ```bash git add crates/erp-server/src/tasks.rs git commit -m "fix(server): 事件清理保留期从 90 天调整为 7 天" ``` --- ## Phase 1 验证清单 完成所有 Task 后执行: - [ ] `cargo check` — 全 workspace 编译通过 - [ ] `cargo test -p erp-plugin` — 测试通过 - [ ] `cd apps/web && pnpm build` — 前端构建通过 - [ ] 浏览器验证:登录页品牌信息、侧边栏菜单图标、主题设置品牌编辑 - [ ] `git push` — 推送到远程 --- ## Chunk 2: Phase 2 — 体验补全 (#7-#11) ### Task 9: 患者详情快捷导航卡片 **Files:** - Modify: `apps/web/src/pages/health/PatientDetail.tsx:222-224` - [ ] **Step 1: 在 PatientDetail.tsx 插入快捷导航卡片** 在第 222 行(患者信息卡片 `` 闭合标签)和第 224 行(Tabs `` 开始标签)之间插入: ```tsx {/* 快捷导航 */} 快捷跳转: ``` 确认 `Button`、`Space`、`Text` 已在文件顶部 import(通常已导入)。确认 `navigate` 来自 `useNavigate()`。 - [ ] **Step 2: 验证编译** Run: `cd apps/web && pnpm build` Expected: 构建成功 - [ ] **Step 3: 提交** ```bash git add apps/web/src/pages/health/PatientDetail.tsx git commit -m "feat(web): 患者详情页增加快捷导航卡片 — 预约/咨询/透析/随访/AI" ``` --- ### Task 10: 列表页面支持 URL 参数 patient_id 过滤 **Files:** - Modify: `apps/web/src/pages/health/AppointmentList.tsx` - Modify: `apps/web/src/pages/health/ConsultationList.tsx` - Modify: `apps/web/src/pages/health/DialysisManageList.tsx` - Modify: `apps/web/src/pages/health/FollowUpTaskList.tsx` - Modify: `apps/web/src/pages/health/AiAnalysisList.tsx` 这 5 个页面需要支持从 URL 读取 `patient_id` 参数作为默认筛选条件。统一模式如下: - [ ] **Step 1: 逐一检查各页面当前的筛选逻辑** 对每个页面,确认: - 是否已使用 `useSearchParams()` 或 `useLocation()` 读取 URL 参数 - 筛选器状态如何初始化(useState 初始值) - API 调用的参数传递方式 > 此步骤为调研,不修改代码。根据调研结果决定每个页面的具体修改方案。 - [ ] **Step 2: 为每个页面添加 URL patient_id 读取** 统一模式:在组件初始化时从 URL 读取 `patient_id`,设为筛选器默认值。 ```typescript import { useSearchParams } from 'react-router-dom'; // 在组件函数体内 const [searchParams] = useSearchParams(); const urlPatientId = searchParams.get('patient_id'); // 在筛选器状态中设置默认值 const [filters, setFilters] = useState({ // ... 已有筛选器 patient_id: urlPatientId || undefined, }); ``` 如果页面使用 `PatientSelect` 组件,将 `urlPatientId` 作为初始值传入。 - [ ] **Step 3: 验证编译** Run: `cd apps/web && pnpm build` Expected: 构建成功 - [ ] **Step 4: 提交** ```bash git add apps/web/src/pages/health/AppointmentList.tsx \ apps/web/src/pages/health/ConsultationList.tsx \ apps/web/src/pages/health/DialysisManageList.tsx \ apps/web/src/pages/health/FollowUpTaskList.tsx \ apps/web/src/pages/health/AiAnalysisList.tsx git commit -m "feat(web): 5 个列表页支持 URL 参数 patient_id 自动筛选" ``` --- ### Task 11: AI 分析列表添加患者 Link **Files:** - Modify: `apps/web/src/pages/health/AiAnalysisList.tsx:1-2,316-323` - [ ] **Step 1: 添加 Link 导入** 在 `apps/web/src/pages/health/AiAnalysisList.tsx` 顶部 import 区域添加: ```typescript import { Link } from 'react-router-dom'; ``` - [ ] **Step 2: 修改 patient_id 列渲染** 将第 316-323 行的列定义从: ```tsx { title: '患者 ID', dataIndex: 'patient_id', key: 'patient_id', width: 120, render: (v: string) => ( {v.slice(0, 8)} ), }, ``` 改为: ```tsx { title: '患者', dataIndex: 'patient_id', key: 'patient_id', width: 140, render: (v: string) => ( {v.slice(0, 8)} ), }, ``` - [ ] **Step 3: 验证编译** Run: `cd apps/web && pnpm build` Expected: 构建成功 - [ ] **Step 4: 提交** ```bash git add apps/web/src/pages/health/AiAnalysisList.tsx git commit -m "feat(web): AI 分析列表患者 ID 改为可点击 Link 跳转详情" ``` --- ### Task 12: 小程序 AI 建议跳转修复 **Files:** - Modify: `apps/miniprogram/src/pages/health/index.tsx:178` - [ ] **Step 1: 修改 AI 建议卡片 onClick** 在 `apps/miniprogram/src/pages/health/index.tsx` 第 178 行,将: ```tsx Taro.navigateTo({ url: '/pages/pkg-profile/settings/index' })}> ``` 改为: ```tsx { const firstSuggestion = aiSuggestions[0]; if (firstSuggestion?.suggestion_type === 'appointment') { Taro.navigateTo({ url: `/pages/pkg-appointment/create/index?patientId=${firstSuggestion.patient_id}` }); } else if (firstSuggestion?.suggestion_type === 'followup') { Taro.navigateTo({ url: `/pages/pkg-profile/followups/index` }); } else { Taro.navigateTo({ url: '/pages/health/index' }); } }}> ``` > **注意**: 卡片整体只有一个 onClick,跳转基于第一条建议的类型。如果需要每条建议单独点击,需要将 onClick 移到 `ai-suggestion-item` 上并传入具体 item。 - [ ] **Step 2: 验证小程序编译** Run: `cd apps/miniprogram && pnpm build:weapp` Expected: 构建成功 - [ ] **Step 3: 提交** ```bash git add apps/miniprogram/src/pages/health/index.tsx git commit -m "fix(miniprogram): AI 建议卡片按 suggestion_type 跳转 — 而非统一跳设置页" ``` --- ### Task 13: 小程序通知 Tab 对接消息 API **Files:** - Create: `apps/miniprogram/src/services/notification.ts` - Modify: `apps/miniprogram/src/pages/messages/index.tsx:34-38` - [ ] **Step 1: 创建通知服务** 创建 `apps/miniprogram/src/services/notification.ts`: ```typescript import { api } from './request'; export const notificationService = { list: (params?: { page?: number; page_size?: number }) => api.get('/messages', params as Record), markRead: (id: string) => api.put(`/messages/${id}/read`), markAllRead: () => api.put('/messages/read-all'), getUnreadCount: () => api.get('/messages/unread-count'), }; ``` - [ ] **Step 2: 替换硬编码空数组** 在 `apps/miniprogram/src/pages/messages/index.tsx` 中: 1. 在顶部添加导入: ```typescript import { notificationService } from '../../services/notification'; ``` 2. 将第 35-38 行的 `else` 分支从: ```typescript } else { // 通知目前从咨询中提取状态变化作为示例 // 后续可对接独立通知 API setNotifications([]); } ``` 改为: ```typescript } else { const res = await notificationService.list({ page: 1, page_size: 20 }); setNotifications(res?.data || []); } ``` - [ ] **Step 3: 验证小程序编译** Run: `cd apps/miniprogram && pnpm build:weapp` Expected: 构建成功 - [ ] **Step 4: 提交** ```bash git add apps/miniprogram/src/services/notification.ts apps/miniprogram/src/pages/messages/index.tsx git commit -m "feat(miniprogram): 通知 Tab 对接 erp-message 消息 API — 替换空壳" ``` --- ### Task 14: 小程序咨询功能入口 **Files:** - Modify: `apps/miniprogram/src/pages/profile/index.tsx:8-22,24-38` - [ ] **Step 1: 在 MENU_ITEMS 添加在线咨询** 在 `apps/miniprogram/src/pages/profile/index.tsx` 的 `MENU_ITEMS` 数组(第 21 行 "设置" 之前)添加: ```typescript { label: '我的预约', icon: '📅', bg: '#E8F0F8' }, { label: '在线咨询', icon: '💬', bg: '#E8F0E8' }, ``` - [ ] **Step 2: 在 MENU_PATHS 添加路径映射** 在 `MENU_PATHS`(第 24-38 行)添加对应路径: ```typescript '我的预约': '/pages/pkg-appointment/index', '在线咨询': '/pages/consultation/index', ``` > **注意**: 需确认 `/pages/pkg-appointment/index` 路径存在。如果预约页面路径不同,按实际路径填写。 - [ ] **Step 3: 验证小程序编译** Run: `cd apps/miniprogram && pnpm build:weapp` Expected: 构建成功 - [ ] **Step 4: 提交** ```bash git add apps/miniprogram/src/pages/profile/index.tsx git commit -m "feat(miniprogram): 个人中心添加我的预约+在线咨询入口" ``` --- ## Phase 2 验证清单 完成所有 Task 后执行: - [ ] `cd apps/web && pnpm build` — Web 前端构建通过 - [ ] `cd apps/miniprogram && pnpm build:weapp` — 小程序构建通过 - [ ] 浏览器验证:患者详情快捷导航 → 目标页面自动过滤 - [ ] 浏览器验证:AI 分析列表患者 Link 跳转正常 - [ ] 小程序验证:AI 建议卡片跳转到正确页面(非设置页) - [ ] 小程序验证:消息页通知 Tab 展示后端消息数据 - [ ] 小程序验证:个人中心出现"在线咨询"入口 - [ ] `git push` — 推送到远程