certification.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  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.name"
  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.idNumber"
  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. <input
  46. class="form-input"
  47. v-model="formData.accountIdentity"
  48. placeholder="请输入账号身份"
  49. placeholder-class="placeholder"
  50. />
  51. </view>
  52. <view class="form-item">
  53. <view class="form-label">
  54. <text class="required">*</text>
  55. <text>机构</text>
  56. </view>
  57. <picker style="flex:1" mode="selector" :range="institutionList" range-key="name" @change="onInstitutionChange">
  58. <view class="form-input picker-input" :class="{ placeholder: !formData.institution }">
  59. {{ formData.institution || '请选择机构' }}
  60. <text class="arrow-right">></text>
  61. </view>
  62. </picker>
  63. </view>
  64. <view class="form-item">
  65. <view class="form-label">
  66. <text class="required">*</text>
  67. <text>科室</text>
  68. </view>
  69. <picker style="flex:1" mode="selector" :range="departmentList" range-key="name" @change="onDepartmentChange">
  70. <view class="form-input picker-input" :class="{ placeholder: !formData.department }">
  71. {{ formData.department || '请选择科室' }}
  72. <text class="arrow-right">></text>
  73. </view>
  74. </picker>
  75. </view>
  76. <view class="form-item">
  77. <view class="form-label">
  78. <text class="required">*</text>
  79. <text>职称</text>
  80. </view>
  81. <picker mode="selector" :range="titleList" range-key="name" @change="onTitleChange">
  82. <view class="form-input picker-input" :class="{ placeholder: !formData.title }">
  83. {{ formData.title || '请选择职称' }}
  84. <text class="arrow-right">></text>
  85. </view>
  86. </picker>
  87. </view>
  88. </view>
  89. <!-- 身份证明 -->
  90. <view class="form-section">
  91. <view class="section-header">
  92. <view class="section-indicator"></view>
  93. <text class="section-title">身份证明</text>
  94. </view>
  95. <view class="section-subtitle">以下资质任意选填其中一个</view>
  96. <!-- 医师职业证 -->
  97. <view class="certificate-item">
  98. <view class="certificate-header">
  99. <view class="certificate-title">医师职业证</view>
  100. <view class="example-btn" @click="goToPracticeExample">
  101. <text class="example-icon">①</text>
  102. <text>示例</text>
  103. </view>
  104. </view>
  105. <view class="certificate-tip">至少需上传编码页和执业点页</view>
  106. <view class="upload-grid">
  107. <view class="upload-item" v-for="(image, index) in formData.practiceCertificate" :key="index">
  108. <image class="uploaded-image" :src="image" mode="aspectFill" @click="previewImage(image, formData.practiceCertificate)"></image>
  109. <view class="delete-btn" @click="removePracticeImage(index)">×</view>
  110. </view>
  111. <view class="upload-item upload-placeholder" @click="choosePracticeImage" v-if="formData.practiceCertificate.length < 2">
  112. <text class="camera-icon">📷</text>
  113. <text class="upload-text">点击上传</text>
  114. </view>
  115. </view>
  116. </view>
  117. <!-- 医师职称证/工牌 -->
  118. <view class="certificate-item">
  119. <view class="certificate-header">
  120. <view class="certificate-title">医师职称证/工牌</view>
  121. <view class="example-btn" @click="goToTitleExample">
  122. <text class="example-icon">①</text>
  123. <text>示例</text>
  124. </view>
  125. </view>
  126. <view class="upload-grid">
  127. <view class="upload-item" v-for="(image, index) in formData.titleCertificate" :key="index">
  128. <image class="uploaded-image" :src="image" mode="aspectFill" @click="previewImage(image, formData.titleCertificate)"></image>
  129. <view class="delete-btn" @click="removeTitleImage(index)">×</view>
  130. </view>
  131. <view class="upload-item upload-placeholder" @click="chooseTitleImage">
  132. <text class="camera-icon">📷</text>
  133. <text class="upload-text">点击上传</text>
  134. </view>
  135. </view>
  136. </view>
  137. </view>
  138. <!-- 银行卡信息 -->
  139. <view class="form-section">
  140. <view class="section-header">
  141. <view class="section-indicator"></view>
  142. <text class="section-title">银行卡信息</text>
  143. </view>
  144. <view class="form-item">
  145. <view class="form-label">
  146. <text class="required">*</text>
  147. <text>开户行</text>
  148. </view>
  149. <picker mode="selector" :range="bankList" range-key="name" @change="onBankChange">
  150. <view class="form-input picker-input" :class="{ placeholder: !formData.bank }">
  151. {{ formData.bank || '请选择开户行' }}
  152. <text class="arrow-right">></text>
  153. </view>
  154. </picker>
  155. </view>
  156. <view class="form-item">
  157. <view class="form-label">
  158. <text class="required">*</text>
  159. <text>支行名称</text>
  160. </view>
  161. <input
  162. class="form-input"
  163. v-model="formData.branchName"
  164. placeholder="请输入支行名称"
  165. />
  166. </view>
  167. <view class="form-item">
  168. <view class="form-label">
  169. <text class="required">*</text>
  170. <text>银行卡号</text>
  171. </view>
  172. <input
  173. class="form-input"
  174. v-model="formData.bankCardNumber"
  175. placeholder="请输入银行卡号"
  176. type="number"
  177. />
  178. </view>
  179. </view>
  180. </scroll-view>
  181. <!-- 底部操作栏 -->
  182. <view class="bottom-bar">
  183. <view class="agreement-checkbox">
  184. <checkbox-group @change="onAgreementChange">
  185. <label class="checkbox-label">
  186. <checkbox value="agree" :checked="agreed" color="#388BFF" />
  187. <text class="agreement-text">
  188. 我已阅读并同意
  189. <text class="link-text" @click.stop="goToUserAgreement">《用户协议》</text>、
  190. <text class="link-text" @click.stop="goToInformedConsent">《知情同意书》</text>
  191. </text>
  192. </label>
  193. </checkbox-group>
  194. </view>
  195. <view class="action-buttons">
  196. <view class="btn btn-cancel" @click="handleCancel">暂不认证</view>
  197. <view class="btn btn-submit" @click="handleSubmit">提交认证</view>
  198. </view>
  199. </view>
  200. </view>
  201. </template>
  202. <script>
  203. import { submitCertification, getCertificationInfo } from '@/api-js/certification'
  204. export default {
  205. data() {
  206. return {
  207. rejectionInfo: '', // 驳回意见
  208. agreed: false,
  209. formData: {
  210. name: '',
  211. idNumber: '',
  212. accountIdentity: '',
  213. institution: '',
  214. department: '',
  215. title: '',
  216. practiceCertificate: [], // 医师职业证图片数组
  217. titleCertificate: [], // 医师职称证/工牌图片数组
  218. bank: '',
  219. branchName: '',
  220. bankCardNumber: ''
  221. },
  222. institutionList: [],
  223. departmentList: [],
  224. titleList: [],
  225. bankList: []
  226. }
  227. },
  228. onLoad(options) {
  229. if (options.rejectionInfo) {
  230. this.rejectionInfo = decodeURIComponent(options.rejectionInfo)
  231. }
  232. this.institutionList = [
  233. { name: '机构1', id: 1 },
  234. { name: '机构2', id: 2 }
  235. ]
  236. this.departmentList = [
  237. { name: '内科', id: 1 },
  238. { name: '外科', id: 2 },
  239. { name: '儿科', id: 3 }
  240. ]
  241. this.titleList = [
  242. { name: '主任医师', id: 1 },
  243. { name: '副主任医师', id: 2 },
  244. { name: '主治医师', id: 3 }
  245. ]
  246. this.bankList = [
  247. { name: '中国工商银行', id: 1 },
  248. { name: '中国建设银行', id: 2 },
  249. { name: '中国银行', id: 3 }
  250. ]
  251. //this.loadData()
  252. },
  253. methods: {
  254. async loadData() {
  255. try {
  256. // 加载认证信息(如果已提交过)
  257. const res = await getCertificationInfo()
  258. if (res.code === 200 && res.data) {
  259. this.formData = { ...this.formData, ...res.data }
  260. if (res.data.rejectionInfo) {
  261. this.rejectionInfo = res.data.rejectionInfo
  262. }
  263. }
  264. // 加载选项数据
  265. this.loadOptions()
  266. } catch (e) {
  267. console.error('加载数据失败', e)
  268. this.loadOptions()
  269. }
  270. },
  271. loadOptions() {
  272. // 这里应该从API获取选项数据
  273. this.institutionList = [
  274. { name: '机构1', id: 1 },
  275. { name: '机构2', id: 2 }
  276. ]
  277. this.departmentList = [
  278. { name: '内科', id: 1 },
  279. { name: '外科', id: 2 },
  280. { name: '儿科', id: 3 }
  281. ]
  282. this.titleList = [
  283. { name: '主任医师', id: 1 },
  284. { name: '副主任医师', id: 2 },
  285. { name: '主治医师', id: 3 }
  286. ]
  287. this.bankList = [
  288. { name: '中国工商银行', id: 1 },
  289. { name: '中国建设银行', id: 2 },
  290. { name: '中国银行', id: 3 }
  291. ]
  292. },
  293. onInstitutionChange(e) {
  294. const index = e.detail.value
  295. this.formData.institution = this.institutionList[index].name
  296. },
  297. onDepartmentChange(e) {
  298. const index = e.detail.value
  299. this.formData.department = this.departmentList[index].name
  300. },
  301. onTitleChange(e) {
  302. const index = e.detail.value
  303. this.formData.title = this.titleList[index].name
  304. },
  305. onBankChange(e) {
  306. const index = e.detail.value
  307. this.formData.bank = this.bankList[index].name
  308. },
  309. onAgreementChange(e) {
  310. this.agreed = e.detail.value.includes('agree')
  311. },
  312. choosePracticeImage() {
  313. uni.chooseImage({
  314. count: 2 - this.formData.practiceCertificate.length,
  315. sizeType: ['compressed'],
  316. sourceType: ['album', 'camera'],
  317. success: (res) => {
  318. this.formData.practiceCertificate = [...this.formData.practiceCertificate, ...res.tempFilePaths]
  319. }
  320. })
  321. },
  322. removePracticeImage(index) {
  323. this.formData.practiceCertificate.splice(index, 1)
  324. },
  325. chooseTitleImage() {
  326. uni.chooseImage({
  327. count: 9 - this.formData.titleCertificate.length,
  328. sizeType: ['compressed'],
  329. sourceType: ['album', 'camera'],
  330. success: (res) => {
  331. this.formData.titleCertificate = [...this.formData.titleCertificate, ...res.tempFilePaths]
  332. }
  333. })
  334. },
  335. removeTitleImage(index) {
  336. this.formData.titleCertificate.splice(index, 1)
  337. },
  338. previewImage(current, urls) {
  339. uni.previewImage({
  340. current: current,
  341. urls: urls
  342. })
  343. },
  344. goToPracticeExample() {
  345. uni.navigateTo({
  346. url: '/pages_user/practiceCertificateExample'
  347. })
  348. },
  349. goToTitleExample() {
  350. uni.navigateTo({
  351. url: '/pages_user/certificationExample'
  352. })
  353. },
  354. goToUserAgreement() {
  355. uni.navigateTo({
  356. url: '/pages_user/userAgreement'
  357. })
  358. },
  359. goToInformedConsent() {
  360. uni.navigateTo({
  361. url: '/pages_user/informedConsent'
  362. })
  363. },
  364. handleCancel() {
  365. uni.navigateBack()
  366. },
  367. async handleSubmit() {
  368. // 表单验证
  369. if (!this.formData.name) {
  370. uni.showToast({
  371. icon: 'none',
  372. title: '请输入姓名'
  373. })
  374. return
  375. }
  376. if (!this.formData.idNumber) {
  377. uni.showToast({
  378. icon: 'none',
  379. title: '请输入身份证号'
  380. })
  381. return
  382. }
  383. if (!this.formData.accountIdentity) {
  384. uni.showToast({
  385. icon: 'none',
  386. title: '请输入账号身份'
  387. })
  388. return
  389. }
  390. if (!this.formData.institution) {
  391. uni.showToast({
  392. icon: 'none',
  393. title: '请选择机构'
  394. })
  395. return
  396. }
  397. if (!this.formData.department) {
  398. uni.showToast({
  399. icon: 'none',
  400. title: '请选择科室'
  401. })
  402. return
  403. }
  404. if (!this.formData.title) {
  405. uni.showToast({
  406. icon: 'none',
  407. title: '请选择职称'
  408. })
  409. return
  410. }
  411. // 至少上传一种证件
  412. if (this.formData.practiceCertificate.length === 0 && this.formData.titleCertificate.length === 0) {
  413. uni.showToast({
  414. icon: 'none',
  415. title: '请至少上传一种身份证明'
  416. })
  417. return
  418. }
  419. // 医师职业证至少上传2张
  420. if (this.formData.practiceCertificate.length > 0 && this.formData.practiceCertificate.length < 2) {
  421. uni.showToast({
  422. icon: 'none',
  423. title: '医师职业证至少需上传编码页和执业点页'
  424. })
  425. return
  426. }
  427. if (!this.formData.bank) {
  428. uni.showToast({
  429. icon: 'none',
  430. title: '请选择开户行'
  431. })
  432. return
  433. }
  434. if (!this.formData.branchName) {
  435. uni.showToast({
  436. icon: 'none',
  437. title: '请输入支行名称'
  438. })
  439. return
  440. }
  441. if (!this.formData.bankCardNumber) {
  442. uni.showToast({
  443. icon: 'none',
  444. title: '请输入银行卡号'
  445. })
  446. return
  447. }
  448. if (!this.agreed) {
  449. uni.showToast({
  450. icon: 'none',
  451. title: '请阅读并同意用户协议和知情同意书'
  452. })
  453. return
  454. }
  455. try {
  456. uni.showLoading({ title: '提交中...' })
  457. const res = await submitCertification(this.formData)
  458. uni.hideLoading()
  459. if (res.code === 200) {
  460. uni.showToast({
  461. icon: 'success',
  462. title: '提交成功'
  463. })
  464. setTimeout(() => {
  465. uni.navigateBack()
  466. }, 1500)
  467. } else {
  468. uni.showToast({
  469. icon: 'none',
  470. title: res.msg || '提交失败'
  471. })
  472. }
  473. } catch (e) {
  474. uni.hideLoading()
  475. uni.showToast({
  476. icon: 'none',
  477. title: '提交失败'
  478. })
  479. }
  480. }
  481. }
  482. }
  483. </script>
  484. <style lang="scss" scoped>
  485. .container {
  486. min-height: 100vh;
  487. background: #f5f5f5;
  488. display: flex;
  489. flex-direction: column;
  490. }
  491. .content {
  492. flex: 1;
  493. padding-bottom: 200rpx;
  494. box-sizing: border-box;
  495. }
  496. .rejection-banner {
  497. display: flex;
  498. align-items: center;
  499. padding: 24rpx;
  500. background: #FF5030;
  501. color: #fff;
  502. margin: 24rpx;
  503. border-radius: 8rpx;
  504. .rejection-icon {
  505. width: 40rpx;
  506. height: 40rpx;
  507. border-radius: 50%;
  508. background: rgba(255, 255, 255, 0.3);
  509. display: flex;
  510. align-items: center;
  511. justify-content: center;
  512. font-size: 24rpx;
  513. margin-right: 16rpx;
  514. }
  515. .rejection-text {
  516. flex: 1;
  517. font-size: 28rpx;
  518. line-height: 1.5;
  519. }
  520. }
  521. .form-section {
  522. background: #fff;
  523. margin: 20rpx;
  524. border-radius: 16rpx;
  525. padding: 32rpx;
  526. .section-header {
  527. display: flex;
  528. align-items: center;
  529. margin-bottom: 24rpx;
  530. .section-indicator {
  531. width: 6rpx;
  532. height: 32rpx;
  533. background: #388BFF;
  534. border-radius: 3rpx;
  535. margin-right: 16rpx;
  536. }
  537. .section-title {
  538. font-size: 32rpx;
  539. font-weight: bold;
  540. color: #333;
  541. }
  542. }
  543. .section-subtitle {
  544. font-size: 24rpx;
  545. color: #999;
  546. margin-bottom: 24rpx;
  547. margin-left: 22rpx;
  548. }
  549. .form-item {
  550. margin-bottom: 32rpx;
  551. display: flex;
  552. align-items: center;
  553. border-bottom: 1px solid #EBEDF0;
  554. &:last-child {
  555. border-bottom: 0;
  556. margin-bottom: 0;
  557. }
  558. .form-label {
  559. display: flex;
  560. align-items: center;
  561. font-size: 28rpx;
  562. color: #333;
  563. margin-bottom: 16rpx;
  564. width: 150rpx;
  565. .required {
  566. color: #FF5030;
  567. margin-right: 4rpx;
  568. }
  569. }
  570. .form-input {
  571. // width: 100%;
  572. flex:1;
  573. height: 80rpx;
  574. padding: 0 24rpx;
  575. font-size: 28rpx;
  576. color: #333;
  577. &.placeholder {
  578. color: #C8C9CC;
  579. }
  580. &.picker-input {
  581. display: flex;
  582. align-items: center;
  583. justify-content: space-between;
  584. .arrow-right {
  585. font-size: 32rpx;
  586. color: #999;
  587. }
  588. }
  589. }
  590. }
  591. .certificate-item {
  592. margin-bottom: 32rpx;
  593. &:last-child {
  594. margin-bottom: 0;
  595. }
  596. .certificate-header {
  597. display: flex;
  598. align-items: center;
  599. justify-content: space-between;
  600. margin-bottom: 16rpx;
  601. .certificate-title {
  602. font-size: 28rpx;
  603. color: #333;
  604. font-weight: 500;
  605. }
  606. .example-btn {
  607. display: flex;
  608. align-items: center;
  609. gap: 8rpx;
  610. font-size: 24rpx;
  611. color: #388BFF;
  612. .example-icon {
  613. width: 32rpx;
  614. height: 32rpx;
  615. border-radius: 50%;
  616. background: #E6F7FF;
  617. display: flex;
  618. align-items: center;
  619. justify-content: center;
  620. font-size: 20rpx;
  621. color: #388BFF;
  622. }
  623. }
  624. }
  625. .certificate-tip {
  626. font-size: 24rpx;
  627. color: #999;
  628. margin-bottom: 16rpx;
  629. }
  630. .upload-grid {
  631. display: flex;
  632. gap: 16rpx;
  633. flex-wrap: wrap;
  634. .upload-item {
  635. width: 200rpx;
  636. height: 200rpx;
  637. border-radius: 8rpx;
  638. overflow: hidden;
  639. position: relative;
  640. .uploaded-image {
  641. width: 100%;
  642. height: 100%;
  643. }
  644. .delete-btn {
  645. position: absolute;
  646. top: 8rpx;
  647. right: 8rpx;
  648. width: 40rpx;
  649. height: 40rpx;
  650. background: rgba(0, 0, 0, 0.5);
  651. border-radius: 50%;
  652. display: flex;
  653. align-items: center;
  654. justify-content: center;
  655. font-size: 32rpx;
  656. color: #fff;
  657. }
  658. & .upload-placeholder {
  659. background: #f5f5f5;
  660. display: flex;
  661. flex-direction: column;
  662. align-items: center;
  663. justify-content: center;
  664. border: 2rpx dashed #ddd;
  665. .camera-icon {
  666. font-size: 60rpx;
  667. margin-bottom: 8rpx;
  668. }
  669. .upload-text {
  670. font-size: 24rpx;
  671. color: #388BFF;
  672. }
  673. }
  674. }
  675. }
  676. }
  677. }
  678. .bottom-bar {
  679. position: fixed;
  680. bottom: 0;
  681. left: 0;
  682. right: 0;
  683. background: #fff;
  684. padding: 24rpx;
  685. border-top: 1rpx solid #f0f0f0;
  686. z-index: 100;
  687. .agreement-checkbox {
  688. margin-bottom: 24rpx;
  689. .checkbox-label {
  690. display: flex;
  691. align-items: flex-start;
  692. font-size: 24rpx;
  693. color: #666;
  694. .agreement-text {
  695. margin-left: 8rpx;
  696. line-height: 1.5;
  697. .link-text {
  698. color: #388BFF;
  699. }
  700. }
  701. }
  702. }
  703. .action-buttons {
  704. display: flex;
  705. gap: 24rpx;
  706. .btn {
  707. flex: 1;
  708. height: 88rpx;
  709. display: flex;
  710. align-items: center;
  711. justify-content: center;
  712. border-radius: 8rpx;
  713. font-size: 32rpx;
  714. font-weight: 500;
  715. &.btn-cancel {
  716. background: #fff;
  717. border: 2rpx solid #388BFF;
  718. color: #388BFF;
  719. }
  720. &.btn-submit {
  721. background: #388BFF;
  722. color: #fff;
  723. }
  724. }
  725. }
  726. }
  727. </style>