فهرست منبع

feat(@six/smart-pharmacy): 智慧药事系统第二版-岗位管理对接修改

cmj 2 هفته پیش
والد
کامیت
d39a318911

+ 135 - 17
apps/smart-pharmacy/src/api/method/personnel-qualification.ts

@@ -13,6 +13,7 @@ import {
   syncEmployeePostDict,
   toEmployeePayload,
   toEmployeeQuery,
+  toEmployeeRecordId,
 } from '#/api/model/personnel-qualification';
 import {
   listDictByCodeMethod,
@@ -48,6 +49,12 @@ export namespace PersonnelQualificationModel {
     organizationType?: OrganizationType;
     enterpriseId?: string;
     enterpriseName: string;
+    hospitalCode?: string;
+    hospitalName?: string;
+    yardCode?: string;
+    yardName?: string;
+    medCenterId?: string;
+    medCenterName?: string;
     decoctionCenterId?: string;
     decoctionCenterName: string;
     name: string;
@@ -63,6 +70,12 @@ export namespace PersonnelQualificationModel {
     id?: string;
     decoctionCenterId: string;
     decoctionCenterName?: string;
+    hospitalCode?: string;
+    hospitalName?: string;
+    yardCode?: string;
+    yardName?: string;
+    medCenterId?: string;
+    medCenterName?: string;
     name: string;
     positions: string[];
     employeeNo: string;
@@ -88,7 +101,7 @@ export namespace PersonnelQualificationModel {
 
 export let POSITION_OPTIONS: Array<{ label: string; value: string }> = [];
 
-export const CERTIFICATE_NAME_OPTIONS = [
+export let CERTIFICATE_NAME_OPTIONS: Array<{ label: string; value: string }> = [
   { label: '健康证', value: '健康证' },
   { label: '中药调剂员', value: '中药调剂员' },
   { label: '中药保管员', value: '中药保管员' },
@@ -119,7 +132,7 @@ export const QUALIFICATION_STATUS_LABELS: Record<
 };
 
 const EMPLOYEE_BASE = '/manager/tcmp-pc/employee';
-const EMPLOYEE_UPLOAD = '/manager/tcmp-pc/external/employeeInfoUpload';
+const EMPLOYEE_UPLOAD = '/manager/tcmp-pc/common/upload';
 const EMPLOYEE_CERT_DICT_CODES = [
   'certificate_name',
   'employee_cert',
@@ -143,7 +156,22 @@ function syncPostTypeOptions(
     POSITION_OPTIONS.length,
     ...items.map((item) => ({
       label: item.dictName,
-      value: item.dictName,
+      value: String(item.dictValue),
+    })),
+  );
+}
+
+function syncCertificateNameOptions(
+  items: Array<{ dictName: string; dictValue: number | string }>,
+) {
+  if (!items.length) return;
+  syncEmployeeCertDict(items);
+  CERTIFICATE_NAME_OPTIONS.splice(
+    0,
+    CERTIFICATE_NAME_OPTIONS.length,
+    ...items.map((item) => ({
+      label: item.dictName,
+      value: String(item.dictValue),
     })),
   );
 }
@@ -161,13 +189,13 @@ async function ensurePostDictLoaded() {
   try {
     const items = await listCertificateNameDictMethod();
     if (items.length) {
-      syncEmployeeCertDict(items);
+      syncCertificateNameOptions(items);
     } else {
       for (const dictCode of EMPLOYEE_CERT_DICT_CODES.slice(1)) {
         try {
           const fallbackItems = await listDictByCodeMethod(dictCode);
           if (fallbackItems.length) {
-            syncEmployeeCertDict(fallbackItems);
+            syncCertificateNameOptions(fallbackItems);
             break;
           }
         } catch {
@@ -188,12 +216,87 @@ export async function optionsPostTypeMethod() {
   return [...POSITION_OPTIONS];
 }
 
+/** 证书名称下拉选项(certificate_name) */
+export async function optionsCertificateNameMethod() {
+  await ensurePostDictLoaded();
+  return [...CERTIFICATE_NAME_OPTIONS];
+}
+
 function toSelectOptions(
   items: Array<{ label: string; value: string }>,
 ): Array<{ label: string; value: string }> {
   return items.filter((item) => item.label && item.value);
 }
 
+export interface PersonnelDecoctionCenterOption {
+  label: string;
+  value: string;
+  decoctionCenterCode: string;
+  decoctionCenterName: string;
+  hospitalCode?: string;
+  hospitalName?: string;
+  yardCode?: string;
+  yardName?: string;
+  medCenterId?: string;
+  medCenterName?: string;
+}
+
+function buildDecoctionCenterOption(
+  item: import('#/api/method/system').SystemModel.Tisane,
+  enterpriseMap: Map<string, import('#/api/method/system').SystemModel.Enterprise>,
+  institutionMap: Map<string, import('#/api/method/system').SystemModel.Organization>,
+): PersonnelDecoctionCenterOption {
+  const decoctionCenterCode = item.code || item.id;
+  const decoctionCenterName = item.name;
+
+  let hospitalCode = item.hospitalCode;
+  let hospitalName = item.hospitalName;
+  let yardCode = item.yardCode;
+  let yardName = item.yardName;
+  let medCenterId = item.medCenterId;
+  let medCenterName = item.medCenterName;
+
+  const institution = item.institutionId
+    ? institutionMap.get(item.institutionId)
+    : undefined;
+  const enterprise = item.enterpriseId
+    ? enterpriseMap.get(item.enterpriseId)
+    : undefined;
+
+  if (institution) {
+    hospitalCode = hospitalCode || institution.code || institution.id;
+    hospitalName = hospitalName || institution.name;
+  } else if (!hospitalCode && item.institutionId) {
+    hospitalCode = item.institutionId;
+  }
+  if (!hospitalName && item.relatedOrganizations) {
+    hospitalName = item.relatedOrganizations;
+  }
+
+  if (enterprise) {
+    medCenterId = medCenterId || enterprise.code || enterprise.id;
+    medCenterName = medCenterName || enterprise.name;
+  } else if (!medCenterId && item.enterpriseId) {
+    medCenterId = item.enterpriseId;
+  }
+  if (!medCenterName && item.relatedEnterprise) {
+    medCenterName = item.relatedEnterprise;
+  }
+
+  return {
+    label: decoctionCenterName,
+    value: decoctionCenterCode,
+    decoctionCenterCode,
+    decoctionCenterName,
+    hospitalCode: hospitalCode || undefined,
+    hospitalName: hospitalName || undefined,
+    yardCode: yardCode || undefined,
+    yardName: yardName || undefined,
+    medCenterId: medCenterId || undefined,
+    medCenterName: medCenterName || undefined,
+  };
+}
+
 /** 岗位人员资质列表 */
 export async function listPersonnelQualificationsMethod(
   page = 1,
@@ -238,7 +341,7 @@ export async function getPersonnelQualificationMethod(id: string) {
   return http.get<PersonnelQualificationModel.Personnel, TransformData>(
     `${EMPLOYEE_BASE}/detailById`,
     {
-      params: { id },
+      params: { id: toEmployeeRecordId(id) },
       cacheFor: 0,
       transform(data) {
         return fromEmployee(data);
@@ -309,9 +412,11 @@ export async function editPersonnelQualificationMethod(
   return fromEmployee(result ?? payload);
 }
 
-/** 删除岗位人员资质(接口文档未提供,保留占位) */
-export function deletePersonnelQualificationMethod(_id: string) {
-  return Promise.reject(new Error('后端暂未提供删除接口'));
+/** 删除岗位人员资质 */
+export function deletePersonnelQualificationMethod(id: string) {
+  return http.delete(`${EMPLOYEE_BASE}/deleteById?id=${toEmployeeRecordId(id)}`, {
+    cacheFor: 0,
+  });
 }
 
 /** 煎药企业选项 */
@@ -342,8 +447,12 @@ export async function optionsPersonnelMedicalInstitutionMethod() {
 export async function optionsPersonnelDecoctionCenterMethod(
   organizationId?: string,
   organizationType: PersonnelQualificationModel.OrganizationType = 'enterprise',
-) {
-  const { listMedicineCentersMethod } = await import('#/api/method/system');
+): Promise<PersonnelDecoctionCenterOption[]> {
+  const {
+    listDecoctionCentersAllMethod,
+    listMedicalInstitutionsMethod,
+    listMedicineCentersAllMethod,
+  } = await import('#/api/method/system');
   const query: TransformData = {};
   if (organizationId) {
     if (organizationType === 'medicalInstitution') {
@@ -353,11 +462,20 @@ export async function optionsPersonnelDecoctionCenterMethod(
     }
   }
 
-  const result = await listMedicineCentersMethod(1, 500, query);
-  return toSelectOptions(
-    (result.items ?? []).map((item) => ({
-      label: item.name,
-      value: item.code || item.id,
-    })),
+  const [centers, enterprises, institutions] = await Promise.all([
+    listMedicineCentersAllMethod(query),
+    listDecoctionCentersAllMethod(),
+    listMedicalInstitutionsMethod(),
+  ]);
+
+  const enterpriseMap = new Map(
+    enterprises.map((item) => [item.id, item] as const),
   );
+  const institutionMap = new Map(
+    institutions.map((item) => [item.id, item] as const),
+  );
+
+  return centers
+    .map((item) => buildDecoctionCenterOption(item, enterpriseMap, institutionMap))
+    .filter((item) => item.label && item.value);
 }

+ 22 - 0
apps/smart-pharmacy/src/api/method/system.ts

@@ -112,6 +112,12 @@ export namespace SystemModel {
     relatedOrganizations?: string;
     enterpriseId?: string;
     relatedEnterprise?: string;
+    hospitalCode?: string;
+    hospitalName?: string;
+    yardCode?: string;
+    yardName?: string;
+    medCenterId?: string;
+    medCenterName?: string;
     remark?: string;
   }
 
@@ -501,6 +507,22 @@ export function listEnterprisesMethod(
   );
 }
 
+/** 获取机构煎药中心全量列表(用于岗位人员资质筛选等) */
+export function listMedicineCentersAllMethod(
+  query?: Partial<SystemModel.Tisane>,
+) {
+  return http.get<SystemModel.Tisane[], TransformData[]>(
+    `/manager/tcmp-pc/medicine/allList`,
+    {
+      params: toTisane(query),
+      cacheFor: 60_000,
+      transform(data) {
+        return normalizeEnterpriseList(data).map((item) => fromTisane(item));
+      },
+    },
+  );
+}
+
 /** 获取煎药中心分页列表 */
 export function listMedicineCentersMethod(
   page = 1,

+ 124 - 37
apps/smart-pharmacy/src/api/model/personnel-qualification.ts

@@ -28,6 +28,7 @@ const QUALIFICATION_STATUS_TO_BACK: Record<
 const postLabelByCode = new Map<string, string>();
 const postCodeByLabel = new Map<string, string>();
 const certLabelByCode = new Map<string, string>();
+const certCodeByLabel = new Map<string, string>();
 
 export function syncEmployeePostDict(
   items: Array<{ dictName: string; dictValue: number | string }>,
@@ -47,11 +48,13 @@ export function syncEmployeeCertDict(
   items: Array<{ dictName: string; dictValue: number | string }>,
 ) {
   certLabelByCode.clear();
+  certCodeByLabel.clear();
   for (const item of items) {
     const code = String(item.dictValue);
     const label = item.dictName;
     if (!code || !label) continue;
     certLabelByCode.set(code, label);
+    certCodeByLabel.set(label, code);
   }
 }
 
@@ -96,6 +99,34 @@ function resolveCertNameLabel(raw?: unknown): string {
   return certLabelByCode.get(code) ?? code;
 }
 
+function resolveCertNameCode(raw?: unknown): string | undefined {
+  if (raw === undefined || raw === null || raw === '') return undefined;
+  const value = String(raw).trim();
+  if (certLabelByCode.has(value)) return value;
+  return certCodeByLabel.get(value) ?? value;
+}
+
+/** 编辑表单回显:岗位中文名/编码 → 下拉 dictValue */
+export function resolvePostValuesForForm(positions?: string[]): string[] {
+  if (!positions?.length) return [];
+  return positions
+    .map((value) => {
+      const normalized = String(value).trim();
+      if (!normalized) return '';
+      if (postLabelByCode.has(normalized)) return normalized;
+      return postCodeByLabel.get(normalized) ?? normalized;
+    })
+    .filter(Boolean);
+}
+
+/** 编辑表单回显:证书中文名/编码 → 下拉 dictValue */
+export function resolveCertNameValueForForm(name?: string): string {
+  if (!name?.trim()) return '';
+  const normalized = name.trim();
+  if (certLabelByCode.has(normalized)) return normalized;
+  return certCodeByLabel.get(normalized) ?? normalized;
+}
+
 function resolveEnterpriseName(data?: TransformData): string {
   const medCenterName = String(data?.medCenterName ?? '').trim();
   const hospitalName = String(data?.hospitalName ?? '').trim();
@@ -108,11 +139,43 @@ function resolveEnterpriseName(data?: TransformData): string {
 function resolvePostCodes(positions?: string[]): string | undefined {
   if (!positions?.length) return undefined;
   const codes = positions
-    .map((label) => postCodeByLabel.get(label) ?? label)
+    .map((value) => {
+      const normalized = String(value).trim();
+      if (!normalized) return '';
+      if (postLabelByCode.has(normalized)) return normalized;
+      return postCodeByLabel.get(normalized) ?? normalized;
+    })
     .filter(Boolean);
   return codes.length ? codes.join('|') : undefined;
 }
 
+function normalizeEndDate(value?: string): string | undefined {
+  if (!value?.trim()) return undefined;
+  const trimmed = value.trim();
+  const match = trimmed.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
+  if (!match?.[1] || !match[2] || !match[3]) return trimmed;
+  const [, year, month, day] = match;
+  return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
+}
+
+function toBackendLong(value?: string): number | string | undefined {
+  const trimmed = value?.trim();
+  if (!trimmed) return undefined;
+  if (!/^\d+$/.test(trimmed)) return trimmed;
+  if (trimmed.length > 15) return trimmed;
+  const num = Number(trimmed);
+  return Number.isFinite(num) ? num : trimmed;
+}
+
+function isPersistedQualificationId(id?: string): boolean {
+  if (!id?.trim()) return false;
+  return (
+    !id.startsWith('cert-') &&
+    !id.startsWith('file-') &&
+    !id.startsWith('upload-')
+  );
+}
+
 function parseAttachmentList(
   raw?: unknown,
 ): PersonnelQualificationModel.CertificateAttachment[] {
@@ -187,11 +250,7 @@ function fromEmployeeQualification(
       ? `cert-${index + 1}`
       : String(data.id);
   const name = resolveCertNameLabel(
-    data?.qualificationName ??
-      data?.certName ??
-      data?.certificateName ??
-      data?.name ??
-      '',
+    data?.certificateName ?? data?.certName ?? data?.name ?? '',
   );
   const longTerm =
     data?.isLongTerm === 1 ||
@@ -213,7 +272,11 @@ function fromEmployeeQualification(
     id,
     name,
     type: String(
-      data?.qualificationType ?? data?.qualType ?? data?.type ?? '',
+      data?.qualificationName ??
+        data?.qualificationType ??
+        data?.qualType ??
+        data?.type ??
+        '',
     ),
     number: String(
       data?.certificateNumber ??
@@ -234,7 +297,8 @@ function fromEmployeeQualification(
         data?.files ??
         data?.fileList ??
         data?.fileUrl ??
-        data?.fileUrls,
+        data?.fileUrls ??
+        data?.picturePath,
     ),
   };
 }
@@ -247,10 +311,16 @@ function resolveWorstStatus(
   return 'valid';
 }
 
+/** 雪花 id 等超长整数统一转字符串,避免 query 传参精度丢失 */
+export function toEmployeeRecordId(id?: unknown): string {
+  if (id === undefined || id === null || id === '') return '';
+  return String(id);
+}
+
 export function fromEmployee(
   data?: TransformData,
 ): PersonnelQualificationModel.Personnel {
-  const id = data?.id === undefined || data?.id === null ? '' : String(data.id);
+  const id = toEmployeeRecordId(data?.id);
   const organizationType = resolveOrganizationType(data);
   const certificates = (
     Array.isArray(data?.qualifications) ? data.qualifications : []
@@ -275,6 +345,12 @@ export function fromEmployee(
         ? String(data?.hospitalCode ?? '')
         : String(data?.medCenterId ?? data?.enterpriseId ?? ''),
     enterpriseName: resolveEnterpriseName(data),
+    hospitalCode: String(data?.hospitalCode ?? ''),
+    hospitalName: String(data?.hospitalName ?? ''),
+    yardCode: String(data?.yardCode ?? ''),
+    yardName: String(data?.yardName ?? ''),
+    medCenterId: String(data?.medCenterId ?? ''),
+    medCenterName: String(data?.medCenterName ?? ''),
     decoctionCenterId: String(
       data?.decoctionCenterCode ?? data?.decoctionCenterId ?? '',
     ),
@@ -297,22 +373,25 @@ function toEmployeeQualification(
     .map((item) => item.url)
     .filter(Boolean)
     .join(',');
+  const endDate = cert.longTerm ? void 0 : normalizeEndDate(cert.expiryDate);
 
-  return {
-    id: cert.id || void 0,
-    qualificationName: cert.name,
-    qualificationType: cert.type || void 0,
-    qualificationNo: cert.number,
-    expiryDate: cert.longTerm ? void 0 : cert.expiryDate || void 0,
+  const payload: TransformData = {
+    certificateName: resolveCertNameCode(cert.name),
+    certificateNumber: cert.number,
+    endDate,
     isLongTerm: cert.longTerm ? 1 : 0,
-    qualificationStatus: QUALIFICATION_STATUS_TO_BACK[cert.status ?? 'valid'],
-    fileUrl: fileUrl || void 0,
-    attachments: attachments.map((item) => ({
-      fileName: item.name,
-      fileUrl: item.url,
-      type: item.type,
-    })),
+    picturePath: fileUrl || void 0,
   };
+
+  if (cert.type?.trim()) {
+    payload.qualificationName = cert.type.trim();
+  }
+
+  if (isPersistedQualificationId(cert.id)) {
+    payload.id = toBackendLong(cert.id);
+  }
+
+  return payload;
 }
 
 export function toEmployeeQuery(
@@ -351,9 +430,9 @@ export function toEmployeeQuery(
   const keyword = query?.keyword?.trim();
   if (keyword) {
     if (/^\d+$/.test(keyword)) {
-      dto.operatorCode = keyword;
+      dto.param = keyword;
     } else {
-      dto.name = keyword;
+      dto.param = keyword;
     }
   }
 
@@ -363,22 +442,30 @@ export function toEmployeeQuery(
 export function toEmployeePayload(
   data: PersonnelQualificationModel.PersonnelForm,
 ): TransformData {
-  const payload: TransformData = {
-    id: data.id || void 0,
-    decoctionCenterCode: data.decoctionCenterId,
-    decoctionCenterName: data.decoctionCenterName,
+  const certificates = data.certificates ?? [];
+  const qualificationStatus = certificates.length
+    ? QUALIFICATION_STATUS_TO_BACK[resolveWorstStatus(certificates)]
+    : void 0;
+
+  const id = toEmployeeRecordId(data.id);
+
+  return {
+    id: id || void 0,
+    hospitalCode: data.hospitalCode || void 0,
+    hospitalName: data.hospitalName || void 0,
+    yardCode: data.yardCode || void 0,
+    yardName: data.yardName || void 0,
+    medCenterId: data.medCenterId || void 0,
+    medCenterName: data.medCenterName || void 0,
+    decoctionCenterCode: data.decoctionCenterId || void 0,
+    decoctionCenterName: data.decoctionCenterName || void 0,
     name: data.name,
-    operatorCode: data.employeeNo,
-    cardNo: data.idNumber,
+    operatorCode: toBackendLong(data.employeeNo),
+    cardNo: toBackendLong(data.idNumber),
     post: resolvePostCodes(data.positions),
-    qualifications: (data.certificates ?? []).map(toEmployeeQualification),
+    qualificationStatus,
+    qualifications: certificates.map(toEmployeeQualification),
   };
-
-  if (data.decoctionCenterId) {
-    payload.decoctionCenterCode = data.decoctionCenterId;
-  }
-
-  return payload;
 }
 
 /** 解析 /employee/list 响应:data.t.rows + expiredNum / isExpired */

+ 15 - 0
apps/smart-pharmacy/src/api/model/tisane.ts

@@ -38,6 +38,21 @@ export function fromTisane(data?: TransformData): SystemModel.Tisane {
         ? undefined
         : String(data.enterpriseId),
     relatedEnterprise: data?.enterpriseName ?? data?.relatedEnterprise,
+    hospitalCode:
+      data?.hospitalCode === undefined || data?.hospitalCode === null
+        ? undefined
+        : String(data.hospitalCode),
+    hospitalName: data?.hospitalName,
+    yardCode:
+      data?.yardCode === undefined || data?.yardCode === null
+        ? undefined
+        : String(data.yardCode),
+    yardName: data?.yardName,
+    medCenterId:
+      data?.medCenterId === undefined || data?.medCenterId === null
+        ? undefined
+        : String(data.medCenterId),
+    medCenterName: data?.medCenterName,
     remark: data?.remark,
     createUser: data?.createBy ?? data?.createUser,
   };

+ 1 - 1
apps/smart-pharmacy/src/views/system/personnel-qualification-query/list.vue

@@ -31,7 +31,7 @@ const [CertificateModal, certificateModalApi] = useVbenModal({
 const [Grid] = useVbenVxeGrid({
   formOptions: {
     schema: usePersonnelQualificationSearchFormSchema(),
-    submitOnChange: false,
+    submitOnChange: true,
     wrapperClass:
       'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 personnel-qualification-search-form',
   },

+ 6 - 3
apps/smart-pharmacy/src/views/system/personnel-qualification/data.ts

@@ -7,7 +7,7 @@ import type { PersonnelQualificationModel } from '#/api/method/personnel-qualifi
 import { h } from 'vue';
 
 import {
-  CERTIFICATE_NAME_OPTIONS,
+  optionsCertificateNameMethod,
   optionsPersonnelDecoctionCenterMethod,
   optionsPostTypeMethod,
   QUALIFICATION_STATUS_LABELS,
@@ -208,7 +208,7 @@ export function usePersonnelQualificationTableColumns(
       field: 'operation',
       fixed: 'right',
       title: '操作',
-      width: 160,
+      width: readonly ? 90 : 160,
     },
   ];
 }
@@ -226,4 +226,7 @@ export function createEmptyCertificate(): PersonnelQualificationModel.Certificat
   };
 }
 
-export { CERTIFICATE_NAME_OPTIONS, optionsPostTypeMethod };
+export {
+  optionsCertificateNameMethod,
+  optionsPostTypeMethod,
+};

+ 1 - 1
apps/smart-pharmacy/src/views/system/personnel-qualification/list.vue

@@ -43,7 +43,7 @@ const [CertificateModal, certificateModalApi] = useVbenModal({
 const [Grid, gridApi] = useVbenVxeGrid({
   formOptions: {
     schema: usePersonnelQualificationSearchFormSchema(),
-    submitOnChange: false,
+    submitOnChange: true,
     wrapperClass:
       'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 personnel-qualification-search-form',
   },

+ 15 - 2
apps/smart-pharmacy/src/views/system/personnel-qualification/modules/certificate.vue

@@ -121,7 +121,7 @@ const [Modal, modalApi] = useVbenModal({
           </div>
           <div class="meta-item">
             <span class="meta-label">证书类型:</span>
-            <span>{{ activeCertificate.type || '-' }}</span>
+            <span>{{ activeCertificate.name || '-' }}</span>
           </div>
           <div class="meta-item">
             <span class="meta-label">资质附件:</span>
@@ -151,8 +151,15 @@ const [Modal, modalApi] = useVbenModal({
           </button>
           <div class="attachment-content">
             <template v-if="currentAttachment">
+              <iframe
+                v-if="currentAttachment.type === 'pdf' && currentAttachment.url"
+                referrerpolicy="no-referrer"
+                :src="currentAttachment.url"
+                class="attachment-pdf"
+                title="PDF预览"
+              />
               <div
-                v-if="currentAttachment.type === 'pdf'"
+                v-else-if="currentAttachment.type === 'pdf'"
                 class="attachment-placeholder"
               >
                 <span class="placeholder-icon">PDF</span>
@@ -259,6 +266,12 @@ const [Modal, modalApi] = useVbenModal({
   object-fit: contain;
 }
 
+.attachment-pdf {
+  width: 100%;
+  height: 390px;
+  border: 0;
+}
+
 .attachment-placeholder {
   display: flex;
   flex-direction: column;

+ 124 - 12
apps/smart-pharmacy/src/views/system/personnel-qualification/modules/form.vue

@@ -2,6 +2,7 @@
 import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
 
 import type { PersonnelQualificationModel } from '#/api';
+import type { PersonnelDecoctionCenterOption } from '#/api/method/personnel-qualification';
 
 import { computed, ref } from 'vue';
 
@@ -25,10 +26,14 @@ import {
   optionsPersonnelDecoctionCenterMethod,
   uploadEmployeeQualificationAttachmentMethod,
 } from '#/api';
+import {
+  resolveCertNameValueForForm,
+  resolvePostValuesForForm,
+} from '#/api/model/personnel-qualification';
 
 import {
-  CERTIFICATE_NAME_OPTIONS,
   createEmptyCertificate,
+  optionsCertificateNameMethod,
   optionsPostTypeMethod,
 } from '../data';
 
@@ -39,10 +44,22 @@ const uploadingCount = ref(0);
 const uploadingUids = new Set<string>();
 const certificateUploadFileLists = ref<UploadFile[][]>([[]]);
 const formData = ref<PersonnelQualificationModel.Personnel>();
-const decoctionCenterOptions = ref<Array<{ label: string; value: string }>>(
+const decoctionCenterOptions = ref<PersonnelDecoctionCenterOption[]>([]);
+const organizationContext = ref<
+  Pick<
+    PersonnelQualificationModel.PersonnelForm,
+    | 'hospitalCode'
+    | 'hospitalName'
+    | 'medCenterId'
+    | 'medCenterName'
+    | 'yardCode'
+    | 'yardName'
+  >
+>({});
+const positionOptions = ref<Array<{ label: string; value: string }>>([]);
+const certificateNameOptions = ref<Array<{ label: string; value: string }>>(
   [],
 );
-const positionOptions = ref<Array<{ label: string; value: string }>>([]);
 
 const formModel = ref({
   certificates: [createEmptyCertificate()] as PersonnelQualificationModel.Certificate[],
@@ -65,6 +82,10 @@ async function loadPositionOptions() {
   positionOptions.value = await optionsPostTypeMethod();
 }
 
+async function loadCertificateNameOptions() {
+  certificateNameOptions.value = await optionsCertificateNameMethod();
+}
+
 function resetForm() {
   formModel.value = {
     certificates: [createEmptyCertificate()],
@@ -74,10 +95,33 @@ function resetForm() {
     name: '',
     positions: [],
   };
+  organizationContext.value = {};
   certificateUploadFileLists.value = [[]];
   uploadingUids.clear();
 }
 
+function resolveOrganizationPayload(
+  center?: PersonnelDecoctionCenterOption,
+): Pick<
+  PersonnelQualificationModel.PersonnelForm,
+  | 'hospitalCode'
+  | 'hospitalName'
+  | 'medCenterId'
+  | 'medCenterName'
+  | 'yardCode'
+  | 'yardName'
+> {
+  return {
+    hospitalCode: center?.hospitalCode || organizationContext.value.hospitalCode,
+    hospitalName: center?.hospitalName || organizationContext.value.hospitalName,
+    medCenterId: center?.medCenterId || organizationContext.value.medCenterId,
+    medCenterName:
+      center?.medCenterName || organizationContext.value.medCenterName,
+    yardCode: center?.yardCode || organizationContext.value.yardCode,
+    yardName: center?.yardName || organizationContext.value.yardName,
+  };
+}
+
 function addCertificate() {
   formModel.value.certificates.push(createEmptyCertificate());
   certificateUploadFileLists.value.push([]);
@@ -109,12 +153,20 @@ function resolveUploadRawFile(file: UploadFile): File | undefined {
   return undefined;
 }
 
+function isUploadedAttachment(file: UploadFile) {
+  return (
+    !!file.url &&
+    file.status !== 'error' &&
+    file.status !== 'removed'
+  );
+}
+
 function syncCertificateAttachments(
   cert: PersonnelQualificationModel.Certificate,
   fileList: UploadFile[],
 ) {
   cert.attachments = fileList
-    .filter((file) => file.status === 'done' && file.url)
+    .filter(isUploadedAttachment)
     .slice(0, 9)
     .map((file, index) => ({
       id: file.uid || `upload-${index}`,
@@ -124,6 +176,34 @@ function syncCertificateAttachments(
     }));
 }
 
+function syncAllCertificateAttachments() {
+  formModel.value.certificates.forEach((cert, index) => {
+    syncCertificateAttachments(
+      cert,
+      certificateUploadFileLists.value[index] ?? [],
+    );
+  });
+}
+
+function updateUploadedFile(
+  index: number,
+  uid: string,
+  url: string,
+  fileList: UploadFile[],
+) {
+  const currentList = certificateUploadFileLists.value[index] ?? [];
+  const uploadedFile =
+    currentList.find((item) => item.uid === uid) ??
+    fileList.find((item) => item.uid === uid);
+  if (uploadedFile) {
+    uploadedFile.status = 'done';
+    uploadedFile.url = url;
+  }
+  certificateUploadFileLists.value[index] = uploadedFile
+    ? [...currentList]
+    : [...fileList];
+}
+
 async function handleCertificateUploadChange(
   index: number,
   cert: PersonnelQualificationModel.Certificate,
@@ -131,6 +211,11 @@ async function handleCertificateUploadChange(
 ) {
   certificateUploadFileLists.value[index] = [...fileList];
 
+  if (file.status === 'removed') {
+    syncCertificateAttachments(cert, certificateUploadFileLists.value[index] ?? []);
+    return;
+  }
+
   const rawFile = resolveUploadRawFile(file);
   const shouldUpload =
     rawFile &&
@@ -142,24 +227,34 @@ async function handleCertificateUploadChange(
     uploadingUids.add(file.uid);
     uploadingCount.value += 1;
     file.status = 'uploading';
+    certificateUploadFileLists.value[index] = [...fileList];
     try {
       const url = await uploadEmployeeQualificationAttachmentMethod(rawFile);
-      file.status = 'done';
-      file.url = url;
-      certificateUploadFileLists.value[index] = [...fileList];
+      updateUploadedFile(index, file.uid, url, fileList);
+      syncCertificateAttachments(
+        cert,
+        certificateUploadFileLists.value[index] ?? [],
+      );
     } catch (error: any) {
       file.status = 'error';
       message.error(error.message || `${file.name} 上传失败`);
+      syncCertificateAttachments(
+        cert,
+        certificateUploadFileLists.value[index] ?? [],
+      );
     } finally {
       uploadingUids.delete(file.uid);
       uploadingCount.value -= 1;
     }
+    return;
   }
 
   syncCertificateAttachments(cert, certificateUploadFileLists.value[index] ?? []);
 }
 
 function validateForm() {
+  syncAllCertificateAttachments();
+
   const { certificates, decoctionCenterId, employeeNo, idNumber, name, positions } =
     formModel.value;
 
@@ -226,7 +321,9 @@ const [Drawer, drawerApi] = useVbenDrawer({
       await editPersonnelQualificationMethod({
         certificates: formModel.value.certificates,
         decoctionCenterId: formModel.value.decoctionCenterId,
-        decoctionCenterName: center?.label,
+        decoctionCenterName:
+          center?.decoctionCenterName ?? center?.label ?? undefined,
+        ...resolveOrganizationPayload(center),
         employeeNo: formModel.value.employeeNo.trim(),
         id: formData.value?.id,
         idNumber: formModel.value.idNumber.trim(),
@@ -249,7 +346,11 @@ const [Drawer, drawerApi] = useVbenDrawer({
       return;
     }
 
-    await Promise.all([loadDecoctionCenterOptions(), loadPositionOptions()]);
+    await Promise.all([
+      loadDecoctionCenterOptions(),
+      loadPositionOptions(),
+      loadCertificateNameOptions(),
+    ]);
     const data = drawerApi.getData<PersonnelQualificationModel.Personnel>();
 
     if (data?.id) {
@@ -257,13 +358,24 @@ const [Drawer, drawerApi] = useVbenDrawer({
       try {
         const detail = await getPersonnelQualificationMethod(data.id);
         formData.value = detail;
+        organizationContext.value = {
+          hospitalCode: detail.hospitalCode || undefined,
+          hospitalName: detail.hospitalName || undefined,
+          medCenterId: detail.medCenterId || undefined,
+          medCenterName: detail.medCenterName || undefined,
+          yardCode: detail.yardCode || undefined,
+          yardName: detail.yardName || undefined,
+        };
         formModel.value = {
-          certificates: detail.certificates.map((item) => ({ ...item })),
+          certificates: detail.certificates.map((item) => ({
+            ...item,
+            name: resolveCertNameValueForForm(item.name),
+          })),
           decoctionCenterId: detail.decoctionCenterId ?? '',
           employeeNo: detail.employeeNo,
           idNumber: detail.idNumber,
           name: detail.name,
-          positions: [...detail.positions],
+          positions: resolvePostValuesForForm(detail.positions),
         };
         certificateUploadFileLists.value = detail.certificates.map((item) =>
           toUploadFileList(item),
@@ -343,7 +455,7 @@ const [Drawer, drawerApi] = useVbenDrawer({
         <div class="form-item required">
           <Select
             v-model:value="cert.name"
-            :options="CERTIFICATE_NAME_OPTIONS"
+            :options="certificateNameOptions"
             allow-clear
             class="w-full"
             placeholder="请选择"