index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <template>
  2. <view class="filing-page">
  3. <view v-if="loading" class="page-loading">
  4. <wd-loading size="48rpx" />
  5. <text>备案信息加载中...</text>
  6. </view>
  7. <template v-else>
  8. <view class="invitation-card">
  9. <view class="invitation-card__title">备案邀请</view>
  10. <view class="invitation-card__content">
  11. <text v-if="entName" class="company-name">【{{ entName }}】</text>
  12. <text>向您发起了备案邀请,请提交您的备案信息。</text>
  13. </view>
  14. </view>
  15. <view class="form-card">
  16. <view class="form-row form-row--record">
  17. <view class="form-label">医药代表资质备案号:</view>
  18. <view class="form-control form-control--column">
  19. <view class="optional-tag">没有备案号可以不填</view>
  20. <input
  21. v-model="form.filingNo"
  22. class="record-input"
  23. :maxlength="50"
  24. placeholder="填写备案号"
  25. placeholder-class="form-placeholder"
  26. />
  27. </view>
  28. </view>
  29. <view class="form-row" @click="openEducationPicker">
  30. <view class="form-label">学历:</view>
  31. <view class="form-control">
  32. <text :class="form.degreeName ? 'form-value' : 'form-placeholder'">
  33. {{ form.degreeName || '请选择学历' }}
  34. </text>
  35. <wd-icon name="right" size="32rpx" color="#a9afb8" />
  36. </view>
  37. </view>
  38. <view class="form-row" @click="openMajorPicker">
  39. <view class="form-label">所学专业类别:</view>
  40. <view class="form-control">
  41. <text :class="form.majorName ? 'form-value' : 'form-placeholder'">
  42. {{ form.majorName || '请选择专业类别' }}
  43. </text>
  44. <wd-icon name="right" size="32rpx" color="#a9afb8" />
  45. </view>
  46. </view>
  47. <view class="form-row" @click="openAreaPicker">
  48. <view class="form-label">所在区域:</view>
  49. <view class="form-control">
  50. <text :class="form.areaName ? 'form-value' : 'form-placeholder'">
  51. {{ form.areaName || '请选择区域' }}
  52. </text>
  53. <wd-icon name="right" size="32rpx" color="#a9afb8" />
  54. </view>
  55. </view>
  56. <view class="form-row form-row--last" @click="openDrugPicker">
  57. <view class="form-label">推广药品:</view>
  58. <view class="form-control">
  59. <text class="form-value">{{ selectedDrugText }}</text>
  60. <wd-icon name="right" size="32rpx" color="#a9afb8" />
  61. </view>
  62. </view>
  63. </view>
  64. <view class="submit-section">
  65. <button class="submit-button" :disabled="submitting" @click="submit">
  66. {{ submitting ? '提交中...' : '提交备案' }}
  67. </button>
  68. </view>
  69. </template>
  70. <wd-picker
  71. v-model:visible="educationPickerVisible"
  72. title="选择学历"
  73. :columns="educationList"
  74. label-key="label"
  75. value-key="value"
  76. root-portal
  77. @confirm="confirmEducation"
  78. />
  79. <wd-picker
  80. v-model:visible="majorPickerVisible"
  81. title="选择专业类别"
  82. :columns="majorList"
  83. label-key="label"
  84. value-key="value"
  85. root-portal
  86. @confirm="confirmMajor"
  87. />
  88. <wd-cascader
  89. v-model="areaPickerValue"
  90. v-model:visible="areaPickerVisible"
  91. title="选择所在区域"
  92. :options="areaOptions"
  93. root-portal
  94. @confirm="confirmArea"
  95. />
  96. <wd-select-picker
  97. v-model="selectedDrugIds"
  98. v-model:visible="drugPickerVisible"
  99. title="选择推广药品"
  100. :columns="drugOptions"
  101. filterable
  102. filter-placeholder="搜索药品"
  103. root-portal
  104. />
  105. </view>
  106. </template>
  107. <script setup lang="ts">
  108. import { computed, reactive, ref } from 'vue'
  109. import { onLoad } from '@dcloudio/uni-app'
  110. import { useCascaderAreaData } from '@vant/area-data'
  111. import { getDictTypeApi } from '@/services/modules/common'
  112. import type { DictItem } from '@/services/modules/common/type'
  113. import {
  114. getUserFilingPendingDetailApi,
  115. submitUserFilingApi,
  116. } from '@/services/modules/userInfo'
  117. import type {
  118. FilingDrugId,
  119. FilingDrugItem,
  120. UserFilingSubmitRequest,
  121. } from '@/services/modules/userInfo/type'
  122. interface PageOptions {
  123. entTaxCode?: string
  124. entName?: string
  125. }
  126. interface AreaOption {
  127. value: string
  128. text: string
  129. children?: AreaOption[]
  130. isLeaf?: boolean
  131. }
  132. interface AreaConfirmEvent {
  133. selectedOptions?: AreaOption[]
  134. }
  135. const entTaxCode = ref('')
  136. const entName = ref('')
  137. const loading = ref(true)
  138. const submitting = ref(false)
  139. const educationPickerVisible = ref(false)
  140. const majorPickerVisible = ref(false)
  141. const areaPickerVisible = ref(false)
  142. const drugPickerVisible = ref(false)
  143. const areaPickerValue = ref<string | number>()
  144. const educationList = ref<DictItem[]>([])
  145. const majorList = ref<DictItem[]>([])
  146. const drugList = ref<FilingDrugItem[]>([])
  147. const selectedDrugIds = ref<string[]>([])
  148. const form = reactive({
  149. filingNo: '',
  150. degree: '',
  151. degreeName: '',
  152. majorCategory: '',
  153. majorName: '',
  154. province: '',
  155. provinceCode: '',
  156. city: '',
  157. cityCode: '',
  158. areaName: '',
  159. })
  160. const rawAreaOptions = useCascaderAreaData() as AreaOption[]
  161. const areaOptions = convertCityLeaf(rawAreaOptions)
  162. const drugOptions = computed(() =>
  163. drugList.value.map((item) => ({
  164. label: item.drug_name,
  165. value: String(item.drug_id),
  166. }))
  167. )
  168. const selectedDrugText = computed(() => {
  169. if (!selectedDrugIds.value.length) return '默认选择全部'
  170. return `已选择${selectedDrugIds.value.length}个药品`
  171. })
  172. onLoad((options?: PageOptions) => {
  173. entTaxCode.value = decodeQueryValue(options?.entTaxCode)
  174. entName.value = decodeQueryValue(options?.entName)
  175. if (!entTaxCode.value) {
  176. loading.value = false
  177. uni.showModal({
  178. title: '提示',
  179. content: '缺少企业税号,无法加载备案信息',
  180. showCancel: false,
  181. success() {
  182. uni.navigateBack()
  183. },
  184. })
  185. return
  186. }
  187. void initPage()
  188. })
  189. const initPage = async () => {
  190. loading.value = true
  191. try {
  192. const [detailRes, degreeRes, majorRes] = await Promise.all([
  193. getUserFilingPendingDetailApi(entTaxCode.value),
  194. getDictTypeApi('user_degree'),
  195. getDictTypeApi('user_filing_major'),
  196. ])
  197. educationList.value = degreeRes.data ?? []
  198. majorList.value = majorRes.data ?? []
  199. const detail = detailRes.data
  200. const record = detail.record
  201. entName.value = detail.drugEntName || record.drugEntName || record.deptName || entName.value
  202. drugList.value = detail.optionalDrugs ?? []
  203. // 详情仅回填备案号,其余备案字段由用户本次重新选择。
  204. form.filingNo = record.filingNo ?? ''
  205. } catch (error) {
  206. console.error('获取待提交备案信息失败:', error)
  207. } finally {
  208. loading.value = false
  209. }
  210. }
  211. const openEducationPicker = () => {
  212. if (showEmptyOptions(educationList.value, '暂无可选学历')) return
  213. educationPickerVisible.value = true
  214. }
  215. const openMajorPicker = () => {
  216. if (showEmptyOptions(majorList.value, '暂无可选专业类别')) return
  217. majorPickerVisible.value = true
  218. }
  219. const openAreaPicker = () => {
  220. areaPickerVisible.value = true
  221. }
  222. const openDrugPicker = () => {
  223. if (showEmptyOptions(drugOptions.value, '暂无可选推广药品')) return
  224. drugPickerVisible.value = true
  225. }
  226. const confirmEducation = (event: PickerConfirmEvent) => {
  227. const item = event.selectedItems[0] as DictItem | undefined
  228. if (!item) return
  229. form.degree = item.value
  230. form.degreeName = item.label
  231. }
  232. const confirmMajor = (event: PickerConfirmEvent) => {
  233. const item = event.selectedItems[0] as DictItem | undefined
  234. if (!item) return
  235. form.majorCategory = item.value
  236. form.majorName = item.label
  237. }
  238. const confirmArea = (event: AreaConfirmEvent) => {
  239. const [province, city] = event.selectedOptions ?? []
  240. if (!province || !city) return
  241. form.province = province.text
  242. form.provinceCode = String(province.value)
  243. form.city = city.text
  244. form.cityCode = String(city.value)
  245. form.areaName = `${province.text}/${city.text}`
  246. areaPickerValue.value = city.value
  247. }
  248. const submit = async () => {
  249. if (submitting.value) return
  250. const payload: UserFilingSubmitRequest = {
  251. entTaxCode: entTaxCode.value,
  252. filingNo: form.filingNo.trim(),
  253. degree: form.degree,
  254. majorCategory: form.majorCategory,
  255. province: form.province,
  256. provinceCode: form.provinceCode,
  257. city: form.city,
  258. cityCode: form.cityCode,
  259. promotionDrugIds: resolvePromotionDrugIds(),
  260. }
  261. try {
  262. submitting.value = true
  263. await submitUserFilingApi(payload)
  264. uni.showToast({
  265. title: '提交成功',
  266. icon: 'success',
  267. })
  268. setTimeout(() => {
  269. uni.navigateBack()
  270. }, 800)
  271. } catch (error) {
  272. console.error('提交备案失败:', error)
  273. submitting.value = false
  274. }
  275. }
  276. const resolvePromotionDrugIds = (): FilingDrugId[] => {
  277. if (!selectedDrugIds.value.length) {
  278. return drugList.value.map((item) => item.drug_id)
  279. }
  280. return [...selectedDrugIds.value]
  281. }
  282. function convertCityLeaf(list: AreaOption[]): AreaOption[] {
  283. return list.map((province) => ({
  284. ...province,
  285. children: province.children?.map((city) => ({
  286. value: city.value,
  287. text: city.text,
  288. isLeaf: true,
  289. })),
  290. }))
  291. }
  292. const showEmptyOptions = (options: unknown[], message: string): boolean => {
  293. if (options.length) return false
  294. uni.showToast({
  295. title: message,
  296. icon: 'none',
  297. })
  298. return true
  299. }
  300. const decodeQueryValue = (value?: string): string => {
  301. if (!value) return ''
  302. try {
  303. return decodeURIComponent(value)
  304. } catch {
  305. return value
  306. }
  307. }
  308. </script>
  309. <style lang="scss" scoped>
  310. .filing-page {
  311. min-height: 100vh;
  312. padding: 28rpx 24rpx calc(150rpx + env(safe-area-inset-bottom));
  313. box-sizing: border-box;
  314. background: #f5f7fa;
  315. }
  316. .page-loading {
  317. min-height: 60vh;
  318. display: flex;
  319. flex-direction: column;
  320. align-items: center;
  321. justify-content: center;
  322. gap: 20rpx;
  323. color: #8a93a0;
  324. font-size: 26rpx;
  325. }
  326. .invitation-card,
  327. .form-card {
  328. background: #ffffff;
  329. border-radius: 24rpx;
  330. box-shadow: 0 10rpx 30rpx rgba(31, 45, 61, 0.06);
  331. }
  332. .invitation-card {
  333. padding: 30rpx;
  334. margin-bottom: 24rpx;
  335. }
  336. .invitation-card__title {
  337. margin-bottom: 14rpx;
  338. color: #1f2329;
  339. font-size: 32rpx;
  340. font-weight: 600;
  341. }
  342. .invitation-card__content {
  343. color: #68707c;
  344. font-size: 28rpx;
  345. line-height: 44rpx;
  346. }
  347. .company-name {
  348. color: #30343b;
  349. font-weight: 600;
  350. }
  351. .form-card {
  352. padding: 0 30rpx;
  353. overflow: hidden;
  354. }
  355. .form-row {
  356. min-height: 104rpx;
  357. display: flex;
  358. align-items: center;
  359. border-bottom: 1rpx solid #eeeeee;
  360. }
  361. .form-row--record {
  362. min-height: 140rpx;
  363. }
  364. .form-row--last {
  365. border-bottom: none;
  366. }
  367. .form-label {
  368. width: 230rpx;
  369. flex-shrink: 0;
  370. color: #4f5661;
  371. font-size: 28rpx;
  372. }
  373. .form-control {
  374. min-width: 0;
  375. flex: 1;
  376. display: flex;
  377. align-items: center;
  378. justify-content: flex-end;
  379. gap: 10rpx;
  380. text-align: right;
  381. }
  382. .form-control--column {
  383. flex-direction: column;
  384. align-items: flex-end;
  385. justify-content: center;
  386. }
  387. .optional-tag {
  388. padding: 5rpx 14rpx;
  389. border-radius: 30rpx;
  390. background: #edf3ff;
  391. color: #537cf3;
  392. font-size: 22rpx;
  393. line-height: 30rpx;
  394. }
  395. .record-input {
  396. width: 100%;
  397. height: 54rpx;
  398. color: #30343b;
  399. font-size: 28rpx;
  400. line-height: 54rpx;
  401. text-align: right;
  402. }
  403. .form-value,
  404. .form-placeholder {
  405. max-width: 370rpx;
  406. overflow: hidden;
  407. font-size: 28rpx;
  408. white-space: nowrap;
  409. text-overflow: ellipsis;
  410. }
  411. .form-value {
  412. color: #30343b;
  413. }
  414. .form-placeholder {
  415. color: #a9afb8;
  416. }
  417. .submit-section {
  418. position: fixed;
  419. z-index: 20;
  420. left: 0;
  421. right: 0;
  422. bottom: 0;
  423. padding: 20rpx 36rpx calc(20rpx + env(safe-area-inset-bottom));
  424. background: rgba(255, 255, 255, 0.96);
  425. box-shadow: 0 -8rpx 24rpx rgba(31, 45, 61, 0.08);
  426. }
  427. .submit-button {
  428. width: 100%;
  429. height: 88rpx;
  430. padding: 0;
  431. border: none;
  432. border-radius: 44rpx;
  433. background: #537cf3;
  434. color: #ffffff;
  435. font-size: 32rpx;
  436. font-weight: 600;
  437. line-height: 88rpx;
  438. &::after {
  439. border: none;
  440. }
  441. &[disabled] {
  442. opacity: 0.65;
  443. color: #ffffff;
  444. }
  445. }
  446. </style>