91 lines
7.5 KiB
TypeScript
91 lines
7.5 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
||
import { api } from './api';
|
||
|
||
type FormVersion = { form_key: string; version: number; status: string };
|
||
type ProcessDefinition = { id: string; key: string; name?: string; version: number; suspended: boolean };
|
||
type Binding = {
|
||
id: string; business_type: string; form_key: string; form_version: number;
|
||
process_definition_key: string; leave_type?: string; min_duration_minutes?: number;
|
||
max_duration_minutes?: number; priority: number; status: 'ACTIVE' | 'INACTIVE';
|
||
};
|
||
|
||
const leaveTypes = [{ value: '', label: '全部请假类型' }, { value: 'PERSONAL', label: '事假' }, { value: 'SICK', label: '病假' }, { value: 'ANNUAL', label: '年假' }];
|
||
const minutesToDays = (value?: number) => value == null ? '不限' : `${Number((value / 480).toFixed(1))} 天`;
|
||
|
||
export function ProcessBindings() {
|
||
const [forms, setForms] = useState<FormVersion[]>([]);
|
||
const [definitions, setDefinitions] = useState<ProcessDefinition[]>([]);
|
||
const [bindings, setBindings] = useState<Binding[]>([]);
|
||
const [message, setMessage] = useState('');
|
||
const [formKey, setFormKey] = useState('leave-request');
|
||
const published = forms.filter(form => form.status === 'PUBLISHED');
|
||
const selectedVersions = published.filter(form => form.form_key === formKey);
|
||
const [formVersion, setFormVersion] = useState(1);
|
||
const [processKey, setProcessKey] = useState('leaveApproval');
|
||
const [leaveType, setLeaveType] = useState('');
|
||
const [minDays, setMinDays] = useState('');
|
||
const [maxDays, setMaxDays] = useState('');
|
||
const [priority, setPriority] = useState(100);
|
||
|
||
async function load() {
|
||
try {
|
||
const [formResult, definitionResult, bindingResult] = await Promise.all([
|
||
api<FormVersion[]>('/admin/process-configuration/forms'),
|
||
api<ProcessDefinition[]>('/admin/workflows/definitions'),
|
||
api<Binding[]>('/admin/process-configuration/bindings'),
|
||
]);
|
||
setForms(formResult); setDefinitions(definitionResult); setBindings(bindingResult);
|
||
const firstPublished = formResult.find(form => form.status === 'PUBLISHED');
|
||
if (firstPublished) { setFormKey(firstPublished.form_key); setFormVersion(firstPublished.version); }
|
||
const firstDefinition = definitionResult.find(definition => !definition.suspended);
|
||
if (firstDefinition) setProcessKey(firstDefinition.key);
|
||
} catch (error) { setMessage(error instanceof Error ? error.message : '配置加载失败'); }
|
||
}
|
||
useEffect(() => { void load(); }, []);
|
||
useEffect(() => { if (selectedVersions.length && !selectedVersions.some(form => form.version === formVersion)) setFormVersion(selectedVersions[0].version); }, [formKey, forms]);
|
||
|
||
const ruleSummary = useMemo(() => {
|
||
const type = leaveTypes.find(item => item.value === leaveType)?.label ?? leaveType;
|
||
return `${type} · ${minDays || '0'}–${maxDays || '∞'} 天 · 优先级 ${priority}`;
|
||
}, [leaveType, minDays, maxDays, priority]);
|
||
|
||
async function createBinding() {
|
||
try {
|
||
await api('/admin/process-configuration/bindings', { method: 'POST', body: JSON.stringify({
|
||
businessType: 'LEAVE_REQUEST', formKey, formVersion, processDefinitionKey: processKey,
|
||
leaveType: leaveType || null,
|
||
minDurationMinutes: minDays === '' ? null : Math.round(Number(minDays) * 480),
|
||
maxDurationMinutes: maxDays === '' ? null : Math.round(Number(maxDays) * 480), priority,
|
||
}) });
|
||
setMessage('流程绑定已启用'); await load();
|
||
} catch (error) { setMessage(error instanceof Error ? error.message : '绑定创建失败'); }
|
||
}
|
||
|
||
async function toggle(binding: Binding) {
|
||
try {
|
||
const status = binding.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE';
|
||
await api(`/admin/process-configuration/bindings/${binding.id}/status`, { method: 'PUT', body: JSON.stringify({ status }) });
|
||
setMessage(status === 'ACTIVE' ? '绑定已启用' : '绑定已停用'); await load();
|
||
} catch (error) { setMessage(error instanceof Error ? error.message : '状态修改失败'); }
|
||
}
|
||
|
||
return <section className="bindingPage">
|
||
{message && <div className="toast" onClick={() => setMessage('')}>{message}</div>}
|
||
<div className="bindingIntro"><div><h1>流程选择与绑定</h1><p>客户端只提交业务数据,服务端按类型、时长和优先级选择 Flowable 流程。</p></div><span className="safeBadge">服务端权威路由</span></div>
|
||
<div className="bindingGrid">
|
||
<div className="configCard"><h2>新建路由规则</h2>
|
||
<label>业务类型<input value="请假申请" disabled /></label>
|
||
<div className="twoColumns"><label>表单<select value={formKey} onChange={e => setFormKey(e.target.value)}>{[...new Set(published.map(form => form.form_key))].map(key => <option key={key}>{key}</option>)}</select></label><label>版本<select value={formVersion} onChange={e => setFormVersion(Number(e.target.value))}>{selectedVersions.map(form => <option key={form.version} value={form.version}>v{form.version} · 已发布</option>)}</select></label></div>
|
||
<label>Flowable 流程<select value={processKey} onChange={e => setProcessKey(e.target.value)}>{definitions.filter(definition => !definition.suspended).map(definition => <option key={definition.id} value={definition.key}>{definition.name || definition.key} · v{definition.version}</option>)}</select></label>
|
||
<label>请假类型<select value={leaveType} onChange={e => setLeaveType(e.target.value)}>{leaveTypes.map(type => <option key={type.value} value={type.value}>{type.label}</option>)}</select></label>
|
||
<div className="twoColumns"><label>最少天数<input type="number" min="0" step="0.5" value={minDays} onChange={e => setMinDays(e.target.value)} placeholder="不限" /></label><label>最多天数<input type="number" min="0" step="0.5" value={maxDays} onChange={e => setMaxDays(e.target.value)} placeholder="不限" /></label></div>
|
||
<label>优先级<input type="number" value={priority} onChange={e => setPriority(Number(e.target.value))} /></label>
|
||
<div className="rulePreview"><strong>匹配规则</strong><span>{ruleSummary}</span></div>
|
||
<button className="primaryWide" onClick={createBinding} disabled={!formKey || !processKey}>创建并启用绑定</button>
|
||
</div>
|
||
<div className="flowCard"><h2>运行时选择</h2><div className="flowPreview"><div className="flowNode source">业务提交<span>LEAVE_REQUEST</span></div><i>→</i><div className="flowNode decision">规则匹配<span>类型 · 时长 · 优先级</span></div><i>→</i><div className="flowNode target">启动流程<span>{processKey || '请选择流程'}</span></div></div><div className="guardrails"><p>✓ 仅引用已发布表单</p><p>✓ 优先级高的精确规则先匹配</p><p>✓ 无匹配规则时拒绝启动流程</p><p>✓ 已运行实例不会随绑定变更</p></div></div>
|
||
</div>
|
||
<div className="bindingList"><h2>现有绑定</h2>{bindings.length === 0 ? <p className="empty">暂无绑定</p> : bindings.map(binding => <article key={binding.id} className={binding.status === 'ACTIVE' ? '' : 'inactive'}><div><strong>{binding.form_key} · v{binding.form_version}</strong><span>{binding.business_type} → {binding.process_definition_key}</span></div><div className="conditions"><span>{binding.leave_type || '全部类型'}</span><span>{minutesToDays(binding.min_duration_minutes)} – {minutesToDays(binding.max_duration_minutes)}</span><span>P{binding.priority}</span></div><button className={binding.status === 'ACTIVE' ? 'dangerGhost' : ''} onClick={() => toggle(binding)}>{binding.status === 'ACTIVE' ? '停用' : '启用'}</button></article>)}</div>
|
||
</section>;
|
||
}
|