index.vue 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. <template>
  2. <view class="personal-card-page">
  3. <UserInfoCard
  4. v-if="personalBusinessCardInfo"
  5. v-model="personalBusinessCardInfo"
  6. :disabled="identityLocked"
  7. :gender-columns="genderColumns"
  8. :degree-columns="degreeColumns"
  9. :major-columns="majorColumns"
  10. />
  11. <PlatformExperienceCard v-if="personalBusinessCardInfo" :data="personalBusinessCardInfo" />
  12. <ProfessionalSkillCard
  13. v-if="personalBusinessCardInfo"
  14. v-model="personalBusinessCardInfo"
  15. :disabled="currentUserInfo?.faceAuthStatus ?? false"
  16. />
  17. <view v-if="personalBusinessCardInfo" class="save-section">
  18. <button class="save-btn" :disabled="saving" @click="handleSave">
  19. {{ saving ? '保存中...' : '保存' }}
  20. </button>
  21. </view>
  22. </view>
  23. </template>
  24. <script setup lang="ts">
  25. import { computed, ref } from 'vue'
  26. import { onLoad } from '@dcloudio/uni-app'
  27. import { getDictTypeApi } from '@/services/modules/common'
  28. import type { DictItem } from '@/services/modules/common/type'
  29. import {
  30. getPersonalBusinessCardApi,
  31. savePersonalBusinessCardApi,
  32. } from '@/services/modules/mine/personalCard/index'
  33. import type {
  34. PersonalBusinessCardResponse,
  35. PersonalCardFilingItem,
  36. PromotionDrugApiItem,
  37. PromotionDrugItem,
  38. SavePersonalBusinessCardRequest,
  39. } from '@/services/modules/mine/personalCard/type'
  40. import { useUserStore } from '@/stores/modules/user'
  41. import PlatformExperienceCard from './components/PlatformExperienceCard.vue'
  42. import ProfessionalSkillCard from './components/ProfessionalSkillCard.vue'
  43. import UserInfoCard from './components/UserInfoCard.vue'
  44. const userStore = useUserStore()
  45. const currentUserInfo = computed(() => userStore.currentUserInfo)
  46. const personalBusinessCardInfo = ref<PersonalBusinessCardResponse>()
  47. const originalIdentity =
  48. ref<Pick<PersonalBusinessCardResponse, 'realname' | 'idCardNumber' | 'gender'>>()
  49. const identityLocked = computed(
  50. () =>
  51. currentUserInfo.value?.faceAuthStatus === true ||
  52. personalBusinessCardInfo.value?.isLivenessAuth === 1
  53. )
  54. const genderColumns = ref<DictItem[]>([])
  55. const degreeColumns = ref<DictItem[]>([])
  56. const majorColumns = ref<DictItem[]>([])
  57. onLoad(() => {
  58. initPage()
  59. })
  60. async function initPage() {
  61. await Promise.all([getDict(), getPersonalBusinessCardInfo()])
  62. }
  63. async function getPersonalBusinessCardInfo() {
  64. const userId = currentUserInfo.value?.userId
  65. if (!userId) return
  66. const res = await getPersonalBusinessCardApi(userId)
  67. const data = res.data
  68. originalIdentity.value = {
  69. realname: data.realname,
  70. idCardNumber: data.idCardNumber,
  71. gender: data.gender,
  72. }
  73. personalBusinessCardInfo.value = {
  74. ...data,
  75. qualificationsUrl: Array.isArray(data.qualificationsUrl) ? data.qualificationsUrl : [],
  76. filingList: normalizeFilingList(data.filingList),
  77. }
  78. }
  79. async function getDict() {
  80. const [genderRes, degreeRes, majorRes] = await Promise.all([
  81. getDictTypeApi('gender'),
  82. getDictTypeApi('user_degree'),
  83. getDictTypeApi('user_filing_major'),
  84. ])
  85. genderColumns.value = genderRes?.data ?? []
  86. degreeColumns.value = degreeRes?.data ?? []
  87. majorColumns.value = majorRes?.data ?? []
  88. }
  89. const saving = ref(false)
  90. function buildSavePayload(): SavePersonalBusinessCardRequest | null {
  91. const info = personalBusinessCardInfo.value
  92. if (!info) return null
  93. const filingList = Array.isArray(info.filingList) ? info.filingList : []
  94. const identity = identityLocked.value && originalIdentity.value ? originalIdentity.value : info
  95. return {
  96. degree: info.degree ?? null,
  97. gender: identity.gender ?? null,
  98. idCardNumber: identity.idCardNumber ?? null,
  99. qualificationRecordNumber: info.qualificationRecordNumber ?? null,
  100. qualificationsUrl: Array.isArray(info.qualificationsUrl) ? info.qualificationsUrl : [],
  101. realname: identity.realname ?? null,
  102. userId: info.userId,
  103. majorCategory: normalizeValue(info.majorCategory),
  104. province: info.province ?? '',
  105. provinceCode: normalizeValue(info.provinceCode),
  106. city: info.city ?? '',
  107. cityCode: normalizeValue(info.cityCode),
  108. filingDrugUpdates: filingList.map((item) => ({
  109. entTaxCode: item.entTaxCode ?? '',
  110. promotionDrugIds: normalizePromotionDrugs(item.promotionDrugs).map((drug) => drug.drugId),
  111. })),
  112. }
  113. }
  114. const handleSave = async () => {
  115. if (saving.value) return
  116. const payload = buildSavePayload()
  117. if (!payload) {
  118. uni.showToast({
  119. title: '数据异常,请重新进入页面',
  120. icon: 'none',
  121. })
  122. return
  123. }
  124. if (payload.qualificationRecordNumber && payload.qualificationsUrl.length === 0) {
  125. uni.showToast({
  126. title: '请上传资质证书',
  127. icon: 'none',
  128. })
  129. return
  130. }
  131. try {
  132. saving.value = true
  133. await savePersonalBusinessCardApi(payload)
  134. uni.showToast({
  135. title: '提交成功',
  136. icon: 'success',
  137. })
  138. } catch (error) {
  139. console.error('[PersonalBusinessCard] save failed:', error)
  140. } finally {
  141. saving.value = false
  142. }
  143. }
  144. function normalizeFilingList(
  145. list: PersonalCardFilingItem[] | null | undefined
  146. ): PersonalCardFilingItem[] {
  147. if (!Array.isArray(list)) return []
  148. return list.map((item) => ({
  149. ...item,
  150. promotionDrugs: normalizePromotionDrugs(item.promotionDrugs),
  151. }))
  152. }
  153. function normalizePromotionDrugs(
  154. list: Array<PromotionDrugApiItem | PromotionDrugItem> | null | undefined
  155. ): PromotionDrugItem[] {
  156. if (!Array.isArray(list)) return []
  157. const seen = new Set<string>()
  158. return list.reduce<PromotionDrugItem[]>((result, item) => {
  159. const apiItem = item as PromotionDrugApiItem
  160. const sourceDrugId = apiItem.drugId ?? apiItem.drug_id ?? apiItem.id
  161. if (sourceDrugId === undefined || sourceDrugId === null || sourceDrugId === '') {
  162. return result
  163. }
  164. const drugId = String(sourceDrugId)
  165. if (seen.has(drugId)) return result
  166. seen.add(drugId)
  167. result.push({
  168. drugId,
  169. drugName: apiItem.drugName ?? apiItem.drug_name ?? apiItem.name ?? '',
  170. })
  171. return result
  172. }, [])
  173. }
  174. function normalizeValue(value: string | number | null | undefined): string {
  175. return value === undefined || value === null ? '' : String(value)
  176. }
  177. </script>
  178. <style lang="scss" scoped>
  179. .personal-card-page {
  180. min-height: 100vh;
  181. padding: 24rpx 24rpx 64rpx;
  182. box-sizing: border-box;
  183. background: linear-gradient(180deg, #f1f6ff 0%, #f6f7fb 48%, #f7f8fa 100%);
  184. }
  185. .save-section {
  186. margin-top: 8rpx;
  187. padding: 8rpx 6rpx 32rpx;
  188. box-sizing: border-box;
  189. }
  190. .save-btn {
  191. display: flex;
  192. align-items: center;
  193. justify-content: center;
  194. width: 100%;
  195. height: 92rpx;
  196. padding: 0;
  197. margin: 0;
  198. border: none;
  199. border-radius: 999rpx;
  200. background: linear-gradient(90deg, #2f84ff 0%, #4ba0ff 100%);
  201. color: #ffffff;
  202. font-size: 32rpx;
  203. font-weight: 600;
  204. line-height: 92rpx;
  205. letter-spacing: 2rpx;
  206. box-shadow:
  207. 0 14rpx 30rpx rgba(47, 132, 255, 0.26),
  208. 0 4rpx 10rpx rgba(47, 132, 255, 0.14);
  209. &::after {
  210. border: none;
  211. }
  212. &[disabled] {
  213. opacity: 0.68;
  214. color: #ffffff;
  215. background: linear-gradient(90deg, #8bbdff 0%, #9dccff 100%);
  216. box-shadow: 0 8rpx 20rpx rgba(47, 132, 255, 0.14);
  217. }
  218. }
  219. </style>