<script lang="ts" setup>
|
import type { FormSchema } from '@jnpf/ui/form';
|
import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
|
|
import type { FieldBatchMode, FieldDataType, FieldDetail, FieldReadSource, FieldVersion, FieldVersionRule, ProviderProperty } from '#/api/x/eln/fields';
|
|
import { computed, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicDrawer, useDrawerInner } from '@jnpf/ui/drawer';
|
import { BasicForm, useForm } from '@jnpf/ui/form';
|
import { BasicModal, useModal } from '@jnpf/ui/modal';
|
import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
|
|
import { SettingOutlined } from '@ant-design/icons-vue';
|
|
import {
|
cancelFieldVersion,
|
createFieldVersion,
|
getFieldDetail,
|
getProviderProperties,
|
preReleaseFieldVersion,
|
publishFieldVersion,
|
switchCurrentFieldVersion,
|
updateFieldVersion,
|
} from '#/api/x/eln/fields';
|
import { getSourceReference } from '#/api/x/eln/sources';
|
import { onlineUtils } from '#/utils/jnpf';
|
|
import { buildControlConfig, controlConfigFormValues, dropdownOptionsForSelect, validateDropdownOptions } from './controlConfig';
|
import { canUseBatchShared, versionActions } from './fieldRules';
|
|
defineOptions({ name: 'ElnFieldVersionDrawer' });
|
|
const emit = defineEmits(['register', 'reload']);
|
|
type ReasonAction = 'cancel' | 'setCurrent';
|
|
const { createMessage } = useMessage();
|
const field = ref<FieldDetail>();
|
const providerProperties = ref<ProviderProperty[]>([]);
|
let providerRequestSequence = 0;
|
const editingVersionId = ref('');
|
const isViewingVersion = ref(false);
|
const versionModalVersion = ref<FieldVersion>();
|
const selectedReadSource = ref<FieldReadSource>('NONE');
|
const editingControlConfigExtras = ref<Record<string, unknown>>({});
|
const reason = ref('');
|
const reasonAction = ref<ReasonAction>();
|
const reasonVersion = ref<FieldVersion>();
|
|
const versions = computed(() => field.value?.versions || []);
|
const currentVersion = computed(() => versions.value.find((version) => version.id === field.value?.dangqianZiduanBanbenId));
|
const drawerTitle = computed(() => (field.value ? `字段详情 - ${field.value.ziduanBianma}` : '字段详情'));
|
const versionModalTitle = computed(() => {
|
if (!isViewingVersion.value) return '编辑草稿版本';
|
const version = versionModalVersion.value;
|
return version ? `查看字段版本 - V${version.banbenHao}` : '查看字段版本';
|
});
|
const reasonModalTitle = computed(() => (reasonAction.value === 'cancel' ? '作废字段版本' : '切换当前版本'));
|
const reasonPrompt = computed(() => {
|
const version = reasonVersion.value;
|
if (!version) return '';
|
if (reasonAction.value === 'cancel') return `V${version.banbenHao} 作废后不能恢复,也不会复用该版本号。`;
|
if (!currentVersion.value) return `当前未设置生效版本,将切换为 V${version.banbenHao}。`;
|
return `当前版本将从 V${currentVersion.value.banbenHao} 切换为 V${version.banbenHao}。`;
|
});
|
|
const READ_SOURCE_OPTIONS = [
|
{ fullName: '无外部来源', id: 'NONE' },
|
{ fullName: 'LIMS 任务数据', id: 'LIMS_TASK' },
|
{ fullName: '系统数据', id: 'SYSTEM' },
|
];
|
const NONE_READ_SOURCE_OPTIONS = [{ fullName: '无外部来源', id: 'NONE' }];
|
const BATCH_MODE_OPTIONS = [
|
{ fullName: '单记录独立值', id: 'RECORD_PRIVATE' },
|
{ fullName: '同批次共享', id: 'BATCH_SHARED' },
|
];
|
const DATA_TYPE_OPTIONS: { fullName: string; id: FieldDataType }[] = [
|
{ fullName: '单行文本', id: 'TEXT' },
|
{ fullName: '多行文本', id: 'TEXTAREA' },
|
{ fullName: '数值', id: 'DECIMAL' },
|
{ fullName: '日期', id: 'DATE' },
|
{ fullName: '日期时间', id: 'DATETIME' },
|
{ fullName: '下拉选择', id: 'SELECT' },
|
{ fullName: '复选框', id: 'CHECKBOX' },
|
];
|
|
const versionDetails = computed(() => {
|
const version = versionModalVersion.value;
|
if (!version) return [];
|
const source = field.value?.zuoyongFanwei === 'GLOBAL' ? version.binding?.read?.source || 'NONE' : 'NONE';
|
const property = version.binding?.read?.property;
|
const config = controlConfigFormValues(version.kongjianPeizhi).values;
|
const rows: { label: string; show?: boolean; value: boolean | number | string | undefined }[] = [
|
{ label: '字段名称', value: version.ziduanMingcheng },
|
{ label: '数据类型', value: fieldDataTypeLabel(version.shujuLeixing) },
|
{ label: '单位', show: version.shujuLeixing === 'DECIMAL', value: version.danwei },
|
{ label: '是否必填', value: version.shifouBitian === 1 },
|
{ label: '读取来源', value: READ_SOURCE_OPTIONS.find((option) => option.id === source)?.fullName || source },
|
{
|
label: '来源属性',
|
show: source !== 'NONE',
|
value:
|
providerPropertyLabel(
|
providerProperties.value.find((item) => item.property === property),
|
version.binding?.read?.propertyRevision,
|
) || property,
|
},
|
{ label: '批次模式', value: BATCH_MODE_OPTIONS.find((option) => option.id === (source === 'NONE' ? version.piciMoshi : 'RECORD_PRIVATE'))?.fullName },
|
];
|
if (version.shujuLeixing === 'CHECKBOX') {
|
rows.push({ label: '默认勾选', value: config.controlDefaultChecked });
|
if (field.value?.zuoyongFanwei === 'GLOBAL' && source === 'NONE') {
|
const unlimited = Boolean(version.recommendedCheckboxGroupCode?.trim()) && version.recommendedCheckboxMaxSelected == null;
|
rows.push(
|
{ label: '推荐选择组编码', value: version.recommendedCheckboxGroupCode },
|
{ label: '推荐最少选择数', value: version.recommendedCheckboxMinSelected ?? 0 },
|
{ label: '推荐最多不限', value: unlimited },
|
{ label: '推荐最多选择数', show: !unlimited, value: version.recommendedCheckboxMaxSelected ?? 1 },
|
);
|
}
|
} else {
|
rows.push({ label: '占位文本', show: ['DECIMAL', 'SELECT', 'TEXT', 'TEXTAREA'].includes(version.shujuLeixing), value: config.controlPlaceholder });
|
if (version.shujuLeixing === 'SELECT') {
|
const options = dropdownOptionsForSelect(config.controlOptionsText);
|
rows.push(
|
{ label: '下拉选项', value: config.controlOptionsText },
|
{ label: '默认选项', value: options.find((option) => option.value === config.controlDefaultOption)?.display || config.controlDefaultOption },
|
);
|
} else if (['DATE', 'DATETIME'].includes(version.shujuLeixing)) {
|
rows.push({ label: '显示格式', value: config.controlDateFormat }, { label: '默认值', value: config.controlDefaultDate });
|
} else {
|
rows.push({ label: '默认值', value: config.controlDefaultValue });
|
}
|
}
|
return rows
|
.filter((row) => row.show !== false)
|
.map(({ label, value }) => ({ label, value: typeof value === 'boolean' ? (value ? '是' : '否') : value === '' || value == null ? '-' : value }));
|
});
|
|
const columns: BasicColumn[] = [
|
{ dataIndex: 'banbenHao', title: '版本', width: 80, slots: { default: 'banbenHao' } },
|
{ dataIndex: 'ziduanMingcheng', minWidth: 150, title: '字段名称' },
|
{ dataIndex: 'shujuLeixing', title: '数据类型', width: 110, slots: { default: 'shujuLeixing' } },
|
{ dataIndex: 'bizStatus', title: '状态', width: 100, slots: { default: 'bizStatus' } },
|
{ dataIndex: 'current', title: '当前使用', width: 90, slots: { default: 'current' } },
|
{ dataIndex: 'chuangjianShijian', format: 'date|YYYY-MM-DD HH:mm:ss', title: '创建时间', width: 165 },
|
];
|
|
const [registerTable] = useVxeTable({
|
actionColumn: { dataIndex: 'action', fixed: 'right', title: '操作', width: 250 },
|
columns,
|
dataSource: versions,
|
immediate: false,
|
pagination: false,
|
rowKey: 'id',
|
showIndexColumn: false,
|
showTableSetting: false,
|
});
|
|
const versionSchemas: FormSchema[] = [
|
{
|
component: 'Input',
|
componentProps: { maxlength: 200, placeholder: '请输入字段名称' },
|
field: 'ziduanMingcheng',
|
label: '字段名称',
|
rules: [{ message: '请输入字段名称', required: true, trigger: 'blur' }],
|
},
|
{
|
component: 'Select',
|
componentProps: { options: DATA_TYPE_OPTIONS },
|
field: 'shujuLeixing',
|
label: '数据类型',
|
rules: [{ message: '请选择数据类型', required: true, trigger: 'change' }],
|
},
|
{ component: 'Input', componentProps: { maxlength: 50, placeholder: '请输入单位' }, field: 'danwei', label: '单位' },
|
{ component: 'Switch', field: 'shifouBitian', label: '是否必填' },
|
{
|
component: 'Select',
|
componentProps: { onChange: handleReadSourceChange, options: READ_SOURCE_OPTIONS },
|
field: 'readSource',
|
label: '读取来源',
|
},
|
{
|
component: 'Select',
|
componentProps: { options: [], placeholder: '请选择来源属性' },
|
dynamicDisabled: ({ model }) => isViewingVersion.value || model.readSource === 'NONE',
|
dynamicRules: ({ model }) => (['LIMS_TASK', 'SYSTEM'].includes(model.readSource) ? [{ message: '请选择来源属性', required: true, trigger: 'change' }] : []),
|
field: 'providerProperty',
|
label: '来源属性',
|
},
|
{
|
component: 'Select',
|
componentProps: { options: BATCH_MODE_OPTIONS },
|
field: 'piciMoshi',
|
label: '批次模式',
|
rules: [{ message: '请选择批次模式', required: true, trigger: 'change' }],
|
},
|
{
|
component: 'Input',
|
componentProps: { maxlength: 200, placeholder: '例如:请输入供试品名称' },
|
field: 'controlPlaceholder',
|
ifShow: ({ model }) => model.shujuLeixing !== 'CHECKBOX',
|
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: 'Input',
|
componentProps: { maxlength: 50, placeholder: '例如:yyyy-MM-dd' },
|
field: 'controlDateFormat',
|
ifShow: ({ model }) => ['DATE', 'DATETIME'].includes(model.shujuLeixing),
|
label: '显示格式',
|
},
|
{
|
component: 'Input',
|
componentProps: { maxlength: 30, placeholder: '例如:2026-08-27' },
|
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 }) => field.value?.zuoyongFanwei === 'GLOBAL' && model.shujuLeixing === 'CHECKBOX' && model.readSource === 'NONE',
|
label: '推荐选择组编码',
|
rules: [{ pattern: /^[\w-]{1,80}$/, message: '只能输入字母、数字、下划线或连字符', trigger: 'blur' }],
|
},
|
{
|
component: 'InputNumber',
|
componentProps: { min: 0, precision: 0 },
|
defaultValue: 0,
|
field: 'recommendedCheckboxMinSelected',
|
ifShow: ({ model }) =>
|
field.value?.zuoyongFanwei === 'GLOBAL' &&
|
model.shujuLeixing === 'CHECKBOX' &&
|
model.readSource === 'NONE' &&
|
Boolean(model.recommendedCheckboxGroupCode?.trim()),
|
label: '推荐最少选择数',
|
},
|
{
|
component: 'Switch',
|
defaultValue: false,
|
field: 'checkboxGroupUnlimited',
|
ifShow: ({ model }) =>
|
field.value?.zuoyongFanwei === 'GLOBAL' &&
|
model.shujuLeixing === 'CHECKBOX' &&
|
model.readSource === 'NONE' &&
|
Boolean(model.recommendedCheckboxGroupCode?.trim()),
|
label: '推荐最多不限',
|
},
|
{
|
component: 'InputNumber',
|
componentProps: { min: 1, precision: 0 },
|
defaultValue: 1,
|
field: 'recommendedCheckboxMaxSelected',
|
ifShow: ({ model }) =>
|
field.value?.zuoyongFanwei === 'GLOBAL' &&
|
model.shujuLeixing === 'CHECKBOX' &&
|
model.readSource === 'NONE' &&
|
Boolean(model.recommendedCheckboxGroupCode?.trim()) &&
|
!model.checkboxGroupUnlimited,
|
label: '推荐最多选择数',
|
dynamicRules: ({ model }) => [
|
{
|
message: '推荐最多选择数不能小于最少选择数',
|
trigger: 'change',
|
validator: (_rule, value) =>
|
Number(value) >= Number(model.recommendedCheckboxMinSelected ?? 0)
|
? Promise.resolve()
|
: Promise.reject(new Error('推荐最多选择数不能小于最少选择数')),
|
},
|
],
|
},
|
];
|
|
const [registerVersionForm, { resetFields, setFieldsValue, updateSchema, validate }] = useForm({
|
baseColProps: { span: 24 },
|
labelWidth: 90,
|
schemas: versionSchemas,
|
showActionButtonGroup: false,
|
});
|
const [registerVersionModal, { closeModal: closeVersionModal, openModal: openVersionModal }] = useModal();
|
const [registerReasonModal, { closeModal: closeReasonModal, openModal: openReasonModal }] = useModal();
|
const [registerDrawer, { changeLoading }] = useDrawerInner(init);
|
|
async function init(data: { fieldId: string }) {
|
providerRequestSequence++;
|
field.value = undefined;
|
changeLoading(true);
|
try {
|
const response = await getFieldDetail(data.fieldId);
|
field.value = response.data;
|
} finally {
|
changeLoading(false);
|
}
|
}
|
|
function getTableActions(version: FieldVersion): ActionItem[] {
|
const actions = versionActions({
|
current: version.id === field.value?.dangqianZiduanBanbenId,
|
enabled: field.value?.bizEnabled === 'enabled',
|
status: version.bizStatus,
|
});
|
const actionItems = actions.map((action): ActionItem => {
|
if (action === 'edit') return { label: '编辑', onClick: () => openVersion(version, true) };
|
if (action === 'preRelease') {
|
return {
|
label: '提交预发布',
|
modelConfirm: { content: `确认将 V${version.banbenHao} 提交为预发布版本?`, onOk: () => preRelease(version) },
|
};
|
}
|
if (action === 'publish') {
|
return {
|
color: 'success',
|
label: '发布',
|
modelConfirm: { content: `确认发布 V${version.banbenHao}?发布后该版本不可修改。`, onOk: () => publish(version) },
|
};
|
}
|
if (action === 'cancel') {
|
return { color: 'error', label: '作废', onClick: () => openReason(version, 'cancel') };
|
}
|
if (action === 'setCurrent') {
|
return { label: '设为当前', onClick: () => openReason(version, 'setCurrent') };
|
}
|
return {
|
label: '复制新版本',
|
modelConfirm: { content: `基于 V${version.banbenHao} 创建新的草稿版本?`, onOk: () => copyVersion(version) },
|
};
|
});
|
if (version.bizStatus === 'PUBLISHED') {
|
actionItems.unshift({ label: '查看', onClick: () => openVersion(version, false) });
|
}
|
return actionItems;
|
}
|
|
async function openVersion(version: FieldVersion, editable: boolean) {
|
const requestSequence = ++providerRequestSequence;
|
editingVersionId.value = editable ? version.id : '';
|
isViewingVersion.value = !editable;
|
versionModalVersion.value = version;
|
const controlConfig = controlConfigFormValues(version.kongjianPeizhi);
|
editingControlConfigExtras.value = controlConfig.extras;
|
providerProperties.value = [];
|
await resetFields();
|
const readSourceOptions = field.value?.zuoyongFanwei === 'GLOBAL' ? READ_SOURCE_OPTIONS : NONE_READ_SOURCE_OPTIONS;
|
await updateSchema({ componentProps: { onChange: handleReadSourceChange, options: readSourceOptions }, field: 'readSource' });
|
let source = version.binding?.read?.source || 'NONE';
|
if (field.value?.zuoyongFanwei !== 'GLOBAL') source = 'NONE';
|
selectedReadSource.value = source;
|
if (source !== 'NONE') {
|
const response = await getProviderProperties(source);
|
if (requestSequence !== providerRequestSequence) return;
|
providerProperties.value = response.data || [];
|
const fixedProperty = version.binding?.read?.property;
|
const fixedRevision = version.binding?.read?.propertyRevision;
|
if (
|
source === 'LIMS_TASK' &&
|
fixedProperty &&
|
fixedRevision != null &&
|
!providerProperties.value.some((item) => item.property === fixedProperty && item.propertyRevision === fixedRevision)
|
) {
|
try {
|
const fixedResponse = await getSourceReference(fixedProperty, fixedRevision);
|
if (requestSequence !== providerRequestSequence) return;
|
providerProperties.value = [
|
{
|
dataType: fixedResponse.data.dataType as FieldDataType,
|
enabled: fixedResponse.data.enabled,
|
label: fixedResponse.data.displayName,
|
property: fixedResponse.data.propertyCode,
|
propertyRevision: fixedResponse.data.revision,
|
readOnly: true,
|
source: 'LIMS_TASK',
|
},
|
...providerProperties.value.filter((item) => item.property !== fixedProperty),
|
];
|
} catch {
|
if (requestSequence !== providerRequestSequence) return;
|
providerProperties.value = [
|
{
|
dataType: version.shujuLeixing,
|
enabled: false,
|
label: `${fixedProperty}(固定版本不可用)`,
|
property: fixedProperty,
|
propertyRevision: fixedRevision,
|
readOnly: true,
|
source: 'LIMS_TASK',
|
},
|
...providerProperties.value.filter((item) => item.property !== fixedProperty),
|
];
|
}
|
}
|
}
|
await updateProviderSchema(source, providerProperties.value, requestSequence);
|
if (requestSequence !== providerRequestSequence) return;
|
await setFieldsValue({
|
checkboxGroupUnlimited: version.recommendedCheckboxGroupCode != null && version.recommendedCheckboxMaxSelected == null,
|
danwei: version.danwei,
|
...controlConfig.values,
|
piciMoshi: source === 'NONE' ? version.piciMoshi : 'RECORD_PRIVATE',
|
providerProperty: source === 'NONE' ? undefined : version.binding.read.property,
|
readSource: source,
|
recommendedCheckboxGroupCode: version.recommendedCheckboxGroupCode || '',
|
recommendedCheckboxMaxSelected: version.recommendedCheckboxMaxSelected ?? 1,
|
recommendedCheckboxMinSelected: version.recommendedCheckboxMinSelected ?? 0,
|
shifouBitian: version.shifouBitian === 1,
|
shujuLeixing: version.shujuLeixing,
|
ziduanMingcheng: version.ziduanMingcheng,
|
});
|
openVersionModal(true);
|
}
|
|
async function handleReadSourceChange(source: FieldReadSource) {
|
const requestSequence = ++providerRequestSequence;
|
selectedReadSource.value = source;
|
providerProperties.value = [];
|
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',
|
});
|
}
|
|
function openLimsSources() {
|
onlineUtils.route('/eln/sources', { title: 'LIMS 数据项' });
|
}
|
|
async function saveDraft() {
|
const values = await validate();
|
if (!values || !editingVersionId.value) return;
|
const source = values.readSource as FieldReadSource;
|
const selectedProperty = source === 'NONE' ? undefined : providerProperties.value.find((item) => item.property === values.providerProperty);
|
const payload: FieldVersionRule = {
|
binding: {
|
read: {
|
...(source === 'NONE'
|
? {}
|
: {
|
property: values.providerProperty,
|
...(selectedProperty?.propertyRevision == null ? {} : { propertyRevision: selectedProperty.propertyRevision }),
|
}),
|
source,
|
},
|
write: { target: 'ELN_RECORD' },
|
},
|
danwei: trimOrUndefined(values.danwei),
|
kongjianPeizhi: buildControlConfig(values.shujuLeixing as FieldDataType, values, editingControlConfigExtras.value),
|
...selectionGroupRecommendation(values),
|
piciMoshi: values.piciMoshi as FieldBatchMode,
|
shifouBitian: values.shifouBitian ? 1 : 0,
|
shujuLeixing: values.shujuLeixing as FieldDataType,
|
ziduanMingcheng: values.ziduanMingcheng.trim(),
|
};
|
await updateFieldVersion(editingVersionId.value, payload);
|
createMessage.success('草稿版本已保存');
|
closeVersionModal();
|
await reloadField();
|
}
|
|
function selectionGroupRecommendation(values: Record<string, any>) {
|
const code = values.recommendedCheckboxGroupCode?.trim();
|
if (field.value?.zuoyongFanwei !== 'GLOBAL' || values.shujuLeixing !== 'CHECKBOX' || values.readSource !== 'NONE' || !code) return {};
|
return {
|
recommendedCheckboxGroupCode: code,
|
recommendedCheckboxMaxSelected: values.checkboxGroupUnlimited ? null : Number(values.recommendedCheckboxMaxSelected ?? 1),
|
recommendedCheckboxMinSelected: Number(values.recommendedCheckboxMinSelected ?? 0),
|
};
|
}
|
|
async function preRelease(version: FieldVersion) {
|
await preReleaseFieldVersion(version.id);
|
createMessage.success(`V${version.banbenHao} 已提交预发布`);
|
await reloadField();
|
}
|
|
async function publish(version: FieldVersion) {
|
await publishFieldVersion(version.id);
|
createMessage.success(`V${version.banbenHao} 已发布`);
|
await reloadField();
|
}
|
|
async function copyVersion(version: FieldVersion) {
|
if (!field.value) return;
|
await createFieldVersion(field.value.id, { sourceVersionId: version.id });
|
createMessage.success('新的草稿版本已创建');
|
await reloadField();
|
}
|
|
function openReason(version: FieldVersion, action: ReasonAction) {
|
reason.value = '';
|
reasonVersion.value = version;
|
reasonAction.value = action;
|
openReasonModal(true);
|
}
|
|
async function confirmReasonAction() {
|
const version = reasonVersion.value;
|
const actualReason = reason.value.trim();
|
if (!version || !reasonAction.value || !actualReason) {
|
createMessage.warning('请输入操作原因');
|
return;
|
}
|
if (reasonAction.value === 'cancel') {
|
await cancelFieldVersion(version.id, actualReason);
|
createMessage.success(`V${version.banbenHao} 已作废`);
|
} else if (field.value) {
|
await switchCurrentFieldVersion(field.value.id, { reason: actualReason, versionId: version.id });
|
createMessage.success(`当前版本已切换为 V${version.banbenHao}`);
|
}
|
closeReasonModal();
|
await reloadField();
|
}
|
|
async function reloadField() {
|
if (!field.value) return;
|
const response = await getFieldDetail(field.value.id);
|
field.value = response.data;
|
emit('reload');
|
}
|
|
function trimOrUndefined(value?: string) {
|
const trimmed = value?.trim();
|
return trimmed || undefined;
|
}
|
|
function versionStatusLabel(status: FieldVersion['bizStatus']) {
|
return { CANCELLED: '已作废', DRAFT: '草稿', PRE_RELEASE: '预发布', PUBLISHED: '已发布' }[status];
|
}
|
|
function versionStatusColor(status: FieldVersion['bizStatus']) {
|
return { CANCELLED: 'default', DRAFT: 'processing', PRE_RELEASE: 'warning', PUBLISHED: 'success' }[status];
|
}
|
|
function fieldDataTypeLabel(dataType: FieldDataType) {
|
return DATA_TYPE_OPTIONS.find((option) => option.id === dataType)?.fullName || dataType;
|
}
|
|
function providerPropertyLabel(property?: ProviderProperty, fallbackRevision?: number) {
|
if (!property) return '';
|
const revision = property.propertyRevision ?? fallbackRevision;
|
if (revision == null) return property.label;
|
return `${property.label}(版本 ${revision}${property.enabled ? '' : ',已停用'})`;
|
}
|
</script>
|
|
<template>
|
<BasicDrawer v-bind="$attrs" @register="registerDrawer" class="full-drawer" keyboard :show-footer="false" :title="drawerTitle" width="920px">
|
<div v-if="field" class="field-version-content">
|
<div class="field-summary">
|
<a-descriptions :column="2" size="small">
|
<a-descriptions-item label="字段编码">{{ field.ziduanBianma }}</a-descriptions-item>
|
<a-descriptions-item label="作用域">{{ field.zuoyongFanwei === 'GLOBAL' ? '全局' : '模板私有' }}</a-descriptions-item>
|
<a-descriptions-item label="归属模板">{{ field.mubanBianhao || '-' }}</a-descriptions-item>
|
<a-descriptions-item label="备注">{{ field.beizhu || '-' }}</a-descriptions-item>
|
<a-descriptions-item label="当前版本">{{ currentVersion ? `V${currentVersion.banbenHao}` : '未设置' }}</a-descriptions-item>
|
<a-descriptions-item label="启用状态">{{ field.bizEnabled === 'enabled' ? '启用' : '停用' }}</a-descriptions-item>
|
</a-descriptions>
|
</div>
|
<div class="version-table">
|
<BasicVxeTable @register="registerTable" :data-source="versions">
|
<template #banbenHao="{ record }">V{{ record.banbenHao }}</template>
|
<template #shujuLeixing="{ record }">{{ fieldDataTypeLabel(record.shujuLeixing) }}</template>
|
<template #bizStatus="{ record }">
|
<a-tag :color="versionStatusColor(record.bizStatus)">{{ versionStatusLabel(record.bizStatus) }}</a-tag>
|
</template>
|
<template #current="{ record }">
|
<a-tag v-if="record.id === field.dangqianZiduanBanbenId" color="blue">当前</a-tag>
|
<span v-else>-</span>
|
</template>
|
<template #action="{ record }">
|
<TableAction :actions="getTableActions(record)" />
|
</template>
|
</BasicVxeTable>
|
</div>
|
</div>
|
|
<BasicModal
|
@register="registerVersionModal"
|
force-render
|
:cancel-text="isViewingVersion ? '关闭' : '取消'"
|
:show-ok-btn="!isViewingVersion"
|
:title="versionModalTitle"
|
width="640px"
|
@ok="saveDraft">
|
<dl v-if="isViewingVersion" class="version-details">
|
<div v-for="item in versionDetails" :key="item.label" class="version-detail-row">
|
<dt>{{ item.label }}</dt>
|
<dd>{{ item.value }}</dd>
|
</div>
|
</dl>
|
<BasicForm v-show="!isViewingVersion" @register="registerVersionForm" :disabled="isViewingVersion" />
|
<div v-if="!isViewingVersion && 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>
|
</BasicModal>
|
<BasicModal @register="registerReasonModal" :title="reasonModalTitle" destroy-on-close @ok="confirmReasonAction">
|
<a-alert :message="reasonPrompt" show-icon type="warning" />
|
<a-form-item class="reason-input" label="操作原因" required>
|
<a-textarea v-model:value="reason" :maxlength="500" :rows="4" placeholder="请输入操作原因" show-count />
|
</a-form-item>
|
</BasicModal>
|
</BasicDrawer>
|
</template>
|
|
<style lang="scss" scoped>
|
.field-version-content {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
min-width: 0;
|
}
|
|
.field-summary {
|
flex: none;
|
padding: 16px 24px;
|
border-bottom: 1px solid var(--border-color-base);
|
}
|
|
.version-table {
|
flex: 1;
|
min-height: 240px;
|
padding: 12px 24px 0;
|
}
|
|
.reason-input {
|
margin-top: 16px;
|
}
|
|
.version-details {
|
margin: 0;
|
padding: 8px 12px;
|
}
|
|
.source-maintenance {
|
padding: 0 24px 16px 114px;
|
}
|
|
@media (max-width: 720px) {
|
.source-maintenance {
|
padding-right: 0;
|
padding-left: 0;
|
}
|
}
|
|
.version-detail-row {
|
display: grid;
|
grid-template-columns: minmax(0, 126px) minmax(0, 1fr);
|
gap: 16px;
|
padding: 10px 0;
|
line-height: 22px;
|
overflow-wrap: anywhere;
|
|
dt {
|
color: var(--text-color-secondary);
|
text-align: right;
|
}
|
|
dd {
|
margin: 0;
|
color: var(--text-color-base);
|
white-space: pre-wrap;
|
}
|
}
|
</style>
|