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 && (
<>
} loading={saving} disabled={!dirty.length} onClick={save}>
保存并热重载
{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 = (
)
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}}
} />
)
}
function ObjectEditForm({ session, id, bump }: { session: EditSession; id: string; bump: () => void }) {
const m1 = session.toJS('M1')
const loc = locateObject(m1, id)
if (!loc) return
const basePath: Path = loc.kind === 'root' ? ['aggregates', loc.aggIdx, 'aggregateRoot'] : ['aggregates', loc.aggIdx, 'entities', loc.entIdx!]
const attrsPath: Path = [...basePath, 'attributes']
const idField = loc.kind === 'root' ? 'identifier' : 'localIdentifier'
const attrs: any[] = loc.node.attributes || []
const set = (p: Path, v: any) => { session.setIn('M1', p, v); bump() }
return (
{loc.node.name}
{loc.kind === 'root' ? '聚合根' : '子实体'}
{loc.agg.aggregateId}
(对象改名请用源码编辑,避免跨层引用错乱)
🔑 主键字段名}>
set([...basePath, idField], e.target.value)} style={{ maxWidth: 260 }} />
{loc.kind === 'root' && (
set(['aggregates', loc.aggIdx, 'description'], e.target.value)} />
)}
属性({attrs.length})
} onClick={() => { session.addIn('M1', attrsPath, { name: 'newField', type: 'string' }); bump() }}>添加属性
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) => (