feat(web): add admin workflows and template library tab

Admin console can list workflow definitions, validate/publish draft versions,
and view bootstrap template assets from the server API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 02:50:22 +00:00
committed by weichen
co-authored by Cursor
parent 967dd9becd
commit ebbfc2db18
2 changed files with 255 additions and 1 deletions
+82
View File
@@ -0,0 +1,82 @@
import { authClient } from './auth';
export interface WorkflowDefinitionSummary {
id: number;
key: string;
name: string;
description: string;
status: string;
latest_published_version: number;
product_domain?: string;
}
export interface WorkflowVersionSummary {
id: number;
workflow_id: number;
version: number;
status: string;
definition_hash: string;
}
export interface ValidationIssue {
code: string;
message: string;
field?: string;
}
async function adminFetch(path: string, init?: RequestInit): Promise<Response> {
const token = authClient.getSessionToken();
return fetch(path, {
...init,
headers: {
...(init?.headers ?? {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
}
export async function listAdminWorkflows(): Promise<WorkflowDefinitionSummary[]> {
const resp = await adminFetch('/api/v1/admin/workflows');
if (!resp.ok) throw new Error('获取工作流列表失败');
const data = await resp.json();
return data.workflows ?? [];
}
export async function getAdminWorkflow(id: number): Promise<{
definition: WorkflowDefinitionSummary;
versions: WorkflowVersionSummary[];
}> {
const resp = await adminFetch(`/api/v1/admin/workflows/${id}`);
if (!resp.ok) throw new Error('获取工作流详情失败');
return resp.json();
}
export async function validateWorkflowVersion(workflowId: number, version: number): Promise<{
valid: boolean;
issues: ValidationIssue[];
}> {
const resp = await adminFetch(`/api/v1/admin/workflows/${workflowId}/versions/${version}/validate`, {
method: 'POST',
});
if (!resp.ok) throw new Error('校验失败');
return resp.json();
}
export async function publishWorkflowVersion(workflowId: number, version: number): Promise<void> {
const resp = await adminFetch(`/api/v1/admin/workflows/${workflowId}/versions/${version}/publish`, {
method: 'POST',
});
if (!resp.ok) {
const txt = await resp.text();
throw new Error(txt || '发布失败');
}
}
export async function listAdminTemplates(): Promise<
Array<{ id: number; asset_key: string; name: string; status: string }>
> {
const resp = await adminFetch('/api/v1/admin/templates');
if (!resp.ok) throw new Error('获取模板列表失败');
const data = await resp.json();
return data.templates ?? [];
}
+173 -1
View File
@@ -14,11 +14,21 @@ import {
Shield,
Search,
UserPlus,
GitBranch,
} from 'lucide-react';
import { UserProfile } from '../../types/docgov';
import { RoleDefinition, PermissionKey } from '../../types/rbac';
import { SystemAuthConfig, UserAccount } from '../../types/auth';
import { authClient } from '../../api/auth';
import {
listAdminWorkflows,
getAdminWorkflow,
validateWorkflowVersion,
publishWorkflowVersion,
listAdminTemplates,
WorkflowDefinitionSummary,
WorkflowVersionSummary,
} from '../../api/workflowAdmin';
export interface AdminSettingsModalProps {
isOpen: boolean;
@@ -96,7 +106,9 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
onUpdateSystemConfig,
env = 'dev',
}) => {
const [activeTab, setActiveTab] = useState<'users' | 'approvals' | 'policy' | 'roles' | 'status'>('users');
const [activeTab, setActiveTab] = useState<
'users' | 'approvals' | 'policy' | 'roles' | 'workflows' | 'status'
>('users');
// 全量账号列表(含待审核及禁用状态)
const [adminUsers, setAdminUsers] = useState<UserAccount[]>([]);
@@ -124,6 +136,12 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
const [allowReg, setAllowReg] = useState(systemConfig.allowRegistration);
const [requireAppr, setRequireAppr] = useState(systemConfig.requireApproval);
const [adminWorkflows, setAdminWorkflows] = useState<WorkflowDefinitionSummary[]>([]);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<number | null>(null);
const [workflowVersions, setWorkflowVersions] = useState<WorkflowVersionSummary[]>([]);
const [adminTemplates, setAdminTemplates] = useState<Array<{ asset_key: string; name: string }>>([]);
const [validationMsg, setValidationMsg] = useState<string>('');
// 刷新全量用户列表
const refreshUsers = async () => {
try {
@@ -134,6 +152,52 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
}
};
const refreshWorkflows = async () => {
try {
const list = await listAdminWorkflows();
setAdminWorkflows(list);
const templates = await listAdminTemplates();
setAdminTemplates(templates.map((t) => ({ asset_key: t.asset_key, name: t.name })));
} catch {
setAdminWorkflows([]);
}
};
const loadWorkflowDetail = async (id: number) => {
setSelectedWorkflowId(id);
try {
const detail = await getAdminWorkflow(id);
setWorkflowVersions(detail.versions ?? []);
} catch {
setWorkflowVersions([]);
}
};
const handleValidateVersion = async (workflowId: number, version: number) => {
try {
const res = await validateWorkflowVersion(workflowId, version);
if (res.valid) {
setValidationMsg(`v${version} 校验通过`);
} else {
setValidationMsg(res.issues.map((i) => i.message).join(';'));
}
} catch (err: any) {
setValidationMsg(err.message || '校验失败');
}
};
const handlePublishVersion = async (workflowId: number, version: number) => {
if (!window.confirm(`确认发布工作流版本 v${version}?发布后不可原地修改。`)) return;
try {
await publishWorkflowVersion(workflowId, version);
showNotification('success', `已发布 v${version}`);
refreshWorkflows();
loadWorkflowDetail(workflowId);
} catch (err: any) {
showNotification('error', err.message || '发布失败');
}
};
const refreshRoles = async () => {
try {
const list = await authClient.listRoles();
@@ -147,6 +211,7 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
if (isOpen) {
refreshUsers();
refreshRoles();
refreshWorkflows();
setAllowReg(systemConfig.allowRegistration);
setRequireAppr(systemConfig.requireApproval);
}
@@ -587,6 +652,28 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
<span>RBAC 角色权限矩阵</span>
</button>
<button
type="button"
onClick={() => setActiveTab('workflows')}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '13px',
fontWeight: 500,
backgroundColor: activeTab === 'workflows' ? 'var(--bg-panel)' : 'transparent',
color: activeTab === 'workflows' ? 'var(--brand-500)' : 'var(--text)',
border: activeTab === 'workflows' ? '1px solid var(--border)' : '1px solid transparent',
cursor: 'pointer',
textAlign: 'left',
}}
>
<GitBranch size={16} />
<span>工作流与模板库</span>
</button>
<button
type="button"
onClick={() => setActiveTab('status')}
@@ -1273,6 +1360,91 @@ export const AdminSettingsModal: React.FC<AdminSettingsModalProps> = ({
)}
{/* Tab 5: 系统状态与审计 */}
{activeTab === 'workflows' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<h3 style={{ margin: 0, fontSize: '15px', color: 'var(--text-strong)' }}>工作流定义(DB 权威)</h3>
<p style={{ margin: 0, fontSize: '12px', color: 'var(--text-muted)' }}>
已发布版本不可变;新项目选用固定版本快照(项目激活冻结见 Phase 3)。
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
{adminWorkflows.map((wf) => (
<button
key={wf.id}
type="button"
onClick={() => loadWorkflowDetail(wf.id)}
style={{
display: 'block',
width: '100%',
textAlign: 'left',
padding: '10px 12px',
marginBottom: '8px',
borderRadius: '8px',
border:
selectedWorkflowId === wf.id
? '1px solid var(--brand-500)'
: '1px solid var(--border)',
background: 'var(--bg-card)',
cursor: 'pointer',
color: 'var(--text)',
}}
>
<div style={{ fontWeight: 600 }}>{wf.name}</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)' }}>
{wf.key} · 最新发布 v{wf.latest_published_version || '-'}
</div>
</button>
))}
</div>
<div>
{selectedWorkflowId && (
<>
<div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px' }}>版本列表</div>
{workflowVersions.map((v) => (
<div
key={v.id}
style={{
padding: '10px',
border: '1px solid var(--border)',
borderRadius: '8px',
marginBottom: '8px',
fontSize: '12px',
}}
>
<div>
v{v.version} · {v.status}
</div>
<div style={{ color: 'var(--text-muted)', wordBreak: 'break-all' }}>
hash: {v.definition_hash.slice(0, 16)}…
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button type="button" onClick={() => handleValidateVersion(selectedWorkflowId, v.version)}>
校验
</button>
{v.status === 'draft' && (
<button type="button" onClick={() => handlePublishVersion(selectedWorkflowId, v.version)}>
发布
</button>
)}
</div>
</div>
))}
{validationMsg && (
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>{validationMsg}</div>
)}
</>
)}
</div>
</div>
<div>
<div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '8px' }}>模板库 ({adminTemplates.length})</div>
<div style={{ fontSize: '12px', color: 'var(--text-muted)' }}>
{adminTemplates.map((t) => t.asset_key).join(', ') || '暂无模板(需 PG 环境 bootstrap)'}
</div>
</div>
</div>
)}
{activeTab === 'status' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '18px' }}>
<div style={{ paddingBottom: '12px', borderBottom: '1px solid var(--border)' }}>