刘光辉
4 小时以前 cf11e63b659afcc9792ab3e395f1b45d844431a8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
<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>