certification.vue 31 KB

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