REAL SHOWCASE SOURCEform-session-order-entry

1 个文件 · 360 行 · TypeScript

// ===== showcase/src/examples/components/form_session.ts ===== import {  FormBindingBag,  FormSession,  RenderButton,  RenderComboBox,  RenderFieldPresenter,  RenderFormPanel,  RenderLookupEdit,  RenderNumberInput,  RenderPageHeader,  RenderStackPanel,  RenderText,  RenderTextArea,  RenderTextBox,  RenderWrapPanel,} from 'ds-ui' export interface MedicationCatalogRow {  code: string  name: string  specification: string  unit: string  price: number  maxSingleDose: number  active: boolean} interface MedicationOrderDraft {  drugCode: string  drugName: string  specification: string  unit: string  price: number  maxSingleDose: number  dose: number  frequency: string  route: string  remark: string} export const medicationCatalogRows: MedicationCatalogRow[] = [  {    code: 'MED001',    name: '阿莫西林胶囊',    specification: '0.25g × 24粒',    unit: '盒',    price: 18.5,    maxSingleDose: 1,    active: true,  },  {    code: 'MED002',    name: '头孢呋辛酯片',    specification: '0.25g × 12片',    unit: '盒',    price: 32.8,    maxSingleDose: 0.5,    active: true,  },  {    code: 'MED003',    name: '旧版复方制剂',    specification: '10ml × 6支',    unit: '盒',    price: 25,    maxSingleDose: 10,    active: false,  },] const lookupColumns = [  { key: 'code', title: '院内编码', width: 100 },  { key: 'name', title: '药品名称', width: 180 },  { key: 'specification', title: '规格', width: 130 },  { key: 'unit', title: '单位', width: 64 },  { key: 'price', title: '单价', width: 72 },] class FormSessionExampleRoot extends RenderStackPanel {  private ownedDisposed = false   constructor(    private readonly formSession: FormSession<MedicationOrderDraft>,    private readonly bindings: FormBindingBag<MedicationOrderDraft>,  ) {    super({      orientation: 'vertical',      spacing: 16,      crossAxisAlignment: 'stretch',      padding: 24,    })  }   override dispose(): void {    if (!this.ownedDisposed) {      this.ownedDisposed = true      this.bindings.dispose()      this.formSession.dispose()    }    super.dispose()  }} export function createFormSessionExample(): RenderStackPanel {  const initial = medicationCatalogRows[0]!  const form = new FormSession<MedicationOrderDraft>({    drugCode: initial.code,    drugName: initial.name,    specification: initial.specification,    unit: initial.unit,    price: initial.price,    maxSingleDose: initial.maxSingleDose,    dose: 0.5,    frequency: 'bid',    route: 'oral',    remark: '',  })  const bindings = new FormBindingBag(form)   const feedback = new RenderText('选择药品后,关联字段会作为一次修改统一回填。')  const sessionState = new RenderText('', { role: 'secondary' })  const drugCode = new RenderTextBox({ value: initial.code, placeholder: '药品编码', readonly: true })  const drugName = new RenderTextBox({ value: initial.name, placeholder: '药品名称', readonly: true })  const specification = new RenderTextBox({ value: initial.specification, placeholder: '药品规格', readonly: true })  const unit = new RenderTextBox({ value: initial.unit, placeholder: '计价单位', readonly: true })  const price = new RenderNumberInput({ value: initial.price, decimals: 2, prefixText: '¥', readonly: true })  const dose = new RenderNumberInput({ value: 0.5, min: 0, max: 100, step: 0.25, decimals: 2 })  const frequency = new RenderComboBox({    value: 'bid',    options: [      { value: 'qd', label: '每日一次' },      { value: 'bid', label: '每日两次' },      { value: 'tid', label: '每日三次' },    ],  })  const route = new RenderComboBox({    value: 'oral',    options: [      { value: 'oral', label: '口服' },      { value: 'iv', label: '静脉滴注' },      { value: 'im', label: '肌内注射' },    ],  })  const remark = new RenderTextArea({    value: '',    placeholder: '补充用药说明',    minLines: 2,    maxLines: 4,  })  const drugLookup = new RenderLookupEdit<MedicationCatalogRow>({    columns: lookupColumns,    rows: medicationCatalogRows,    valueKey: 'code',    labelKey: 'name',    queryKeys: ['code', 'name', 'specification'],    value: initial.code,    placeholder: '输入药品名称、编码或规格',    clearable: true,  })   const drugField = new RenderFieldPresenter({    label: '药品',    required: true,    validationDisplay: 'inline',    child: drugLookup,  })  const doseField = new RenderFieldPresenter({    label: '单次剂量',    required: true,    unit: 'g',    validationDisplay: 'inline',    child: dose,  })  const frequencyField = new RenderFieldPresenter({    label: '频次',    required: true,    validationDisplay: 'inline',    child: frequency,  })  const routeField = new RenderFieldPresenter({    label: '给药途径',    required: true,    validationDisplay: 'inline',    child: route,  })   const syncView = (): void => {    const draft = form.snapshot()    drugCode.setValue(draft.drugCode)    drugName.setValue(draft.drugName)    specification.setValue(draft.specification)    unit.setValue(draft.unit)    price.setValue(draft.price)     sessionState.text = [      form.isDirty ? '草稿已修改' : '草稿未修改',      form.isTouched ? '已有字段操作' : '尚未操作字段',      form.isValidating ? '正在校验' : '校验空闲',      form.isSubmitting ? '正在保存' : '未在保存',    ].join(' · ')  }   bindings.bindField('drugCode', drugLookup, {    presenter: drugField,    triggers: ['change', 'blur', 'submit'],    mapChangeToValues: change => {      const row = change.detail      return {        drugCode: row?.code ?? '',        drugName: row?.name ?? '',        specification: row?.specification ?? '',        unit: row?.unit ?? '',        price: row?.price ?? 0,        maxSingleDose: row?.maxSingleDose ?? 0,      }    },    rules: [      value => value ? undefined : '请选择药品',      async (value, context) => {        await Promise.resolve()        if (context.signal.aborted || !value) return undefined        const selected = medicationCatalogRows.find(row => row.code === value)        return selected && !selected.active ? '该药品已经停用,请重新选择' : undefined      },    ],  })  bindings.bindField('dose', dose, {    presenter: doseField,    triggers: ['change', 'blur', 'submit'],    rules: [      value => value > 0 ? undefined : '单次剂量必须大于 0',    ],  })  bindings.bindField('frequency', frequency, {    presenter: frequencyField,    rules: [value => value ? undefined : '请选择用药频次'],  })  bindings.bindField('route', route, {    presenter: routeField,    rules: [value => value ? undefined : '请选择给药途径'],  })  bindings.bindField('remark', remark, {    triggers: [],  })  form.registerFormRule(    'single-dose-limit',    draft => draft.dose <= draft.maxSingleDose      ? undefined      : {          field: 'dose',          message: `单次剂量不能超过 ${draft.maxSingleDose}g`,          severity: 'error',        },    {      triggers: ['change', 'blur', 'submit'],      dependsOn: ['drugCode', 'dose'],    },  )  form.subscribe(syncView)   drugLookup.onChange = (_value, row) => {    feedback.text = row      ? `已一次回填药品编码、名称、规格、单位和价格:${row.name}`      : '已清空药品及其关联字段'  }   const submitOrder = async (): Promise<void> => {    feedback.text = '正在执行提交校验……'    const result = await form.runSubmit(async (draft, signal) => {      await Promise.resolve()      if (signal.aborted) return draft      return { ...draft }    })    if (form.disposed) return     if (result.status === 'submitted') {      form.acceptChanges(result.value)      feedback.text = `医嘱草稿已保存:${result.value.drugName}`    } else if (result.status === 'invalid') {      feedback.text = '校验未通过,已定位到第一个错误字段'    } else if (result.status === 'busy') {      feedback.text = '当前保存尚未结束,请勿重复提交'    } else {      feedback.text = '本次保存已取消'    }  }   const saveButton = new RenderButton({    label: '校验并保存',    variant: 'primary',    onClick: () => { void submitOrder() },  })  form.subscribe(() => saveButton.setLoading(form.isSubmitting))   const actions = new RenderWrapPanel({ spacing: 8, runSpacing: 8 })  actions.addChild(saveButton)  actions.addChild(new RenderButton({    label: '制造无效剂量',    onClick: () => {      form.batchUpdate(() => {        form.setValue('dose', 0)        form.markTouched('dose')      })      void form.validateField('dose', 'change')      feedback.text = '已把单次剂量改为 0,可验证字段错误和首错定位'    },  }))  actions.addChild(new RenderButton({    label: '模拟服务端错误',    onClick: () => {      form.setExternalIssues('server', [{        field: 'drugCode',        code: 'insurance-restriction',        message: '服务端返回:当前患者不符合该药品的医保限制条件',        severity: 'error',      }])      feedback.text = '已注入一条服务端字段错误;本地校验不会把它清除'    },  }))  actions.addChild(new RenderButton({    label: '取消修改',    onClick: () => {      form.reset()      feedback.text = '已恢复到进入页面时的原始医嘱'    },  }))   const formPanel = new RenderFormPanel({    columns: 3,    minColumnWidth: 220,    labelWidth: 88,    adaptive: true,    columnGap: 16,    rowGap: 12,  })  formPanel.addField(drugField, { columnSpan: 2 })  formPanel.addField(new RenderFieldPresenter({ label: '药品编码', child: drugCode }))  formPanel.addField(new RenderFieldPresenter({ label: '药品名称', child: drugName }))  formPanel.addField(new RenderFieldPresenter({ label: '规格', child: specification }))  formPanel.addField(new RenderFieldPresenter({ label: '单位', child: unit }))  formPanel.addField(new RenderFieldPresenter({ label: '单价', unit: '元', child: price }))  formPanel.addField(doseField)  formPanel.addField(frequencyField)  formPanel.addField(routeField)  formPanel.addField(new RenderFieldPresenter({ label: '用药说明', child: remark }), { columnSpan: 3 })   const root = new FormSessionExampleRoot(form, bindings)  root.addChild(new RenderPageHeader({    title: 'FormSession 医嘱录入',    description: '用显式字段绑定维护草稿、原子回填关联字段,并统一处理编辑提交、校验、服务端错误和取消。',  }))  root.addChild(formPanel)  root.addChild(actions)  root.addChild(feedback)  root.addChild(sessionState)  syncView()  return root}