feat(admin): Complete Phase 3.5.1-3.5.4 Prompt Management System (83%)

Summary:
- Implement Prompt management infrastructure and core services
- Build admin portal frontend with light theme
- Integrate CodeMirror 6 editor for non-technical users

Phase 3.5.1: Infrastructure Setup
- Create capability_schema for Prompt storage
- Add prompt_templates and prompt_versions tables
- Add prompt:view/edit/debug/publish permissions
- Migrate RVW prompts to database (RVW_EDITORIAL, RVW_METHODOLOGY)

Phase 3.5.2: PromptService Core
- Implement gray preview logic (DRAFT for debuggers, ACTIVE for users)
- Module-level debug control (setDebugMode)
- Handlebars template rendering
- Variable extraction and validation (extractVariables, validateVariables)
- Three-level disaster recovery (database -> cache -> hardcoded fallback)

Phase 3.5.3: Management API
- 8 RESTful endpoints (/api/admin/prompts/*)
- Permission control (PROMPT_ENGINEER can edit, SUPER_ADMIN can publish)

Phase 3.5.4: Frontend Management UI
- Build admin portal architecture (AdminLayout, OrgLayout)
- Add route system (/admin/*, /org/*)
- Implement PromptListPage (filter, search, debug switch)
- Implement PromptEditor (CodeMirror 6 simplified for clinical users)
- Implement PromptEditorPage (edit, save, publish, test, version history)

Technical Details:
- Backend: 6 files, ~2044 lines (prompt.service.ts 596 lines)
- Frontend: 9 files, ~1735 lines (PromptEditorPage.tsx 399 lines)
- CodeMirror 6: Line numbers, auto-wrap, variable highlight, search, undo/redo
- Chinese-friendly: 15px font, 1.8 line-height, system fonts

Next Step: Phase 3.5.5 - Integrate RVW module with PromptService

Tested: Backend API tests passed (8/8), Frontend pending user testing
Status: Ready for Phase 3.5.5 RVW integration
This commit is contained in:
2026-01-11 21:25:16 +08:00
parent cdfbc9927a
commit 5523ef36ea
297 changed files with 15914 additions and 1266 deletions

View File

@@ -0,0 +1,236 @@
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,
} 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/tenants',
icon: <TeamOutlined />,
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