<script lang="ts" setup>
|
import type { SourceBusinessObject, SourceCandidateField, SourcePreviewSample } from '#/api/x/eln/sources';
|
|
import { computed, reactive, ref } from 'vue';
|
|
import { BasicDrawer, useDrawerInner } from '@jnpf/ui/drawer';
|
|
import { suggestFieldCode } from '#/api/x/eln/fields';
|
import { createSourcePath, getSourceObjectFields, getSourceObjects, getSourcePreviewSamples } from '#/api/x/eln/sources';
|
|
defineOptions({ name: 'ElnSourcePathDrawer' });
|
|
const emit = defineEmits(['register', 'success']);
|
const form = reactive({
|
displayName: '',
|
fromFieldId: '',
|
outputFieldId: '',
|
rootObjectId: '',
|
sampleId: '',
|
targetFieldId: '',
|
targetObjectId: '',
|
});
|
const errorMessage = ref('');
|
const fieldsLoading = ref(false);
|
const sampleLoadMessage = ref('');
|
const samples = ref<SourcePreviewSample[]>([]);
|
const rootFields = ref<SourceCandidateField[]>([]);
|
const targetFields = ref<SourceCandidateField[]>([]);
|
const businessObjects = ref<SourceBusinessObject[]>([]);
|
const objectFields = new Map<string, SourceCandidateField[]>();
|
let businessObjectsRequest: Promise<void> | undefined;
|
let businessObjectsLoaded = false;
|
let samplesRequest: Promise<void> | undefined;
|
let samplesLoaded = false;
|
const TYPE_LABELS: Record<string, string> = { BOOLEAN: '布尔值', DATE: '日期', DATETIME: '日期时间', DECIMAL: '数值', TEXT: '文本' };
|
|
const targetObjectOptions = computed(() => businessObjects.value.map((item) => ({ label: item.displayName, value: item.id })));
|
const rootFieldOptions = computed(() => rootFields.value.map((field) => fieldOption(field)));
|
const targetFieldOptions = computed(() => targetFields.value.map((field) => fieldOption(field)));
|
const sampleOptions = computed(() => samples.value.map((item) => ({ label: sampleLabel(item), value: item.id })));
|
|
const [registerDrawer, { changeLoading, changeOkLoading, closeDrawer }] = useDrawerInner(init);
|
|
async function init() {
|
errorMessage.value = '';
|
sampleLoadMessage.value = '';
|
Object.assign(form, { displayName: '', fromFieldId: '', outputFieldId: '', rootObjectId: '', sampleId: '', targetFieldId: '', targetObjectId: '' });
|
rootFields.value = [];
|
targetFields.value = [];
|
changeLoading(true);
|
try {
|
await Promise.all([ensureBusinessObjects(), ensurePreviewSamples()]);
|
const root = businessObjects.value.find((item) => item.rootAnchor);
|
if (!root) return fail('没有可用的检验项来源对象');
|
form.rootObjectId = root.id;
|
form.targetObjectId = root.id;
|
rootFields.value = await loadObjectFields(root.id);
|
targetFields.value = rootFields.value;
|
} catch (error) {
|
fail(sourceErrorMessage(error, '业务路径元数据加载失败,请重试'));
|
} finally {
|
changeLoading(false);
|
}
|
}
|
|
async function ensureBusinessObjects() {
|
if (businessObjectsLoaded) return;
|
if (businessObjectsRequest) return businessObjectsRequest;
|
businessObjectsRequest = getSourceObjects()
|
.then((response) => {
|
businessObjects.value = response.data || [];
|
businessObjectsLoaded = true;
|
})
|
.finally(() => {
|
businessObjectsRequest = undefined;
|
});
|
return businessObjectsRequest;
|
}
|
|
async function ensurePreviewSamples() {
|
if (samplesLoaded) return;
|
if (samplesRequest) return samplesRequest;
|
samplesRequest = getSourcePreviewSamples()
|
.then((response) => {
|
samples.value = response.data || [];
|
samplesLoaded = true;
|
})
|
.catch(() => {
|
samples.value = [];
|
sampleLoadMessage.value = '预览检验项暂不可用,请稍后重试';
|
})
|
.finally(() => {
|
samplesRequest = undefined;
|
});
|
return samplesRequest;
|
}
|
|
async function loadObjectFields(objectId: string) {
|
const cached = objectFields.get(objectId);
|
if (cached) return cached;
|
const response = await getSourceObjectFields(objectId);
|
const fields = response.data || [];
|
objectFields.set(objectId, fields);
|
return fields;
|
}
|
|
async function changeTargetObject(objectId: string) {
|
Object.assign(form, { targetObjectId: objectId, fromFieldId: '', targetFieldId: '', outputFieldId: '' });
|
fieldsLoading.value = true;
|
errorMessage.value = '';
|
try {
|
targetFields.value = objectId === form.rootObjectId ? rootFields.value : await loadObjectFields(objectId);
|
} catch (error) {
|
targetFields.value = [];
|
fail(sourceErrorMessage(error, '目标业务对象字段加载失败,请重试'));
|
} finally {
|
fieldsLoading.value = false;
|
}
|
}
|
|
async function createPath() {
|
if (!form.displayName.trim() || !form.outputFieldId || !form.sampleId) return fail('请填写路径名称、可用内容并选择预览检验项');
|
const related = form.targetObjectId !== form.rootObjectId;
|
if (related && (!form.fromFieldId || !form.targetFieldId)) return fail('请选择业务关联依据');
|
changeOkLoading(true);
|
errorMessage.value = '';
|
try {
|
const outputName = targetFields.value.find((item) => item.id === form.outputFieldId)?.displayName || 'value';
|
const [pathCode, outputCode] = await Promise.all([suggestFieldCode(form.displayName.trim()), suggestFieldCode(outputName)]);
|
await createSourcePath({
|
code: pathCode.data.code,
|
displayName: form.displayName.trim(),
|
...(related ? { fromFieldId: form.fromFieldId, targetFieldId: form.targetFieldId } : {}),
|
outputFieldId: form.outputFieldId,
|
outputKey: outputCode.data.code,
|
rootObjectId: form.rootObjectId,
|
sampleId: form.sampleId,
|
targetObjectId: form.targetObjectId,
|
});
|
emit('success');
|
closeDrawer();
|
} catch (error) {
|
fail(sourceErrorMessage(error, '来源路径保存失败,请检查关联和预览数据'));
|
} finally {
|
changeOkLoading(false);
|
}
|
}
|
|
function fail(message: string) {
|
errorMessage.value = message;
|
}
|
|
function fieldOption(field: SourceCandidateField) {
|
return { label: `${field.displayName} · ${typeLabel(field.dataType)}`, value: field.id };
|
}
|
|
function sampleLabel(sample: SourcePreviewSample) {
|
return [sample.testNo, sample.sampleName, sample.batchNo, sample.testName].filter(Boolean).join(' · ') || sample.id;
|
}
|
|
function typeLabel(type: string) {
|
return TYPE_LABELS[type] || type;
|
}
|
|
function sourceErrorMessage(error: unknown, fallback: string) {
|
const response = (error as { response?: { data?: { data?: { errorCode?: string }; msg?: string } } })?.response?.data;
|
const code = response?.data?.errorCode;
|
if (code === 'SOURCE_CONFLICT') return '配置已被其他人修改,请刷新后重试';
|
if (code === 'SOURCE_FORBIDDEN') return '没有配置或预览数据访问权限';
|
if (code === 'SOURCE_INVALID') return '来源配置校验失败,请检查数据来源和预览样例';
|
if (code === 'SOURCE_LIMIT') return '来源查询超过限制,请缩小范围后重试';
|
if (code === 'SOURCE_UNAVAILABLE') return 'LIMS 数据暂不可用,请稍后重试';
|
return response?.msg || (error instanceof Error && error.message) || fallback;
|
}
|
</script>
|
|
<template>
|
<BasicDrawer v-bind="$attrs" @register="registerDrawer" @ok="createPath" destroy-on-close ok-text="校验并发布" show-footer title="新增来源路径" width="640px">
|
<div class="editor-body">
|
<a-alert v-if="errorMessage" :message="errorMessage" show-icon type="warning" />
|
<section class="form-section">
|
<h3>路径信息</h3>
|
<label
|
><span>路径名称</span><a-input v-model:value="form.displayName" data-test="path-display-name" placeholder="请输入路径名称,例如:请验单生产日期"
|
/></label>
|
<label
|
><span>到达对象</span
|
><a-select
|
:loading="fieldsLoading"
|
:options="targetObjectOptions"
|
:value="form.targetObjectId || undefined"
|
data-test="path-target-object"
|
placeholder="请选择到达对象"
|
@change="changeTargetObject"
|
/></label>
|
</section>
|
<section v-if="form.targetObjectId !== form.rootObjectId" class="form-section">
|
<h3>业务关联</h3>
|
<div class="relation-row">
|
<a-select
|
:options="rootFieldOptions"
|
:value="form.fromFieldId || undefined"
|
placeholder="请选择检验项中的关联字段"
|
@change="form.fromFieldId = $event" /><span>对应</span
|
><a-select
|
:loading="fieldsLoading"
|
:options="targetFieldOptions"
|
:value="form.targetFieldId || undefined"
|
placeholder="请选择目标对象中的关联字段"
|
@change="form.targetFieldId = $event" />
|
</div>
|
</section>
|
<section class="form-section">
|
<h3>可用内容</h3>
|
<a-select
|
:loading="fieldsLoading"
|
:options="targetFieldOptions"
|
:value="form.outputFieldId || undefined"
|
class="full-width-select"
|
data-test="path-output-field"
|
placeholder="请选择可返回的业务内容"
|
show-search
|
@change="form.outputFieldId = $event" />
|
</section>
|
<section class="form-section">
|
<h3>发布校验</h3>
|
<a-alert v-if="sampleLoadMessage" :message="sampleLoadMessage" show-icon type="warning" />
|
<a-select
|
:options="sampleOptions"
|
:value="form.sampleId || undefined"
|
class="full-width-select"
|
data-test="path-sample"
|
placeholder="请选择用于校验发布的检验项"
|
show-search
|
@change="form.sampleId = $event" />
|
</section>
|
</div>
|
</BasicDrawer>
|
</template>
|
|
<style lang="scss" scoped>
|
.editor-body {
|
padding: 0 20px 24px;
|
}
|
.form-section {
|
padding: 20px 0;
|
border-bottom: 1px solid #eef0f3;
|
}
|
.form-section:last-child {
|
border-bottom: 0;
|
}
|
.form-section h3 {
|
margin: 0 0 16px;
|
color: #1f2937;
|
font-size: 15px;
|
font-weight: 600;
|
letter-spacing: 0;
|
}
|
.form-section label {
|
display: grid;
|
grid-template-columns: 92px minmax(0, 1fr);
|
align-items: center;
|
gap: 12px;
|
margin-bottom: 14px;
|
}
|
.form-section label:last-child {
|
margin-bottom: 0;
|
}
|
.form-section label > span {
|
color: #4b5563;
|
}
|
.relation-row {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
.relation-row :deep(.ant-select) {
|
min-width: 0;
|
flex: 1;
|
}
|
.relation-row > span {
|
flex: none;
|
color: #6b7280;
|
}
|
:deep(.full-width-select) {
|
width: 100%;
|
}
|
</style>
|