App.tsx 11.3 KB
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>
    </>
  )
}