10 个 Task,3 个 Chunk: - Chunk 1: 后端 API 补全(PromptService/AnalysisService/UsageService/Handler) - Chunk 2: 前端 API 封装(3 个 service 文件) - Chunk 3: 前端 3 管理页面 + 菜单路由 + 集成验证
24 KiB
切片 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 中追加:
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 方法
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 方法
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(激活指定旧版本)
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: 提交
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 中追加:
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 方法
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 式:
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: 提交
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 方法
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 方法
#[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: 提交
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 行):
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 行):
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 中添加以下函数(分析历史之后):
// === 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 函数
// === 用量统计 ===
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,在现有路由后追加:
.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: 提交
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:
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:
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:
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: 提交
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 细化):
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: 验证编译 + 提交
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: 验证编译 + 提交
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: 验证编译 + 提交
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 项:
{ 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 添加路由
在健康模块路由区域追加:
<Route path="/health/ai-prompts" element={<AiPromptList />} />
<Route path="/health/ai-analysis" element={<AiAnalysisList />} />
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
- Step 3: 验证编译 + 提交
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: 推送
git push