<script lang="ts" setup>
|
import type { FormSchema } from '@jnpf/ui/form';
|
|
import type { CreateFieldParams, FieldBatchMode, FieldDataType, FieldReadSource, ProviderProperty } from '#/api/x/eln/fields';
|
|
import { nextTick, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicForm, useForm } from '@jnpf/ui/form';
|
import { BasicModal, useModal, useModalInner } from '@jnpf/ui/modal';
|
|
import { SettingOutlined, TranslationOutlined } from '@ant-design/icons-vue';
|
|
import { createField, getProviderProperties, suggestFieldCode } from '#/api/x/eln/fields';
|
import { onlineUtils } from '#/utils/jnpf';
|
|
import CodeAssistModal from './CodeAssistModal.vue';
|
import { buildControlConfig, dropdownOptionsForSelect, validateDropdownOptions } from './controlConfig';
|
import { canUseBatchShared } from './fieldRules';
|
|
defineOptions({ name: 'ElnFieldForm' });
|
|
const emit = defineEmits(['register', 'reload']);
|
const { createMessage } = useMessage();
|
const providerProperties = ref<ProviderProperty[]>([]);
|
const providerPropertyValidationEnabled = ref(false);
|
const selectedReadSource = ref<FieldReadSource>('NONE');
|
let providerRequestSequence = 0;
|
|
const DATA_TYPE_OPTIONS = [
|
{ fullName: '单行文本', id: 'TEXT' },
|
{ fullName: '多行文本', id: 'TEXTAREA' },
|
{ fullName: '数值', id: 'DECIMAL' },
|
{ fullName: '日期', id: 'DATE' },
|
{ fullName: '日期时间', id: 'DATETIME' },
|
{ fullName: '下拉选择', id: 'SELECT' },
|
{ fullName: '复选框', id: 'CHECKBOX' },
|
];
|
const BATCH_MODE_OPTIONS = [
|
{ fullName: '单记录独立值', id: 'RECORD_PRIVATE' },
|
{ fullName: '同批次共享', id: 'BATCH_SHARED' },
|
];
|
const DATE_FORMAT_OPTIONS = [
|
{ fullName: '年-月-日(2026-09-05)', id: 'yyyy-MM-dd' },
|
{ fullName: '年/月/日(2026/09/05)', id: 'yyyy/MM/dd' },
|
{ fullName: '中文日期(2026年09月05日)', id: 'yyyy年MM月dd日' },
|
];
|
const DATETIME_FORMAT_OPTIONS = [
|
{ fullName: '精确到分钟(2026-09-05 14:30)', id: 'yyyy-MM-dd HH:mm' },
|
{ fullName: '精确到秒(2026-09-05 14:30:25)', id: 'yyyy-MM-dd HH:mm:ss' },
|
{ fullName: '斜杠日期,精确到分钟(2026/09/05 14:30)', id: 'yyyy/MM/dd HH:mm' },
|
{ fullName: '斜杠日期,精确到秒(2026/09/05 14:30:25)', id: 'yyyy/MM/dd HH:mm:ss' },
|
{ fullName: '中文日期时间(2026年09月05日 14:30:25)', id: 'yyyy年MM月dd日 HH:mm:ss' },
|
];
|
|
const schemas: FormSchema[] = [
|
{
|
component: 'Input',
|
componentProps: { maxlength: 200, placeholder: '请输入字段名称' },
|
field: 'ziduanMingcheng',
|
label: '字段名称',
|
rules: [{ message: '请输入字段名称', required: true, trigger: 'blur' }],
|
},
|
{
|
component: 'Select',
|
componentProps: { onChange: handleDataTypeChange, options: DATA_TYPE_OPTIONS, placeholder: '请选择数据类型' },
|
defaultValue: 'TEXT',
|
field: 'shujuLeixing',
|
label: '数据类型',
|
rules: [{ message: '请选择数据类型', required: true, trigger: 'change' }],
|
},
|
{
|
component: 'Input',
|
componentProps: { maxlength: 50, placeholder: '请输入单位' },
|
field: 'danwei',
|
ifShow: ({ model }) => model.shujuLeixing === 'DECIMAL',
|
label: '单位',
|
},
|
{
|
component: 'Switch',
|
defaultValue: false,
|
field: 'shifouBitian',
|
label: '是否必填',
|
},
|
{
|
component: 'Select',
|
componentProps: {
|
onChange: handleReadSourceChange,
|
options: [
|
{ fullName: '无外部来源', id: 'NONE' },
|
{ fullName: 'LIMS 任务数据', id: 'LIMS_TASK' },
|
{ fullName: '系统数据', id: 'SYSTEM' },
|
],
|
},
|
defaultValue: 'NONE',
|
field: 'readSource',
|
label: '读取来源',
|
},
|
{
|
component: 'Select',
|
componentProps: { options: [], placeholder: '请选择来源属性' },
|
dynamicRules: ({ model }) =>
|
model.readSource !== 'NONE' && providerPropertyValidationEnabled.value ? [{ message: '请选择来源属性', required: true, trigger: 'change' }] : [],
|
field: 'providerProperty',
|
ifShow: ({ model }) => model.readSource !== 'NONE',
|
label: '来源属性',
|
},
|
{
|
component: 'Select',
|
componentProps: { options: BATCH_MODE_OPTIONS },
|
defaultValue: 'RECORD_PRIVATE',
|
field: 'piciMoshi',
|
label: '批次模式',
|
rules: [{ message: '请选择批次模式', required: true, trigger: 'change' }],
|
},
|
{
|
component: 'Input',
|
componentProps: { maxlength: 200, placeholder: '例如:请输入供试品名称' },
|
field: 'controlPlaceholder',
|
ifShow: ({ model }) => ['DECIMAL', 'SELECT', 'TEXT', 'TEXTAREA'].includes(model.shujuLeixing),
|
label: '占位文本',
|
},
|
{
|
component: 'Textarea',
|
componentProps: { maxlength: 2000, placeholder: '请输入默认值', rows: 2 },
|
field: 'controlDefaultValue',
|
ifShow: ({ model }) => ['DECIMAL', 'TEXT', 'TEXTAREA'].includes(model.shujuLeixing),
|
label: '默认值',
|
},
|
{
|
component: 'Textarea',
|
componentProps: { placeholder: '每行一个选项,例如:合格|pass', rows: 4 },
|
dynamicRules: ({ model }) => (model.shujuLeixing === 'SELECT' ? [{ trigger: 'blur', validator: (_rule, value) => validateDropdownOptions(value) }] : []),
|
field: 'controlOptionsText',
|
ifShow: ({ model }) => model.shujuLeixing === 'SELECT',
|
label: '下拉选项',
|
},
|
{
|
component: 'Select',
|
componentProps: ({ formModel }) => ({
|
options: dropdownOptionsForSelect(formModel.controlOptionsText).map((option) => ({ fullName: option.display, id: option.value })),
|
placeholder: '请选择默认选项',
|
}),
|
field: 'controlDefaultOption',
|
ifShow: ({ model }) => model.shujuLeixing === 'SELECT',
|
label: '默认选项',
|
},
|
{
|
component: 'Select',
|
componentProps: ({ formModel }) => ({
|
options: formModel.shujuLeixing === 'DATETIME' ? DATETIME_FORMAT_OPTIONS : DATE_FORMAT_OPTIONS,
|
placeholder: '请选择显示格式',
|
}),
|
field: 'controlDateFormat',
|
ifShow: ({ model }) => ['DATE', 'DATETIME'].includes(model.shujuLeixing),
|
label: '显示格式',
|
},
|
{
|
component: 'DatePicker',
|
componentProps: ({ formModel }) => datePickerProps(formModel.shujuLeixing, formModel.controlDateFormat),
|
field: 'controlDefaultDate',
|
ifShow: ({ model }) => ['DATE', 'DATETIME'].includes(model.shujuLeixing),
|
label: '默认值',
|
},
|
{
|
component: 'Switch',
|
defaultValue: false,
|
field: 'controlDefaultChecked',
|
ifShow: ({ model }) => model.shujuLeixing === 'CHECKBOX',
|
label: '默认勾选',
|
},
|
{
|
component: 'Input',
|
componentProps: { maxlength: 80, placeholder: '例如:review_result' },
|
field: 'recommendedCheckboxGroupCode',
|
ifShow: ({ model }) => model.shujuLeixing === 'CHECKBOX' && model.readSource === 'NONE',
|
label: '推荐选择组编码',
|
rules: [{ pattern: /^[\w-]{1,80}$/u, message: '只能输入字母、数字、下划线或连字符', trigger: 'blur' }],
|
slot: 'recommendedCheckboxGroupCode',
|
},
|
{
|
component: 'InputNumber',
|
componentProps: { min: 0, precision: 0 },
|
defaultValue: 0,
|
dynamicDisabled: ({ model }) => !model.recommendedCheckboxGroupCode?.trim(),
|
field: 'recommendedCheckboxMinSelected',
|
ifShow: ({ model }) => model.shujuLeixing === 'CHECKBOX' && model.readSource === 'NONE',
|
label: '推荐最少选择数',
|
},
|
{
|
component: 'Switch',
|
defaultValue: false,
|
dynamicDisabled: ({ model }) => !model.recommendedCheckboxGroupCode?.trim(),
|
field: 'checkboxGroupUnlimited',
|
ifShow: ({ model }) => model.shujuLeixing === 'CHECKBOX' && model.readSource === 'NONE',
|
label: '推荐最多不限',
|
},
|
{
|
component: 'InputNumber',
|
componentProps: { min: 1, precision: 0 },
|
defaultValue: 1,
|
dynamicDisabled: ({ model }) => !model.recommendedCheckboxGroupCode?.trim(),
|
field: 'recommendedCheckboxMaxSelected',
|
ifShow: ({ model }) => model.shujuLeixing === 'CHECKBOX' && model.readSource === 'NONE' && !model.checkboxGroupUnlimited,
|
label: '推荐最多选择数',
|
dynamicRules: ({ model }) =>
|
model.recommendedCheckboxGroupCode?.trim()
|
? [
|
{
|
message: '推荐最多选择数不能小于最少选择数',
|
trigger: 'change',
|
validator: (_rule, value) =>
|
Number(value) >= Number(model.recommendedCheckboxMinSelected ?? 0)
|
? Promise.resolve()
|
: Promise.reject(new Error('推荐最多选择数不能小于最少选择数')),
|
},
|
]
|
: [],
|
},
|
{
|
component: 'Textarea',
|
componentProps: { maxlength: 500, placeholder: '请输入备注', rows: 3, showCount: true },
|
field: 'beizhu',
|
label: '备注',
|
},
|
];
|
|
const [registerForm, { resetFields, setFieldsValue, updateSchema, validate }] = useForm({
|
baseColProps: { span: 24 },
|
labelWidth: 130,
|
schemas,
|
showActionButtonGroup: false,
|
});
|
const [registerModal, { changeOkLoading, closeModal }] = useModalInner(init);
|
const [registerCodeAssistModal, { openModal: openCodeAssistModal }] = useModal();
|
|
async function init() {
|
const requestSequence = ++providerRequestSequence;
|
providerProperties.value = [];
|
providerPropertyValidationEnabled.value = false;
|
selectedReadSource.value = 'NONE';
|
await resetFields();
|
await setFieldsValue({
|
controlDateFormat: 'yyyy-MM-dd',
|
controlDefaultChecked: false,
|
controlDefaultDate: '',
|
controlDefaultOption: undefined,
|
controlDefaultValue: '',
|
controlOptionsText: '',
|
controlPlaceholder: '',
|
checkboxGroupUnlimited: false,
|
piciMoshi: 'RECORD_PRIVATE',
|
readSource: 'NONE',
|
recommendedCheckboxGroupCode: '',
|
recommendedCheckboxMaxSelected: 1,
|
recommendedCheckboxMinSelected: 0,
|
shifouBitian: false,
|
shujuLeixing: 'TEXT',
|
});
|
await updateProviderSchema('NONE', [], requestSequence);
|
}
|
|
async function handleReadSourceChange(source: FieldReadSource) {
|
const requestSequence = ++providerRequestSequence;
|
selectedReadSource.value = source;
|
providerProperties.value = [];
|
providerPropertyValidationEnabled.value = false;
|
await setFieldsValue({ piciMoshi: 'RECORD_PRIVATE', providerProperty: undefined });
|
if (requestSequence !== providerRequestSequence) return;
|
await updateProviderSchema(source, [], requestSequence);
|
if (source === 'NONE' || requestSequence !== providerRequestSequence) return;
|
try {
|
const response = await getProviderProperties(source);
|
if (requestSequence !== providerRequestSequence) return;
|
const properties = response.data || [];
|
providerProperties.value = properties;
|
await updateProviderSchema(source, properties, requestSequence);
|
} catch {
|
if (requestSequence === providerRequestSequence) providerProperties.value = [];
|
}
|
}
|
|
async function updateProviderSchema(source: FieldReadSource, properties: ProviderProperty[], requestSequence: number) {
|
if (requestSequence !== providerRequestSequence) return;
|
await updateSchema({
|
componentProps: {
|
options: properties.map((item) => ({ fullName: providerPropertyLabel(item), id: item.property })),
|
placeholder: source === 'NONE' ? '无' : '请选择来源属性',
|
},
|
field: 'providerProperty',
|
});
|
if (requestSequence !== providerRequestSequence) return;
|
await updateSchema({
|
componentProps: {
|
options: BATCH_MODE_OPTIONS.map((item) => ({
|
...item,
|
disabled: item.id === 'BATCH_SHARED' && !canUseBatchShared({ readSource: source }),
|
})),
|
},
|
field: 'piciMoshi',
|
});
|
}
|
|
async function handleSubmit() {
|
providerPropertyValidationEnabled.value = true;
|
await nextTick();
|
const values = await validate();
|
if (!values) return;
|
const source = values.readSource as FieldReadSource;
|
const selectedProperty = source === 'NONE' ? undefined : providerProperties.value.find((item) => item.property === values.providerProperty);
|
const payload: CreateFieldParams = {
|
beizhu: trimOrUndefined(values.beizhu),
|
binding: {
|
read: {
|
...(source === 'NONE'
|
? {}
|
: {
|
property: values.providerProperty,
|
...(selectedProperty?.propertyRevision == null ? {} : { propertyRevision: selectedProperty.propertyRevision }),
|
}),
|
source,
|
},
|
write: { target: 'ELN_RECORD' },
|
},
|
danwei: values.shujuLeixing === 'DECIMAL' ? trimOrUndefined(values.danwei) : undefined,
|
kongjianPeizhi: buildControlConfig(values.shujuLeixing as FieldDataType, values),
|
...selectionGroupRecommendation(values),
|
piciMoshi: values.piciMoshi as FieldBatchMode,
|
shifouBitian: values.shifouBitian ? 1 : 0,
|
shujuLeixing: values.shujuLeixing as FieldDataType,
|
ziduanMingcheng: values.ziduanMingcheng.trim(),
|
zuoyongFanwei: 'GLOBAL',
|
};
|
changeOkLoading(true);
|
try {
|
const response = await createField(payload);
|
createMessage.success(response.msg || '字段创建成功');
|
closeModal();
|
emit('reload');
|
} finally {
|
changeOkLoading(false);
|
}
|
}
|
|
async function handleDataTypeChange(dataType: FieldDataType) {
|
if (dataType === 'DATE') {
|
await setFieldsValue({ controlDateFormat: 'yyyy-MM-dd', controlDefaultDate: '' });
|
} else if (dataType === 'DATETIME') {
|
await setFieldsValue({ controlDateFormat: 'yyyy-MM-dd HH:mm:ss', controlDefaultDate: '' });
|
}
|
}
|
|
function openLimsSources() {
|
onlineUtils.route('/eln/sources', { title: 'LIMS 数据项' });
|
}
|
|
function datePickerProps(dataType: FieldDataType, documentFormat?: string) {
|
const isDateTime = dataType === 'DATETIME';
|
const valueFormat = isDateTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
const format = toPickerFormat(documentFormat || (isDateTime ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd'));
|
return {
|
allowClear: true,
|
format,
|
placeholder: isDateTime ? '请选择日期时间' : '请选择日期',
|
showTime: isDateTime ? { format: format.includes('ss') ? 'HH:mm:ss' : 'HH:mm' } : false,
|
valueFormat,
|
};
|
}
|
|
function toPickerFormat(documentFormat: string) {
|
return documentFormat.replaceAll('yyyy', 'YYYY').replaceAll('dd', 'DD');
|
}
|
|
function selectionGroupRecommendation(values: Record<string, any>) {
|
const code = values.recommendedCheckboxGroupCode?.trim();
|
if (values.shujuLeixing !== 'CHECKBOX' || values.readSource !== 'NONE' || !code) return {};
|
return {
|
recommendedCheckboxGroupCode: code,
|
recommendedCheckboxMaxSelected: values.checkboxGroupUnlimited ? null : Number(values.recommendedCheckboxMaxSelected ?? 1),
|
recommendedCheckboxMinSelected: Number(values.recommendedCheckboxMinSelected ?? 0),
|
};
|
}
|
|
function trimOrUndefined(value?: string) {
|
const trimmed = value?.trim();
|
return trimmed || undefined;
|
}
|
|
function providerPropertyLabel(property: ProviderProperty) {
|
if (property.propertyRevision == null) return property.label;
|
return `${property.label}(版本 ${property.propertyRevision}${property.enabled ? '' : ',已停用'})`;
|
}
|
|
function openCodeAssist() {
|
openCodeAssistModal(true);
|
}
|
|
async function generateCheckboxGroupCode(text: string) {
|
const response = await suggestFieldCode(text);
|
return response.data.code;
|
}
|
|
function applyCheckboxGroupCode(code: string) {
|
setFieldsValue({ recommendedCheckboxGroupCode: code });
|
}
|
</script>
|
|
<template>
|
<BasicModal v-bind="$attrs" @register="registerModal" destroy-on-close title="新增全局字段" width="640px" @ok="handleSubmit">
|
<BasicForm @register="registerForm">
|
<template #recommendedCheckboxGroupCode="{ model, field }">
|
<a-input v-model:value="model[field]" :maxlength="80" placeholder="例如:review_result">
|
<template #suffix>
|
<a-tooltip title="中文转拼音">
|
<a-button
|
aria-label="中文转拼音"
|
data-test="recommended-checkbox-code-assist"
|
size="small"
|
type="text"
|
@click.prevent.stop="openCodeAssist"
|
@mousedown.prevent.stop>
|
<TranslationOutlined />
|
</a-button>
|
</a-tooltip>
|
</template>
|
</a-input>
|
</template>
|
</BasicForm>
|
<div v-if="selectedReadSource === 'LIMS_TASK'" class="source-maintenance">
|
<a-button data-test="maintain-lims-source" type="dashed" @click="openLimsSources">
|
<template #icon><SettingOutlined /></template>
|
维护 LIMS 数据项
|
</a-button>
|
</div>
|
<CodeAssistModal @register="registerCodeAssistModal" :suggest-code="generateCheckboxGroupCode" @confirm="applyCheckboxGroupCode" />
|
</BasicModal>
|
</template>
|
|
<style lang="scss" scoped>
|
.source-maintenance {
|
padding: 0 24px 16px 154px;
|
}
|
|
@media (max-width: 720px) {
|
.source-maintenance {
|
padding-right: 0;
|
padding-left: 0;
|
}
|
}
|
</style>
|