From 8fbe1543cbaf1822ab2b5db5c3f3476bacb616cf Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 19 May 2026 17:48:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai):=20ChatPage=20import/layout=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20+=20=E8=BF=81=E7=A7=BB=E8=A1=A8=E5=90=8D=E5=88=97?= =?UTF-8?q?=E5=90=8D=E4=BF=AE=E6=AD=A3=20+=20=E8=B7=AF=E7=94=B1=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatPage: 图标从 antd 移到 @ant-design/icons,Layout/Sider 改为 div 布局避免 Header 遮挡 - routeConfig: 注册 /ai/chat 路由权限 (ai.chat.session.list/manage) - 迁移 153: ai_tenant_configs → ai_tenant_config 表名修正 - 迁移 154: menus.name/is_external/status → title/visible/menu_type 列名修正 - 迁移 151/152: AI 配置菜单父级修复 + AI Provider 权限 seed --- apps/web/src/pages/ai/ChatPage.tsx | 29 +++++----- apps/web/src/routeConfig.ts | 6 ++ ...260518_000151_fix_ai_config_menu_parent.rs | 30 ++++++++++ ...0518_000152_seed_ai_provider_permission.rs | 57 +++++++++++++++++++ .../m20260518_000153_ai_health_butler_v2.rs | 10 ++-- ...19_000154_seed_ai_knowledge_permissions.rs | 6 +- 6 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 crates/erp-server/migration/src/m20260518_000151_fix_ai_config_menu_parent.rs create mode 100644 crates/erp-server/migration/src/m20260518_000152_seed_ai_provider_permission.rs diff --git a/apps/web/src/pages/ai/ChatPage.tsx b/apps/web/src/pages/ai/ChatPage.tsx index cae3738..be93eb4 100644 --- a/apps/web/src/pages/ai/ChatPage.tsx +++ b/apps/web/src/pages/ai/ChatPage.tsx @@ -1,15 +1,10 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import { - Layout, List, Button, Input, Typography, Spin, - PlusOutlined, - MessageOutlined, - DeleteOutlined, - EditOutlined, theme, Modal, Space, @@ -17,6 +12,10 @@ import { import { SendOutlined, RobotOutlined, + PlusOutlined, + MessageOutlined, + DeleteOutlined, + EditOutlined, } from '@ant-design/icons'; import { aiChatApi, @@ -27,7 +26,6 @@ import { import RichMessage from '../../components/ai/RichMessage'; import { useAuthStore } from '../../stores/auth'; -const { Sider, Content } = Layout; const { Text } = Typography; const { TextArea } = Input; @@ -183,13 +181,15 @@ export default function ChatPage() { const activeSession = sessions.find((s) => s.id === activeId); return ( - - + {/* 左侧会话列表 */} +
@@ -254,9 +254,10 @@ export default function ChatPage() { )} /> - +
- + {/* 右侧聊天区 */} +
{/* 标题栏 */}
)} - - +
+
); } diff --git a/apps/web/src/routeConfig.ts b/apps/web/src/routeConfig.ts index c820f92..7dab879 100644 --- a/apps/web/src/routeConfig.ts +++ b/apps/web/src/routeConfig.ts @@ -244,6 +244,12 @@ const ENTRIES: RoutePermissionEntry[] = [ path: "/health/schedules", permissions: ["health.appointment.list", "health.appointment.manage"], }, + + // ===== AI 聊天 ===== + { + path: "/ai/chat", + permissions: ["ai.chat.session.list", "ai.chat.session.manage"], + }, ]; /** 活跃路由的权限映射 — 自动从配置生成,供 PrivateRoute 使用 */ diff --git a/crates/erp-server/migration/src/m20260518_000151_fix_ai_config_menu_parent.rs b/crates/erp-server/migration/src/m20260518_000151_fix_ai_config_menu_parent.rs new file mode 100644 index 0000000..5b4765d --- /dev/null +++ b/crates/erp-server/migration/src/m20260518_000151_fix_ai_config_menu_parent.rs @@ -0,0 +1,30 @@ +//! 修复 AI 配置菜单的 parent_id:从 AI Prompt 管理子级移到 AI 分析分组下 + +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#" + UPDATE menus mc + SET parent_id = mp.parent_id, sort_order = 55, updated_at = NOW(), version = mc.version + 1 + FROM menus mp + WHERE mp.path = '/health/ai-prompts' AND mp.deleted_at IS NULL + AND mc.path = '/health/ai-config' AND mc.deleted_at IS NULL + AND mc.parent_id = mp.id + "#, + ).await?; + + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} diff --git a/crates/erp-server/migration/src/m20260518_000152_seed_ai_provider_permission.rs b/crates/erp-server/migration/src/m20260518_000152_seed_ai_provider_permission.rs new file mode 100644 index 0000000..98cb7f8 --- /dev/null +++ b/crates/erp-server/migration/src/m20260518_000152_seed_ai_provider_permission.rs @@ -0,0 +1,57 @@ +//! 新增 ai.provider.manage 权限码,允许管理员管理 AI Provider 配置(API Key/URL/Model) + +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(); + let sys = "00000000-0000-0000-0000-000000000000"; + + // ai.provider.manage 绑定到所有租户的管理员角色 + let code = "ai.provider.manage"; + let name = "管理 AI 供应商配置"; + let desc = "修改 AI 供应商(Claude/OpenAI/Ollama)的 API Key、Base URL、模型等配置"; + + db.execute_unprepared(&format!( + r#" + INSERT INTO permissions (id, tenant_id, code, name, resource, action, description, + created_at, updated_at, created_by, updated_by, deleted_at, version) + SELECT gen_random_uuid(), t.id, '{code}', '{name}', 'ai', '{code}', '{desc}', + NOW(), NOW(), '{sys}', '{sys}', NULL, 1 + FROM tenant t + WHERE NOT EXISTS ( + SELECT 1 FROM permissions p + WHERE p.code = '{code}' AND p.tenant_id = t.id AND p.deleted_at IS NULL + ) + "# + )).await?; + + // 绑定到管理员角色 + db.execute_unprepared(&format!( + r#" + INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope, + created_at, updated_at, created_by, updated_by, deleted_at, version) + SELECT r.id, p.id, t.id, 'all', + NOW(), NOW(), '{sys}', '{sys}', NULL, 1 + FROM tenant t + JOIN roles r ON r.tenant_id = t.id AND r.code = 'admin' AND r.deleted_at IS NULL + JOIN permissions p ON p.tenant_id = t.id AND p.code = '{code}' AND p.deleted_at IS NULL + WHERE NOT EXISTS ( + SELECT 1 FROM role_permissions rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id AND rp.deleted_at IS NULL + ) + ON CONFLICT (role_id, permission_id) DO NOTHING + "# + )).await?; + + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} diff --git a/crates/erp-server/migration/src/m20260518_000153_ai_health_butler_v2.rs b/crates/erp-server/migration/src/m20260518_000153_ai_health_butler_v2.rs index 6b59a3f..b2d150b 100644 --- a/crates/erp-server/migration/src/m20260518_000153_ai_health_butler_v2.rs +++ b/crates/erp-server/migration/src/m20260518_000153_ai_health_butler_v2.rs @@ -84,9 +84,9 @@ impl MigrationTrait for Migration { ) .await?; - // 4. ai_tenant_configs 增加 billing_enabled 列 + // 4. ai_tenant_config 增加 billing_enabled 列 db.execute_unprepared( - "ALTER TABLE ai_tenant_configs ADD COLUMN IF NOT EXISTS billing_enabled BOOLEAN NOT NULL DEFAULT false", + "ALTER TABLE ai_tenant_config ADD COLUMN IF NOT EXISTS billing_enabled BOOLEAN NOT NULL DEFAULT false", ) .await?; @@ -177,10 +177,8 @@ impl MigrationTrait for Migration { .await?; db.execute_unprepared("DROP TABLE IF EXISTS ai_feature_flags") .await?; - db.execute_unprepared( - "ALTER TABLE ai_tenant_configs DROP COLUMN IF EXISTS billing_enabled", - ) - .await?; + db.execute_unprepared("ALTER TABLE ai_tenant_config DROP COLUMN IF EXISTS billing_enabled") + .await?; Ok(()) } diff --git a/crates/erp-server/migration/src/m20260519_000154_seed_ai_knowledge_permissions.rs b/crates/erp-server/migration/src/m20260519_000154_seed_ai_knowledge_permissions.rs index 9c12f37..8f81d99 100644 --- a/crates/erp-server/migration/src/m20260519_000154_seed_ai_knowledge_permissions.rs +++ b/crates/erp-server/migration/src/m20260519_000154_seed_ai_knowledge_permissions.rs @@ -62,13 +62,13 @@ impl MigrationTrait for Migration { // 2. 添加知识库菜单项(AI 配置下方) db.execute_unprepared(&format!( r#" - INSERT INTO menus (id, tenant_id, parent_id, name, path, icon, sort_order, - permission, menu_type, is_external, status, + INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, + permission, menu_type, visible, created_at, updated_at, created_by, updated_by, deleted_at, version) SELECT gen_random_uuid(), t.id, (SELECT m.id FROM menus m WHERE m.path = '/health/ai-config' AND m.tenant_id = t.id AND m.deleted_at IS NULL LIMIT 1), 'AI 知识库', '/health/ai-knowledge', 'BookOutlined', 4, - 'ai.knowledge.list', 1, false, 1, + 'ai.knowledge.list', 'page', true, NOW(), NOW(), '{sys}', '{sys}', NULL, 1 FROM tenant t WHERE NOT EXISTS (