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:
committed by
weichen
co-authored by
Cursor
parent
967dd9becd
commit
ebbfc2db18
@@ -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 ?? [];
|
||||
}
|
||||
@@ -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)' }}>
|
||||
|
||||
Reference in New Issue
Block a user