App.tsx
9.77 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
import { useEffect, useState } from 'react'
import { Layout, Card, Tabs, Button, Space, Typography, Tag, message, Spin, Modal, Select } 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'
const { Header, Sider, Content } = Layout
const { Title, Text, Paragraph } = Typography
export default function App() {
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])
async function reloadModels() {
try {
const r = await api.reload(principal)
message.success(r.message)
await loadScenes() // 重新拉取场景列表:模板增删 / hasTemplate 变化即时反映到侧栏
setFormKey((k) => k + 1) // 强制重新拉取当前场景 schema + 重挂载表单
} 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 }}>
<BranchesOutlined style={{ color: '#69b1ff', fontSize: 22 }} />
<Title level={4} style={{ color: '#fff', margin: 0 }}>本体驱动 DDD+EDA · 元数据引擎 Demo</Title>
<Tag color="blue">改 YAML → 页面与行为随之变</Tag>
<div style={{ flex: 1 }} />
<Space>
<UserOutlined style={{ color: '#69b1ff' }} />
<Select size="small" value={principal} onChange={setPrincipal} style={{ width: 140 }}
options={[{ value: 'backend_admin', label: '后台管理员' }, { value: 'normal_user', label: '普通用户' }]} />
</Space>
<Button icon={<ReloadOutlined />} onClick={reloadModels} ghost>热重载模型</Button>
<HelpButton />
</Header>
<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 模板动态渲染并端到端执行(订单/客户/商品/审核…)。
末位「演示:全部 stepType」无表单,点开可直接运行、观察引擎逐类步骤的通用解释。
部分场景需管理员主体(M5 权限,可在右上切换)。
</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>
</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>编辑 <Text code>models/M8-front-schema.yaml</Text>:给订单主信息卡片再加一个绑定 M1 字段的组件(如 totalAmount 旁加一个 input 绑 totalCurrency 的副本),或删掉某字段 → 点顶部<b>「热重载模型」</b> → 表单立即多/少一个字段。</li>
<li>编辑 <Text code>models/MetaRule-business.yaml</Text>:把 <Text code>single_order_max_amount</Text> 从 50000 改为 100 → 热重载 → 普通用户下单超 100 即被 <Tag color="red">REJECT</Tag>。</li>
<li>编辑 <Text code>models/M1-domain.yaml</Text>:把某字段 type 由 string 改为 int → 热重载 → 该组件输入类型随之改变;配合 M3 增列后可落库。</li>
<li>编辑 <Text code>models/M5-security.yaml</Text> 的 maskRules → 热重载 → 脱敏展示规则随之变化。</li>
</ol>
<Paragraph type="secondary">「模型查看器」标签页可实时查看引擎当前加载的各层模型内容。</Paragraph>
</Modal>
</>
)
}