App.tsx
11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import { useEffect, useState, lazy, Suspense } from 'react'
import { Layout, Card, Tabs, Button, Space, Typography, Tag, message, Spin, Modal, Select, Menu } from 'antd'
import { ReloadOutlined, ThunderboltOutlined, BranchesOutlined, UserOutlined } from '@ant-design/icons'
import { api } from './api'
import type { SceneSchema } from './types'
import { SceneForm } from './engine/SceneForm'
import { TracePanel, ProvenancePanel, ModelViewer, OrdersEventsPanel } from './panels'
// 重量级视图(模型编辑器 + 三张可视化图,含 cytoscape/xyflow/bpmn-js)按需懒加载,
// 让默认「场景运行台」的首屏包保持轻量。
const ModelEditor = lazy(() => import('./model/ModelEditor').then((m) => ({ default: m.ModelEditor })))
const OntologyCanvas = lazy(() => import('./viz/OntologyCanvas').then((m) => ({ default: m.OntologyCanvas })))
const LazyFallback = () => (
<div style={{ padding: 60, textAlign: 'center' }}><Spin tip="加载视图…"><div style={{ width: 120, height: 60 }} /></Spin></div>
)
const { Header, Sider, Content } = Layout
const { Title, Text, Paragraph } = Typography
type View = 'runner' | 'editor' | 'er' | 'network' | 'process'
const VIEW_ITEMS = [
{ key: 'runner', label: '场景运行台' },
{ key: 'editor', label: '模型编辑器' },
{ key: 'er', label: '数据结构图' },
{ key: 'network', label: '本体网络图' },
{ key: 'process', label: '场景流程图' },
]
export default function App() {
const [view, setView] = useState<View>('runner')
const [scenes, setScenes] = useState<any[]>([])
const [sceneId, setSceneId] = useState<string>('') // 初始不硬编码,待场景列表返回后取首个可渲染场景
const [schema, setSchema] = useState<SceneSchema | null>(null)
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<any>(null)
const [refreshKey, setRefreshKey] = useState(0)
const [formKey, setFormKey] = useState(0)
// 演示态主体(M5 principal):随手切换以观察权限"通过/拒绝"分支;管理员方可热重载/保存模型/新增客户
const [principal, setPrincipal] = useState<string>('backend_admin')
// 拉取场景列表(首个可渲染场景兜底选中;已有选择则保留,以便热重载后停留在当前场景)
const loadScenes = () =>
api.scenes().then((list) => {
setScenes(list)
setSceneId((cur) => cur || list.find((s: any) => s.hasTemplate)?.sceneId || '')
}).catch(() => message.error('无法连接引擎(8080),请先启动后端'))
useEffect(() => { loadScenes() }, [])
useEffect(() => {
if (!sceneId) return
setLoading(true); setResult(null)
api.sceneSchema(sceneId).then(setSchema).catch((e) => message.error('加载场景失败:' + e.message)).finally(() => setLoading(false))
}, [sceneId, formKey])
// 模型(含热重载/保存)变化后:重拉场景列表 + 重挂当前表单,让改动即时反映
const afterModelChange = async () => {
await loadScenes()
setFormKey((k) => k + 1)
}
async function reloadModels() {
try {
const r = await api.reload(principal)
message.success(r.message)
await afterModelChange()
} catch (e: any) { message.error('热重载失败:' + e.message) }
}
const onExecuted = (r: any) => { setResult(r); if (r.success) setRefreshKey((k) => k + 1) }
return (
<Layout style={{ minHeight: '100vh' }}>
<Header style={{ background: '#001529', display: 'flex', alignItems: 'center', gap: 16, paddingInline: 20 }}>
<BranchesOutlined style={{ color: '#69b1ff', fontSize: 22 }} />
<Title level={4} style={{ color: '#fff', margin: 0, whiteSpace: 'nowrap' }}>本体驱动 DDD+EDA · 元数据引擎</Title>
<Menu
mode="horizontal"
theme="dark"
selectedKeys={[view]}
onClick={(e) => setView(e.key as View)}
items={VIEW_ITEMS}
style={{ flex: 1, minWidth: 0, background: 'transparent', borderBottom: 'none' }}
/>
<Space>
<UserOutlined style={{ color: '#69b1ff' }} />
<Select size="small" value={principal} onChange={setPrincipal} style={{ width: 130 }}
options={[{ value: 'backend_admin', label: '后台管理员' }, { value: 'normal_user', label: '普通用户' }]} />
</Space>
<Button icon={<ReloadOutlined />} onClick={reloadModels} ghost>热重载</Button>
<HelpButton />
</Header>
{view === 'runner' && (
<Layout>
<Sider width={260} theme="light" style={{ padding: 12 }}>
<Text type="secondary">业务场景(M4)</Text>
<div style={{ marginTop: 8 }}>
{scenes.map((s) => {
const active = sceneId === s.sceneId
return (
<div
key={s.sceneId}
onClick={() => setSceneId(s.sceneId)}
style={{
padding: '10px 12px', marginBottom: 8, borderRadius: 8, cursor: 'pointer',
background: active ? '#e6f4ff' : '#fff',
border: `1px solid ${active ? '#91caff' : '#f0f0f0'}`, transition: 'all .2s',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<ThunderboltOutlined style={{ color: active ? '#1677ff' : '#999' }} />
<span style={{ fontWeight: 500, color: active ? '#1677ff' : 'inherit' }}>{s.sceneName}</span>
</div>
<div style={{ fontSize: 11, color: '#999', marginTop: 4, marginLeft: 22 }}>
{s.hasTemplate ? s.templateName : '无表单 · 直接运行 flowSteps'}
</div>
</div>
)
})}
</div>
<Paragraph type="secondary" style={{ fontSize: 12, marginTop: 16 }}>
业务场景均由 M8 模板动态渲染并端到端执行。「模型编辑器」可视化查看/编辑本体;
「数据结构图 / 本体网络图 / 场景流程图」由 M0–M8 本体动态合成渲染。
</Paragraph>
</Sider>
<Content style={{ padding: 16 }}>
<Spin spinning={loading}>
<div style={{ display: 'flex', gap: 16, alignItems: 'flex-start' }}>
<Card style={{ flex: '1 1 62%' }} title={<Space><b>{schema?.sceneName || '加载中'}</b><Tag color="green">{sceneId}</Tag></Space>}>
{schema?.template ? <SceneForm key={formKey + sceneId} schema={schema} onExecuted={onExecuted} principal={principal} />
: schema ? <RunOnlyScene key={sceneId} schema={schema} onExecuted={onExecuted} principal={principal} />
: null}
</Card>
<Card style={{ flex: '1 1 38%', position: 'sticky', top: 16 }} styles={{ body: { paddingTop: 8 } }}>
<Tabs
items={[
{ key: 'trace', label: '执行追踪', children: <TracePanel result={result} /> },
{ key: 'prov', label: '溯源/规则', children: schema ? <ProvenancePanel schema={schema} /> : null },
{ key: 'orders', label: '数据/事件', children: <OrdersEventsPanel refreshKey={refreshKey} aggregateId={schema?.template?.masterAggregate || schema?.aggregateScope?.[0]} /> },
{ key: 'model', label: '模型查看器', children: <ModelViewer /> },
]} />
</Card>
</div>
</Spin>
</Content>
</Layout>
)}
{view === 'editor' && (
<Content style={{ padding: 16 }}>
<Suspense fallback={<LazyFallback />}>
<ModelEditor principal={principal} onReloaded={afterModelChange} />
</Suspense>
</Content>
)}
{(view === 'er' || view === 'network' || view === 'process') && (
<Content>
<Suspense fallback={<LazyFallback />}>
<OntologyCanvas which={view} />
</Suspense>
</Content>
)}
</Layout>
)
}
/**
* 无表单场景运行器:用于未绑定 M8 模板的场景(如"演示:全部 stepType",其字段为合成变量、非 M1 领域字段)。
* 直接以空 payload 调用同一执行接口,引擎会逐条"通用解释"M4 flowSteps,产出可读执行轨迹(见右侧「执行追踪」)。
*/
function RunOnlyScene({ schema, onExecuted, principal }:
{ schema: SceneSchema; onExecuted: (r: any) => void; principal: string }) {
const [running, setRunning] = useState(false)
async function run() {
setRunning(true)
try {
const r = await api.execute(schema.sceneId, { master: {}, details: {} }, principal)
onExecuted(r)
if (r.success) message.success('场景执行完成,见右侧「执行追踪」')
else message.error(`${r.stage} · ${r.message}`)
} catch (e: any) {
message.error('执行失败:' + e.message)
} finally {
setRunning(false)
}
}
return (
<div>
<Space wrap style={{ marginBottom: 12 }}>
<Tag color="purple">场景 {schema.sceneId}</Tag>
<Tag color="geekblue">无 M8 表单模板</Tag>
<Tag color="gold">权限 {schema.permissionBind?.join(',')}</Tag>
</Space>
<Paragraph type="secondary">
该场景的字段为演示用合成变量(非 M1 领域字段),故不绑定 M8 表单模板。点下方按钮,引擎会以空 payload
逐条<b>通用解释</b> M4 <Text code>flowSteps</Text> —— 起止 / 数据与派生 / 控制流(condition·switch·while·parallel)
/ Saga 可靠性(retry·compensate·timer)/ EDA(emitEvent·waitEvent)/ 人工任务,产出可读执行轨迹。
</Paragraph>
<Button type="primary" loading={running} onClick={run}>运行(无表单,直接解释 flowSteps)</Button>
</div>
)
}
function HelpButton() {
const [open, setOpen] = useState(false)
return (
<>
<Button ghost onClick={() => setOpen(true)}>验证「改模型→改行为」</Button>
<Modal open={open} onCancel={() => setOpen(false)} footer={null} width={720} title="如何验证:改了模型文件,操作定义同时改变">
<Paragraph>本 Demo 的核心是<b>元数据驱动</b>:前端表单与后端行为都由 <Text code>models/*.yaml</Text> 实时解释,改模型即改行为,无需改任何代码。试试:</Paragraph>
<ol style={{ lineHeight: 2 }}>
<li>进入<b>「模型编辑器 · 源码编辑」</b>,选 <Text code>M8-front-schema.yaml</Text> 给订单主信息卡片加/删一个绑定 M1 字段的组件 → <b>保存并热重载</b> → 回「场景运行台」,表单立即多/少一个字段。</li>
<li>编辑 <Text code>MetaRule-business.yaml</Text>:把 <Text code>single_order_max_amount</Text> 从 50000 改为 100 → 保存 → 普通用户下单超 100 即被 <Tag color="red">REJECT</Tag>。</li>
<li>编辑 <Text code>M1-domain.yaml</Text>:把某字段 type 由 string 改为 int → 保存 → 组件输入类型随之改变;<b>「数据结构图」</b>同步更新。</li>
<li><b>「本体网络图 / 场景流程图」</b>亦由本体动态合成,改模型后点视图内「刷新」即见变化。</li>
</ol>
<Paragraph type="secondary">保存需管理员主体(右上切换为「后台管理员」)。</Paragraph>
</Modal>
</>
)
}