panels.tsx 7.78 KB
import { useEffect, useState } from 'react'
import { Timeline, Tag, Table, Descriptions, Alert, Empty, Select, Button, Space, Typography, List } from 'antd'
import { api } from './api'
import type { SceneSchema } from './types'

const { Text, Paragraph } = Typography

const LAYER_COLOR: Record<string, string> = {
  M8: 'blue', M1: 'green', M2: 'volcano', 'M2/M8': 'volcano', M3: 'geekblue',
  M4: 'purple', M5: 'orange', ME: 'magenta', MetaRule: 'red',
}

/** 执行追踪:把引擎返回的 trace/事件/扣库存/风控可视化,直观展示"元数据解释执行"全过程。 */
export function TracePanel({ result }: { result: any }) {
  if (!result) return <Empty description="尚未执行。请在左侧填写并提交一次「新增订单」,这里会显示引擎逐层解释执行的过程。" />

  if (result.success === false) {
    return (
      <div>
        <Alert type="error" showIcon message={`执行被拦截:${result.stage}`} description={result.message} />
        {result.appliedRules?.length > 0 && (
          <List size="small" style={{ marginTop: 12 }} header={<b>命中的 MetaRule 规则</b>}
            dataSource={result.appliedRules}
            renderItem={(r: any) => <List.Item><Tag color={r.effect === 'REJECT' ? 'red' : 'orange'}>{r.effect}</Tag>[{r.ruleId}] {r.message}</List.Item>} />
        )}
        <TraceTimeline trace={result.trace} />
      </div>
    )
  }

  return (
    <div>
      <Descriptions size="small" column={1} bordered>
        <Descriptions.Item label={`聚合根标识 ${result.rootIdField || ''}`}>{result.rootId}</Descriptions.Item>
        {result.childCount != null && <Descriptions.Item label="写入子实体数">{result.childCount}</Descriptions.Item>}
      </Descriptions>

      <TraceTimeline trace={result.trace} />

      <Text strong>领域事件(ME → Outbox)</Text>
      <div style={{ margin: '6px 0' }}>
        {(result.events || []).map((e: any) => <Tag key={e.eventId} color="magenta" style={{ marginBottom: 4 }}>{e.eventName} → {e.topic}</Tag>)}
      </div>

      {result.stockActions?.length > 0 && (
        <>
          <Text strong>跨聚合最终一致(ME 规则 + M2 effect 通用执行)</Text>
          <Table size="small" pagination={false} style={{ marginTop: 6 }} rowKey={(_, i) => String(i)}
            dataSource={result.stockActions}
            columns={[
              { title: '目标聚合', dataIndex: 'targetAggregate' },
              { title: '命令', dataIndex: 'targetCommand' },
              { title: '键', render: (_: any, a: any) => `${a.keyField}=${a.key}` },
              { title: '变更', render: (_: any, a: any) => `${a.op} ${a.amount}` },
              { title: '剩余', dataIndex: 'remaining' },
              { title: '状态', dataIndex: 'status', render: (s: string) => <Tag color={s === 'OK' ? 'green' : 'red'}>{s}</Tag> },
            ]} />
        </>
      )}

      {result.warnings?.length > 0 && (
        <Alert style={{ marginTop: 12 }} type="warning" showIcon message="MetaRule 预警(ALERT,不阻断)"
          description={result.warnings.map((w: any, i: number) => <div key={i}>⚠️ [{w.ruleId}] {w.message}</div>)} />
      )}
    </div>
  )
}

function TraceTimeline({ trace }: { trace: any[] }) {
  if (!trace?.length) return null
  return (
    <Timeline style={{ marginTop: 16 }}
      items={trace.map((s) => ({
        color: LAYER_COLOR[s.layer] || 'gray',
        children: <span><Tag color={LAYER_COLOR[s.layer] || 'default'}>{s.layer}</Tag><b>{s.step}</b> — {s.detail}</span>,
      }))} />
  )
}

