feat: update process designer, controller, and docs

This commit is contained in:
selfrelease
2026-07-19 07:55:12 +08:00
parent 34a279721e
commit ca42abea74
7 changed files with 58 additions and 4 deletions
+20 -2
View File
@@ -5,6 +5,7 @@ type Mode = 'SERIAL' | 'PARALLEL' | 'CONDITIONAL';
type Assignee = 'approverId' | 'oaAdministratorId' | 'hrReviewerId';
type Step = { id: string; name: string; assigneeVariable: Assignee };
type ApprovalRule = { variable: Assignee; label: string; sourceType: string; selector: string; scope: string; emptyPolicy: string; description: string };
type ProcessHistory = { key: string; version: number; name: string; mode: Mode; status: string; createdAt: string; template: { key: string; name: string; mode: Mode; steps: { name: string; assigneeVariable: Assignee }[]; conditionThreshold: number } };
const fallbackRules: ApprovalRule[] = [
{ variable: 'approverId', label: '发起人所在部门主管', sourceType: 'POSITION', selector: 'manager', scope: 'APPLICANT_DEPARTMENT', emptyPolicy: 'REJECT_SUBMISSION', description: '根据发起人的主任职解析部门主管。' },
{ variable: 'oaAdministratorId', label: '租户 OA 管理员', sourceType: 'ROLE', selector: 'oa_admin', scope: 'TENANT', emptyPolicy: 'REJECT_SUBMISSION', description: '选择当前租户有效的 OA 管理员。' },
@@ -28,7 +29,17 @@ export function ProcessDesigner() {
const [selected, setSelected] = useState(steps[0].id);
const [message, setMessage] = useState('');
const [rules, setRules] = useState<ApprovalRule[]>(fallbackRules);
useEffect(() => { api<ApprovalRule[]>('/admin/process-configuration/approver-rules').then(setRules).catch(error => setMessage(error instanceof Error ? error.message : '审批规则加载失败')); }, []);
const [history, setHistory] = useState<ProcessHistory[]>([]);
async function loadConfiguration() {
try {
const [ruleResult, historyResult] = await Promise.all([
api<ApprovalRule[]>('/admin/process-configuration/approver-rules'),
api<ProcessHistory[]>('/admin/process-configuration/processes'),
]);
setRules(ruleResult); setHistory(historyResult);
} catch (error) { setMessage(error instanceof Error ? error.message : '流程配置加载失败'); }
}
useEffect(() => { void loadConfiguration(); }, []);
const selectedStep = steps.find(step => step.id === selected);
const effectiveSteps = mode === 'CONDITIONAL' ? steps.slice(0, 2) : steps;
const summary = useMemo(() => modes.find(item => item.value === mode)?.hint, [mode]);
@@ -49,6 +60,12 @@ export function ProcessDesigner() {
if (to < 0 || to >= steps.length) return;
const next = [...steps]; [next[from], next[to]] = [next[to], next[from]]; setSteps(next);
}
function loadTemplate(item: ProcessHistory) {
setKey(item.template.key); setName(item.template.name); setMode(item.template.mode);
const restored = item.template.steps.map(step => ({ id: crypto.randomUUID(), name: step.name, assigneeVariable: step.assigneeVariable }));
setSteps(restored); setSelected(restored[0]?.id ?? ''); setThresholdDays(item.template.conditionThreshold / 480);
setMessage(`${item.key} v${item.version} 已加载,可修改后部署新版本`);
}
async function deploy() {
try {
const result = await api<{ key: string; version: number }>('/admin/process-configuration/processes/deploy', { method: 'POST', body: JSON.stringify({
@@ -56,6 +73,7 @@ export function ProcessDesigner() {
conditionVariable: 'durationMinutes', conditionThreshold: Math.round(thresholdDays * 480),
}) });
setMessage(`${result.key} v${result.version} 已部署到 Flowable`);
await loadConfiguration();
} catch (error) { setMessage(error instanceof Error ? error.message : '部署失败'); }
}
@@ -71,7 +89,7 @@ export function ProcessDesigner() {
<div className={mode === 'PARALLEL' ? 'parallelBranches' : 'linearNodes'}>{effectiveSteps.map((step, index) => <div className="graphStep" key={step.id}><button className={`approvalNode ${selected === step.id ? 'nodeSelected' : ''}`} onClick={() => setSelected(step.id)}><span>{index + 1}</span><strong>{step.name}</strong><small>{rules.find(item => item.variable === step.assigneeVariable)?.label}</small></button>{mode !== 'PARALLEL' && index < effectiveSteps.length - 1 && <Arrow label={mode === 'CONDITIONAL' ? `>${thresholdDays}` : undefined} />}</div>)}</div>
{mode === 'PARALLEL' && <><Arrow /><ProcessNode kind="gateway" title="并行汇聚" subtitle="全部通过" /></>}<Arrow /><ProcessNode kind="end" title="结束" subtitle="批准 / 驳回" /></div>
</div>
<aside className="processProperties"><h2></h2>{mode === 'CONDITIONAL' && <label><input type="number" min="0" step="0.5" value={thresholdDays} onChange={e => setThresholdDays(Number(e.target.value))} /></label>}<h2></h2>{selectedStep ? <><label><input value={selectedStep.name} onChange={e => update({ name: e.target.value })} /></label><label><select value={selectedStep.assigneeVariable} onChange={e => update({ assigneeVariable: e.target.value as Assignee })}>{rules.map(item => <option key={item.variable} value={item.variable}>{item.label}</option>)}</select></label>{selectedRule && <div className="ruleDetail"><div><span></span><strong>{selectedRule.sourceType === 'POSITION' ? '岗位' : '角色'} · {selectedRule.selector}</strong></div><div><span></span><strong>{selectedRule.scope === 'TENANT' ? '当前租户' : '发起人所在部门'}</strong></div><p>{selectedRule.description}</p><small></small></div>}<div className="nodeActions"><button onClick={() => move(selectedStep.id, -1)}></button><button onClick={() => move(selectedStep.id, 1)}></button><button className="dangerGhost" disabled={effectiveSteps.length <= (mode === 'CONDITIONAL' ? 2 : 1)} onClick={() => setSteps(steps.filter(step => step.id !== selectedStep.id))}></button></div></> : <p></p>}{mode !== 'CONDITIONAL' && steps.length < 6 && <button className="primaryWide" onClick={addStep}> </button>}{duplicateRestrictedRule && <div className="validationError"></div>}<div className="securityNote"><strong></strong><p></p></div></aside>
<aside className="processProperties"><h2></h2>{mode === 'CONDITIONAL' && <label><input type="number" min="0" step="0.5" value={thresholdDays} onChange={e => setThresholdDays(Number(e.target.value))} /></label>}<h2></h2>{selectedStep ? <><label><input value={selectedStep.name} onChange={e => update({ name: e.target.value })} /></label><label><select value={selectedStep.assigneeVariable} onChange={e => update({ assigneeVariable: e.target.value as Assignee })}>{rules.map(item => <option key={item.variable} value={item.variable}>{item.label}</option>)}</select></label>{selectedRule && <div className="ruleDetail"><div><span></span><strong>{selectedRule.sourceType === 'POSITION' ? '岗位' : '角色'} · {selectedRule.selector}</strong></div><div><span></span><strong>{selectedRule.scope === 'TENANT' ? '当前租户' : '发起人所在部门'}</strong></div><p>{selectedRule.description}</p><small></small></div>}<div className="nodeActions"><button onClick={() => move(selectedStep.id, -1)}></button><button onClick={() => move(selectedStep.id, 1)}></button><button className="dangerGhost" disabled={effectiveSteps.length <= (mode === 'CONDITIONAL' ? 2 : 1)} onClick={() => setSteps(steps.filter(step => step.id !== selectedStep.id))}></button></div></> : <p></p>}{mode !== 'CONDITIONAL' && steps.length < 6 && <button className="primaryWide" onClick={addStep}> </button>}{duplicateRestrictedRule && <div className="validationError"></div>}<div className="securityNote"><strong></strong><p></p></div><div className="processHistory"><h2></h2>{history.length === 0 ? <p></p> : history.map(item => <button key={`${item.key}:${item.version}`} onClick={() => loadTemplate(item)}><span><strong>{item.name}</strong><small>{item.key} · v{item.version} · {item.mode}</small></span><b></b></button>)}</div></aside>
</div>
</section>;
}
File diff suppressed because one or more lines are too long