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