/** `section.form` 区块:把插件声明的 `fields[]` 渲染成表单并提交给声明的动作。 * * 三条硬口径: * ① 控件来自**通用 schema 渲染**(`../schema/fields.tsx`,与渠道配置页同一份实现), * 本组件不自己画控件、也不重复实现「留空 = 不改动」; * ② 提交前先在本地跑必填校验(`schema/values.planSubmit`),拦下「填了却起不来」; * ③ viewer 一律看不到表单(后端已在快照里裁掉,这里是第二道闸)。 * * 状态与规则在 `formState.ts`(值状态 + 提交两个 hook;候选由面板级拉好传进来), * 字段呈现与说明文案在 `FormFields.tsx`;本文件只剩「拼装 + 提交按钮 + 反馈行」, * 因此每个函数都在 80 行以内。 */ import { useMemo, type ReactNode } from "react"; import { Button, Surface, Tag } from "../../components/primitives"; import type { Role } from "../../lib/types"; import { useAuditStore } from "../audit"; import { Notice } from "../components"; import type { SchemaField } from "../schema/fieldTypes"; import { FormFieldsGrid } from "./FormFields"; import { useFormSubmit, useFormValues, type FormFlash } from "./formState"; import { canWrite } from "./nav"; import { SectionHeader } from "./SectionHeader"; import type { PanelFieldDto, PanelOptionDto, PanelSectionDto } from "./types"; /** 声明字段 → 通用 schema(`options` 由 `optionsFrom` 拉到后由栅格填进来)。 */ export function panelFieldToSchema(f: PanelFieldDto): SchemaField { return { key: f.key, label: f.label, type: f.type, required: f.required, placeholder: f.placeholder, default: f.default, min: f.min, max: f.max, }; } /** 提交/校验反馈行:错误码与文案分两处显示(§5.4.6 的错误码 ↔ 文案分离)。 */ function FormFlashRow({ flash }: { flash: FormFlash | null }): ReactNode { if (!flash) return null; return ( <> {flash.code ? {flash.code} : null} {flash.text} ); } export interface PanelFormSectionProps { pluginId: string; section: PanelSectionDto; role: Role | null; /** 提交成功后重取一次快照(下拉候选与 status 事实面跟着刷新)。 */ onReload: () => void; /** 行内动作「带回表单」的值(design/09 §5.2.1 示例的 reauth);`null` = 没有。 * 只认显式 `values` / `formValues`(见 `types.formValuesOfResult`),不做形状猜测。 */ prefill?: Record | null; /** 下拉候选(面板级只拉一次,见 `formState.useFieldOptions`):`optionsFrom` → 候选项。 * 本组件**不再自己拉** —— 同一份候选两处各拉一次会重复请求,且表格那份拿它做显示名。 */ options: Record; /** `optionsFrom` → 失败原因(取不到候选不是致命错误,但要如实说出来)。 */ optionErrors: Record; } export function PanelFormSection({ pluginId, section, role, onReload, prefill = null, options, optionErrors, }: PanelFormSectionProps): ReactNode { const log = useAuditStore((s) => s.log); // 依赖键用 JSON 串:快照每 30s 重取一次,对象身份每次都变,直接依赖对象会把用户正在 // 输入的内容重置掉(「打着字被清空」是比不刷新更糟的体验)。 const fieldsKey = JSON.stringify(section.fields ?? []); const prefillKey = JSON.stringify(prefill ?? null); const external = useMemo( () => ({ ...(section.values ?? {}), ...(prefill ?? {}) }), [JSON.stringify(section.values ?? {}), prefillKey], ); const fields = useMemo(() => (section.fields ?? []).map((f) => panelFieldToSchema(f)), [fieldsKey]); const state = useFormValues(fields, fieldsKey, external); const action = section.submit?.action ?? ""; const { submit, busy, flash } = useFormSubmit({ pluginId, action, fields, state, onReload, log }); if (!canWrite(role)) { return ( 当前角色没有写权限,本区块的表单不显示(写动作一律 owner / admin)。 ); } return ( f.required === true).map((f) => f.key)} missing={state.missing} configured={state.configured} cleared={state.cleared} options={options} optionErrors={optionErrors} onValue={state.setValue} onToggleClear={state.toggleClear} /> {fields.length === 0 ? 该表单没有声明任何字段。 : null}
{action === "" ? 声明缺少 submit.action,无法提交。 : null}
); }