fix(ai): ChatPage import/layout 修复 + 迁移表名列名修正 + 路由权限注册
- 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
This commit is contained in:
@@ -1,15 +1,10 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Layout,
|
|
||||||
List,
|
List,
|
||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Typography,
|
Typography,
|
||||||
Spin,
|
Spin,
|
||||||
PlusOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
theme,
|
theme,
|
||||||
Modal,
|
Modal,
|
||||||
Space,
|
Space,
|
||||||
@@ -17,6 +12,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
SendOutlined,
|
SendOutlined,
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
MessageOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
aiChatApi,
|
aiChatApi,
|
||||||
@@ -27,7 +26,6 @@ import {
|
|||||||
import RichMessage from '../../components/ai/RichMessage';
|
import RichMessage from '../../components/ai/RichMessage';
|
||||||
import { useAuthStore } from '../../stores/auth';
|
import { useAuthStore } from '../../stores/auth';
|
||||||
|
|
||||||
const { Sider, Content } = Layout;
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
@@ -183,13 +181,15 @@ export default function ChatPage() {
|
|||||||
const activeSession = sessions.find((s) => s.id === activeId);
|
const activeSession = sessions.find((s) => s.id === activeId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ height: 'calc(100vh - 64px)', background: token.colorBgContainer }}>
|
<div style={{ display: 'flex', height: 'calc(100vh - 64px)', background: token.colorBgContainer }}>
|
||||||
<Sider
|
{/* 左侧会话列表 */}
|
||||||
width={260}
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
width: 260,
|
||||||
|
flexShrink: 0,
|
||||||
background: token.colorBgLayout,
|
background: token.colorBgLayout,
|
||||||
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||||
overflow: 'auto',
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ padding: 12 }}>
|
<div style={{ padding: 12 }}>
|
||||||
@@ -254,9 +254,10 @@ export default function ChatPage() {
|
|||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</div>
|
||||||
|
|
||||||
<Content style={{ display: 'flex', flexDirection: 'column' }}>
|
{/* 右侧聊天区 */}
|
||||||
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||||
{/* 标题栏 */}
|
{/* 标题栏 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -373,7 +374,7 @@ export default function ChatPage() {
|
|||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Content>
|
</div>
|
||||||
</Layout>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,6 +244,12 @@ const ENTRIES: RoutePermissionEntry[] = [
|
|||||||
path: "/health/schedules",
|
path: "/health/schedules",
|
||||||
permissions: ["health.appointment.list", "health.appointment.manage"],
|
permissions: ["health.appointment.list", "health.appointment.manage"],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===== AI 聊天 =====
|
||||||
|
{
|
||||||
|
path: "/ai/chat",
|
||||||
|
permissions: ["ai.chat.session.list", "ai.chat.session.manage"],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 活跃路由的权限映射 — 自动从配置生成,供 PrivateRoute 使用 */
|
/** 活跃路由的权限映射 — 自动从配置生成,供 PrivateRoute 使用 */
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,9 +84,9 @@ impl MigrationTrait for Migration {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 4. ai_tenant_configs 增加 billing_enabled 列
|
// 4. ai_tenant_config 增加 billing_enabled 列
|
||||||
db.execute_unprepared(
|
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?;
|
.await?;
|
||||||
|
|
||||||
@@ -177,10 +177,8 @@ impl MigrationTrait for Migration {
|
|||||||
.await?;
|
.await?;
|
||||||
db.execute_unprepared("DROP TABLE IF EXISTS ai_feature_flags")
|
db.execute_unprepared("DROP TABLE IF EXISTS ai_feature_flags")
|
||||||
.await?;
|
.await?;
|
||||||
db.execute_unprepared(
|
db.execute_unprepared("ALTER TABLE ai_tenant_config DROP COLUMN IF EXISTS billing_enabled")
|
||||||
"ALTER TABLE ai_tenant_configs DROP COLUMN IF EXISTS billing_enabled",
|
.await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ impl MigrationTrait for Migration {
|
|||||||
// 2. 添加知识库菜单项(AI 配置下方)
|
// 2. 添加知识库菜单项(AI 配置下方)
|
||||||
db.execute_unprepared(&format!(
|
db.execute_unprepared(&format!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO menus (id, tenant_id, parent_id, name, path, icon, sort_order,
|
INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order,
|
||||||
permission, menu_type, is_external, status,
|
permission, menu_type, visible,
|
||||||
created_at, updated_at, created_by, updated_by, deleted_at, version)
|
created_at, updated_at, created_by, updated_by, deleted_at, version)
|
||||||
SELECT gen_random_uuid(), t.id,
|
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),
|
(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 知识库', '/health/ai-knowledge', 'BookOutlined', 4,
|
||||||
'ai.knowledge.list', 1, false, 1,
|
'ai.knowledge.list', 'page', true,
|
||||||
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
NOW(), NOW(), '{sys}', '{sys}', NULL, 1
|
||||||
FROM tenant t
|
FROM tenant t
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
|
|||||||
Reference in New Issue
Block a user