瀏覽代碼

完成个人名片功能开发

yuanmingze 1 月之前
父節點
當前提交
35e23ea705

+ 340 - 7
src/pages-mine/personal-card/components/ProfessionalSkillCard.vue

@@ -15,9 +15,49 @@
           placeholder="请输入备案号"
           align-right
           clearable
+          :readonly="disabled"
         />
       </view>
 
+      <view class="filing-section">
+        <text class="section-label">备案状态</text>
+
+        <view v-if="filingList.length" class="filing-list">
+          <view v-for="item in filingList" :key="getFilingItemKey(item)" class="filing-item">
+            <text class="enterprise-name">{{ item.drugEntName || '--' }}</text>
+            <text class="filing-status" :class="getFilingStatusClass(item.status)">
+              {{ item.statusDesc || '--' }}
+            </text>
+          </view>
+        </view>
+        <text v-else class="empty-text">暂无备案信息</text>
+      </view>
+
+      <view class="promotion-section">
+        <text class="section-label">推广产品</text>
+
+        <view v-if="filingList.length" class="promotion-list">
+          <view
+            v-for="(item, index) in filingList"
+            :key="`promotion-${getFilingItemKey(item)}`"
+            class="promotion-item"
+            @click="openPromotionDrugPicker(index)"
+          >
+            <text class="enterprise-name">{{ item.drugEntName || '--' }}</text>
+            <view class="promotion-value-wrapper">
+              <text
+                class="promotion-value"
+                :class="{ 'promotion-value--placeholder': !item.promotionDrugs?.length }"
+              >
+                {{ getPromotionDrugDisplayText(item) }}
+              </text>
+              <wd-icon name="arrow-right" size="30rpx" color="#a9afb8" />
+            </view>
+          </view>
+        </view>
+        <text v-else class="empty-text">暂无可选择的推广产品</text>
+      </view>
+
       <view class="certificate-section">
         <text class="section-label">资质证书</text>
 
@@ -46,13 +86,33 @@
         </view>
       </view>
     </view>
+
+    <wd-select-picker
+      v-model="selectedPromotionDrugIds"
+      v-model:visible="promotionDrugPickerVisible"
+      :title="promotionDrugPickerTitle"
+      :columns="currentPromotionDrugOptions"
+      :loading="promotionDrugLoading"
+      filterable
+      filter-placeholder="搜索药品"
+      root-portal
+      @confirm="confirmPromotionDrugs"
+      @close="resetPromotionDrugPicker"
+    />
   </view>
 </template>
 
 <script setup lang="ts">
 import { computed, ref } from 'vue'
 
-import type { PersonalBusinessCardResponse } from '@/services/modules/mine/personalCard/type'
+import { listPromotionDrugByEntApi } from '@/services/modules/mine/personalCard'
+import type {
+  PersonalBusinessCardResponse,
+  PersonalCardFilingItem,
+  PromotionDrugApiItem,
+  PromotionDrugItem,
+  PromotionDrugListResponse,
+} from '@/services/modules/mine/personalCard/type'
 
 import { useUserStore } from '@/stores/modules/user'
 
@@ -65,23 +125,51 @@ interface UploadResponse {
   } | null
 }
 
