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

24 KiB
Raw Blame History

切片 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_promptsupdate_promptactivate_promptrollback_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_analyzecomplete_analysisfail_analysisfind_cached。需新增 list_analysisget_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_overviewget_trendget_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) / 模型 / 创建时间

  • 状态 Tagcompleted=绿色, failed=红色, streaming=蓝色

  • 详情查看:点击行展开,显示 result_contentMarkdown 渲染)

  • 筛选:分析类型下拉 + 时间范围

  • 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