import { useEffect, useMemo, useRef, useState } from 'react' import { Tree, Card, Descriptions, Tag, Table, Select, Button, Space, message, Alert, Segmented, Empty, Typography, Input, Spin, Tooltip, InputNumber, Checkbox, Popconfirm, Popover, } from 'antd' import { SaveOutlined, ReloadOutlined, ApartmentOutlined, CodeOutlined, EditOutlined, EyeOutlined, PlusOutlined, DeleteOutlined, } from '@ant-design/icons' import { api } from '../api' import { loadOntology } from '../ontology/build' import type { Ontology, OntoObject, OntoBehavior, OntoRule, OntoEvent } from '../ontology/types' import { EditSession, type Path } from './editSession' const { Text, Paragraph } = Typography const ATTR_TYPES = ['string', 'int', 'decimal', 'boolean', 'date', 'datetime', 'enum'] const EFFECTS = ['REJECT', 'ALERT', 'COMPENSATE'] const EDIT_LAYERS = ['M1', 'MetaRule', 'ME'] // 结构化可编辑的层(对象/规则/事件) /** * 模型编辑器:models/*.yaml 是唯一事实源。既可在「结构预览」里可视化浏览 / 结构化编辑, * 也可在「源码编辑」里改 YAML 原文。两种编辑都保存即写盘并热重载。等价 onto-app 的「本体模型」页。 */ export function ModelEditor({ principal, onReloaded }: { principal: string; onReloaded?: () => void }) { const [mode, setMode] = useState<'structure' | 'source'>('structure') return (
setMode(v as any)} style={{ marginBottom: 12 }} options={[ { label: '结构预览 / 编辑', value: 'structure', icon: }, { label: '源码编辑', value: 'source', icon: }, ]} /> {mode === 'structure' ? : }
) } // ============================================================ // 结构预览 + 结构化编辑:本体导航树 + 详情(可切换编辑) // ============================================================ function StructureBrowser({ principal, onReloaded }: { principal: string; onReloaded?: () => void }) { const [onto, setOnto] = useState(null) const [err, setErr] = useState('') const [sel, setSel] = useState('') const [editing, setEditing] = useState(false) const [entering, setEntering] = useState(false) const [saving, setSaving] = useState(false) const sessionRef = useRef(new EditSession()) const [, setVer] = useState(0) const bump = () => setVer((v) => v + 1) const reloadOnto = () => loadOntology().then(setOnto).catch((e) => setErr(String(e?.message || e))) useEffect(() => { reloadOnto() }, []) const treeData = useMemo(() => (onto ? buildTree(onto) : []), [onto]) const dirty = [...sessionRef.current.dirty] async function enterEdit() { setEntering(true) try { await Promise.all(EDIT_LAYERS.map((c) => sessionRef.current.ensure(c))) setEditing(true); bump() } catch (e: any) { message.error('进入编辑失败:' + e.message) } finally { setEntering(false) } } async function save() { setSaving(true) try { const saved = await sessionRef.current.saveAll(principal) message.success('已保存并热重载:' + (saved.join(', ') || '(无改动)')) await reloadOnto() onReloaded?.() bump() } catch (e: any) { message.error('保存失败:' + e.message) } finally { setSaving(false) } } function exitEdit() { if (dirty.length && !window.confirm('有未保存的结构改动,退出将丢弃。确定?')) return sessionRef.current.reset() setEditing(false); bump() } if (err) return if (!onto) return
return (
(v === 'edit' ? enterEdit() : exitEdit())} options={[ { label: '预览', value: 'view', icon: }, { label: '编辑', value: 'edit', icon: }, ]} /> {entering && } {editing && ( <> {dirty.length > 0 && 改动层:{dirty.map((c) => {c})}} 结构化编辑对象(M1)/规则(MetaRule)/事件(ME);只改被编辑的节点,其余原文(含注释)不动。保存需管理员(当前 {principal})。 )}
setSel((k[0] as string) || '')} showLine blockNode /> {editing ? : }
) } const TYPE_TAG: Record = { REJECT: { color: 'red', text: 'REJECT' }, ALERT: { color: 'orange', text: 'ALERT' }, COMPENSATE: { color: 'purple', text: 'COMPENSATE' }, } function buildTree(onto: Ontology): any[] { const roots = onto.objects.filter((o) => o.is_aggregate_root) const objChildren = roots.map((r) => ({ key: `obj:${r.id}`, title: {r.name} {r.alias}, children: onto.objects.filter((o) => o.parent_id === r.id).map((e) => ({ key: `obj:${e.id}`, title: {e.name} 子实体, })), })) const behByOwner = new Map() onto.behaviors.forEach((b) => { const k = b.owner_object_id; behByOwner.set(k, [...(behByOwner.get(k) || []), b]) }) const behChildren = [...behByOwner.entries()].map(([owner, list]) => ({ key: `bg:${owner}`, selectable: false, title: {owner}, children: list.map((b) => ({ key: `beh:${b.id}`, title: b.name })), })) const evtByAgg = new Map() onto.events.forEach((e) => { const k = e.aggregate || '其他'; evtByAgg.set(k, [...(evtByAgg.get(k) || []), e]) }) const evtChildren = [...evtByAgg.entries()].map(([agg, list]) => ({ key: `eg:${agg}`, selectable: false, title: {agg}, children: list.map((e) => ({ key: `evt:${e.id}`, title: e.name })), })) return [ { key: 'g-obj', selectable: false, title: 对象模型 ({onto.objects.length}), children: objChildren }, { key: 'g-beh', selectable: false, title: 行为模型 ({onto.behaviors.length}), children: behChildren }, { key: 'g-rule', selectable: false, title: 规则模型 ({onto.rules.length}), children: onto.rules.map((r) => ({ key: `rule:${r.id}`, title: r.name })) }, { key: 'g-evt', selectable: false, title: 事件模型 ({onto.events.length}), children: evtChildren }, { key: 'g-scene', selectable: false, title: 场景流程 ({onto.processModels.length}), children: onto.processModels.map((p) => ({ key: `scene:${p.id}`, title: p.name })) }, ] } function splitSel(sel: string): [string, string] { const i = sel.indexOf(':') return [sel.slice(0, i), sel.slice(i + 1)] } // ============================================================ // 只读详情 // ============================================================ function Detail({ onto, sel }: { onto: Ontology; sel: string }) { if (!sel) return const [kind, id] = splitSel(sel) if (kind === 'obj') { const o = onto.objects.find((x) => x.id === id); return o ? : null } if (kind === 'beh') { const b = onto.behaviors.find((x) => x.id === id); return b ? : null } if (kind === 'rule') { const r = onto.rules.find((x) => x.id === id); return r ? : null } if (kind === 'evt') { const e = onto.events.find((x) => x.id === id); return e ? : null } if (kind === 'scene') { const p = onto.processModels.find((x) => x.id === id) if (!p) return null return (
{p.name}{p.id} 流程节点(由 M4 flowSteps 合成,完整可视化见「场景流程图」视图): {t} }, { title: '关联命令', dataIndex: 'behaviorId', render: (v: string) => v || '—' }, ]} /> ) } return null } function ObjectDetail({ o }: { o: OntoObject }) { return (
{o.name} {o.alias} {o.is_aggregate_root ? '聚合根' : `子实体 · 属于 ${o.parent_id}`} {o.description && {o.description}} 属性({o.attributes.length})
( {a.is_primary_key && 🔑} {a.is_foreign_key && 🔗} {v} ), }, { title: '类型', dataIndex: 'type' }, { title: '引用', dataIndex: 'ref', render: (r: any) => (r ? → {r.root}.{r.identifier} : '—') }, { title: '约束/标注', key: 'c', render: (_: any, a: any) => ( {a.transient && 瞬态} {a.unique && 唯一} {a.constraints && Object.entries(a.constraints).filter(([k]) => k !== 'patternMessage').map(([k, v]) => {k}={String(v)})} ), }, ]} /> {o.relationships.length > 0 && ( <> 关系({o.relationships.length}) {o.relationships.map((r, i) => (
{r.refType === 'composition' ? '组合' : '引用'} {r.fk_field ? `${r.fk_field} → ` : ''}{r.target_object_id} {r.kind}
))}
)} ) } function BehaviorDetail({ b, onto }: { b: OntoBehavior; onto: Ontology }) { const item = (label: string, node: any) => {node} return (
{b.name}COMMAND{b.commandType} {item('归属对象', b.owner_object_id)} {b.description && item('说明', b.description)} {b.produced_events.length > 0 && item('产出事件', b.produced_events.map((e) => {e}))} {b.applied_rules.length > 0 && item('适用规则', b.applied_rules.map((r) => {onto.rules.find((x) => x.id === r)?.name || r}))} {b.preconditions.length > 0 && item('前置校验',
    {b.preconditions.map((p, i) =>
  • {p}
  • )}
)} {b.effects.length > 0 && item('派生/副作用',
    {b.effects.map((e, i) =>
  • {e}
  • )}
)}
) } function RuleDetail({ r }: { r: OntoRule }) { const t = TYPE_TAG[r.type] || { color: 'default', text: r.type } return (
{r.name}{t.text}{r.id} {r.expression} {r.description} {r.bindScene || '—'}
) } function EventDetail({ e }: { e: OntoEvent }) { return (
{e.name}事件 {e.aggregate} {e.topic} {(e.payload || []).map((p) => {p})} {(e.consumers || []).map((c) => {c})}
) } // ============================================================ // 结构化编辑详情:对象(M1) / 规则(MetaRule) / 事件(ME) // ============================================================ function EditDetail({ session, sel, bump }: { session: EditSession; sel: string; bump: () => void }) { if (!sel) return const [kind, id] = splitSel(sel) if (kind === 'obj') return if (kind === 'rule') return if (kind === 'evt') return return } function locateObject(m1: any, id: string): { aggIdx: number; kind: 'root' | 'entity'; entIdx?: number; node: any; agg: any } | null { const aggs = m1?.aggregates || [] for (let ai = 0; ai < aggs.length; ai++) { const root = aggs[ai].aggregateRoot || {} if (root.name === id) return { aggIdx: ai, kind: 'root', node: root, agg: aggs[ai] } const ents = aggs[ai].entities || [] for (let ei = 0; ei < ents.length; ei++) if (ents[ei].name === id) return { aggIdx: ai, kind: 'entity', entIdx: ei, node: ents[ei], agg: aggs[ai] } } return null } /** 约束编辑:展示全部现有约束(与预览一致)+ 弹层编辑全集(唯一/min/max/exclusiveMin/exclusiveMax/pattern/patternMessage)。 */ function ConstraintCell({ session, path, a, bump }: { session: EditSession; path: Path; a: any; bump: () => void }) { const cons = a.constraints || {} const setC = (key: string, val: any) => { session.setConstraint('M1', path, key, val); bump() } // v ?? undefined 保留 0(min:0 / exclusiveMin:0 均为合法值),清空(null)才删除该约束键 const num = (key: string) => ( {key} setC(key, v ?? undefined)} /> ) const content = (
setC('unique', e.target.checked)}>唯一 unique {num('min')}{num('max')} {num('exclusiveMin')}{num('exclusiveMax')}
正则 pattern setC('pattern', e.target.value)} placeholder="如 ^\d{11}$" />
校验提示 patternMessage setC('patternMessage', e.target.value)} placeholder="不满足正则时的提示语" />
) const keys = Object.keys(cons) return ( {keys.length === 0 && } {keys.filter((k) => k !== 'patternMessage').map((k) => {k}={String(cons[k])})} {a.refAggregate && → {a.refRoot}.{a.refIdentifier}}
String(i)} dataSource={attrs} columns={[ { title: '字段名', width: 200, render: (_: any, a: any, j: number) => ( {loc.node[idField] === a.name && 🔑} {a.refAggregate && 🔗} set([...attrsPath, j, 'name'], e.target.value)} /> ), }, { title: '类型', width: 130, render: (_: any, a: any, j: number) => ( set('ruleName', e.target.value)} /> set('topic', e.target.value)} /> set('consumers', v)} placeholder="回车添加消费方服务" /> ) } // ============================================================ // 源码编辑:逐层编辑 YAML 原文,保存即热重载 // ============================================================ function SourceEditor({ principal, onReloaded }: { principal: string; onReloaded?: () => void }) { const [layers, setLayers] = useState<{ code: string; fileName: string }[]>([]) const [code, setCode] = useState('M1') const [text, setText] = useState('') const [orig, setOrig] = useState('') const [dir, setDir] = useState('') const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) useEffect(() => { api.models().then((d) => { setLayers(d.layers); setDir(d.modelsDir) }) }, []) const loadLayer = (c: string) => { setLoading(true) api.model(c).then((d) => { setText(d.raw ?? ''); setOrig(d.raw ?? '') }).finally(() => setLoading(false)) } useEffect(() => { if (code) loadLayer(code) }, [code]) const dirty = text !== orig const switchLayer = (c: string) => { if (dirty && !window.confirm('当前层有未保存的修改,切换将丢弃。确定切换?')) return setCode(c) } async function save() { setSaving(true) try { const r = await api.saveModel(code, text, principal) message.success(r.message || '已保存') setOrig(text) onReloaded?.() } catch (e: any) { message.error('保存失败:' + e.message) } finally { setSaving(false) } } return (