SceneForm.tsx 10.1 KB
import { useEffect, useMemo, useState } from 'react'
import {
  Card, Row, Col, Form, Button, Table, Space, Tag, Typography, Alert, message, Divider, Empty,
} from 'antd'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
import { api } from '../api'
import type { DragComponent, OptionItem, SceneSchema } from '../types'
import { FieldControl } from './FieldControl'

const { Text } = Typography

type Role = 'master' | 'single' | 'table'

function roleOf(container: DragComponent): Role {
  const fb = container.fieldBind || ({} as any)
  if (fb.bindSourceType === 'aggregate_root') return 'master'
  if (container.compType === 'table') return 'table'
  return 'single'
}

function collectOptionSources(comps: DragComponent[], acc: Set<string>) {
  for (const c of comps) {
    const src = c.fieldBind?.optionsSource
    if (src) acc.add(src.aggregateId)
    if (c.childComponents) collectOptionSources(c.childComponents, acc)
  }
}

/**
 * 场景通用渲染引擎:读取合成后的场景 Schema,动态生成表单。
 * 完全由 M8 模板结构驱动——主表(aggregate_root)渲染为卡片字段,
 * 子实体(aggregate_entity)按 table/card 渲染为多行表格或单行卡片。改模板即改页面。
 */
export function SceneForm({ schema, onExecuted, principal }:
  { schema: SceneSchema; onExecuted: (r: any) => void; principal: string }) {
  const template = schema.template!
  const containers = template.rootComponents || []

  const [master, setMaster] = useState<Record<string, any>>({})
  const [singles, setSingles] = useState<Record<string, Record<string, any>>>({})
  const [tables, setTables] = useState<Record<string, Array<Record<string, any>>>>({})
  const [optionsMap, setOptionsMap] = useState<Record<string, OptionItem[]>>({})
  const [precheck, setPrecheck] = useState<any>(null)
  const [submitting, setSubmitting] = useState(false)

  // 初始化表单状态(据模板结构)
  useEffect(() => {
    const s: Record<string, Record<string, any>> = {}
    const t: Record<string, Array<Record<string, any>>> = {}
    for (const c of containers) {
      const role = roleOf(c)
      const entity = c.fieldBind?.domainEntityName
      if (role === 'single' && entity) s[entity] = {}
      if (role === 'table' && entity) t[entity] = [{}]
    }
    setMaster({})
    setSingles(s)
    setTables(t)
    setPrecheck(null)
    // 拉取所有引用下拉数据源
    const aggs = new Set<string>()
    collectOptionSources(containers, aggs)
    Promise.all([...aggs].map((a) => api.options(a).then((opts) => [a, opts] as const)))
      .then((pairs) => setOptionsMap(Object.fromEntries(pairs)))
      .catch(() => {})
  }, [schema.sceneId])

  function buildPayload() {
    const details: Record<string, any[]> = {}
    for (const c of containers) {
      const role = roleOf(c)
      const entity = c.fieldBind?.domainEntityName
      if (!entity) continue
      if (role === 'table') details[entity] = tables[entity] || []
      if (role === 'single') details[entity] = [singles[entity] || {}]
    }
    return { master, details }
  }

  async function doPrecheck() {
    try {
      const r = await api.precheck(schema.sceneId, buildPayload(), principal)
      setPrecheck(r)
      if (r.reject) message.warning('预检未通过,请查看提示')
      else message.success('预检通过,可提交')
    } catch (e: any) {
      message.error('预检失败:' + e.message)
    }
  }

  async function doSubmit() {
    setSubmitting(true)
    try {
      const r = await api.execute(schema.sceneId, buildPayload(), principal)
      onExecuted(r)
      if (r.success) {
        message.success(`${schema.sceneName}成功:${r.rootId ?? ''}`)
        setPrecheck(null)
      } else {
        message.error(`${r.stage} · ${r.message}`)
        setPrecheck({ reject: true, errors: r.errors || [], rejectRules: r.appliedRules?.filter((x: any) => x.effect === 'REJECT') || [], warnings: r.warnings || [] })
      }
    } catch (e: any) {
      message.error('提交失败:' + e.message)
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <div>
      <Space wrap style={{ marginBottom: 12 }}>
        <Tag color="blue">模板 {template.templateId}</Tag>
        <Tag color="geekblue">{template.templateType}</Tag>
        <Tag color="purple">主聚合 {template.masterAggregate}</Tag>
        <Tag color="cyan">命令 API {schema.commandApi}</Tag>
        <Tag color="gold">权限 {schema.permissionBind?.join(',')}</Tag>
      </Space>

      {precheck && (precheck.reject || precheck.warnings?.length > 0) && (
        <Alert
          style={{ marginBottom: 12 }}
          type={precheck.reject ? 'error' : 'warning'}
          showIcon
          message={precheck.reject ? '预检拦截(MetaRule / 校验)' : '预检提示'}
          description={
            <div>
              {(precheck.errors || []).map((e: string, i: number) => <div key={'e' + i}>❌ {e}</div>)}
              {(precheck.rejectRules || []).map((r: any, i: number) => <div key={'r' + i}>⛔ [{r.ruleId}] {r.message}</div>)}
              {(precheck.warnings || []).map((w: any, i: number) => <div key={'w' + i}>⚠️ [{w.ruleId}] {w.message}</div>)}
            </div>
          }
        />
      )}

      <Form layout="vertical">
        {containers.map((c) => {
          const role = roleOf(c)
          if (role === 'master') return <MasterCard key={c.compId} container={c} values={master} setValues={setMaster} optionsMap={optionsMap} />
          if (role === 'single') return <SingleCard key={c.compId} container={c} state={singles} setState={setSingles} optionsMap={optionsMap} />
          return <TableCard key={c.compId} container={c} state={tables} setState={setTables} optionsMap={optionsMap} />
        })}
      </Form>

      <Divider />
      <Space>
        <Button onClick={doPrecheck}>提交前预检(MetaRule 实时风控)</Button>
        <Button type="primary" loading={submitting} onClick={doSubmit}>提交「{schema.sceneName}」</Button>
      </Space>
    </div>
  )
}

// ---------- 字段标签(展示 M1 绑定溯源,强化"字段强绑定领域模型") ----------
function FieldLabel({ comp }: { comp: DragComponent }) {
  const fb = comp.fieldBind!
  const path = `${fb.bindAggregateId}${fb.domainEntityName ? '.' + fb.domainEntityName : ''}.${fb.domainFieldName}`
  return (
    <span>
      {fb.formLabel}
      {fb.required && <Text type="danger"> *</Text>}
      <Text type="secondary" style={{ fontSize: 11, marginLeft: 6 }}>[{fb.fieldType}]</Text>
      {fb.masked && <Tag color="orange" style={{ marginLeft: 6, fontSize: 10 }}>脱敏</Tag>}
      <div title={path} style={{ fontSize: 10, color: '#999', fontWeight: 400,
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '100%' }}>⇠ {path}</div>
    </span>
  )
}

function MasterCard({ container, values, setValues, optionsMap }: any) {
  const children: DragComponent[] = container.childComponents || []
  return (
    <Card size="small" title={<b>{container.fieldBind?.formLabel}</b>} style={{ marginBottom: 12 }}>
      <Row gutter={16}>
        {children.map((ch) => (
          <Col span={ch.span || 12} key={ch.compId}>
            <Form.Item label={<FieldLabel comp={ch} />}>
              <FieldControl comp={ch} value={values[ch.fieldBind!.domainFieldName]}
                onChange={(v) => setValues((prev: any) => ({ ...prev, [ch.fieldBind!.domainFieldName]: v }))}
                optionsMap={optionsMap} />
            </Form.Item>
          </Col>
        ))}
      </Row>
    </Card>
  )
}

function SingleCard({ container, state, setState, optionsMap }: any) {
  const entity = container.fieldBind?.domainEntityName
  const children: DragComponent[] = container.childComponents || []
  const values = state[entity] || {}
  return (
    <Card size="small" title={<b>{container.fieldBind?.formLabel}</b>} extra={<Tag>子实体 {entity}</Tag>} style={{ marginBottom: 12 }}>
      <Row gutter={16}>
        {children.map((ch) => (
          <Col span={ch.span || 12} key={ch.compId}>
            <Form.Item label={<FieldLabel comp={ch} />}>
              <FieldControl comp={ch} value={values[ch.fieldBind!.domainFieldName]}
                onChange={(v) => setState((prev: any) => ({ ...prev, [entity]: { ...prev[entity], [ch.fieldBind!.domainFieldName]: v } }))}
                optionsMap={optionsMap} />
            </Form.Item>
          </Col>
        ))}
      </Row>
    </Card>
  )
}

function TableCard({ container, state, setState, optionsMap }: any) {
  const entity = container.fieldBind?.domainEntityName
  const children: DragComponent[] = container.childComponents || []
  const rows: Array<Record<string, any>> = state[entity] || []

  const setCell = (idx: number, field: string, v: any) =>
    setState((prev: any) => {
      const next = [...(prev[entity] || [])]
      next[idx] = { ...next[idx], [field]: v }
      return { ...prev, [entity]: next }
    })
  const addRow = () => setState((prev: any) => ({ ...prev, [entity]: [...(prev[entity] || []), {}] }))
  const delRow = (idx: number) => setState((prev: any) => ({ ...prev, [entity]: (prev[entity] || []).filter((_: any, i: number) => i !== idx) }))

  const columns = [
    ...children.map((ch) => ({
      title: <FieldLabel comp={ch} />,
      key: ch.compId,
      render: (_: any, __: any, idx: number) => (
        <FieldControl comp={ch} value={rows[idx]?.[ch.fieldBind!.domainFieldName]}
          onChange={(v) => setCell(idx, ch.fieldBind!.domainFieldName, v)} optionsMap={optionsMap} />
      ),
    })),
    {
      title: '操作', key: '_op', width: 70,
      render: (_: any, __: any, idx: number) => (
        <Button danger size="small" icon={<DeleteOutlined />} onClick={() => delRow(idx)} />
      ),
    },
  ]

  return (
    <Card size="small" title={<b>{container.fieldBind?.formLabel}</b>} extra={<Tag>子实体 {entity}(多行)</Tag>}
      style={{ marginBottom: 12 }}>
      <Table size="small" pagination={false} rowKey={(_, i) => String(i)}
        dataSource={rows.map((r, i) => ({ ...r, _k: i }))} columns={columns as any}
        locale={{ emptyText: <Empty description="暂无明细行" /> }} />
      <Button type="dashed" block icon={<PlusOutlined />} style={{ marginTop: 8 }} onClick={addRow}>新增一行明细</Button>
    </Card>
  )
}