feat: add process binding designer
This commit is contained in:
+6
-1
@@ -1,6 +1,11 @@
|
||||
# AIOA 管理设计中心
|
||||
|
||||
React + TypeScript 管理端,当前提供可视化表单设计器 MVP:字段添加与排序、属性配置、移动端卡片预览、Schema 生成、草稿保存和版本发布。
|
||||
React + TypeScript 管理端,当前提供两个配置工作台:
|
||||
|
||||
- 可视化表单设计器:字段添加与排序、属性配置、移动端卡片预览、Schema 生成、草稿保存和版本发布。
|
||||
- 流程选择与绑定:选择已发布表单和 Flowable 定义,配置业务类型、请假类型、时长范围、优先级以及绑定启停。
|
||||
|
||||
流程绑定仅负责从权威业务数据选择已经部署的流程定义;客户端不能指定流程,已启动实例也不会因绑定变化而切换流程版本。
|
||||
|
||||
## 本地运行
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import { api } from './api';
|
||||
import { userManager } from './auth';
|
||||
import { buildSchemas, type ControlType, type Field } from './schema';
|
||||
import { ProcessBindings } from './ProcessBindings';
|
||||
|
||||
type FormVersion = { form_key: string; version: number; status: string; published_at?: string };
|
||||
const palette: { control: ControlType; label: string }[] = [
|
||||
@@ -20,14 +21,19 @@ export function App() {
|
||||
}, []);
|
||||
if (loading) return <div className="center">正在恢复管理会话…</div>;
|
||||
if (!authenticated) return <Login />;
|
||||
return <Designer onLogout={() => userManager.signoutRedirect()} />;
|
||||
return <Workspace onLogout={() => userManager.signoutRedirect()} />;
|
||||
}
|
||||
|
||||
function Login() {
|
||||
return <main className="login"><div className="brandMark">A</div><h1>AIOA 管理设计中心</h1><p>设计表单、发布版本并绑定 Flowable 流程</p><button onClick={() => userManager.signinRedirect()}>使用 Keycloak 登录</button></main>;
|
||||
}
|
||||
|
||||
function Designer({ onLogout }: { onLogout: () => void }) {
|
||||
function Workspace({ onLogout }: { onLogout: () => void }) {
|
||||
const [view, setView] = useState<'forms' | 'bindings'>('forms');
|
||||
return <div className="workspace"><header><div><strong>AIOA</strong><span>管理设计中心</span></div><nav><button className={view === 'forms' ? 'navActive' : 'navButton'} onClick={() => setView('forms')}>表单设计</button><button className={view === 'bindings' ? 'navActive' : 'navButton'} onClick={() => setView('bindings')}>流程绑定</button></nav><button className="ghost" onClick={onLogout}>退出</button></header>{view === 'forms' ? <Designer /> : <ProcessBindings />}</div>;
|
||||
}
|
||||
|
||||
function Designer() {
|
||||
const [formKey, setFormKey] = useState('leave-request');
|
||||
const [title, setTitle] = useState('请假申请');
|
||||
const [fields, setFields] = useState<Field[]>([
|
||||
@@ -62,7 +68,7 @@ function Designer({ onLogout }: { onLogout: () => void }) {
|
||||
}
|
||||
|
||||
return <div className="appShell">
|
||||
<header><div><strong>AIOA</strong><span>表单设计器</span></div><div className="headerActions"><button className="ghost" onClick={onLogout}>退出</button><button onClick={save}>保存草稿</button></div></header>
|
||||
<div className="subHeader"><div><strong>可视化表单设计器</strong><span>Schema 驱动移动端卡片</span></div><button onClick={save}>保存草稿</button></div>
|
||||
{message && <div className="toast" onClick={() => setMessage('')}>{message}</div>}
|
||||
<section className="meta"><label>表单 Key<input value={formKey} onChange={e => setFormKey(e.target.value)} /></label><label>表单标题<input value={title} onChange={e => setTitle(e.target.value)} /></label></section>
|
||||
<main className="designer">
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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>;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user