-interface ProfessionalSkillFields {
-  qualificationRecordNumber?: string | null
-  qualificationsUrl?: string[] | null
+interface PromotionDrugOption extends PromotionDrugItem {
+  label: string
+  value: string
 }
 
-type Model = PersonalBusinessCardResponse & ProfessionalSkillFields
+interface SelectPickerConfirmEvent {
+  value: Array<string | number | boolean>
+  selectedItems: PromotionDrugOption[]
+}
 
-withDefaults(defineProps<{ title?: string; maxCount?: number }>(), {
+withDefaults(defineProps<{ title?: string; maxCount?: number; disabled?: boolean }>(), {
   title: '专业技能',
   maxCount: 9,
+  disabled: false,
 })
 
-const model = defineModel<Model>({ required: true })
+const model = defineModel<PersonalBusinessCardResponse>({ required: true })
 
 const userStore = useUserStore()
 const uploading = ref(false)
 const baseUrl = (import.meta.env.VITE_BASE_API as string).replace(/\/$/, '')
+const promotionDrugPickerVisible = ref(false)
+const promotionDrugLoading = ref(false)
+const currentFilingIndex = ref(-1)
+const currentDrugEnterpriseName = ref('')
+const currentPromotionDrugOptions = ref<PromotionDrugOption[]>([])
+const selectedPromotionDrugIds = ref<string[]>([])
+const promotionDrugOptionCache = new Map<string, PromotionDrugItem[]>()
+let promotionDrugLoadToken = 0
+
+const filingStatusClassMap: Record<string, string> = {
+  NOT_FILED: 'filing-status--not-filed',
+  APPLIED: 'filing-status--applied',
+  SUBMITTED: 'filing-status--submitted',
+  CONFIRMED: 'filing-status--confirmed',
+  REJECTED: 'filing-status--rejected',
+  REVOKED: 'filing-status--revoked',
+}
+
+const filingList = computed<PersonalCardFilingItem[]>(() => model.value.filingList ?? [])
+const promotionDrugPickerTitle = computed(() =>
+  currentDrugEnterpriseName.value
+    ? `选择推广药品(${currentDrugEnterpriseName.value})`
+    : '选择推广药品'
+)
 
 const certificateList = computed<string[]>({
   get: () => model.value.qualificationsUrl ?? [],
@@ -186,6 +274,161 @@ function previewCertificate(index: number) {
 function removeCertificate(index: number) {
   certificateList.value = certificateList.value.filter((_, i) => i !== index)
 }
+
+async function openPromotionDrugPicker(index: number) {
+  const filingItem = filingList.value[index]
+  if (!filingItem) {
+    uni.showToast({ title: '当前备案数据不存在', icon: 'none' })
+    return
+  }
+
+  const drugEntId = getDrugEntId(filingItem)
+  if (!drugEntId) {
+    uni.showToast({ title: '当前备案数据缺少药企ID', icon: 'none' })
+    return
+  }
+
+  const selectedDrugs = normalizePromotionDrugs(filingItem.promotionDrugs)
+  const currentToken = ++promotionDrugLoadToken
+
+  currentFilingIndex.value = index
+  currentDrugEnterpriseName.value = filingItem.drugEntName ?? ''
+  selectedPromotionDrugIds.value = selectedDrugs.map((item) => item.drugId)
+  currentPromotionDrugOptions.value = toPromotionDrugOptions(selectedDrugs)
+  promotionDrugPickerVisible.value = true
+  promotionDrugLoading.value = true
+
+  try {
+    const cacheKey = String(drugEntId)
+    let drugOptions = promotionDrugOptionCache.get(cacheKey)
+
+    if (!drugOptions) {
+      const response = await listPromotionDrugByEntApi(drugEntId)
+      if (currentToken !== promotionDrugLoadToken) return
+
+      drugOptions = normalizePromotionDrugs(extractResponseList(response.data))
+      promotionDrugOptionCache.set(cacheKey, drugOptions)
+    }
+
+    currentPromotionDrugOptions.value = toPromotionDrugOptions(
+      mergePromotionDrugOptions(drugOptions, selectedDrugs)
+    )
+  } catch (error) {
+    console.error('[ProfessionalSkillCard] load promotion drugs failed:', error)
+    if (currentToken === promotionDrugLoadToken) {
+      uni.showToast({ title: '获取推广药品失败', icon: 'none' })
+    }
+  } finally {
+    if (currentToken === promotionDrugLoadToken) {
+      promotionDrugLoading.value = false
+    }
+  }
+}
+
+function confirmPromotionDrugs(event: SelectPickerConfirmEvent) {
+  const filingItem = filingList.value[currentFilingIndex.value]
+  if (!filingItem) return
+
+  const selectedIdSet = new Set(event.value.map(String))
+  const promotionDrugs = currentPromotionDrugOptions.value
+    .filter((item) => selectedIdSet.has(item.value))
+    .map((item) => ({
+      drugId: item.drugId,
+      drugName: item.drugName,
+    }))
+
+  const updatedFilingList = [...filingList.value]
+  updatedFilingList[currentFilingIndex.value] = {
+    ...filingItem,
+    promotionDrugs,
+  }
+  model.value.filingList = updatedFilingList
+}
+
+function resetPromotionDrugPicker() {
+  promotionDrugLoadToken += 1
+  promotionDrugLoading.value = false
+  currentFilingIndex.value = -1
+  currentDrugEnterpriseName.value = ''
+  currentPromotionDrugOptions.value = []
+  selectedPromotionDrugIds.value = []
+}
+
+function getDrugEntId(item: PersonalCardFilingItem): string {
+  const value = item.drugEntId ?? item.entId ?? item.drugEnterpriseId
+  return value === undefined || value === null ? '' : String(value)
+}
+
+function extractResponseList(data: PromotionDrugListResponse): PromotionDrugApiItem[] {
+  if (Array.isArray(data)) return data
+  if (Array.isArray(data.records)) return data.records
+  if (Array.isArray(data.list)) return data.list
+  return []
+}
+
+function normalizePromotionDrugs(
+  list: Array<PromotionDrugApiItem | PromotionDrugItem> | null | undefined
+): PromotionDrugItem[] {
+  if (!Array.isArray(list)) return []
+
+  const seen = new Set<string>()
+  const result: PromotionDrugItem[] = []
+
+  list.forEach((item) => {
+    if (!item) return
+
+    const apiItem = item as PromotionDrugApiItem
+    const sourceDrugId = apiItem.drugId ?? apiItem.drug_id ?? apiItem.id
+    if (sourceDrugId === undefined || sourceDrugId === null || sourceDrugId === '') return
+
+    const drugId = String(sourceDrugId)
+    if (seen.has(drugId)) return
+
+    seen.add(drugId)
+    result.push({
+      drugId,
+      drugName: apiItem.drugName ?? apiItem.drug_name ?? apiItem.name ?? '',
+    })
+  })
+
+  return result
+}
+
+function mergePromotionDrugOptions(
+  apiOptions: PromotionDrugItem[],
+  selectedOptions: PromotionDrugItem[]
+): PromotionDrugItem[] {
+  return normalizePromotionDrugs([...apiOptions, ...selectedOptions])
+}
+
+function toPromotionDrugOptions(list: PromotionDrugItem[]): PromotionDrugOption[] {
+  return list.map((item) => ({
+    ...item,
+    label: item.drugName || `药品 ${item.drugId}`,
+    value: item.drugId,
+  }))
+}
+
+function getPromotionDrugDisplayText(item: PersonalCardFilingItem): string {
+  const drugs = normalizePromotionDrugs(item.promotionDrugs)
+  if (!drugs.length) return '请选择推广产品'
+
+  const drugNames = drugs.map((drug) => drug.drugName).filter(Boolean)
+  if (!drugNames.length) return `已选择${drugs.length}个药品`
+  if (drugNames.length <= 2) return drugNames.join('、')
+
+  return `${drugNames.slice(0, 2).join('、')} 等${drugNames.length}个`
+}
+
+function getFilingItemKey(item: PersonalCardFilingItem): string {
+  return String(item.recordId ?? item.entTaxCode ?? item.drugEntId ?? item.drugEntName ?? '')
+}
+
+function getFilingStatusClass(status?: string | null): string {
+  return status
+    ? filingStatusClassMap[status] || 'filing-status--default'
+    : 'filing-status--default'
+}
 </script>
 
 <style lang="scss" scoped>
@@ -250,6 +493,96 @@ function removeCertificate(index: number) {
   padding: 22rpx 24rpx 28rpx;
 }
 
+.filing-section,
+.promotion-section {
+  padding: 22rpx 24rpx;
+  border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
+}
+
+.filing-list,
+.promotion-list {
+  margin-top: 8rpx;
+}
+
+.filing-item,
+.promotion-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 76rpx;
+  gap: 24rpx;
+}
+
+.enterprise-name {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  color: #4f5661;
+  font-size: 27rpx;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.filing-status {
+  flex-shrink: 0;
+  padding: 7rpx 16rpx;
+  border-radius: 999rpx;
+  background: #f2f4f7;
+  color: #7c8491;
+  font-size: 24rpx;
+}
+
+.filing-status--not-filed,
+.filing-status--rejected,
+.filing-status--revoked {
+  background: #fff1f0;
+  color: #e85c55;
+}
+
+.filing-status--applied,
+.filing-status--submitted {
+  background: #fff7e8;
+  color: #e99928;
+}
+
+.filing-status--confirmed {
+  background: #eaf8ef;
+  color: #35a765;
+}
+
+.promotion-item {
+  cursor: pointer;
+}
+
+.promotion-value-wrapper {
+  display: flex;
+  flex: 1;
+  min-width: 0;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 8rpx;
+}
+
+.promotion-value {
+  max-width: 360rpx;
+  overflow: hidden;
+  color: #30343b;
+  font-size: 27rpx;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.promotion-value--placeholder,
+.empty-text {
+  color: #a9afb8;
+}
+
+.empty-text {
+  display: block;
+  padding: 16rpx 0 6rpx;
+  font-size: 26rpx;
+}
+
 .section-label {
   display: block;
   margin-bottom: 24rpx;

+ 170 - 17
src/pages-mine/personal-card/components/UserInfoCard.vue

@@ -24,7 +24,14 @@
         </wd-form-item>
 
         <wd-form-item title="身份证号" prop="idCardNumber">
-          <wd-input v-model="model.idCardNumber" type="text" align-right :readonly="disabled" />
+          <wd-input
+            v-model="model.idCardNumber"
+            type="text"
+            align-right
+            placeholder="请输入身份证号"
+            :readonly="disabled"
+            @blur="handleIdCardBlur"
+          />
         </wd-form-item>
 
         <wd-form-item title="手机号" prop="username">
@@ -46,24 +53,80 @@
             @click="showPicker('degree')"
           />
         </wd-form-item>
+
+        <wd-form-item title="专业" prop="majorCategory">
+          <wd-input
+            v-model="majorLabel"
+            readonly
+            type="text"
+            align-right
+            placeholder="请选择所学专业类别"
+            suffix-icon="down"
+            @click="showPicker('major')"
+          />
+        </wd-form-item>
+
+        <wd-form-item title="所在区域">
+          <wd-input
+            v-model="areaLabel"
+            readonly
+            type="text"
+            align-right
+            placeholder="请选择所在区域"
+            suffix-icon="down"
+            @click="showAreaPicker"
+          />
+        </wd-form-item>
       </wd-form>
     </view>
 
-    <wd-picker v-model:visible="pickerShow" :columns="pickerColumns" @confirm="pickerConfirm" />
+    <wd-picker
+      v-model:visible="pickerShow"
+      :columns="pickerColumns"
+      label-key="label"
+      value-key="value"
+      root-portal
+      @confirm="pickerConfirm"
+    />
+
+    <wd-cascader
+      v-model="areaPickerValue"
+      v-model:visible="areaPickerShow"
+      title="选择所在区域"
+      :options="areaOptions"
+      root-portal
+      @confirm="areaConfirm"
+    />
   </view>
 </template>
 <script setup lang="ts">
 import { computed, ref } from 'vue'
 
+import { useCascaderAreaData } from '@vant/area-data'
+
 import type { DictItem } from '@/services/modules/common/type'
 import type { PersonalBusinessCardResponse } from '@/services/modules/mine/personalCard/type'
 
-type PickerType = 'gender' | 'degree'
+import { getAgeFromChineseResidentIdCard, isValidChineseResidentIdCard } from '@/utils/id-card'
+
+type PickerType = 'gender' | 'degree' | 'major'
+
+interface AreaOption {
+  value: string
+  text: string
+  children?: AreaOption[]
+  isLeaf?: boolean
+}
+
+interface AreaConfirmEvent {
+  selectedOptions?: AreaOption[]
+}
 
 interface Props {
   disabled?: boolean
   genderColumns: DictItem[]
   degreeColumns: DictItem[]
+  majorColumns: DictItem[]
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -76,23 +139,38 @@ const model = defineModel<PersonalBusinessCardResponse>({
 
 const pickerType = ref<PickerType>('gender')
 const pickerShow = ref(false)
+const areaPickerShow = ref(false)
+const areaPickerValue = ref<string | number>()
 
-const pickerColumns = computed<DictItem[]>(() =>
-  pickerType.value === 'gender' ? props.genderColumns : props.degreeColumns
-)
+const rawAreaOptions = useCascaderAreaData() as AreaOption[]
+const areaOptions = convertCityLeaf(rawAreaOptions)
 
-const getDictLabel = (columns: DictItem[], value?: string | number) => {
-  if (!value) return ''
+const pickerColumns = computed<DictItem[]>(() => {
+  if (pickerType.value === 'gender') return props.genderColumns
+  if (pickerType.value === 'degree') return props.degreeColumns
+  return props.majorColumns
+})
+
+const getDictLabel = (columns: DictItem[], value?: string | number | null) => {
+  if (value === undefined || value === null || value === '') return ''
 
-  return columns.find((item) => item.value === value)?.label ?? ''
+  return columns.find((item) => String(item.value) === String(value))?.label ?? ''
 }
 
 const createLabelComputed = (type: PickerType) =>
   computed<string>({
     get: () =>
       getDictLabel(
-        type === 'gender' ? props.genderColumns : props.degreeColumns,
-        type === 'gender' ? model.value.gender : model.value.degree
+        type === 'gender'
+          ? props.genderColumns
+          : type === 'degree'
+            ? props.degreeColumns
+            : props.majorColumns,
+        type === 'gender'
+          ? model.value.gender
+          : type === 'degree'
+            ? model.value.degree
+            : model.value.majorCategory
       ),
 
     set: () => {},
@@ -101,25 +179,100 @@ const createLabelComputed = (type: PickerType) =>
 const genderLabel = createLabelComputed('gender')
 
 const degreeLabel = createLabelComputed('degree')
+const majorLabel = computed<string>({
+  get: () =>
+    model.value.majorLabel ||
+    model.value.majorName ||
+    getDictLabel(props.majorColumns, model.value.majorCategory),
+  set: () => {},
+})
+const areaLabel = computed<string>({
+  get: () => [model.value.province, model.value.city].filter(Boolean).join('-'),
+  set: () => {},
+})
 
 const showPicker = (type: PickerType): void => {
-  if (props.disabled) return
+  const columns =
+    type === 'gender'
+      ? props.genderColumns
+      : type === 'degree'
+        ? props.degreeColumns
+        : props.majorColumns
+
+  if (!columns.length) {
+    uni.showToast({
+      title: '暂无可选内容',
+      icon: 'none',
+    })
+    return
+  }
 
   pickerType.value = type
   pickerShow.value = true
 }
 
 const pickerConfirm = (event: PickerConfirmEvent): void => {
-  const value = Array.isArray(event.value) ? event.value[0] : event.value
-
-  if (!value) return
+  const item = event.selectedItems[0] as DictItem | undefined
+  if (!item) return
 
   if (pickerType.value === 'gender') {
-    model.value.gender = value
+    model.value.gender = item.value
+    return
+  }
+
+  if (pickerType.value === 'degree') {
+    model.value.degree = item.value
+    return
+  }
+
+  model.value.majorCategory = item.value
+  model.value.majorLabel = item.label
+}
+
+const showAreaPicker = () => {
+  areaPickerValue.value = model.value.cityCode || undefined
+  areaPickerShow.value = true
+}
+
+const areaConfirm = (event: AreaConfirmEvent) => {
+  const [province, city] = event.selectedOptions ?? []
+  if (!province || !city) return
+
+  model.value.province = province.text
+  model.value.provinceCode = String(province.value)
+  model.value.city = city.text
+  model.value.cityCode = String(city.value)
+  areaPickerValue.value = city.value
+}
+
+const handleIdCardBlur = () => {
+  if (props.disabled) return
+
+  const idCardNumber = model.value.idCardNumber?.trim()
+  if (!idCardNumber) return
+
+  if (!isValidChineseResidentIdCard(idCardNumber)) {
+    model.value.idCardNumber = ''
+    uni.showToast({
+      title: '请检查输入的身份证号码是否正确',
+      icon: 'none',
+    })
     return
   }
 
-  model.value.degree = value
+  model.value.idCardNumber = idCardNumber
+  model.value.age = getAgeFromChineseResidentIdCard(idCardNumber)
+}
+
+function convertCityLeaf(list: AreaOption[]): AreaOption[] {
+  return list.map((province) => ({
+    ...province,
+    children: province.children?.map((city) => ({
+      value: city.value,
+      text: city.text,
+      isLeaf: true,
+    })),
+  }))
 }
 </script>
 

+ 76 - 7
src/pages-mine/personal-card/index.vue

@@ -6,6 +6,7 @@
       :disabled="disabled"
       :gender-columns="genderColumns"
       :degree-columns="degreeColumns"
+      :major-columns="majorColumns"
     />
 
     <PlatformExperienceCard v-if="personalBusinessCardInfo" :data="personalBusinessCardInfo" />
@@ -13,7 +14,7 @@
     <ProfessionalSkillCard
       v-if="personalBusinessCardInfo"
       v-model="personalBusinessCardInfo"
-      :upload-api="uploadCertificate"
+      :disabled="disabled"
     />
 
     <view v-if="personalBusinessCardInfo" class="save-section">
@@ -37,6 +38,9 @@ import {
 } from '@/services/modules/mine/personalCard/index'
 import type {
   PersonalBusinessCardResponse,
+  PersonalCardFilingItem,
+  PromotionDrugApiItem,
+  PromotionDrugItem,
   SavePersonalBusinessCardRequest,
 } from '@/services/modules/mine/personalCard/type'
 
@@ -54,6 +58,7 @@ const personalBusinessCardInfo = ref<PersonalBusinessCardResponse>()
 
 const genderColumns = ref<DictItem[]>([])
 const degreeColumns = ref<DictItem[]>([])
+const majorColumns = ref<DictItem[]>([])
 
 onLoad(() => {
   initPage()
@@ -72,21 +77,25 @@ async function getPersonalBusinessCardInfo() {
   if (!userId) return
 
   const res = await getPersonalBusinessCardApi(userId)
-  personalBusinessCardInfo.value = res.data
+  const data = res.data
+
+  personalBusinessCardInfo.value = {
+    ...data,
+    qualificationsUrl: Array.isArray(data.qualificationsUrl) ? data.qualificationsUrl : [],
+    filingList: normalizeFilingList(data.filingList),
+  }
 }
 
 async function getDict() {
-  const [genderRes, degreeRes] = await Promise.all([
+  const [genderRes, degreeRes, majorRes] = await Promise.all([
     getDictTypeApi('gender'),
     getDictTypeApi('user_degree'),
+    getDictTypeApi('user_filing_major'),
   ])
 
   genderColumns.value = genderRes?.data ?? []
   degreeColumns.value = degreeRes?.data ?? []
-}
-
-async function uploadCertificate(filePath: string): Promise<string> {
-  return filePath
+  majorColumns.value = majorRes?.data ?? []
 }
 
 const saving = ref(false)
@@ -96,6 +105,8 @@ function buildSavePayload(): SavePersonalBusinessCardRequest | null {
 
   if (!info) return null
 
+  const filingList = Array.isArray(info.filingList) ? info.filingList : []
+
   return {
     degree: info.degree ?? null,
     gender: info.gender ?? null,
@@ -104,6 +115,15 @@ function buildSavePayload(): SavePersonalBusinessCardRequest | null {
     qualificationsUrl: Array.isArray(info.qualificationsUrl) ? info.qualificationsUrl : [],
     realname: info.realname ?? null,
     userId: info.userId,
+    majorCategory: normalizeValue(info.majorCategory),
+    province: info.province ?? '',
+    provinceCode: normalizeValue(info.provinceCode),
+    city: info.city ?? '',
+    cityCode: normalizeValue(info.cityCode),
+    filingDrugUpdates: filingList.map((item) => ({
+      entTaxCode: item.entTaxCode ?? '',
+      promotionDrugIds: normalizePromotionDrugs(item.promotionDrugs).map((drug) => drug.drugId),
+    })),
   }
 }
 
@@ -117,6 +137,14 @@ const handleSave = async () => {
     })
     return
   }
+
+  if (payload.qualificationRecordNumber && payload.qualificationsUrl.length === 0) {
+    uni.showToast({
+      title: '请上传资质证书',
+      icon: 'none',
+    })
+    return
+  }
   try {
     saving.value = true
     await savePersonalBusinessCardApi(payload)
@@ -130,6 +158,47 @@ const handleSave = async () => {
     saving.value = false
   }
 }
+
+function normalizeFilingList(
+  list: PersonalCardFilingItem[] | null | undefined
+): PersonalCardFilingItem[] {
+  if (!Array.isArray(list)) return []
+
+  return list.map((item) => ({
+    ...item,
+    promotionDrugs: normalizePromotionDrugs(item.promotionDrugs),
+  }))
+}
+
+function normalizePromotionDrugs(
+  list: Array<PromotionDrugApiItem | PromotionDrugItem> | null | undefined
+): PromotionDrugItem[] {
+  if (!Array.isArray(list)) return []
+
+  const seen = new Set<string>()
+
+  return list.reduce<PromotionDrugItem[]>((result, item) => {
+    const apiItem = item as PromotionDrugApiItem
+    const sourceDrugId = apiItem.drugId ?? apiItem.drug_id ?? apiItem.id
+    if (sourceDrugId === undefined || sourceDrugId === null || sourceDrugId === '') {
+      return result
+    }
+
+    const drugId = String(sourceDrugId)
+    if (seen.has(drugId)) return result
+
+    seen.add(drugId)
+    result.push({
+      drugId,
+      drugName: apiItem.drugName ?? apiItem.drug_name ?? apiItem.name ?? '',
+    })
+    return result
+  }, [])
+}
+
+function normalizeValue(value: string | number | null | undefined): string {
+  return value === undefined || value === null ? '' : String(value)
+}
 </script>
 
 <style lang="scss" scoped>

+ 12 - 1
src/services/modules/mine/personalCard/index.ts

@@ -1,5 +1,9 @@
 import http from '../../../index'
-import type { PersonalBusinessCardResponse, SavePersonalBusinessCardRequest } from './type'
+import type {
+  PersonalBusinessCardResponse,
+  PromotionDrugListResponse,
+  SavePersonalBusinessCardRequest,
+} from './type'
 
 export const getPersonalBusinessCardApi = (id: string) => {
   return http.get<PersonalBusinessCardResponse>(`/admin/api/get-personal-business-card/${id}`)
@@ -11,3 +15,10 @@ export const savePersonalBusinessCardApi = (data: SavePersonalBusinessCardReques
     loadingText: '保存中...',
   })
 }
+
+export const listPromotionDrugByEntApi = (drugEntId: string | number) => {
+  return http.get<PromotionDrugListResponse>(
+    '/admin/wmdadrugentdrugtable/list-promotion-drug-by-ent',
+    { drugEntId }
+  )
+}

+ 67 - 13
src/services/modules/mine/personalCard/type.d.ts

@@ -1,27 +1,81 @@
+export type PersonalCardValue = string | number
+
+export interface PromotionDrugItem {
+  drugId: string
+  drugName: string
+}
+
+export interface PromotionDrugApiItem {
+  drug_id?: PersonalCardValue | null
+  drug_name?: string | null
+  drugId?: PersonalCardValue | null
+  drugName?: string | null
+  id?: PersonalCardValue | null
+  name?: string | null
+}
+
+export type PromotionDrugListResponse =
+  | PromotionDrugApiItem[]
+  | {
+      records?: PromotionDrugApiItem[]
+      list?: PromotionDrugApiItem[]
+    }
+
+export interface PersonalCardFilingItem {
+  recordId?: PersonalCardValue | null
+  drugEntId?: PersonalCardValue | null
+  entId?: PersonalCardValue | null
+  drugEnterpriseId?: PersonalCardValue | null
+  drugEntName?: string | null
+  entTaxCode?: string | null
+  status?: string | null
+  statusDesc?: string | null
+  promotionDrugs?: PromotionDrugItem[] | null
+}
+
 export interface PersonalBusinessCardResponse {
-  age: number
+  age: number | null
   approvedTaskCount: number
   approvedTaskRate: string
-  degree: string
+  degree: string | null
   finishTaskCount: number
-  gender: string
-  idCardNumber: string
+  gender: string | null
+  idCardNumber: string | null
   isLivenessAuth: 0 | 1
-  qualificationRecordNumber: string
+  qualificationRecordNumber: string | null
   qualificationsUrl: string[]
-  realname: string
+  realname: string | null
   serviceDeptName: string
   serviceDuration: string
   userId: number
   username: string
+  majorCategory?: string | null
+  majorLabel?: string | null
+  majorName?: string | null
+  province?: string | null
+  provinceCode?: string | null
+  city?: string | null
+  cityCode?: string | null
+  filingList?: PersonalCardFilingItem[] | null
+}
+
+export interface FilingDrugUpdate {
+  entTaxCode: string
+  promotionDrugIds: string[]
 }
 
 export interface SavePersonalBusinessCardRequest {
-  degree: string | undefined
-  gender: string | undefined
-  idCardNumber: string | undefined
-  qualificationRecordNumber: string | undefined
-  qualificationsUrl: string[] | undefined
-  realname: string | undefined
-  userId: number | undefined
+  degree: string | null
+  gender: string | null
+  idCardNumber: string | null
+  qualificationRecordNumber: string | null
+  qualificationsUrl: string[]
+  realname: string | null
+  userId: number
+  majorCategory: string
+  province: string
+  provinceCode: string
+  city: string
+  cityCode: string
+  filingDrugUpdates: FilingDrugUpdate[]
 }