certification.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140
  1. <template>
  2. <view class="container">
  3. <scroll-view class="content" scroll-y>
  4. <!-- 驳回意见提示 -->
  5. <view class="rejection-banner" v-if="rejectionInfo">
  6. <view class="rejection-icon">✕</view>
  7. <view class="rejection-text">驳回意见: {{ rejectionInfo }}</view>
  8. </view>
  9. <!-- 基本信息 -->
  10. <view class="form-section">
  11. <view class="section-header">
  12. <view class="section-indicator"></view>
  13. <text class="section-title">基本信息</text>
  14. </view>
  15. <view class="form-item">
  16. <view class="form-label">
  17. <text class="required">*</text>
  18. <text>姓名</text>
  19. </view>
  20. <input
  21. class="form-input"
  22. v-model="formData.doctorName"
  23. placeholder="请输入姓名"
  24. placeholder-class="placeholder"
  25. />
  26. </view>
  27. <view class="form-item">
  28. <view class="form-label">
  29. <text class="required">*</text>
  30. <text>身份证号</text>
  31. </view>
  32. <input
  33. class="form-input"
  34. v-model="formData.idCard"
  35. placeholder="请输入身份证号"
  36. maxlength="18"
  37. placeholder-class="placeholder"
  38. />
  39. </view>
  40. <view class="form-item">
  41. <view class="form-label">
  42. <text class="required">*</text>
  43. <text>账号身份</text>
  44. </view>
  45. <picker
  46. style="flex:1"
  47. mode="selector"
  48. :range="accountIdentityOptions"
  49. range-key="label"
  50. :value="getAccountIdentityIndex()"
  51. @change="onAccountIdentityChange"
  52. >
  53. <view class="form-input picker-input" :class="{ placeholder: !formData.accountType }">
  54. {{ getAccountIdentityLabel() || '请选择账号身份' }}
  55. <image class="w32 h32" src="/static/image/icon_my_more.png" mode=""></image>
  56. </view>
  57. </picker>
  58. </view>
  59. <view class="form-item">
  60. <view class="form-label">
  61. <text class="required">*</text>
  62. <text>机构</text>
  63. </view>
  64. <view style="flex:1" @click="openInstitutionPicker">
  65. <view class="form-input picker-input" :class="{ placeholder: !formData.institution }">
  66. {{ getInstitutionLabel() || '请选择机构' }}
  67. <image class="w32 h32" src="/static/image/icon_my_more.png" mode=""></image>
  68. </view>
  69. </view>
  70. </view>
  71. <view class="form-item">
  72. <view class="form-label">
  73. <text class="required">*</text>
  74. <text>科室</text>
  75. </view>
  76. <picker
  77. style="flex:1"
  78. mode="selector"
  79. :range="departmentList"
  80. range-key="label"
  81. :value="getDepartmentIndex()"
  82. @change="onDepartmentChange"
  83. >
  84. <view class="form-input picker-input" :class="{ placeholder: !formData.department }">
  85. {{ getDepartmentLabel() || '请选择科室' }}
  86. <image class="w32 h32" src="/static/image/icon_my_more.png" mode=""></image>
  87. </view>
  88. </picker>
  89. </view>
  90. <view class="form-item">
  91. <view class="form-label">
  92. <text class="required">*</text>
  93. <text>职称</text>
  94. </view>
  95. <picker
  96. style="flex:1"
  97. mode="selector"
  98. :range="titleList"
  99. range-key="label"
  100. :value="getTitleIndex()"
  101. @change="onTitleChange"
  102. >
  103. <view class="form-input picker-input" :class="{ placeholder: !formData.jobTitle }">
  104. {{ getTitleLabel() || '请选择职称' }}
  105. <image class="w32 h32" src="/static/image/icon_my_more.png" mode=""></image>
  106. </view>
  107. </picker>
  108. </view>
  109. </view>
  110. <!-- 身份证明 -->
  111. <view class="form-section">
  112. <view class="section-header">
  113. <view class="section-indicator"></view>
  114. <text class="section-title">身份证明</text>
  115. <view class="section-subtitle">以下资质任意选填其中一个</view>
  116. </view>
  117. <!-- 医师职业证 -->
  118. <view class="certificate-item">
  119. <view class="certificate-header">
  120. <view class="x-f">
  121. <view class="certificate-title">医师职业证</view>
  122. <view class="certificate-tip">-至少需上传编码页和执业点页</view>
  123. </view>
  124. <view class="example-btn" @click="goToPracticeExample">
  125. <image class="w28 h28" src="@/static/image/icon_example.png" mode=""></image>
  126. <text>示例</text>
  127. </view>
  128. </view>
  129. <view class="upload-grid">
  130. <view class="upload-item" v-for="(image, index) in formData.practiceCertificate" :key="index">
  131. <image class="uploaded-image" :src="image" mode="aspectFill" @click="previewImage(image, formData.practiceCertificate)"></image>
  132. <view class="delete-btn" @click="removePracticeImage(index)">×</view>
  133. </view>
  134. <view class="upload-item upload-placeholder" @click="choosePracticeImage" v-if="formData.practiceCertificate.length < 2">
  135. <image class="bg" src="@/static/image/img_idcard_Front.png" mode=""></image>
  136. <view class="img-btn">
  137. <image class="w56 h56" src="@/static/image/icon_uplodeidcard.png" mode=""></image>
  138. <text class="upload-text">点击上传</text>
  139. </view>
  140. </view>
  141. </view>
  142. </view>
  143. <!-- 医师职称证/工牌 -->
  144. <view class="certificate-item">
  145. <view class="certificate-header">
  146. <view class="certificate-title">医师职称证/工牌</view>
  147. <view class="example-btn" @click="goToTitleExample">
  148. <image class="w28 h28" src="@/static/image/icon_example.png" mode=""></image>
  149. <text>示例</text>
  150. </view>
  151. </view>
  152. <view class="upload-grid">
  153. <view class="upload-item" v-for="(image, index) in formData.titleCertificate" :key="index">
  154. <image class="uploaded-image" :src="image" mode="aspectFill" @click="previewImage(image, formData.titleCertificate)"></image>
  155. <view class="delete-btn" @click="removeTitleImage(index)">×</view>
  156. </view>
  157. <view class="upload-item upload-placeholder" @click="chooseTitleImage" v-if="formData.titleCertificate.length < 2">
  158. <image class="bg" src="@/static/image/img_idcard_Front.png" mode=""></image>
  159. <view class="img-btn">
  160. <image class="w56 h56" src="@/static/image/icon_uplodeidcard.png" mode=""></image>
  161. <text class="upload-text">点击上传</text>
  162. </view>
  163. </view>
  164. </view>
  165. </view>
  166. </view>
  167. <!-- 银行卡信息 -->
  168. <view class="form-section">
  169. <view class="section-header">
  170. <view class="section-indicator"></view>
  171. <text class="section-title">银行卡信息</text>
  172. </view>
  173. <view class="form-item">
  174. <view class="form-label">
  175. <text class="required">*</text>
  176. <text>开户行</text>
  177. </view>
  178. <picker
  179. style="flex:1"
  180. mode="selector"
  181. :range="bankList"
  182. range-key="label"
  183. :value="getBankIndex()"
  184. @change="onBankChange"
  185. >
  186. <view class="form-input picker-input" :class="{ placeholder: !formData.bankName }">
  187. {{ getBankLabel() || '请选择开户行' }}
  188. <image class="w32 h32" src="/static/image/icon_my_more.png" mode=""></image>
  189. </view>
  190. </picker>
  191. </view>
  192. <view class="form-item">
  193. <view class="form-label">
  194. <text class="required">*</text>
  195. <text>支行名称</text>
  196. </view>
  197. <input
  198. class="form-input"
  199. v-model="formData.bankBranch"
  200. placeholder="请输入支行名称"
  201. />
  202. </view>
  203. <view class="form-item">
  204. <view class="form-label">
  205. <text class="required">*</text>
  206. <text>银行卡号</text>
  207. </view>
  208. <input
  209. class="form-input"
  210. v-model="formData.bankCardNo"
  211. placeholder="请输入银行卡号"
  212. type="number"
  213. />
  214. </view>
  215. </view>
  216. </scroll-view>
  217. <!-- 机构搜索弹窗 -->
  218. <view class="search-picker-popup" v-if="showInstitutionPicker" @click="closeInstitutionPicker">
  219. <view class="popup-content" @click.stop>
  220. <view class="popup-header">
  221. <text class="popup-title">选择机构</text>
  222. <view class="popup-close" @click="closeInstitutionPicker">×</view>
  223. </view>
  224. <view class="search-box">
  225. <input
  226. class="search-input"
  227. v-model="institutionSearchKeyword"
  228. placeholder="请输入机构名称搜索"
  229. @input="onInstitutionSearch"
  230. />
  231. </view>
  232. <scroll-view class="search-list" scroll-y>
  233. <view
  234. class="search-item"
  235. v-for="(item, index) in filteredInstitutionList"
  236. :key="index"
  237. :class="{ active: formData.institution === item.label }"
  238. @click="selectInstitution(item)"
  239. >
  240. <text>{{ item.label }}</text>
  241. <text v-if="formData.institution === item.label" class="check-icon">✓</text>
  242. </view>
  243. <view class="search-empty" v-if="filteredInstitutionList.length === 0">
  244. <text>暂无匹配结果</text>
  245. </view>
  246. </scroll-view>
  247. </view>
  248. </view>
  249. <!-- 底部操作栏 -->
  250. <view class="bottom-bar">
  251. <view class="action-buttons">
  252. <view class="btn btn-cancel" @click="handleCancel">暂不认证</view>
  253. <view class="btn btn-submit" @click="handleSubmit">提交认证</view>
  254. </view>
  255. <view class="agreement-checkbox x-c">
  256. <checkbox-group @change="onAgreementChange">
  257. <label class="checkbox-label">
  258. <checkbox value="agree" :checked="agreed" color="#388BFF" />
  259. <text class="agreement-text">
  260. 我已阅读并同意
  261. <text class="link-text" @click.stop="goToUserAgreement">《用户协议》</text>
  262. <text class="link-text" @click.stop="goToInformedConsent">《知情同意书》</text>
  263. </text>
  264. </label>
  265. </checkbox-group>
  266. </view>
  267. </view>
  268. </view>
  269. </template>
  270. <script>
  271. import {
  272. submitCertification,
  273. getCertificationInfo,
  274. getCertificationStatus,
  275. identityType,
  276. getHospitalList,
  277. getDeptData,
  278. getBankTypeList
  279. } from '@/api/certification'
  280. import { uploadOSS } from '@/api/common'
  281. export default {
  282. data() {
  283. return {
  284. rejectionInfo: '', // 驳回意见
  285. agreed: false,
  286. certificationStatus: null, // 认证状态
  287. formData: {
  288. doctorName: '',
  289. idCard: '',
  290. accountType: '',
  291. institution: '',
  292. companyId: '',
  293. department: '',
  294. jobTitle: '',
  295. bankName: '',
  296. bankBranch: '',
  297. bankCardNo: '',
  298. practiceCertificate: [],
  299. titleCertificate: []
  300. },
  301. institutionList: [],
  302. departmentList: [],
  303. bankList: [],
  304. titleList: [],
  305. accountIdentityOptions: [],
  306. showInstitutionPicker: false,
  307. institutionSearchKeyword: '',
  308. filteredInstitutionList: [],
  309. }
  310. },
  311. onLoad(options) {
  312. const user = JSON.parse(uni.getStorageSync('userInfo'))
  313. this.formData.companyId = user.companyId
  314. if (options.rejectionInfo) {
  315. this.rejectionInfo = decodeURIComponent(options.rejectionInfo)
  316. }
  317. this.loadData()
  318. },
  319. methods: {
  320. extractArray(data) {
  321. if (Array.isArray(data)) return data
  322. const candidates = [
  323. data?.data,
  324. data?.list,
  325. data?.rows,
  326. data?.titleList,
  327. data?.hospitalList,
  328. data?.departmentList,
  329. data?.bankList
  330. ]
  331. return candidates.find(Array.isArray) || []
  332. },
  333. getAccountIdentityLabel() {
  334. return this.formData.accountType || ''
  335. },
  336. getAccountIdentityIndex() {
  337. if (!this.formData.accountType) return 0
  338. const idx = this.accountIdentityOptions.findIndex(opt => opt.label === this.formData.accountType)
  339. return idx >= 0 ? idx : 0
  340. },
  341. getInstitutionLabel() {
  342. return this.formData.institution || ''
  343. },
  344. getDepartmentLabel() {
  345. return this.formData.department || ''
  346. },
  347. getDepartmentIndex() {
  348. if (!this.formData.department) return 0
  349. const idx = this.departmentList.findIndex(opt => opt.label === this.formData.department)
  350. return idx >= 0 ? idx : 0
  351. },
  352. getBankLabel() {
  353. return this.formData.bankName || ''
  354. },
  355. getBankIndex() {
  356. if (!this.formData.bankName) return 0
  357. const idx = this.bankList.findIndex(opt => opt.label === this.formData.bankName)
  358. return idx >= 0 ? idx : 0
  359. },
  360. getTitleLabel() {
  361. return this.formData.jobTitle || ''
  362. },
  363. getTitleIndex() {
  364. if (!this.formData.jobTitle) return 0
  365. const idx = this.titleList.findIndex(opt => opt.label === this.formData.jobTitle)
  366. return idx >= 0 ? idx : 0
  367. },
  368. async loadData() {
  369. try {
  370. uni.showLoading({ title: '加载中...' })
  371. const [identityRes, bankRes, instRes, departRes, titleRes] = await Promise.all([
  372. identityType({ dictType: 'doctor_account_identity_type' }).catch(() => ({ code: 0 })),
  373. getBankTypeList().catch(() => ({ code: 0 })),
  374. getHospitalList().catch(() => ({ code: 0 })),
  375. getDeptData().catch(() => ({ code: 0 })),
  376. identityType({ dictType: 'hospital_title' }).catch(() => ({ code: 0 })),
  377. ])
  378. if (identityRes.code === 200) {
  379. const arr = this.extractArray(identityRes.data)
  380. this.accountIdentityOptions = arr.map(item => ({
  381. label: item.dictLabel || '',
  382. value: item.dictValue
  383. })).filter(opt => opt.label)
  384. }
  385. if (bankRes.code === 200) {
  386. const arr = this.extractArray(bankRes.data)
  387. this.bankList = arr.map(item => ({
  388. label: item.bankName || '',
  389. value: item.id
  390. })).filter(opt => opt.label)
  391. }
  392. if (instRes.code === 200) {
  393. const arr = this.extractArray(instRes.data)
  394. this.institutionList = arr.map(item => ({
  395. label: item.hospitalName || '',
  396. value: item.id
  397. })).filter(opt => opt.label)
  398. }
  399. if (departRes.code === 200) {
  400. const arr = this.extractArray(departRes.data)
  401. this.departmentList = arr.map(item => ({
  402. label: item.departmentName || '',
  403. value: item.id
  404. })).filter(opt => opt.label)
  405. }
  406. if (titleRes.code === 200) {
  407. const arr = this.extractArray(titleRes.data)
  408. this.titleList = arr.map(item => ({
  409. label: item.dictLabel || '',
  410. value: item.dictValue
  411. })).filter(opt => opt.label)
  412. }
  413. uni.hideLoading()
  414. } catch (e) {
  415. uni.hideLoading()
  416. console.error('加载数据失败', e)
  417. }
  418. },
  419. onAccountIdentityChange(e) {
  420. const selected = this.accountIdentityOptions[e.detail.value]
  421. if (selected) {
  422. this.formData.accountType = selected.label
  423. }
  424. },
  425. openInstitutionPicker() {
  426. this.showInstitutionPicker = true
  427. this.institutionSearchKeyword = ''
  428. this.filteredInstitutionList = this.institutionList
  429. },
  430. closeInstitutionPicker() {
  431. this.showInstitutionPicker = false
  432. },
  433. onInstitutionSearch() {
  434. const keyword = this.institutionSearchKeyword.trim().toLowerCase()
  435. if (!keyword) {
  436. this.filteredInstitutionList = this.institutionList
  437. } else {
  438. this.filteredInstitutionList = this.institutionList.filter(item =>
  439. item.label.toLowerCase().includes(keyword)
  440. )
  441. }
  442. },
  443. selectInstitution(item) {
  444. this.formData.institution = item.label
  445. this.closeInstitutionPicker()
  446. },
  447. onDepartmentChange(e) {
  448. const selected = this.departmentList[e.detail.value]
  449. if (selected) {
  450. this.formData.department = selected.label
  451. }
  452. },
  453. onBankChange(e) {
  454. const selected = this.bankList[e.detail.value]
  455. if (selected) {
  456. this.formData.bankName = selected.label
  457. }
  458. },
  459. onTitleChange(e) {
  460. const selected = this.titleList[e.detail.value]
  461. if (selected) {
  462. this.formData.jobTitle = selected.label
  463. }
  464. },
  465. onAgreementChange(e) {
  466. this.agreed = e.detail.value.includes('agree')
  467. },
  468. choosePracticeImage() {
  469. uni.chooseImage({
  470. count: 2 - this.formData.practiceCertificate.length,
  471. sizeType: ['compressed'],
  472. sourceType: ['album', 'camera'],
  473. success: (res) => {
  474. // 立即上传图片
  475. uni.showLoading({ title: '上传中...' })
  476. this.uploadImages(res.tempFilePaths, (urls) => {
  477. uni.hideLoading()
  478. // 保存上传后的URL
  479. this.formData.practiceCertificate = [...this.formData.practiceCertificate, ...urls]
  480. uni.showToast({ icon: 'success', title: '上传成功' })
  481. }, (error) => {
  482. uni.hideLoading()
  483. uni.showToast({ icon: 'none', title: error || '上传失败' })
  484. })
  485. }
  486. })
  487. },
  488. removePracticeImage(index) {
  489. this.formData.practiceCertificate.splice(index, 1)
  490. },
  491. async chooseTitleImage() {
  492. uni.chooseImage({
  493. count: 9 - this.formData.titleCertificate.length,
  494. sizeType: ['compressed'],
  495. sourceType: ['album', 'camera'],
  496. success: (res) => {
  497. // 立即上传图片
  498. uni.showLoading({ title: '上传中...' })
  499. this.uploadImages(res.tempFilePaths, (urls) => {
  500. uni.hideLoading()
  501. // 保存上传后的URL
  502. this.formData.titleCertificate = [...this.formData.titleCertificate, ...urls]
  503. uni.showToast({ icon: 'success', title: '上传成功' })
  504. }, (error) => {
  505. uni.hideLoading()
  506. uni.showToast({ icon: 'none', title: error || '上传失败' })
  507. })
  508. }
  509. })
  510. },
  511. // 上传多张图片(不使用 Promise)
  512. uploadImages(filePaths, successCallback, failCallback) {
  513. const requestPath = uni.getStorageSync('requestPath') || 'http://t9794bec.natappfree.cc'
  514. const urls = []
  515. let completed = 0
  516. let hasError = false
  517. if (filePaths.length === 0) {
  518. successCallback([])
  519. return
  520. }
  521. filePaths.forEach((filePath, index) => {
  522. uni.uploadFile({
  523. url: `${requestPath}/app/common/uploadOSS`,
  524. filePath: filePath,
  525. name: 'file',
  526. success: (uploadRes) => {
  527. if (hasError) return
  528. try {
  529. const result = typeof uploadRes.data === 'string' ? JSON.parse(uploadRes.data) : uploadRes.data
  530. if (result.code == 200) {
  531. urls[index] = result.url || result.data?.url || ''
  532. } else {
  533. hasError = true
  534. failCallback(result.msg || '上传失败')
  535. return
  536. }
  537. } catch (e) {
  538. hasError = true
  539. failCallback('解析上传结果失败')
  540. return
  541. }
  542. completed++
  543. if (completed === filePaths.length) {
  544. successCallback(urls.filter(Boolean))
  545. }
  546. },
  547. fail: (err) => {
  548. if (hasError) return
  549. hasError = true
  550. failCallback('上传失败')
  551. }
  552. })
  553. })
  554. },
  555. removeTitleImage(index) {
  556. this.formData.titleCertificate.splice(index, 1)
  557. },
  558. previewImage(current, urls) {
  559. uni.previewImage({
  560. current: current,
  561. urls: urls
  562. })
  563. },
  564. goToPracticeExample() {
  565. uni.navigateTo({
  566. url: '/pages_user/practiceCertificateExample'
  567. })
  568. },
  569. goToTitleExample() {
  570. uni.navigateTo({
  571. url: '/pages_user/certificationExample'
  572. })
  573. },
  574. goToUserAgreement() {
  575. uni.navigateTo({
  576. url: '/pages_user/userAgreement'
  577. })
  578. },
  579. goToInformedConsent() {
  580. uni.navigateTo({
  581. url: '/pages_user/informedConsent'
  582. })
  583. },
  584. handleCancel() {
  585. uni.switchTab({
  586. url: '/pages/user/index'
  587. })
  588. },
  589. async handleSubmit() {
  590. if (!this.formData.doctorName) {
  591. return uni.showToast({ icon: 'none', title: '请输入姓名' })
  592. }
  593. if (!this.formData.idCard) {
  594. return uni.showToast({ icon: 'none', title: '请输入身份证号' })
  595. }
  596. if (!this.formData.accountType) {
  597. return uni.showToast({ icon: 'none', title: '请选择账号身份' })
  598. }
  599. if (!this.formData.institution) {
  600. return uni.showToast({ icon: 'none', title: '请选择机构' })
  601. }
  602. if (!this.formData.department) {
  603. return uni.showToast({ icon: 'none', title: '请选择科室' })
  604. }
  605. if (!this.formData.jobTitle) {
  606. return uni.showToast({ icon: 'none', title: '请选择职称' })
  607. }
  608. if (this.formData.practiceCertificate.length === 0 && this.formData.titleCertificate.length === 0) {
  609. return uni.showToast({ icon: 'none', title: '请至少上传一种身份证明' })
  610. }
  611. if (this.formData.practiceCertificate.length > 0 && this.formData.practiceCertificate.length < 2) {
  612. return uni.showToast({ icon: 'none', title: '医师职业证至少需上传编码页和执业点页' })
  613. }
  614. if (!this.formData.bankName) {
  615. return uni.showToast({ icon: 'none', title: '请选择开户行' })
  616. }
  617. if (!this.formData.bankBranch) {
  618. return uni.showToast({ icon: 'none', title: '请输入支行名称' })
  619. }
  620. if (!this.formData.bankCardNo) {
  621. return uni.showToast({ icon: 'none', title: '请输入银行卡号' })
  622. }
  623. if (!this.agreed) {
  624. return uni.showToast({ icon: 'none', title: '请阅读并同意用户协议和知情同意书' })
  625. }
  626. try {
  627. uni.showLoading({ title: '提交中...' })
  628. const licenseImageUrls = this.formData.practiceCertificate.filter(url => url && (url.startsWith('http://') || url.startsWith('https://')))
  629. const titleCertImageUrls = this.formData.titleCertificate.filter(url => url && (url.startsWith('http://') || url.startsWith('https://')))
  630. const submitData = {
  631. accountType: this.formData.accountType,
  632. bankBranch: this.formData.bankBranch,
  633. bankCardNo: this.formData.bankCardNo,
  634. bankName: this.formData.bankName,
  635. companyId: this.formData.companyId,
  636. companyName: this.formData.institution,
  637. department: this.formData.department,
  638. doctorName: this.formData.doctorName,
  639. idCard: this.formData.idCard,
  640. institution: this.formData.institution,
  641. jobTitle: this.formData.jobTitle,
  642. licenseImage: licenseImageUrls.join(','),
  643. titleCertImage: titleCertImageUrls.join(',')
  644. }
  645. if (this.formData.id) {
  646. submitData.id = this.formData.id
  647. }
  648. const res = await submitCertification(submitData)
  649. uni.hideLoading()
  650. if (res.code === 200) {
  651. uni.showToast({ icon: 'success', title: '提交成功' })
  652. setTimeout(() => {
  653. uni.switchTab({ url: '/pages/user/index' })
  654. }, 1500)
  655. } else {
  656. uni.showToast({ icon: 'none', title: res.msg || '提交失败' })
  657. }
  658. } catch (e) {
  659. uni.hideLoading()
  660. console.error('提交认证失败', e)
  661. uni.showToast({ icon: 'none', title: e.message || '提交失败' })
  662. }
  663. }
  664. }
  665. }
  666. </script>
  667. <style lang="stylus">
  668. .text-placeholder{
  669. color: #C8C9CC !important;
  670. }
  671. </style>
  672. <style lang="scss" scoped>
  673. .container {
  674. min-height: 100vh;
  675. background: #f5f5f5;
  676. display: flex;
  677. flex-direction: column;
  678. }
  679. .content {
  680. flex: 1;
  681. padding-bottom: 200rpx;
  682. box-sizing: border-box;
  683. }
  684. .rejection-banner {
  685. display: flex;
  686. align-items: center;
  687. padding: 24rpx;
  688. background: #FF5030;
  689. color: #fff;
  690. margin: 24rpx;
  691. border-radius: 8rpx;
  692. .rejection-icon {
  693. width: 40rpx;
  694. height: 40rpx;
  695. border-radius: 50%;
  696. background: rgba(255, 255, 255, 0.3);
  697. display: flex;
  698. align-items: center;
  699. justify-content: center;
  700. font-size: 24rpx;
  701. margin-right: 16rpx;
  702. }
  703. .rejection-text {
  704. flex: 1;
  705. font-size: 28rpx;
  706. line-height: 1.5;
  707. }
  708. }
  709. .form-section {
  710. background: #fff;
  711. margin: 20rpx;
  712. border-radius: 16rpx;
  713. padding: 32rpx;
  714. .section-header {
  715. display: flex;
  716. align-items: center;
  717. margin-bottom: 32rpx;
  718. .section-indicator {
  719. width: 6rpx;
  720. height: 32rpx;
  721. background: #388BFF;
  722. border-radius: 3rpx;
  723. margin-right: 16rpx;
  724. }
  725. .section-title {
  726. font-size: 32rpx;
  727. font-weight: bold;
  728. color: #333;
  729. }
  730. }
  731. .section-subtitle {
  732. font-size: 24rpx;
  733. color: #999;
  734. //margin-bottom: 24rpx;
  735. margin-left: 22rpx;
  736. }
  737. .form-item {
  738. margin-bottom: 32rpx;
  739. display: flex;
  740. align-items: center;
  741. border-bottom: 1px solid #EBEDF0;
  742. &:last-child {
  743. border-bottom: 0;
  744. margin-bottom: 0;
  745. }
  746. .form-label {
  747. display: flex;
  748. align-items: center;
  749. font-size: 28rpx;
  750. color: #333;
  751. // margin-bottom: 16rpx;
  752. width: 160rpx;
  753. .required {
  754. color: #FF5030;
  755. margin-right: 4rpx;
  756. }
  757. }
  758. .form-input {
  759. // width: 100%;
  760. flex:1;
  761. height: 80rpx;
  762. font-size: 28rpx;
  763. &.picker-input {
  764. display: flex;
  765. align-items: center;
  766. justify-content: space-between;
  767. gap: 16rpx;
  768. }
  769. &.placeholder {
  770. color: #C8C9CC !important;
  771. }
  772. }
  773. .radio-group {
  774. flex: 1;
  775. display: flex;
  776. align-items: center;
  777. gap: 48rpx;
  778. height: 80rpx;
  779. .radio-item {
  780. display: flex;
  781. align-items: center;
  782. gap: 8rpx;
  783. .radio-text {
  784. font-size: 28rpx;
  785. color: #333;
  786. }
  787. }
  788. // padding: 0 24rpx;
  789. font-size: 28rpx;
  790. color: #333;
  791. &.placeholder {
  792. color: #C8C9CC;
  793. }
  794. &.picker-input {
  795. display: flex;
  796. align-items: center;
  797. justify-content: space-between;
  798. .arrow-right {
  799. font-size: 32rpx;
  800. color: #999;
  801. }
  802. }
  803. }
  804. }
  805. .certificate-item {
  806. padding-bottom: 32rpx;
  807. margin-bottom: 32rpx;
  808. border-bottom: 2rpx solid #F5F5F5;
  809. &:last-child {
  810. margin-bottom: 0;
  811. border-bottom: 0;
  812. }
  813. .certificate-header {
  814. display: flex;
  815. align-items: center;
  816. justify-content: space-between;
  817. margin-bottom: 24rpx;
  818. .certificate-title {
  819. font-size: 28rpx;
  820. color: #333;
  821. font-weight: 500;
  822. }
  823. .example-btn {
  824. display: flex;
  825. align-items: center;
  826. gap: 8rpx;
  827. font-size: 24rpx;
  828. color: #388BFF;
  829. .example-icon {
  830. width: 32rpx;
  831. height: 32rpx;
  832. border-radius: 50%;
  833. background: #E6F7FF;
  834. display: flex;
  835. align-items: center;
  836. justify-content: center;
  837. font-size: 20rpx;
  838. color: #388BFF;
  839. }
  840. }
  841. }
  842. .certificate-tip {
  843. font-size: 24rpx;
  844. color: #999;
  845. // margin-bottom: 16rpx;
  846. }
  847. .upload-grid {
  848. display: flex;
  849. gap: 16rpx;
  850. flex-wrap: wrap;
  851. .upload-item {
  852. width: 310rpx;
  853. height: 176rpx;
  854. border-radius: 8rpx;
  855. overflow: hidden;
  856. position: relative;
  857. .uploaded-image {
  858. width: 100%;
  859. height: 100%;
  860. }
  861. .bg{
  862. width: 100%;
  863. height: 100%;
  864. position: absolute;
  865. top: 0;
  866. left: 0;
  867. }
  868. .delete-btn {
  869. position: absolute;
  870. top: 0;
  871. right: 0;
  872. width: 40rpx;
  873. height: 40rpx;
  874. line-height: 40rpx;
  875. background: rgba(0, 0, 0, 0.5);
  876. border-radius: 50%;
  877. display: flex;
  878. align-items: center;
  879. justify-content: center;
  880. font-size: 32rpx;
  881. color: #fff;
  882. }
  883. &.upload-placeholder {
  884. background: #f5f5f5;
  885. display: flex;
  886. flex-direction: column;
  887. align-items: center;
  888. justify-content: center;
  889. // border: 2rpx dashed #ddd;
  890. .img-btn{
  891. z-index: 1;
  892. display: flex;
  893. flex-direction: column;
  894. align-items: center;
  895. justify-content: center;
  896. }
  897. .camera-icon {
  898. font-size: 60rpx;
  899. margin-bottom: 8rpx;
  900. }
  901. .upload-text {
  902. font-size: 24rpx;
  903. color: #388BFF;
  904. }
  905. }
  906. }
  907. }
  908. }
  909. }
  910. .bottom-bar {
  911. position: fixed;
  912. bottom: 0;
  913. left: 0;
  914. right: 0;
  915. background: #fff;
  916. padding: 24rpx;
  917. // border-top: 1rpx solid #f0f0f0;
  918. z-index: 100;
  919. .agreement-checkbox {
  920. margin-bottom: 24rpx;
  921. margin-top: 20rpx;
  922. .checkbox-label {
  923. display: flex;
  924. align-items: flex-start;
  925. font-size: 24rpx;
  926. color: #666;
  927. .agreement-text {
  928. margin-left: 8rpx;
  929. line-height: 1.5;
  930. .link-text {
  931. color: #388BFF;
  932. }
  933. }
  934. }
  935. }
  936. .action-buttons {
  937. display: flex;
  938. gap: 24rpx;
  939. .btn {
  940. flex: 1;
  941. height: 88rpx;
  942. display: flex;
  943. align-items: center;
  944. justify-content: center;
  945. border-radius: 8rpx;
  946. font-size: 32rpx;
  947. font-weight: 500;
  948. border-radius: 200rpx 200rpx 200rpx 200rpx;
  949. &.btn-cancel {
  950. background: #fff;
  951. border: 2rpx solid #388BFF;
  952. color: #388BFF;
  953. }
  954. &.btn-submit {
  955. background: #388BFF;
  956. color: #fff;
  957. }
  958. }
  959. }
  960. }
  961. .search-picker-popup {
  962. position: fixed;
  963. top: 0;
  964. left: 0;
  965. right: 0;
  966. bottom: 0;
  967. background: rgba(0, 0, 0, 0.5);
  968. z-index: 1000;
  969. display: flex;
  970. align-items: flex-end;
  971. .popup-content {
  972. width: 100%;
  973. max-height: 80vh;
  974. background: #fff;
  975. border-radius: 24rpx 24rpx 0 0;
  976. display: flex;
  977. flex-direction: column;
  978. }
  979. .popup-header {
  980. display: flex;
  981. align-items: center;
  982. justify-content: space-between;
  983. padding: 32rpx;
  984. border-bottom: 1rpx solid #f0f0f0;
  985. .popup-title {
  986. font-size: 32rpx;
  987. font-weight: bold;
  988. color: #333;
  989. }
  990. .popup-close {
  991. font-size: 48rpx;
  992. color: #999;
  993. line-height: 1;
  994. }
  995. }
  996. .search-box {
  997. padding: 24rpx 32rpx;
  998. .search-input {
  999. width: 100%;
  1000. height: 72rpx;
  1001. background: #f5f5f5;
  1002. border-radius: 36rpx;
  1003. padding: 0 32rpx;
  1004. font-size: 28rpx;
  1005. }
  1006. }
  1007. .search-list {
  1008. flex: 1;
  1009. max-height: 60vh;
  1010. padding: 0 32rpx;
  1011. .search-item {
  1012. display: flex;
  1013. align-items: center;
  1014. justify-content: space-between;
  1015. padding: 28rpx 0;
  1016. border-bottom: 1rpx solid #f5f5f5;
  1017. &:last-child {
  1018. border-bottom: none;
  1019. }
  1020. &.active {
  1021. color: #388BFF;
  1022. text {
  1023. color: #388BFF;
  1024. }
  1025. }
  1026. .check-icon {
  1027. color: #388BFF;
  1028. font-size: 32rpx;
  1029. font-weight: bold;
  1030. }
  1031. text {
  1032. font-size: 28rpx;
  1033. color: #333;
  1034. }
  1035. }
  1036. .search-empty {
  1037. display: flex;
  1038. align-items: center;
  1039. justify-content: center;
  1040. padding: 60rpx 0;
  1041. text {
  1042. font-size: 28rpx;
  1043. color: #999;
  1044. }
  1045. }
  1046. }
  1047. }
  1048. </style>