/** 溯源面板:展示页面各部分分别来自哪一层模型,以及自动关联的 MetaRule 规则。 */
export function ProvenancePanel({ schema }: { schema: SceneSchema }) {
  const sl = schema.sourceLayers || {}
  return (
    <div>
      <Paragraph type="secondary">本页面无任何业务硬编码,各部分均由下列本体模型层实时合成:</Paragraph>
      <Table size="small" pagination={false} rowKey="k"
        dataSource={Object.entries(sl).map(([k, v]) => ({ k, v }))}
        columns={[
          { title: '页面要素', dataIndex: 'k' },
          { title: '来源模型层', dataIndex: 'v', render: (v: string) => <Tag color="geekblue">{v}</Tag> },
        ]} />
      <Text strong>自动关联的 MetaRule 动态规则</Text>
      <List size="small" style={{ marginTop: 6 }} dataSource={schema.rules || []}
        renderItem={(r: any) => (
          <List.Item>
            <div>
              <Tag color={r.effect === 'REJECT' ? 'red' : 'orange'}>{r.effect}</Tag>
              <b>{r.ruleName}</b> <Text code>{r.ruleId}</Text>
              <div><Text type="secondary" style={{ fontSize: 12 }}>{r.matchCondition}</Text></div>
              <div><Text type="secondary" style={{ fontSize: 12 }}>→ {r.message}</Text></div>
            </div>
          </List.Item>
        )} />
      <Text strong>规则全局参数</Text>
      <div style={{ marginTop: 6 }}>
        {Object.entries(schema.ruleGlobalParams || {}).map(([k, v]) => <Tag key={k}>{k} = {String(v)}</Tag>)}
      </div>
    </div>
  )
}

/** 模型查看器:直接查看任意本体层解析后的内容,证明"页面即模型"。 */
export function ModelViewer() {
  const [layers, setLayers] = useState<any[]>([])
  const [code, setCode] = useState<string>('M8')
  const [content, setContent] = useState<any>(null)
  const [dir, setDir] = useState('')

  useEffect(() => { api.models().then((d) => { setLayers(d.layers); setDir(d.modelsDir) }) }, [])
  useEffect(() => { if (code) api.model(code).then((d) => setContent(d.content)) }, [code])

  return (
    <div>
      <Space style={{ marginBottom: 8 }}>
        <Select value={code} style={{ width: 320 }} onChange={setCode}
          options={layers.map((l) => ({ value: l.code, label: `${l.code} · ${l.fileName}` }))} />
      </Space>
      <Paragraph type="secondary" style={{ fontSize: 12 }}>模型目录:{dir}</Paragraph>
      <pre style={{ background: '#0f172a', color: '#e2e8f0', padding: 12, borderRadius: 6, maxHeight: 460, overflow: 'auto', fontSize: 12 }}>
        {content ? JSON.stringify(content, null, 2) : '加载中…'}
      </pre>
    </div>
  )
}

/** 聚合根记录与事件列表(列由 M1 属性驱动,通用)。 */
export function OrdersEventsPanel({ refreshKey, aggregateId }: { refreshKey: number; aggregateId?: string }) {
  const [cols, setCols] = useState<any[]>([])
  const [rowKeyField, setRowKeyField] = useState<string>('')
  const [rows, setRows] = useState<any[]>([])
  const [events, setEvents] = useState<any[]>([])

  const load = () => {
    if (aggregateId) {
      api.aggregate(aggregateId).then((meta: any) => {
        setRowKeyField(meta.rootIdentifier)
        setCols([
          { title: meta.rootIdentifier, dataIndex: meta.rootIdentifier },
          ...(meta.attributes || []).map((a: any) => ({ title: a.name, dataIndex: a.name })),
        ])
      })
      api.list(aggregateId).then(setRows)
    }
    api.events().then(setEvents)
  }
  useEffect(load, [refreshKey, aggregateId])

  return (
    <div>
      <Space style={{ marginBottom: 8 }}><Button size="small" onClick={load}>刷新</Button></Space>
      <Text strong>{aggregateId} 记录(列源自 M1,敏感字段脱敏)</Text>
      <Table size="small" pagination={false} scroll={{ x: true }} style={{ marginTop: 6, marginBottom: 16 }}
        rowKey={(r: any, i) => String(r[rowKeyField] ?? i)} dataSource={rows} columns={cols} />
      <Text strong>事件 Outbox(最近)</Text>
      <Table size="small" pagination={false} style={{ marginTop: 6 }} rowKey="event_id"
        dataSource={events}
        columns={[
          { title: '事件', dataIndex: 'event_name' },
          { title: 'topic', dataIndex: 'topic' },
          { title: '状态', dataIndex: 'status', render: (s: string) => <Tag color={s === 'PUBLISHED' ? 'green' : 'blue'}>{s}</Tag> },
        ]} />
    </div>
  )
}