Files
AIclinicalresearch/frontend-v2/src/framework/layout/AdminLayout.tsx
HaHafeng 5db4a7064c feat(iit): Implement real-time quality control system
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>
2026-02-07 21:56:11 +08:00

255 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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