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}