Files
hms/docs/superpowers/plans/2026-04-25-slice2-ai-admin-pages.md
iven b27a2402fc
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs(plan): 切片 2 AI 管理端 3 页面实施计划
10 个 Task,3 个 Chunk:
- Chunk 1: 后端 API 补全(PromptService/AnalysisService/UsageService/Handler)
- Chunk 2: 前端 API 封装(3 个 service 文件)
- Chunk 3: 前端 3 管理页面 + 菜单路由 + 集成验证
2026-04-25 22:42:29 +08:00

885 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 切片 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 / 分析历史查询 / 用量统计端点为空壳或缺失。先补全后端 APIhandler + 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<String>,
pagination: Pagination,
) -> AiResult<(Vec<ai_prompt::Model>, 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<String>,
user_prompt_template: Option<String>,
model_config: Option<serde_json::Value>,
description: Option<String>,
) -> AiResult<ai_prompt::Model> {
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<ai_prompt::Model> {
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<ai_prompt::Model> {
// 回滚 = 激活指定版本
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<Uuid>,
analysis_type: Option<String>,
pagination: Pagination,
) -> AiResult<(Vec<ai_analysis::Model>, 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::Model> {
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<ai_analysis::Model> {
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<UsageOverview> {
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::<UsageOverview>()
.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<Vec<TypeCount>> {
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::<TypeCount>()
.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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<ListAnalysisQuery>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<ai_analysis::Model>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<String>,
pub page: Option<u64>,
pub page_size: Option<u64>,
}
pub async fn list_prompts<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<ListPromptsQuery>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<String>,
pub system_prompt: String,
pub user_prompt_template: String,
pub model_config: serde_json::Value,
pub category: String,
}
pub async fn create_prompt<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Json(body): Json<CreatePromptBody>,
) -> Result<Json<ApiResponse<ai_prompt::Model>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<ai_prompt::Model>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<uuid::Uuid>,
) -> Result<Json<ApiResponse<ai_prompt::Model>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<serde_json::Value>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<S>(
State(state): State<AiState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<Vec<serde_json::Value>>>, erp_core::error::AppError>
where
AiState: FromRef<S>,
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<serde_json::Value> = 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<string, unknown>;
version: number;
is_active: boolean;
category: string;
tags: Record<string, unknown> | null;
created_at: string;
updated_at: string;
}
export interface CreatePromptReq {
name: string;
description?: string;
system_prompt: string;
user_prompt_template: string;
model_config: Record<string, unknown>;
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<PromptItem>;
},
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<string, unknown> | 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<AnalysisItem>;
},
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 定义
// 新建/激活/回滚按钮用 <AuthButton code="ai.prompt.manage"> 包裹
// 表格渲染 + 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) / 模型 / 创建时间
- 状态 Tagcompleted=绿色, failed=红色, streaming=蓝色
- 详情查看:点击行展开,显示 result_contentMarkdown 渲染)
- 筛选:分析类型下拉 + 时间范围
- 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
<Route path="/health/ai-prompts" element={<AiPromptList />} />
<Route path="/health/ai-analysis" element={<AiAnalysisList />} />
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
```
- [ ] **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
```