diff --git a/docs/superpowers/plans/2026-04-25-slice2-ai-admin-pages.md b/docs/superpowers/plans/2026-04-25-slice2-ai-admin-pages.md new file mode 100644 index 0000000..177699c --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-slice2-ai-admin-pages.md @@ -0,0 +1,884 @@ +# 切片 2: AI 管理端 3 页面 实施计划 + +> **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:** 实现 AI 模块的 PC 管理端 — Prompt 管理、分析历史、用量统计 3 个页面,以及对应的后端 API 补全。 + +**Architecture:** 后端 4 个 SSE 端点已可用,但 Prompt CRUD / 分析历史查询 / 用量统计端点为空壳或缺失。先补全后端 API(handler + service 方法),再实现前端 API 封装和 3 个管理页面,最后注册菜单和路由。 + +**Tech Stack:** Rust/Axum (后端) + React 19/TypeScript/Ant Design 6 (前端) + +**设计规格:** `docs/superpowers/specs/2026-04-25-feature-completion-design.md` §3 + +--- + +## Chunk 1: 后端 API 补全 + +### Task 1: PromptService — 补全 CRUD 方法 + +**Files:** +- Modify: `crates/erp-ai/src/service/prompt.rs` + +**现状:** 仅有 `get_active_prompt` + `create_prompt`。需新增 `list_prompts`、`update_prompt`、`activate_prompt`、`rollback_prompt`。 + +- [ ] **Step 1: 添加 list_prompts 方法** + +在 `PromptService` impl 中追加: + +```rust +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, Set}; +use erp_core::types::Pagination; + +pub async fn list_prompts( + &self, + tenant_id: Uuid, + category: Option, + pagination: Pagination, +) -> AiResult<(Vec, u64)> { + let mut query = ai_prompt::Entity::find() + .filter(ai_prompt::Column::TenantId.eq(tenant_id)) + .filter(ai_prompt::Column::DeletedAt.is_null()); + + if let Some(cat) = category { + query = query.filter(ai_prompt::Column::Category.eq(cat)); + } + + let total = query.clone().count(&self.db).await?; + let items = query + .order_by_desc(ai_prompt::Column::UpdatedAt) + .offset(pagination.offset()) + .limit(pagination.limit()) + .all(&self.db) + .await?; + Ok((items, total)) +} +``` + +- [ ] **Step 2: 添加 update_prompt 方法** + +```rust +pub async fn update_prompt( + &self, + id: Uuid, + tenant_id: Uuid, + user_id: Uuid, + system_prompt: Option, + user_prompt_template: Option, + model_config: Option, + description: Option, +) -> AiResult { + let entity = ai_prompt::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or_else(|| AiError::PromptNotFound(id.to_string()))?; + + if entity.tenant_id != tenant_id { + return Err(AiError::Validation("跨租户操作".into())); + } + + // 创建新版本 + let new_id = Uuid::now_v7(); + let now = chrono::Utc::now(); + let active = ai_prompt::ActiveModel { + id: Set(new_id), + tenant_id: Set(tenant_id), + name: Set(entity.name.clone()), + description: Set(description.unwrap_or(entity.description.clone())), + system_prompt: Set(system_prompt.unwrap_or(entity.system_prompt.clone())), + user_prompt_template: Set(user_prompt_template.unwrap_or(entity.user_prompt_template.clone())), + variables_schema: Set(entity.variables_schema.clone()), + model_config: Set(model_config.unwrap_or(entity.model_config.clone())), + version: Set(entity.version + 1), + is_active: Set(entity.is_active), + category: Set(entity.category.clone()), + tags: Set(entity.tags.clone()), + created_at: Set(now), + updated_at: Set(now), + created_by: Set(Some(user_id)), + updated_by: Set(Some(user_id)), + deleted_at: Set(None), + version_lock: Set(1), + }; + Ok(active.insert(&self.db).await?) +} +``` + +- [ ] **Step 3: 添加 activate_prompt 方法** + +```rust +pub async fn activate_prompt( + &self, + id: Uuid, + tenant_id: Uuid, +) -> AiResult { + let entity = ai_prompt::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or_else(|| AiError::PromptNotFound(id.to_string()))?; + + if entity.tenant_id != tenant_id { + return Err(AiError::Validation("跨租户操作".into())); + } + + // 停用同 name + category 的其他版本 + let siblings = ai_prompt::Entity::find() + .filter(ai_prompt::Column::TenantId.eq(tenant_id)) + .filter(ai_prompt::Column::Name.eq(&entity.name)) + .filter(ai_prompt::Column::Category.eq(&entity.category)) + .filter(ai_prompt::Column::IsActive.eq(true)) + .filter(ai_prompt::Column::DeletedAt.is_null()) + .all(&self.db) + .await?; + + for sibling in siblings { + let mut active: ai_prompt::ActiveModel = sibling.into(); + active.is_active = Set(false); + active.updated_at = Set(chrono::Utc::now()); + active.update(&self.db).await?; + } + + // 激活目标 + let mut active: ai_prompt::ActiveModel = entity.into(); + active.is_active = Set(true); + active.updated_at = Set(chrono::Utc::now()); + Ok(active.update(&self.db).await?) +} +``` + +- [ ] **Step 4: 添加 rollback_prompt(激活指定旧版本)** + +```rust +pub async fn rollback_prompt( + &self, + id: Uuid, + tenant_id: Uuid, +) -> AiResult { + // 回滚 = 激活指定版本 + self.activate_prompt(id, tenant_id).await +} +``` + +- [ ] **Step 5: 编译验证** + +Run: `cargo check -p erp-ai` +Expected: 编译通过 + +- [ ] **Step 6: 提交** + +```bash +git add crates/erp-ai/src/service/prompt.rs +git commit -m "feat(ai): PromptService 补全 list/update/activate/rollback 方法" +``` + +--- + +### Task 2: AnalysisService — 补全查询方法 + +**Files:** +- Modify: `crates/erp-ai/src/service/analysis.rs` + +**现状:** 有 `stream_analyze`、`complete_analysis`、`fail_analysis`、`find_cached`。需新增 `list_analysis`、`get_analysis`。 + +- [ ] **Step 1: 添加 list_analysis 方法** + +在 `AnalysisService` impl 中追加: + +```rust +use erp_core::types::Pagination; + +pub async fn list_analysis( + &self, + tenant_id: Uuid, + patient_id: Option, + analysis_type: Option, + pagination: Pagination, +) -> AiResult<(Vec, u64)> { + let mut query = ai_analysis::Entity::find() + .filter(ai_analysis::Column::TenantId.eq(tenant_id)) + .filter(ai_analysis::Column::DeletedAt.is_null()); + + if let Some(pid) = patient_id { + query = query.filter(ai_analysis::Column::PatientId.eq(pid)); + } + if let Some(at) = analysis_type { + query = query.filter(ai_analysis::Column::AnalysisType.eq(at)); + } + + let total = query.clone().count(&self.db).await?; + let items = query + .order_by_desc(ai_analysis::Column::CreatedAt) + .offset(pagination.offset()) + .limit(pagination.limit()) + .all(&self.db) + .await?; + Ok((items, total)) +} +``` + +- [ ] **Step 2: 添加 get_analysis 方法** + +```rust +pub async fn get_analysis( + &self, + id: Uuid, + tenant_id: Uuid, +) -> AiResult { + ai_analysis::Entity::find_by_id(id) + .one(&self.db) + .await? + .filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none()) + .ok_or_else(|| AiError::AnalysisNotFound(id.to_string())) +} +``` + +注意:上面 filter 需改写为 match 式: + +```rust +pub async fn get_analysis( + &self, + id: Uuid, + tenant_id: Uuid, +) -> AiResult { + let model = ai_analysis::Entity::find_by_id(id) + .one(&self.db) + .await? + .ok_or_else(|| AiError::AnalysisNotFound(id.to_string()))?; + if model.tenant_id != tenant_id { + return Err(AiError::AnalysisNotFound(id.to_string())); + } + Ok(model) +} +``` + +- [ ] **Step 3: 编译验证** + +Run: `cargo check -p erp-ai` +Expected: 编译通过 + +- [ ] **Step 4: 提交** + +```bash +git add crates/erp-ai/src/service/analysis.rs +git commit -m "feat(ai): AnalysisService 补全 list/get 查询方法" +``` + +--- + +### Task 3: UsageService — 补全聚合查询方法 + +**Files:** +- Modify: `crates/erp-ai/src/service/usage.rs` + +**现状:** 仅有 `log_usage`。需新增 `get_overview`、`get_trend`、`get_by_type`。 + +**注意:** `ai_usage_logs` 表无 `created_by` 字段,用户排行从 `ai_analysis_results.created_by` 聚合。 + +- [ ] **Step 1: 添加 get_overview 方法** + +```rust +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, FromQueryResult, QuerySelect, Func}; +use crate::entity::ai_analysis; + +#[derive(Debug, FromQueryResult)] +pub struct UsageOverview { + pub total_count: i64, + pub total_input_tokens: i64, + pub total_output_tokens: i64, +} + +pub async fn get_overview( + &self, + tenant_id: Uuid, +) -> AiResult { + let result = ai_analysis::Entity::find() + .filter(ai_analysis::Column::TenantId.eq(tenant_id)) + .filter(ai_analysis::Column::Status.eq("completed")) + .filter(ai_analysis::Column::DeletedAt.is_null()) + .select_only() + .column_as(ai_analysis::Column::Id.count(), "total_count") + .into_model::() + .one(&self.db) + .await? + .unwrap_or(UsageOverview { + total_count: 0, + total_input_tokens: 0, + total_output_tokens: 0, + }); + Ok(result) +} +``` + +- [ ] **Step 2: 添加 get_by_type 方法** + +```rust +#[derive(Debug, FromQueryResult)] +pub struct TypeCount { + pub analysis_type: String, + pub count: i64, +} + +pub async fn get_by_type( + &self, + tenant_id: Uuid, +) -> AiResult> { + let result = ai_analysis::Entity::find() + .filter(ai_analysis::Column::TenantId.eq(tenant_id)) + .filter(ai_analysis::Column::Status.eq("completed")) + .filter(ai_analysis::Column::DeletedAt.is_null()) + .select_only() + .column(ai_analysis::Column::AnalysisType) + .column_as(ai_analysis::Column::Id.count(), "count") + .group_by(ai_analysis::Column::AnalysisType) + .into_model::() + .all(&self.db) + .await?; + Ok(result) +} +``` + +- [ ] **Step 3: 编译验证** + +Run: `cargo check -p erp-ai` +Expected: 编译通过 + +- [ ] **Step 4: 提交** + +```bash +git add crates/erp-ai/src/service/usage.rs +git commit -m "feat(ai): UsageService 补全 get_overview/get_by_type 聚合方法" +``` + +--- + +### Task 4: Handler — 补全路由端点 + +**Files:** +- Modify: `crates/erp-ai/src/handler/mod.rs` +- Modify: `crates/erp-ai/src/module.rs` (路由注册) + +**现状:** +- 4 个 SSE 端点:可用 +- `list_analysis` / `get_analysis`:空壳(返回 `ApiResponse::ok(())`) +- Prompt CRUD、用量统计:完全缺失 + +- [ ] **Step 1: 实现 list_analysis 真实查询** + +替换 `handler/mod.rs` 中的 `list_analysis` 函数(第 272-283 行): + +```rust +pub async fn list_analysis( + State(state): State, + Extension(ctx): Extension, + Query(params): Query, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.analysis.list")?; + let pagination = erp_core::types::Pagination::new( + params.page.unwrap_or(1), + params.page_size.unwrap_or(20), + ); + let (items, total) = state.analysis.list_analysis( + ctx.tenant_id, + params.patient_id, + params.analysis_type, + pagination, + ).await?; + let data = serde_json::json!({ + "data": items, + "total": total, + "page": pagination.page, + "page_size": pagination.page_size, + }); + Ok(Json(ApiResponse::ok(data))) +} +``` + +- [ ] **Step 2: 实现 get_analysis 真实查询** + +替换 `get_analysis` 函数(第 285-296 行): + +```rust +pub async fn get_analysis( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.analysis.list")?; + let analysis = state.analysis.get_analysis(id, ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(analysis))) +} +``` + +需在文件顶部添加 `use crate::entity::ai_analysis;`。 + +- [ ] **Step 3: 新增 Prompt CRUD handler 函数** + +在 handler/mod.rs 中添加以下函数(分析历史之后): + +```rust +// === Prompt 管理 === + +#[derive(Debug, Deserialize)] +pub struct ListPromptsQuery { + pub category: Option, + pub page: Option, + pub page_size: Option, +} + +pub async fn list_prompts( + State(state): State, + Extension(ctx): Extension, + Query(params): Query, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.list")?; + let pagination = erp_core::types::Pagination::new( + params.page.unwrap_or(1), + params.page_size.unwrap_or(20), + ); + let (items, total) = state.prompt.list_prompts( + ctx.tenant_id, params.category, pagination, + ).await?; + Ok(Json(ApiResponse::ok(serde_json::json!({ + "data": items, "total": total, + "page": pagination.page, "page_size": pagination.page_size, + })))) +} + +#[derive(Debug, Deserialize)] +pub struct CreatePromptBody { + pub name: String, + pub description: Option, + pub system_prompt: String, + pub user_prompt_template: String, + pub model_config: serde_json::Value, + pub category: String, +} + +pub async fn create_prompt( + State(state): State, + Extension(ctx): Extension, + Json(body): Json, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.manage")?; + let prompt = state.prompt.create_prompt( + ctx.tenant_id, ctx.user_id, + body.name, body.system_prompt, body.user_prompt_template, + body.model_config, body.category, + ).await?; + Ok(Json(ApiResponse::ok(prompt))) +} + +pub async fn activate_prompt( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.manage")?; + let prompt = state.prompt.activate_prompt(id, ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(prompt))) +} + +pub async fn rollback_prompt( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.prompt.manage")?; + let prompt = state.prompt.rollback_prompt(id, ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(prompt))) +} +``` + +需在文件顶部添加 `use crate::entity::ai_prompt;`。 + +- [ ] **Step 4: 新增用量统计 handler 函数** + +```rust +// === 用量统计 === + +pub async fn usage_overview( + State(state): State, + Extension(ctx): Extension, +) -> Result>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.usage.list")?; + let overview = state.usage.get_overview(ctx.tenant_id).await?; + Ok(Json(ApiResponse::ok(serde_json::json!({ + "total_count": overview.total_count, + })))) +} + +pub async fn usage_by_type( + State(state): State, + Extension(ctx): Extension, +) -> Result>>, erp_core::error::AppError> +where + AiState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "ai.usage.list")?; + let types = state.usage.get_by_type(ctx.tenant_id).await?; + let result: Vec = types.into_iter().map(|t| { + serde_json::json!({ + "analysis_type": t.analysis_type, + "count": t.count, + }) + }).collect(); + Ok(Json(ApiResponse::ok(result))) +} +``` + +- [ ] **Step 5: 在 module.rs 注册新路由** + +修改 `AiModule::protected_routes`,在现有路由后追加: + +```rust +.route("/ai/prompts", axum::routing::get(crate::handler::list_prompts)) +.route("/ai/prompts", axum::routing::post(crate::handler::create_prompt)) +.route("/ai/prompts/{id}/activate", axum::routing::post(crate::handler::activate_prompt)) +.route("/ai/prompts/{id}/rollback", axum::routing::post(crate::handler::rollback_prompt)) +.route("/ai/usage/overview", axum::routing::get(crate::handler::usage_overview)) +.route("/ai/usage/by-type", axum::routing::get(crate::handler::usage_by_type)) +``` + +- [ ] **Step 6: 编译验证** + +Run: `cargo check -p erp-ai && cargo check -p erp-server` +Expected: 编译通过 + +- [ ] **Step 7: 提交** + +```bash +git add crates/erp-ai/src/handler/mod.rs crates/erp-ai/src/module.rs +git commit -m "feat(ai): 补全 Prompt CRUD + 分析历史 + 用量统计 handler 和路由" +``` + +--- + +## Chunk 2: 前端 API 封装 + +### Task 5: 创建 AI API service 文件 + +**Files:** +- Create: `apps/web/src/api/ai/prompts.ts` +- Create: `apps/web/src/api/ai/analysis.ts` +- Create: `apps/web/src/api/ai/usage.ts` + +- [ ] **Step 1: 创建 prompts.ts** + +`apps/web/src/api/ai/prompts.ts`: + +```typescript +import client from '../client'; +import type { PaginatedResponse } from '../types'; + +export interface PromptItem { + id: string; + name: string; + description: string; + system_prompt: string; + user_prompt_template: string; + model_config: Record; + version: number; + is_active: boolean; + category: string; + tags: Record | null; + created_at: string; + updated_at: string; +} + +export interface CreatePromptReq { + name: string; + description?: string; + system_prompt: string; + user_prompt_template: string; + model_config: Record; + category: string; +} + +export const promptApi = { + list: async (params?: { category?: string; page?: number; page_size?: number }) => { + const resp = await client.get('/ai/prompts', { params }); + return resp.data.data as PaginatedResponse; + }, + create: async (data: CreatePromptReq) => { + const resp = await client.post('/ai/prompts', data); + return resp.data.data as PromptItem; + }, + activate: async (id: string) => { + const resp = await client.post(`/ai/prompts/${id}/activate`); + return resp.data.data as PromptItem; + }, + rollback: async (id: string) => { + const resp = await client.post(`/ai/prompts/${id}/rollback`); + return resp.data.data as PromptItem; + }, +}; +``` + +- [ ] **Step 2: 创建 analysis.ts** + +`apps/web/src/api/ai/analysis.ts`: + +```typescript +import client from '../client'; +import type { PaginatedResponse } from '../types'; + +export interface AnalysisItem { + id: string; + patient_id: string; + analysis_type: string; + source_ref: string; + model_used: string; + status: string; + result_content: string | null; + result_metadata: Record | null; + error_message: string | null; + created_at: string; + updated_at: string; +} + +export const analysisApi = { + list: async (params?: { patient_id?: string; analysis_type?: string; page?: number; page_size?: number }) => { + const resp = await client.get('/ai/analysis/history', { params }); + return resp.data.data as PaginatedResponse; + }, + get: async (id: string) => { + const resp = await client.get(`/ai/analysis/${id}`); + return resp.data.data as AnalysisItem; + }, +}; +``` + +- [ ] **Step 3: 创建 usage.ts** + +`apps/web/src/api/ai/usage.ts`: + +```typescript +import client from '../client'; + +export interface UsageOverview { + total_count: number; +} + +export interface TypeDistribution { + analysis_type: string; + count: number; +} + +export const usageApi = { + overview: async () => { + const resp = await client.get('/ai/usage/overview'); + return resp.data.data as UsageOverview; + }, + byType: async () => { + const resp = await client.get('/ai/usage/by-type'); + return resp.data.data as TypeDistribution[]; + }, +}; +``` + +- [ ] **Step 4: 验证编译** + +Run: `cd apps/web && npx tsc --noEmit` +Expected: 无错误 + +- [ ] **Step 5: 提交** + +```bash +git add apps/web/src/api/ai/ +git commit -m "feat(web): AI API 前端封装 — prompts/analysis/usage" +``` + +--- + +## Chunk 3: 前端管理页面 + +### Task 6: AI Prompt 管理页面 + +**Files:** +- Create: `apps/web/src/pages/health/AiPromptList.tsx` + +- [ ] **Step 1: 创建 AiPromptList 页面** + +使用 Ant Design Table + Modal 模式(参考 PatientList.tsx 结构): + +核心功能: +- 表格列:名称 / 类别 / 版本 / 状态(active/inactive) / 更新时间 +- 新建 Prompt 按钮 → Modal 表单 +- 操作列:激活 / 回滚 +- AuthButton 权限控制(`ai.prompt.manage`) + +页面大致结构(骨架,实现时根据 Ant Design 6 API 细化): + +```tsx +import { useEffect, useState, useCallback } from 'react'; +import { Table, Button, Space, Modal, Form, Input, Select, Tag, message } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { promptApi, type PromptItem, type CreatePromptReq } from '../../api/ai/prompts'; +import { AuthButton } from '../../components/AuthButton'; +import { useThemeMode } from '../../hooks/useThemeMode'; + +const CATEGORIES = [ + { value: 'lab_report_interpretation', label: '化验单解读' }, + { value: 'health_trend_analysis', label: '趋势分析' }, + { value: 'personalized_checkup_plan', label: '体检方案' }, + { value: 'report_summary_generation', label: '报告摘要' }, +]; + +export default function AiPromptList() { + // ... useState, fetchPrompts, columns 定义 + // 新建/激活/回滚按钮用 包裹 + // 表格渲染 + Modal 表单 +} +``` + +- [ ] **Step 2: 验证编译 + 提交** + +```bash +cd apps/web && npx tsc --noEmit +git add apps/web/src/pages/health/AiPromptList.tsx +git commit -m "feat(web): AI Prompt 管理页面" +``` + +--- + +### Task 7: AI 分析历史页面 + +**Files:** +- Create: `apps/web/src/pages/health/AiAnalysisList.tsx` + +- [ ] **Step 1: 创建 AiAnalysisList 页面** + +核心功能: +- 表格列:分析类型 / 患者 ID / 状态(streaming/completed/failed) / 模型 / 创建时间 +- 状态 Tag:completed=绿色, failed=红色, streaming=蓝色 +- 详情查看:点击行展开,显示 result_content(Markdown 渲染) +- 筛选:分析类型下拉 + 时间范围 +- AuthButton 权限控制(`ai.analysis.list`) + +- [ ] **Step 2: 验证编译 + 提交** + +```bash +cd apps/web && npx tsc --noEmit +git add apps/web/src/pages/health/AiAnalysisList.tsx +git commit -m "feat(web): AI 分析历史页面" +``` + +--- + +### Task 8: AI 用量统计页面 + +**Files:** +- Create: `apps/web/src/pages/health/AiUsageDashboard.tsx` + +- [ ] **Step 1: 创建 AiUsageDashboard 页面** + +核心功能: +- 顶部 StatCard:总分析次数 +- 饼图:分析类型分布(使用 Ant Design Charts Pie) +- AuthButton 权限控制(`ai.usage.list`) + +- [ ] **Step 2: 验证编译 + 提交** + +```bash +cd apps/web && npx tsc --noEmit +git add apps/web/src/pages/health/AiUsageDashboard.tsx +git commit -m "feat(web): AI 用量统计页面" +``` + +--- + +### Task 9: 菜单注册 + 路由配置 + +**Files:** +- Modify: `apps/web/src/layouts/MainLayout.tsx` +- Modify: `apps/web/src/App.tsx` + +- [ ] **Step 1: 在 MainLayout 添加菜单项** + +在 `healthMenuItems` 数组中追加 3 项: + +```typescript +{ key: '/health/ai-prompts', label: 'AI Prompt 管理', icon: ... }, +{ key: '/health/ai-analysis', label: 'AI 分析历史', icon: ... }, +{ key: '/health/ai-usage', label: 'AI 用量统计', icon: ... }, +``` + +- [ ] **Step 2: 在 App.tsx 添加路由** + +在健康模块路由区域追加: + +```tsx +} /> +} /> +} /> +``` + +- [ ] **Step 3: 验证编译 + 提交** + +```bash +cd apps/web && npx tsc --noEmit +git add apps/web/src/layouts/MainLayout.tsx apps/web/src/App.tsx +git commit -m "feat(web): AI 管理端菜单注册 + 路由配置" +``` + +--- + +### Task 10: 集成验证 + +- [ ] **Step 1: 后端编译 + 启动** + +Run: `cargo check --workspace && cd crates/erp-server && cargo run` + +验证: +- `/api/v1/ai/prompts` 返回空列表(200) +- `/api/v1/ai/analysis/history` 返回空列表(200) +- `/api/v1/ai/usage/overview` 返回 0 计数(200) + +- [ ] **Step 2: 前端编译 + 启动** + +Run: `cd apps/web && pnpm build && pnpm dev` + +验证: +- 3 个新页面在菜单中可见 +- 页面正常加载,无白屏/报错 +- Prompt 列表为空时显示空状态 +- 分析历史列表为空时显示空状态 + +- [ ] **Step 3: 生产构建** + +Run: `cd apps/web && pnpm build` +Expected: 构建成功 + +- [ ] **Step 4: 推送** + +```bash +git push +```