25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

724 lines
18 KiB

  1. <template>
  2. <view class="page">
  3. <!-- 驳回原因提示 -->
  4. <view v-if="info.status === 2 && info.reject_reason" class="reject-tip">
  5. <u-icon name="warning-fill" size="20" color="#fa8c16" />
  6. <text class="reject-text">驳回原因:{{ info.reject_reason }}</text>
  7. </view>
  8. <!-- 基本信息 -->
  9. <view class="section">
  10. <view class="section-title">
  11. <u-icon name="account-fill" size="18" color="#0e63e3" />
  12. <text>基本信息</text>
  13. </view>
  14. <view class="form-group">
  15. <text class="form-label">姓名</text>
  16. <view class="readonly-input">{{ info.name }}</view>
  17. </view>
  18. <view class="form-group">
  19. <text class="form-label">身份证号</text>
  20. <view class="readonly-input">{{ maskedIdCard }}</view>
  21. </view>
  22. <view class="form-group">
  23. <text class="form-label">手机号</text>
  24. <view class="readonly-input">{{ maskedPhone }}</view>
  25. </view>
  26. <view class="form-group" v-if="info.gender">
  27. <text class="form-label">性别</text>
  28. <view class="readonly-input">{{ info.gender }}</view>
  29. </view>
  30. <view class="form-group" v-else>
  31. <text class="form-label">性别</text>
  32. <view class="gender-row">
  33. <view class="gender-item" :class="{ active: form.gender === '男' }" @tap="form.gender = '男'">男</view>
  34. <view class="gender-item" :class="{ active: form.gender === '女' }" @tap="form.gender = '女'">女</view>
  35. </view>
  36. </view>
  37. <view class="form-group">
  38. <text class="form-label">联系地址</text>
  39. <view class="region-row" @tap="showRegionPicker = true">
  40. <text :class="['region-text', regionText ? '' : 'placeholder']">{{ regionText || '请选择省/市/区' }}</text>
  41. <text class="arrow">›</text>
  42. </view>
  43. <u-input v-model="form.address" placeholder="详细街道地址" border="surround"
  44. :customStyle="{ marginTop: '16rpx' }" />
  45. </view>
  46. <view class="form-group">
  47. <text class="form-label">紧急联系人(选填)</text>
  48. <view class="contact-row">
  49. <view class="contact-input">
  50. <u-input v-model="form.emergency_contact" placeholder="联系人姓名" border="surround" />
  51. </view>
  52. <view class="contact-input">
  53. <u-input v-model="form.emergency_phone" type="number" placeholder="联系人电话" border="surround" maxlength="11" />
  54. </view>
  55. </view>
  56. </view>
  57. </view>
  58. <!-- 资料上传 -->
  59. <view class="section">
  60. <view class="section-title">
  61. <u-icon name="attach" size="18" color="#fa8c16" />
  62. <text>资料上传</text>
  63. </view>
  64. <view class="upload-tip">请上传您的检查报告单或出院诊断证明书,上传图片请尽量平整清晰。可上传多张。</view>
  65. <view class="upload-row">
  66. <view class="upload-item" v-for="(doc, idx) in form.documents" :key="idx">
  67. <image class="upload-img" :src="doc" mode="aspectFill" @tap="previewImage(idx)" />
  68. <view class="upload-del" @tap="form.documents.splice(idx, 1)">×</view>
  69. </view>
  70. <view class="upload-box" @tap="chooseDocument">
  71. <text class="upload-icon">+</text>
  72. <text class="upload-text">上传图片</text>
  73. </view>
  74. </view>
  75. </view>
  76. <!-- 授权签名 -->
  77. <view class="section">
  78. <view class="section-title">
  79. <u-icon name="edit-pen-fill" size="18" color="#52c41a" />
  80. <text>授权签名</text>
  81. </view>
  82. <view class="sign-item">
  83. <view class="sign-left">
  84. <text class="sign-name">个人可支配收入声明</text>
  85. <text :class="['sign-status', signedIncome ? 'signed' : '']">{{ signedIncome ? '已签署' : '未签署' }}</text>
  86. </view>
  87. <view class="sign-btns" v-if="signedIncome">
  88. <view class="sign-btn view" @tap="previewSign('income')">查看</view>
  89. <view class="sign-btn resign" @tap="goSign('income')">重签</view>
  90. </view>
  91. <view class="sign-btn primary" v-else @tap="goSign('income')">去签署</view>
  92. </view>
  93. <view class="sign-item">
  94. <view class="sign-left">
  95. <text class="sign-name">个人信息处理同意书</text>
  96. <text :class="['sign-status', signedPrivacy ? 'signed' : '']">{{ signedPrivacy ? '已签署' : '未签署' }}</text>
  97. </view>
  98. <view class="sign-btns" v-if="signedPrivacy">
  99. <view class="sign-btn view" @tap="previewSign('privacy')">查看</view>
  100. <view class="sign-btn resign" @tap="goSign('privacy')">重签</view>
  101. </view>
  102. <view class="sign-btn primary" v-else @tap="goSign('privacy')">去签署</view>
  103. </view>
  104. <view class="sign-item">
  105. <view class="sign-left">
  106. <text class="sign-name">声明与承诺</text>
  107. <text :class="['sign-status', signedPromise ? 'signed' : '']">{{ signedPromise ? '已签署' : '未签署' }}</text>
  108. </view>
  109. <view class="sign-btns" v-if="signedPromise">
  110. <view class="sign-btn view" @tap="previewSign('promise')">查看</view>
  111. <view class="sign-btn resign" @tap="goSign('promise')">重签</view>
  112. </view>
  113. <view class="sign-btn primary" v-else @tap="goSign('promise')">去签署</view>
  114. </view>
  115. </view>
  116. <!-- 提交按钮 -->
  117. <view class="btn-wrap">
  118. <u-button text="提交审核" :loading="submitting" @click="handleSubmit" color="#0E63E3" size="large" />
  119. </view>
  120. <!-- 地区选择器 -->
  121. <u-picker v-if="regionColumns[0].length" :show="showRegionPicker" :columns="regionColumns" @confirm="onRegionConfirm"
  122. @cancel="showRegionPicker = false" @change="onRegionChange" :defaultIndex="regionDefaultIndex" />
  123. <!-- 已通过重新提交确认弹窗 -->
  124. <u-popup :show="showConfirmPopup" mode="center" round="12" :safeAreaInsetBottom="false" @close="showConfirmPopup = false">
  125. <view class="confirm-popup">
  126. <view class="confirm-title">提示</view>
  127. <view class="confirm-content">您的资料审核已通过,如果重新提交审核会变为待审核,需要平台重新审核,是否确认提交?</view>
  128. <view class="confirm-btns">
  129. <u-button text="取消" size="normal" :plain="true" shape="circle" @click="showConfirmPopup = false" />
  130. <u-button text="确认提交" size="normal" color="#0E63E3" shape="circle" @click="doSubmit" />
  131. </view>
  132. </view>
  133. </u-popup>
  134. </view>
  135. </template>
  136. <script setup>
  137. import { ref, reactive, computed, onBeforeUnmount } from 'vue'
  138. import { onLoad } from '@dcloudio/uni-app'
  139. import { get, post, upload } from '@/utils/request.js'
  140. const info = ref({})
  141. const form = reactive({
  142. gender: '',
  143. province_code: '',
  144. city_code: '',
  145. district_code: '',
  146. address: '',
  147. emergency_contact: '',
  148. emergency_phone: '',
  149. tag: '',
  150. documents: [],
  151. sign_income: '',
  152. sign_privacy: '',
  153. sign_promise: '',
  154. income_amount: ''
  155. })
  156. const submitting = ref(false)
  157. const showRegionPicker = ref(false)
  158. const showConfirmPopup = ref(false)
  159. const subscribeTmplId = ref('')
  160. // 加载订阅消息模板配置
  161. const loadSubscribeConfig = async () => {
  162. try {
  163. const res = await get('/api/mp/subscribeConfig')
  164. if (res.data && res.data.audit_result) {
  165. subscribeTmplId.value = res.data.audit_result
  166. }
  167. } catch (e) {}
  168. }
  169. // 请求订阅消息授权
  170. const requestSubscribe = () => {
  171. return new Promise((resolve) => {
  172. if (!subscribeTmplId.value) return resolve(false)
  173. // #ifdef MP-WEIXIN
  174. wx.requestSubscribeMessage({
  175. tmplIds: [subscribeTmplId.value],
  176. success: () => resolve(true),
  177. fail: () => resolve(false)
  178. })
  179. // #endif
  180. // #ifndef MP-WEIXIN
  181. resolve(false)
  182. // #endif
  183. })
  184. }
  185. // 地区数据
  186. const allRegions = ref([])
  187. const regionColumns = ref([[], [], []])
  188. const regionDefaultIndex = ref([0, 0, 0])
  189. const maskedIdCard = computed(() => {
  190. const v = info.value.id_card || ''
  191. if (v.length === 18) return v.slice(0, 3) + '***********' + v.slice(-4)
  192. return v
  193. })
  194. const maskedPhone = computed(() => {
  195. const v = info.value.phone || ''
  196. if (v.length === 11) return v.slice(0, 3) + '****' + v.slice(-4)
  197. return v
  198. })
  199. // 签署状态:form 中有新签的 URL 或 info 中有已保存的 URL
  200. const signedIncome = computed(() => form.sign_income || info.value.sign_income)
  201. const signedPrivacy = computed(() => form.sign_privacy || info.value.sign_privacy)
  202. const signedPromise = computed(() => form.sign_promise || info.value.sign_promise)
  203. const regionText = computed(() => {
  204. const parts = []
  205. if (form.province_code) {
  206. const p = allRegions.value.find(r => r.code === form.province_code)
  207. if (p) parts.push(p.name)
  208. }
  209. if (form.city_code) {
  210. const prov = allRegions.value.find(r => r.code === form.province_code)
  211. if (prov && prov.children) {
  212. const c = prov.children.find(r => r.code === form.city_code)
  213. if (c) parts.push(c.name)
  214. }
  215. }
  216. if (form.district_code) {
  217. const prov = allRegions.value.find(r => r.code === form.province_code)
  218. if (prov && prov.children) {
  219. const city = prov.children.find(r => r.code === form.city_code)
  220. if (city && city.children) {
  221. const d = city.children.find(r => r.code === form.district_code)
  222. if (d) parts.push(d.name)
  223. }
  224. }
  225. }
  226. return parts.join(' ')
  227. })
  228. // 签署结果事件监听
  229. const onSignResult = (data) => {
  230. if (data.type === 'income') {
  231. form.sign_income = data.url
  232. if (data.amount) form.income_amount = data.amount
  233. } else if (data.type === 'privacy') {
  234. form.sign_privacy = data.url
  235. } else if (data.type === 'promise') {
  236. form.sign_promise = data.url
  237. }
  238. }
  239. onLoad(async () => {
  240. uni.$on('signResult', onSignResult)
  241. await loadRegions()
  242. await loadInfo()
  243. loadSubscribeConfig()
  244. })
  245. onBeforeUnmount(() => {
  246. uni.$off('signResult', onSignResult)
  247. })
  248. const goSign = (type) => {
  249. uni.navigateTo({ url: `/pages/sign/sign?type=${type}` })
  250. }
  251. const previewSign = (type) => {
  252. const urlMap = {
  253. income: form.sign_income || info.value.sign_income,
  254. privacy: form.sign_privacy || info.value.sign_privacy,
  255. promise: form.sign_promise || info.value.sign_promise
  256. }
  257. const url = urlMap[type]
  258. if (url) uni.previewImage({ urls: [url], current: 0 })
  259. }
  260. const loadRegions = async () => {
  261. try {
  262. const res = await get('/common/regions')
  263. allRegions.value = res.data || []
  264. buildRegionColumns()
  265. } catch (e) {}
  266. }
  267. const buildRegionColumns = (pIdx = 0, cIdx = 0) => {
  268. const provinces = allRegions.value
  269. const col0 = provinces.map(p => p.name)
  270. const cities = (provinces[pIdx] && provinces[pIdx].children) || []
  271. const col1 = cities.map(c => c.name)
  272. const districts = (cities[cIdx] && cities[cIdx].children) || []
  273. const col2 = districts.map(d => d.name)
  274. regionColumns.value = [col0, col1, col2]
  275. }
  276. const onRegionChange = (e) => {
  277. const { columnIndex, index } = e
  278. if (columnIndex === 0) {
  279. buildRegionColumns(index, 0)
  280. regionDefaultIndex.value = [index, 0, 0]
  281. } else if (columnIndex === 1) {
  282. const pIdx = regionDefaultIndex.value[0]
  283. buildRegionColumns(pIdx, index)
  284. regionDefaultIndex.value = [pIdx, index, 0]
  285. }
  286. }
  287. const onRegionConfirm = (e) => {
  288. const idxs = e.indexs || e.index || [0, 0, 0]
  289. const provinces = allRegions.value
  290. const prov = provinces[idxs[0]]
  291. const city = prov && prov.children ? prov.children[idxs[1]] : null
  292. const dist = city && city.children ? city.children[idxs[2]] : null
  293. form.province_code = prov ? prov.code : ''
  294. form.city_code = city ? city.code : ''
  295. form.district_code = dist ? dist.code : ''
  296. showRegionPicker.value = false
  297. }
  298. const loadInfo = async () => {
  299. try {
  300. const res = await get('/api/mp/myInfo')
  301. if (!res.data) return
  302. info.value = res.data
  303. // 填充表单
  304. form.gender = res.data.gender || ''
  305. form.province_code = res.data.province_code || ''
  306. form.city_code = res.data.city_code || ''
  307. form.district_code = res.data.district_code || ''
  308. form.address = res.data.address || ''
  309. form.emergency_contact = res.data.emergency_contact || ''
  310. form.emergency_phone = res.data.emergency_phone || ''
  311. form.tag = res.data.tag || ''
  312. form.documents = res.data.documents || []
  313. form.sign_income = res.data.sign_income || ''
  314. form.sign_privacy = res.data.sign_privacy || ''
  315. form.sign_promise = res.data.sign_promise || ''
  316. form.income_amount = res.data.income_amount || ''
  317. // 设置地区选择器默认索引
  318. if (form.province_code && allRegions.value.length) {
  319. const pIdx = allRegions.value.findIndex(r => r.code === form.province_code)
  320. if (pIdx >= 0) {
  321. const cities = allRegions.value[pIdx].children || []
  322. const cIdx = cities.findIndex(r => r.code === form.city_code)
  323. const ci = cIdx >= 0 ? cIdx : 0
  324. const districts = (cities[ci] && cities[ci].children) || []
  325. const dIdx = districts.findIndex(r => r.code === form.district_code)
  326. buildRegionColumns(pIdx, ci)
  327. regionDefaultIndex.value = [pIdx, ci, dIdx >= 0 ? dIdx : 0]
  328. }
  329. }
  330. } catch (e) {}
  331. }
  332. const chooseDocument = () => {
  333. uni.chooseImage({
  334. count: 9 - form.documents.length,
  335. sizeType: ['compressed'],
  336. sourceType: ['album', 'camera'],
  337. success: async (res) => {
  338. for (const filePath of res.tempFilePaths) {
  339. try {
  340. const uploadRes = await upload('/api/mp/upload', { filePath, name: 'file' })
  341. if (uploadRes.data && uploadRes.data.url) {
  342. form.documents.push(uploadRes.data.url)
  343. }
  344. } catch (e) {}
  345. }
  346. }
  347. })
  348. }
  349. const previewImage = (idx) => {
  350. uni.previewImage({ urls: form.documents, current: idx })
  351. }
  352. const handleSubmit = async () => {
  353. if (!info.value.gender && !form.gender) {
  354. return uni.showToast({ title: '请选择性别', icon: 'none' })
  355. }
  356. if (!form.province_code || !form.city_code || !form.district_code) {
  357. return uni.showToast({ title: '请选择省市区', icon: 'none' })
  358. }
  359. if (!form.address.trim()) {
  360. return uni.showToast({ title: '请填写详细地址', icon: 'none' })
  361. }
  362. // 已通过状态需要二次确认
  363. if (info.value.status === 1) {
  364. showConfirmPopup.value = true
  365. return
  366. }
  367. await doSubmit()
  368. }
  369. const doSubmit = async () => {
  370. showConfirmPopup.value = false
  371. // 先请求订阅消息授权(用户拒绝也继续提交)
  372. await requestSubscribe()
  373. submitting.value = true
  374. try {
  375. const params = {
  376. gender: info.value.gender || form.gender,
  377. province_code: form.province_code,
  378. city_code: form.city_code,
  379. district_code: form.district_code,
  380. address: form.address.trim(),
  381. emergency_contact: form.emergency_contact,
  382. emergency_phone: form.emergency_phone,
  383. tag: form.tag,
  384. documents: form.documents,
  385. sign_income: form.sign_income,
  386. sign_privacy: form.sign_privacy,
  387. sign_promise: form.sign_promise,
  388. income_amount: form.income_amount || null,
  389. // #ifdef MP-WEIXIN
  390. mp_env_version: uni.getAccountInfoSync().miniProgram.envVersion || 'release'
  391. // #endif
  392. }
  393. await post('/api/mp/saveMyInfo', params)
  394. uni.showToast({ title: '提交成功', icon: 'success' })
  395. setTimeout(() => uni.navigateBack(), 1500)
  396. } catch (e) {
  397. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  398. } finally {
  399. submitting.value = false
  400. }
  401. }
  402. </script>
  403. <style lang="scss" scoped>
  404. .page {
  405. min-height: 100vh;
  406. background: #f4f4f5;
  407. padding: 24rpx;
  408. padding-bottom: 240rpx;
  409. }
  410. .section {
  411. background: #fff;
  412. border-radius: 10rpx;
  413. padding: 32rpx;
  414. margin-bottom: 24rpx;
  415. border: 1rpx solid #ebeef5;
  416. }
  417. .reject-tip {
  418. display: flex;
  419. align-items: flex-start;
  420. padding: 24rpx 28rpx;
  421. background: #fff2f0;
  422. border: 1rpx solid #ffccc7;
  423. border-radius: 10rpx;
  424. margin-bottom: 24rpx;
  425. .reject-text {
  426. flex: 1;
  427. font-size: 26rpx;
  428. color: #f5222d;
  429. margin-left: 16rpx;
  430. line-height: 1.5;
  431. }
  432. }
  433. .section-title {
  434. display: flex;
  435. align-items: center;
  436. gap: 12rpx;
  437. font-size: 32rpx;
  438. font-weight: 600;
  439. color: #333;
  440. margin-bottom: 28rpx;
  441. }
  442. .form-group {
  443. padding: 20rpx 0;
  444. border-bottom: 1rpx solid #f0f0f0;
  445. &:last-child {
  446. border-bottom: none;
  447. }
  448. }
  449. .form-label {
  450. font-size: 28rpx;
  451. color: #555;
  452. margin-bottom: 16rpx;
  453. display: block;
  454. }
  455. .readonly-input {
  456. padding: 20rpx 24rpx;
  457. background: #f5f5f5;
  458. border: 1rpx solid #ddd;
  459. border-radius: 12rpx;
  460. font-size: 28rpx;
  461. color: #333;
  462. }
  463. .gender-row {
  464. display: flex;
  465. gap: 20rpx;
  466. }
  467. .gender-item {
  468. flex: 1;
  469. text-align: center;
  470. padding: 16rpx 0;
  471. border: 1rpx solid #ddd;
  472. border-radius: 8rpx;
  473. font-size: 28rpx;
  474. color: #333;
  475. &.active {
  476. border-color: #0e63e3;
  477. color: #0e63e3;
  478. background: rgba(14, 99, 227, 0.05);
  479. }
  480. }
  481. .region-row {
  482. display: flex;
  483. align-items: center;
  484. justify-content: space-between;
  485. padding: 20rpx 24rpx;
  486. border: 1rpx solid #ddd;
  487. border-radius: 8rpx;
  488. }
  489. .region-text {
  490. font-size: 28rpx;
  491. color: #333;
  492. &.placeholder {
  493. color: #c0c4cc;
  494. }
  495. }
  496. .arrow {
  497. font-size: 28rpx;
  498. color: #c0c4cc;
  499. }
  500. .contact-row {
  501. display: flex;
  502. gap: 16rpx;
  503. }
  504. .contact-input {
  505. flex: 1;
  506. }
  507. .upload-tip {
  508. font-size: 26rpx;
  509. color: #888;
  510. line-height: 1.6;
  511. margin-bottom: 20rpx;
  512. }
  513. .upload-row {
  514. display: flex;
  515. gap: 16rpx;
  516. flex-wrap: wrap;
  517. }
  518. .upload-item {
  519. position: relative;
  520. width: 180rpx;
  521. height: 180rpx;
  522. }
  523. .upload-img {
  524. width: 180rpx;
  525. height: 180rpx;
  526. border-radius: 8rpx;
  527. border: 1rpx solid #eee;
  528. }
  529. .upload-del {
  530. position: absolute;
  531. top: -10rpx;
  532. right: -10rpx;
  533. width: 40rpx;
  534. height: 40rpx;
  535. background: rgba(0, 0, 0, 0.5);
  536. color: #fff;
  537. border-radius: 50%;
  538. font-size: 24rpx;
  539. display: flex;
  540. align-items: center;
  541. justify-content: center;
  542. }
  543. .upload-box {
  544. width: 180rpx;
  545. height: 180rpx;
  546. border: 2rpx dashed #ccc;
  547. border-radius: 8rpx;
  548. display: flex;
  549. flex-direction: column;
  550. align-items: center;
  551. justify-content: center;
  552. background: #fafafa;
  553. }
  554. .upload-icon {
  555. font-size: 56rpx;
  556. color: #ccc;
  557. }
  558. .upload-text {
  559. font-size: 22rpx;
  560. color: #999;
  561. margin-top: 4rpx;
  562. }
  563. .sign-item {
  564. display: flex;
  565. align-items: center;
  566. justify-content: space-between;
  567. padding: 28rpx 0;
  568. border-bottom: 1rpx solid #f0f0f0;
  569. &:last-child {
  570. border-bottom: none;
  571. }
  572. }
  573. .sign-left {
  574. display: flex;
  575. flex-direction: column;
  576. gap: 8rpx;
  577. }
  578. .sign-name {
  579. font-size: 28rpx;
  580. color: #333;
  581. }
  582. .sign-status {
  583. font-size: 24rpx;
  584. color: #f5222d;
  585. &.signed {
  586. color: #52c41a;
  587. }
  588. }
  589. .sign-btns {
  590. display: flex;
  591. gap: 12rpx;
  592. flex-shrink: 0;
  593. }
  594. .sign-btn {
  595. padding: 10rpx 28rpx;
  596. border-radius: 28rpx;
  597. font-size: 24rpx;
  598. text-align: center;
  599. flex-shrink: 0;
  600. &.primary {
  601. background: #0e63e3;
  602. color: #fff;
  603. }
  604. &.view {
  605. background: #f0f5ff;
  606. color: #0e63e3;
  607. border: 1rpx solid #d0e0ff;
  608. }
  609. &.resign {
  610. background: #fff7e6;
  611. color: #fa8c16;
  612. border: 1rpx solid #ffe7ba;
  613. }
  614. &:active {
  615. opacity: 0.7;
  616. }
  617. }
  618. .btn-wrap {
  619. position: fixed;
  620. bottom: 0;
  621. left: 0;
  622. right: 0;
  623. background: #fff;
  624. padding: 20rpx 32rpx;
  625. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  626. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
  627. z-index: 100;
  628. }
  629. .confirm-popup {
  630. padding: 48rpx 40rpx 40rpx;
  631. width: 560rpx;
  632. }
  633. .confirm-title {
  634. font-size: 34rpx;
  635. font-weight: 600;
  636. color: #333;
  637. text-align: center;
  638. margin-bottom: 28rpx;
  639. }
  640. .confirm-content {
  641. font-size: 28rpx;
  642. color: #666;
  643. line-height: 1.7;
  644. text-align: center;
  645. margin-bottom: 40rpx;
  646. }
  647. .confirm-btns {
  648. display: flex;
  649. gap: 24rpx;
  650. }
  651. </style>