Summary: - Add 4 new database tables: iit_field_metadata, iit_qc_logs, iit_record_summary, iit_qc_project_stats - Implement pg-boss debounce mechanism in WebhookController - Refactor QC Worker for dual output: QC logs + record summary - Enhance HardRuleEngine to support form-based rule filtering - Create QcService for QC data queries - Optimize ChatService with new intents: query_enrollment, query_qc_status - Add admin batch operations: one-click full QC + one-click full summary - Create IIT Admin management module: project config, QC rules, user mapping Status: Code complete, pending end-to-end testing Co-authored-by: Cursor <cursoragent@cursor.com>
255 lines
6.9 KiB
TypeScript
255 lines
6.9 KiB
TypeScript
import { Suspense, useState } from 'react'
|
||
import { Outlet, Navigate, useLocation, useNavigate } from 'react-router-dom'
|
||
import { Spin, Layout, Menu, Avatar, Dropdown, Badge } from 'antd'
|
||
import {
|
||
DashboardOutlined,
|
||
CodeOutlined,
|
||
TeamOutlined,
|
||
SettingOutlined,
|
||
UserOutlined,
|
||
LogoutOutlined,
|
||
SwapOutlined,
|
||
MenuFoldOutlined,
|
||
MenuUnfoldOutlined,
|
||
BellOutlined,
|
||
BookOutlined,
|
||
FileTextOutlined,
|
||
ExperimentOutlined,
|
||
} from '@ant-design/icons'
|
||
import type { MenuProps } from 'antd'
|
||
import { useAuth } from '../auth'
|
||
import ErrorBoundary from '../modules/ErrorBoundary'
|
||
|
||
const { Header, Sider, Content } = Layout
|
||
|
||
// 运营管理端主色:翠绿
|
||
const PRIMARY_COLOR = '#10b981'
|
||
|
||
/**
|
||
* 运营管理端布局(方案A:全浅色)
|
||
*
|
||
* @description
|
||
* - 白色侧边栏 + 翠绿强调色
|
||
* - 浅灰内容区,信息清晰
|
||
* - 权限检查:SUPER_ADMIN / PROMPT_ENGINEER
|
||
*/
|
||
const AdminLayout = () => {
|
||
const { isAuthenticated, isLoading, user, logout } = useAuth()
|
||
const location = useLocation()
|
||
const navigate = useNavigate()
|
||
const [collapsed, setCollapsed] = useState(false)
|
||
|
||
// 加载中
|
||
if (isLoading) {
|
||
return (
|
||
<div className="h-screen flex items-center justify-center bg-gray-50">
|
||
<Spin size="large" tip="加载中..." />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 未登录
|
||
if (!isAuthenticated) {
|
||
return <Navigate to="/login" state={{ from: location }} replace />
|
||
}
|
||
|
||
// 权限检查:只有 SUPER_ADMIN 和 PROMPT_ENGINEER 可访问
|
||
const allowedRoles = ['SUPER_ADMIN', 'PROMPT_ENGINEER']
|
||
if (!allowedRoles.includes(user?.role || '')) {
|
||
return (
|
||
<div className="h-screen flex items-center justify-center bg-gray-50">
|
||
<div className="text-center">
|
||
<div className="text-6xl mb-4">🚫</div>
|
||
<h2 className="text-xl mb-2 text-gray-800">无权访问运营管理端</h2>
|
||
<p className="text-gray-500 mb-4">需要 SUPER_ADMIN 或 PROMPT_ENGINEER 权限</p>
|
||
<button
|
||
onClick={() => navigate('/')}
|
||
className="px-4 py-2 text-white rounded hover:opacity-90"
|
||
style={{ background: PRIMARY_COLOR }}
|
||
>
|
||
返回首页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 侧边栏菜单
|
||
const menuItems: MenuProps['items'] = [
|
||
{
|
||
key: '/admin/dashboard',
|
||
icon: <DashboardOutlined />,
|
||
label: '运营概览',
|
||
},
|
||
{
|
||
key: '/admin/prompts',
|
||
icon: <CodeOutlined />,
|
||
label: 'Prompt管理',
|
||
},
|
||
{
|
||
key: '/admin/system-kb',
|
||
icon: <BookOutlined />,
|
||
label: '系统知识库',
|
||
},
|
||
{
|
||
key: '/admin/iit-projects',
|
||
icon: <ExperimentOutlined />,
|
||
label: 'IIT 项目管理',
|
||
},
|
||
{
|
||
key: '/admin/tenants',
|
||
icon: <TeamOutlined />,
|
||
label: '租户管理',
|
||
},
|
||
{
|
||
key: '/admin/activity-logs',
|
||
icon: <FileTextOutlined />,
|
||
label: '运营日志',
|
||
},
|
||
{
|
||
key: '/admin/users',
|
||
icon: <UserOutlined />,
|
||
label: '用户管理',
|
||
},
|
||
{
|
||
key: '/admin/system',
|
||
icon: <SettingOutlined />,
|
||
label: '系统配置',
|
||
},
|
||
]
|
||
|
||
// 用户下拉菜单
|
||
const userMenuItems: MenuProps['items'] = [
|
||
{
|
||
key: 'switch-app',
|
||
icon: <SwapOutlined />,
|
||
label: '切换到业务端',
|
||
},
|
||
{ type: 'divider' },
|
||
{
|
||
key: 'logout',
|
||
icon: <LogoutOutlined />,
|
||
label: '退出登录',
|
||
danger: true,
|
||
},
|
||
]
|
||
|
||
const handleUserMenuClick = ({ key }: { key: string }) => {
|
||
if (key === 'logout') {
|
||
logout()
|
||
navigate('/login')
|
||
} else if (key === 'switch-app') {
|
||
navigate('/')
|
||
}
|
||
}
|
||
|
||
const handleMenuClick = ({ key }: { key: string }) => {
|
||
navigate(key)
|
||
}
|
||
|
||
// 获取当前选中的菜单项
|
||
const selectedKey = menuItems.find(item =>
|
||
location.pathname.startsWith(item?.key as string)
|
||
)?.key as string || '/admin/dashboard'
|
||
|
||
return (
|
||
<Layout className="h-screen">
|
||
{/* 侧边栏 - 白色 */}
|
||
<Sider
|
||
trigger={null}
|
||
collapsible
|
||
collapsed={collapsed}
|
||
style={{ background: '#fff' }}
|
||
width={220}
|
||
className="shadow-sm"
|
||
>
|
||
{/* Logo */}
|
||
<div
|
||
className="h-16 flex items-center justify-center cursor-pointer border-b border-gray-100"
|
||
onClick={() => navigate('/admin/dashboard')}
|
||
>
|
||
<span className="text-2xl">⚙️</span>
|
||
{!collapsed && (
|
||
<span className="ml-2 font-bold" style={{ color: PRIMARY_COLOR }}>
|
||
运营管理中心
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 菜单 */}
|
||
<Menu
|
||
mode="inline"
|
||
selectedKeys={[selectedKey]}
|
||
items={menuItems}
|
||
onClick={handleMenuClick}
|
||
style={{
|
||
borderRight: 'none',
|
||
}}
|
||
/>
|
||
</Sider>
|
||
|
||
<Layout>
|
||
{/* 顶部栏 - 白色 */}
|
||
<Header
|
||
className="flex items-center justify-between shadow-sm"
|
||
style={{ background: '#fff', padding: '0 24px' }}
|
||
>
|
||
{/* 折叠按钮 */}
|
||
<button
|
||
onClick={() => setCollapsed(!collapsed)}
|
||
className="text-gray-500 hover:text-gray-700 text-xl"
|
||
>
|
||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||
</button>
|
||
|
||
{/* 右侧工具栏 */}
|
||
<div className="flex items-center gap-4">
|
||
{/* 通知 */}
|
||
<Badge count={0} size="small">
|
||
<BellOutlined className="text-gray-500 hover:text-gray-700 text-lg cursor-pointer" />
|
||
</Badge>
|
||
|
||
{/* 用户 */}
|
||
<Dropdown
|
||
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
|
||
placement="bottomRight"
|
||
>
|
||
<div className="flex items-center gap-2 cursor-pointer px-3 py-1 rounded hover:bg-gray-50">
|
||
<Avatar
|
||
size="small"
|
||
icon={<UserOutlined />}
|
||
style={{ background: PRIMARY_COLOR }}
|
||
/>
|
||
<div className="flex flex-col">
|
||
<span className="text-gray-700 text-sm">{user?.name || '管理员'}</span>
|
||
<span className="text-xs" style={{ color: PRIMARY_COLOR }}>{user?.role}</span>
|
||
</div>
|
||
</div>
|
||
</Dropdown>
|
||
</div>
|
||
</Header>
|
||
|
||
{/* 主内容区 - 浅灰 */}
|
||
<Content
|
||
className="overflow-auto p-6"
|
||
style={{ background: '#f5f5f5' }}
|
||
>
|
||
<ErrorBoundary moduleName="运营管理">
|
||
<Suspense
|
||
fallback={
|
||
<div className="flex items-center justify-center h-full">
|
||
<Spin size="large" />
|
||
</div>
|
||
}
|
||
>
|
||
<Outlet />
|
||
</Suspense>
|
||
</ErrorBoundary>
|
||
</Content>
|
||
</Layout>
|
||
</Layout>
|
||
)
|
||
}
|
||
|
||
export default AdminLayout
|