Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 
 
 

995 рядки
27 KiB

  1. <template>
  2. <view class="page">
  3. <u-loading-page :loading="pageLoading" loading-text="加载中..." />
  4. <!-- 驳回原因提示 -->
  5. <view v-if="info.status === 2 && info.reject_reason" class="reject-tip">
  6. <u-icon name="warning-fill" size="20" color="#fa8c16" />
  7. <text class="reject-text">驳回原因:{{ info.reject_reason }}</text>
  8. </view>
  9. <!-- 基本信息 -->
  10. <view class="section">
  11. <view class="section-title">
  12. <u-icon name="account-fill" size="18" color="#0e63e3" />
  13. <text>基本信息</text>
  14. </view>
  15. <view class="info-compact">
  16. <view class="info-compact-row">
  17. <text class="info-compact-item">姓名:{{ info.name }}</text>
  18. <text class="info-compact-item" v-if="info.gender">性别:{{ info.gender }}</text>
  19. </view>
  20. <view class="info-compact-row">
  21. <text class="info-compact-item">身份证:{{ maskedIdCard }}</text>
  22. <text class="info-compact-item">手机号:{{ maskedPhone }}</text>
  23. </view>
  24. </view>
  25. <view class="form-group" v-if="!info.gender">
  26. <text class="form-label">性别</text>
  27. <view class="gender-row">
  28. <view class="gender-item" :class="{ active: form.gender === '男', disabled: isInfoApproved }" @tap="setGender('男')">男</view>
  29. <view class="gender-item" :class="{ active: form.gender === '女', disabled: isInfoApproved }" @tap="setGender('女')">女</view>
  30. </view>
  31. </view>
  32. <view class="form-group">
  33. <text class="form-label">联系地址</text>
  34. <view :class="['region-row', isInfoApproved ? 'disabled' : '']" @tap="openRegionPicker">
  35. <text :class="['region-text', regionText ? '' : 'placeholder']">{{ regionText || '请选择省/市/区' }}</text>
  36. <text class="arrow">›</text>
  37. </view>
  38. <u-input v-model="form.address" :disabled="isInfoApproved" placeholder="详细街道地址" border="surround"
  39. :customStyle="{ marginTop: '16rpx' }" />
  40. </view>
  41. <view class="form-group">
  42. <text class="form-label">紧急联系人</text>
  43. <view class="contact-row">
  44. <view class="contact-input">
  45. <u-input v-model="form.emergency_contact" :disabled="isInfoApproved" placeholder="联系人姓名" border="surround" />
  46. </view>
  47. <view class="contact-input">
  48. <u-input v-model="form.emergency_phone" :disabled="isInfoApproved" type="number" placeholder="联系人电话" border="surround" maxlength="11" />
  49. </view>
  50. </view>
  51. </view>
  52. <view class="form-group">
  53. <text class="form-label">医院名称</text>
  54. <view :class="['region-row', isInfoApproved ? 'disabled' : '']" @tap="openHospitalPicker">
  55. <text :class="['region-text', form.hospital ? '' : 'placeholder']">{{ form.hospital || '请选择就诊医院' }}</text>
  56. <text class="arrow">›</text>
  57. </view>
  58. </view>
  59. <view class="form-group">
  60. <text class="form-label">癌种</text>
  61. <u-radio-group v-model="form.tag" placement="row" :wrap="true">
  62. <u-radio v-for="t in tagOptions" :key="t" :label="t" :name="t"
  63. :disabled="isInfoApproved" activeColor="#0E63E3" :customStyle="{ marginRight: '24rpx', marginBottom: '16rpx' }" />
  64. </u-radio-group>
  65. </view>
  66. </view>
  67. <!-- 资料上传 -->
  68. <view class="section">
  69. <view class="section-title">
  70. <u-icon name="attach" size="18" color="#fa8c16" />
  71. <text>资料上传</text>
  72. </view>
  73. <view class="upload-tip">请上传您的检查报告单或出院诊断证明书,上传图片请尽量平整清晰。可上传多张。</view>
  74. <view class="upload-row">
  75. <view class="upload-item" v-for="(doc, idx) in form.documents" :key="idx">
  76. <image class="upload-img" :src="doc" mode="aspectFill" @tap="previewImage(idx)" />
  77. <view v-if="!isInfoApproved" class="upload-del" @tap="form.documents.splice(idx, 1)">×</view>
  78. </view>
  79. <view v-if="!isInfoApproved" class="upload-box" @tap="chooseDocument">
  80. <text class="upload-icon">+</text>
  81. <text class="upload-text">上传图片</text>
  82. </view>
  83. </view>
  84. </view>
  85. <!-- 授权签名 -->
  86. <view class="section">
  87. <view class="section-title">
  88. <u-icon name="edit-pen-fill" size="18" color="#52c41a" />
  89. <text>授权签名</text>
  90. </view>
  91. <view class="sign-item">
  92. <view class="sign-left">
  93. <text class="sign-name">个人可支配收入声明</text>
  94. <text :class="['sign-status', signedIncome ? 'signed' : '']">{{ signedIncome ? '已签署' : '未签署' }}</text>
  95. </view>
  96. <view class="sign-btns" v-if="signedIncome">
  97. <view class="sign-btn view" @tap="previewSign('income')">查看</view>
  98. <view v-if="!isInfoApproved" class="sign-btn resign" @tap="goSign('income')">重签</view>
  99. </view>
  100. <view class="sign-btn primary" v-else-if="!isInfoApproved" @tap="goSign('income')">去签署</view>
  101. </view>
  102. <view class="sign-item">
  103. <view class="sign-left">
  104. <text class="sign-name">个人信息处理同意书</text>
  105. <text :class="['sign-status', signedPrivacy ? 'signed' : '']">{{ signedPrivacy ? '已签署' : '未签署' }}</text>
  106. </view>
  107. <view class="sign-btns" v-if="signedPrivacy">
  108. <view class="sign-btn view" @tap="previewSign('privacy')">查看</view>
  109. <view v-if="!isInfoApproved" class="sign-btn resign" @tap="goSign('privacy')">重签</view>
  110. </view>
  111. <view class="sign-btn primary" v-else-if="!isInfoApproved" @tap="goSign('privacy')">去签署</view>
  112. </view>
  113. <view class="sign-item" v-if="isMinor">
  114. <view class="sign-left">
  115. <text class="sign-name">个人信息处理同意书(监护人)</text>
  116. <text :class="['sign-status', signedPrivacyJhr ? 'signed' : '']">{{ signedPrivacyJhr ? '已签署' : '未签署' }}</text>
  117. </view>
  118. <view class="sign-btns" v-if="signedPrivacyJhr">
  119. <view class="sign-btn view" @tap="previewSign('privacy_jhr')">查看</view>
  120. <view v-if="!isInfoApproved" class="sign-btn resign" @tap="goSign('privacy_jhr')">重签</view>
  121. </view>
  122. <view class="sign-btn primary" v-else-if="!isInfoApproved" @tap="goSign('privacy_jhr')">去签署</view>
  123. </view>
  124. <view class="sign-item">
  125. <view class="sign-left">
  126. <text class="sign-name">声明与承诺</text>
  127. <text :class="['sign-status', signedPromise ? 'signed' : '']">{{ signedPromise ? '已签署' : '未签署' }}</text>
  128. </view>
  129. <view class="sign-btns" v-if="signedPromise">
  130. <view class="sign-btn view" @tap="previewSign('promise')">查看</view>
  131. <view v-if="!isInfoApproved" class="sign-btn resign" @tap="goSign('promise')">重签</view>
  132. </view>
  133. <view class="sign-btn primary" v-else-if="!isInfoApproved" @tap="goSign('promise')">去签署</view>
  134. </view>
  135. </view>
  136. <!-- 提交按钮 -->
  137. <view class="btn-wrap">
  138. <view v-if="!isInfoApproved" class="agree-row" @tap="toggleAgree">
  139. <view :class="['agree-checkbox', agreed ? 'checked' : '']">
  140. <text v-if="agreed" class="agree-check-icon">✓</text>
  141. </view>
  142. <text class="agree-text">请阅读并同意</text>
  143. <text class="agree-link" @tap.stop="openNotice">《患者告知书》</text>
  144. </view>
  145. <u-button :text="submitButtonText" :disabled="isInfoApproved" :loading="submitting" @click="handleSubmit" color="#0E63E3" size="large" />
  146. </view>
  147. <!-- 地区选择器 -->
  148. <u-picker v-if="regionColumns[0].length" :show="showRegionPicker" :columns="regionColumns" @confirm="onRegionConfirm"
  149. @cancel="showRegionPicker = false" @change="onRegionChange" :defaultIndex="regionDefaultIndex" />
  150. <!-- 医院选择器组件 -->
  151. <hospital-picker ref="hospitalPickerRef" :hospital-data="hospitalTree" @confirm="onHospitalConfirm" />
  152. </view>
  153. </template>
  154. <script setup>
  155. import { ref, reactive, computed, onBeforeUnmount } from 'vue'
  156. import { onLoad } from '@dcloudio/uni-app'
  157. import { get, post, upload } from '@/utils/request.js'
  158. const info = ref({})
  159. const form = reactive({
  160. gender: '',
  161. province_code: '',
  162. city_code: '',
  163. district_code: '',
  164. address: '',
  165. hospital: '',
  166. hospital_province_code: '',
  167. hospital_city_code: '',
  168. hospital_district_code: '',
  169. emergency_contact: '',
  170. emergency_phone: '',
  171. tag: '',
  172. documents: [],
  173. sign_income: '',
  174. sign_privacy: '',
  175. sign_privacy_jhr: '',
  176. sign_promise: '',
  177. income_amount: ''
  178. })
  179. const submitting = ref(false)
  180. const pageLoading = ref(true)
  181. const showRegionPicker = ref(false)
  182. const subscribeTmplId = ref('')
  183. const agreed = ref(false)
  184. // 瘤种选项
  185. const tagOptions = ref([])
  186. // 医院选择器
  187. const hospitalTree = ref([])
  188. const hospitalPickerRef = ref(null)
  189. const isInfoApproved = computed(() => Number(info.value.status) === 1)
  190. const submitButtonText = computed(() => isInfoApproved.value ? '已通过审核' : '提交审核')
  191. const showApprovedTip = () => {
  192. uni.showToast({ title: '资料已审核通过,不能修改', icon: 'none' })
  193. }
  194. const setGender = (gender) => {
  195. if (isInfoApproved.value) return showApprovedTip()
  196. form.gender = gender
  197. }
  198. const openRegionPicker = () => {
  199. if (isInfoApproved.value) return showApprovedTip()
  200. showRegionPicker.value = true
  201. }
  202. const openHospitalPicker = () => {
  203. if (isInfoApproved.value) return showApprovedTip()
  204. if (!hospitalTree.value.length) {
  205. uni.showToast({ title: '医院列表加载中,请稍候', icon: 'none' })
  206. loadHospitalTree()
  207. return
  208. }
  209. hospitalPickerRef.value && hospitalPickerRef.value.open({
  210. hospital: form.hospital,
  211. province_code: form.hospital_province_code,
  212. city_code: form.hospital_city_code,
  213. district_code: form.hospital_district_code
  214. })
  215. }
  216. const onHospitalConfirm = (data) => {
  217. if (isInfoApproved.value) return
  218. form.hospital = data.hospitalName || ''
  219. form.hospital_province_code = data.province_code || ''
  220. form.hospital_city_code = data.city_code || ''
  221. form.hospital_district_code = data.district_code || ''
  222. }
  223. // 签署时的额外信息(用于重签回显)
  224. const signExtra = reactive({
  225. income_amount: '',
  226. guardian_name: '',
  227. guardian_id_card: '',
  228. guardian_relation: ''
  229. })
  230. // 加载订阅消息模板配置
  231. const loadSubscribeConfig = async () => {
  232. try {
  233. const res = await get('/api/mp/subscribeConfig')
  234. if (res.data && res.data.audit_result) {
  235. subscribeTmplId.value = res.data.audit_result
  236. }
  237. } catch (e) {}
  238. }
  239. // 请求订阅消息授权
  240. const requestSubscribe = () => {
  241. return new Promise((resolve) => {
  242. if (!subscribeTmplId.value) return resolve(false)
  243. // #ifdef MP-WEIXIN
  244. wx.requestSubscribeMessage({
  245. tmplIds: [subscribeTmplId.value],
  246. success: () => resolve(true),
  247. fail: () => resolve(false)
  248. })
  249. // #endif
  250. // #ifndef MP-WEIXIN
  251. resolve(false)
  252. // #endif
  253. })
  254. }
  255. // 地区数据
  256. const allRegions = ref([])
  257. const regionColumns = ref([[], [], []])
  258. const regionDefaultIndex = ref([0, 0, 0])
  259. const maskedIdCard = computed(() => {
  260. const v = info.value.id_card || ''
  261. if (v.length === 18) return v.slice(0, 3) + '****' + v.slice(-4)
  262. return v
  263. })
  264. const maskedPhone = computed(() => {
  265. const v = info.value.phone || ''
  266. if (v.length === 11) return v.slice(0, 3) + '****' + v.slice(-4)
  267. return v
  268. })
  269. // 签署状态:form 中有新签的 URL 或 info 中有已保存的 URL
  270. const signedIncome = computed(() => form.sign_income || info.value.sign_income)
  271. const signedPrivacy = computed(() => form.sign_privacy || info.value.sign_privacy)
  272. const signedPrivacyJhr = computed(() => form.sign_privacy_jhr || info.value.sign_privacy_jhr)
  273. const signedPromise = computed(() => form.sign_promise || info.value.sign_promise)
  274. // 判断是否未成年(从身份证号解析年龄)
  275. const isMinor = computed(() => {
  276. const idCard = info.value.id_card || ''
  277. if (idCard.length !== 18) return false
  278. const birthYear = parseInt(idCard.substring(6, 10))
  279. const birthMonth = parseInt(idCard.substring(10, 12))
  280. const birthDay = parseInt(idCard.substring(12, 14))
  281. const now = new Date()
  282. let age = now.getFullYear() - birthYear
  283. const monthDiff = (now.getMonth() + 1) - birthMonth
  284. if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthDay)) age--
  285. return age < 18
  286. })
  287. const regionText = computed(() => {
  288. const parts = []
  289. if (form.province_code) {
  290. const p = allRegions.value.find(r => r.code === form.province_code)
  291. if (p) parts.push(p.name)
  292. }
  293. if (form.city_code) {
  294. const prov = allRegions.value.find(r => r.code === form.province_code)
  295. if (prov && prov.children) {
  296. const c = prov.children.find(r => r.code === form.city_code)
  297. if (c) parts.push(c.name)
  298. }
  299. }
  300. if (form.district_code) {
  301. const prov = allRegions.value.find(r => r.code === form.province_code)
  302. if (prov && prov.children) {
  303. const city = prov.children.find(r => r.code === form.city_code)
  304. if (city && city.children) {
  305. const d = city.children.find(r => r.code === form.district_code)
  306. if (d) parts.push(d.name)
  307. }
  308. }
  309. }
  310. return parts.join(' ')
  311. })
  312. // 签署结果事件监听
  313. const onSignResult = (data) => {
  314. if (data.type === 'income') {
  315. form.sign_income = data.url
  316. if (data.amount) {
  317. form.income_amount = data.amount
  318. signExtra.income_amount = data.amount
  319. }
  320. } else if (data.type === 'privacy') {
  321. form.sign_privacy = data.url
  322. } else if (data.type === 'privacy_jhr') {
  323. form.sign_privacy_jhr = data.url
  324. if (data.guardianName) signExtra.guardian_name = data.guardianName
  325. if (data.guardianIdCard) signExtra.guardian_id_card = data.guardianIdCard
  326. if (data.guardianRelation) signExtra.guardian_relation = data.guardianRelation
  327. } else if (data.type === 'promise') {
  328. form.sign_promise = data.url
  329. }
  330. }
  331. onLoad(async () => {
  332. uni.$on('signResult', onSignResult)
  333. pageLoading.value = true
  334. try {
  335. await loadRegions()
  336. await Promise.all([
  337. loadInfo(),
  338. loadSubscribeConfig(),
  339. loadTagOptions(),
  340. loadHospitalTree()
  341. ])
  342. } finally {
  343. pageLoading.value = false
  344. }
  345. })
  346. onBeforeUnmount(() => {
  347. uni.$off('signResult', onSignResult)
  348. })
  349. const goSign = (type) => {
  350. if (isInfoApproved.value) return showApprovedTip()
  351. let url = `/pages/sign/sign?type=${type}`
  352. if (type === 'income') {
  353. const amt = form.income_amount || signExtra.income_amount || ''
  354. if (amt) url += `&amount=${encodeURIComponent(amt)}`
  355. }
  356. if (type === 'privacy_jhr') {
  357. const gn = signExtra.guardian_name || ''
  358. const gi = signExtra.guardian_id_card || ''
  359. const gr = signExtra.guardian_relation || ''
  360. if (gn) url += `&guardianName=${encodeURIComponent(gn)}`
  361. if (gi) url += `&guardianIdCard=${encodeURIComponent(gi)}`
  362. if (gr) url += `&guardianRelation=${encodeURIComponent(gr)}`
  363. }
  364. uni.navigateTo({ url })
  365. }
  366. const previewSign = (type) => {
  367. const urlMap = {
  368. income: form.sign_income || info.value.sign_income,
  369. privacy: form.sign_privacy || info.value.sign_privacy,
  370. privacy_jhr: form.sign_privacy_jhr || info.value.sign_privacy_jhr,
  371. promise: form.sign_promise || info.value.sign_promise
  372. }
  373. const url = urlMap[type]
  374. if (url) uni.previewImage({ urls: [url], current: 0 })
  375. }
  376. const loadRegions = async () => {
  377. try {
  378. const res = await get('/common/regions')
  379. allRegions.value = res.data || []
  380. buildRegionColumns()
  381. } catch (e) {}
  382. }
  383. const buildRegionColumns = (pIdx = 0, cIdx = 0) => {
  384. const provinces = allRegions.value
  385. const col0 = provinces.map(p => p.name)
  386. const cities = (provinces[pIdx] && provinces[pIdx].children) || []
  387. const col1 = cities.map(c => c.name)
  388. const districts = (cities[cIdx] && cities[cIdx].children) || []
  389. const col2 = districts.map(d => d.name)
  390. regionColumns.value = [col0, col1, col2]
  391. }
  392. const onRegionChange = (e) => {
  393. const { columnIndex, index } = e
  394. if (columnIndex === 0) {
  395. buildRegionColumns(index, 0)
  396. regionDefaultIndex.value = [index, 0, 0]
  397. } else if (columnIndex === 1) {
  398. const pIdx = regionDefaultIndex.value[0]
  399. buildRegionColumns(pIdx, index)
  400. regionDefaultIndex.value = [pIdx, index, 0]
  401. }
  402. }
  403. const onRegionConfirm = (e) => {
  404. if (isInfoApproved.value) {
  405. showRegionPicker.value = false
  406. return
  407. }
  408. const idxs = e.indexs || e.index || [0, 0, 0]
  409. const provinces = allRegions.value
  410. const prov = provinces[idxs[0]]
  411. const city = prov && prov.children ? prov.children[idxs[1]] : null
  412. const dist = city && city.children ? city.children[idxs[2]] : null
  413. form.province_code = prov ? prov.code : ''
  414. form.city_code = city ? city.code : ''
  415. form.district_code = dist ? dist.code : ''
  416. showRegionPicker.value = false
  417. }
  418. const loadInfo = async () => {
  419. try {
  420. const res = await get('/api/mp/myInfo')
  421. if (!res.data) return
  422. info.value = res.data
  423. // 填充表单
  424. form.gender = res.data.gender || ''
  425. form.province_code = res.data.province_code || ''
  426. form.city_code = res.data.city_code || ''
  427. form.district_code = res.data.district_code || ''
  428. form.address = res.data.address || ''
  429. form.hospital = res.data.hospital || ''
  430. form.hospital_province_code = res.data.hospital_province_code || ''
  431. form.hospital_city_code = res.data.hospital_city_code || ''
  432. form.hospital_district_code = res.data.hospital_district_code || ''
  433. form.emergency_contact = res.data.emergency_contact || ''
  434. form.emergency_phone = res.data.emergency_phone || ''
  435. form.tag = res.data.tag || ''
  436. form.documents = res.data.documents || []
  437. form.sign_income = res.data.sign_income || ''
  438. form.sign_privacy = res.data.sign_privacy || ''
  439. form.sign_privacy_jhr = res.data.sign_privacy_jhr || ''
  440. form.sign_promise = res.data.sign_promise || ''
  441. form.income_amount = res.data.income_amount || ''
  442. signExtra.income_amount = res.data.income_amount || ''
  443. signExtra.guardian_name = res.data.guardian_name || ''
  444. signExtra.guardian_id_card = res.data.guardian_id_card || ''
  445. signExtra.guardian_relation = res.data.guardian_relation || ''
  446. // 设置地区选择器默认索引
  447. if (form.province_code && allRegions.value.length) {
  448. const pIdx = allRegions.value.findIndex(r => r.code === form.province_code)
  449. if (pIdx >= 0) {
  450. const cities = allRegions.value[pIdx].children || []
  451. const cIdx = cities.findIndex(r => r.code === form.city_code)
  452. const ci = cIdx >= 0 ? cIdx : 0
  453. const districts = (cities[ci] && cities[ci].children) || []
  454. const dIdx = districts.findIndex(r => r.code === form.district_code)
  455. buildRegionColumns(pIdx, ci)
  456. regionDefaultIndex.value = [pIdx, ci, dIdx >= 0 ? dIdx : 0]
  457. }
  458. }
  459. } catch (e) {}
  460. }
  461. const loadTagOptions = async () => {
  462. try {
  463. const res = await get('/common/tagOptions')
  464. tagOptions.value = res.data || []
  465. } catch (e) {}
  466. }
  467. const loadHospitalTree = async () => {
  468. try {
  469. const res = await get('/common/hospitalTree')
  470. hospitalTree.value = res.data || []
  471. } catch (e) {}
  472. }
  473. const chooseDocument = () => {
  474. if (isInfoApproved.value) return showApprovedTip()
  475. uni.chooseImage({
  476. count: 9 - form.documents.length,
  477. sizeType: ['compressed'],
  478. sourceType: ['album', 'camera'],
  479. success: async (res) => {
  480. for (const filePath of res.tempFilePaths) {
  481. try {
  482. const uploadRes = await upload('/api/mp/upload', { filePath, name: 'file' })
  483. if (uploadRes.data && uploadRes.data.url) {
  484. form.documents.push(uploadRes.data.url)
  485. }
  486. } catch (e) {}
  487. }
  488. }
  489. })
  490. }
  491. const previewImage = (idx) => {
  492. uni.previewImage({ urls: form.documents, current: idx })
  493. }
  494. const openNotice = () => {
  495. uni.navigateTo({ url: '/pages/content/content?key=patient_information_sheet' })
  496. }
  497. const toggleAgree = () => {
  498. agreed.value = !agreed.value
  499. }
  500. const handleSubmit = async () => {
  501. if (isInfoApproved.value) return showApprovedTip()
  502. if (!agreed.value) {
  503. return uni.showToast({ title: '请阅读并同意《患者告知书》', icon: 'none' })
  504. }
  505. if (!info.value.gender && !form.gender) {
  506. return uni.showToast({ title: '请选择性别', icon: 'none' })
  507. }
  508. if (!form.province_code || !form.city_code || !form.district_code) {
  509. return uni.showToast({ title: '请选择省市区', icon: 'none' })
  510. }
  511. if (!form.address.trim()) {
  512. return uni.showToast({ title: '请填写详细地址', icon: 'none' })
  513. }
  514. if (!form.emergency_contact || !form.emergency_phone) {
  515. return uni.showToast({ title: '请填写紧急联系人信息', icon: 'none' })
  516. }
  517. if (form.emergency_phone === info.value.phone) {
  518. return uni.showToast({ title: '紧急联系人电话不能与本人手机号一致', icon: 'none' })
  519. }
  520. if (!form.hospital || !form.hospital.trim()) {
  521. return uni.showToast({ title: '请填写医院名称', icon: 'none' })
  522. }
  523. if (!form.tag) {
  524. return uni.showToast({ title: '请选择癌种', icon: 'none' })
  525. }
  526. // 资料上传校验:至少上传一个
  527. if (!form.documents || form.documents.length === 0) {
  528. return uni.showToast({ title: '请至少上传一份检查报告或诊断证明', icon: 'none' })
  529. }
  530. // 签名校验:全部必须签
  531. if (!signedIncome.value) {
  532. return uni.showToast({ title: '请签署个人可支配收入声明', icon: 'none' })
  533. }
  534. if (!signedPrivacy.value) {
  535. return uni.showToast({ title: '请签署个人信息处理同意书', icon: 'none' })
  536. }
  537. if (isMinor.value && !signedPrivacyJhr.value) {
  538. return uni.showToast({ title: '请签署个人信息处理同意书(监护人)', icon: 'none' })
  539. }
  540. if (!signedPromise.value) {
  541. return uni.showToast({ title: '请签署声明与承诺', icon: 'none' })
  542. }
  543. await doSubmit()
  544. }
  545. const doSubmit = async () => {
  546. if (isInfoApproved.value) return showApprovedTip()
  547. // 先请求订阅消息授权(用户拒绝也继续提交)
  548. await requestSubscribe()
  549. submitting.value = true
  550. try {
  551. const params = {
  552. gender: info.value.gender || form.gender,
  553. province_code: form.province_code,
  554. city_code: form.city_code,
  555. district_code: form.district_code,
  556. address: form.address.trim(),
  557. hospital: form.hospital,
  558. hospital_province_code: form.hospital_province_code,
  559. hospital_city_code: form.hospital_city_code,
  560. hospital_district_code: form.hospital_district_code,
  561. emergency_contact: form.emergency_contact,
  562. emergency_phone: form.emergency_phone,
  563. tag: form.tag,
  564. documents: form.documents,
  565. sign_income: form.sign_income,
  566. sign_privacy: form.sign_privacy,
  567. sign_privacy_jhr: form.sign_privacy_jhr,
  568. sign_promise: form.sign_promise,
  569. income_amount: form.income_amount || null,
  570. guardian_name: signExtra.guardian_name || '',
  571. guardian_id_card: signExtra.guardian_id_card || '',
  572. guardian_relation: signExtra.guardian_relation || '',
  573. // #ifdef MP-WEIXIN
  574. mp_env_version: uni.getAccountInfoSync().miniProgram.envVersion || 'release'
  575. // #endif
  576. }
  577. await post('/api/mp/saveMyInfo', params)
  578. uni.showToast({ title: '提交成功', icon: 'success' })
  579. setTimeout(() => uni.navigateBack(), 1500)
  580. } catch (e) {
  581. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  582. } finally {
  583. submitting.value = false
  584. }
  585. }
  586. </script>
  587. <style lang="scss" scoped>
  588. .page {
  589. min-height: 100vh;
  590. background: #f4f4f5;
  591. padding: 24rpx;
  592. padding-bottom: 340rpx;
  593. }
  594. .section {
  595. background: #fff;
  596. border-radius: 10rpx;
  597. padding: 32rpx;
  598. margin-bottom: 24rpx;
  599. border: 1rpx solid #ebeef5;
  600. }
  601. .reject-tip {
  602. display: flex;
  603. align-items: flex-start;
  604. padding: 24rpx 28rpx;
  605. background: #fff2f0;
  606. border: 1rpx solid #ffccc7;
  607. border-radius: 10rpx;
  608. margin-bottom: 24rpx;
  609. .reject-text {
  610. flex: 1;
  611. font-size: 26rpx;
  612. color: #f5222d;
  613. margin-left: 16rpx;
  614. line-height: 1.5;
  615. }
  616. }
  617. .section-title {
  618. display: flex;
  619. align-items: center;
  620. gap: 12rpx;
  621. font-size: 32rpx;
  622. font-weight: 600;
  623. color: #333;
  624. margin-bottom: 28rpx;
  625. }
  626. .form-group {
  627. padding: 20rpx 0;
  628. border-bottom: 1rpx solid #f0f0f0;
  629. &:last-child {
  630. border-bottom: none;
  631. }
  632. }
  633. .info-compact {
  634. padding-bottom: 20rpx;
  635. border-bottom: 1rpx solid #f0f0f0;
  636. }
  637. .info-compact-row {
  638. display: flex;
  639. gap: 32rpx;
  640. margin-bottom: 12rpx;
  641. &:last-child {
  642. margin-bottom: 0;
  643. }
  644. }
  645. .info-compact-item {
  646. font-size: 26rpx;
  647. color: #666;
  648. }
  649. .form-label {
  650. font-size: 28rpx;
  651. color: #555;
  652. margin-bottom: 16rpx;
  653. display: block;
  654. }
  655. .readonly-input {
  656. padding: 20rpx 24rpx;
  657. background: #f5f5f5;
  658. border: 1rpx solid #ddd;
  659. border-radius: 12rpx;
  660. font-size: 28rpx;
  661. color: #333;
  662. }
  663. .gender-row {
  664. display: flex;
  665. gap: 20rpx;
  666. }
  667. .gender-item {
  668. flex: 1;
  669. text-align: center;
  670. padding: 16rpx 0;
  671. border: 1rpx solid #ddd;
  672. border-radius: 8rpx;
  673. font-size: 28rpx;
  674. color: #333;
  675. &.active {
  676. border-color: #0e63e3;
  677. color: #0e63e3;
  678. background: rgba(14, 99, 227, 0.05);
  679. }
  680. &.disabled {
  681. color: #909399;
  682. background: #f5f7fa;
  683. }
  684. }
  685. .region-row {
  686. display: flex;
  687. align-items: center;
  688. justify-content: space-between;
  689. padding: 20rpx 24rpx;
  690. border: 1rpx solid #ddd;
  691. border-radius: 8rpx;
  692. &.disabled {
  693. background: #f5f7fa;
  694. }
  695. }
  696. .region-text {
  697. font-size: 28rpx;
  698. color: #333;
  699. &.placeholder {
  700. color: #c0c4cc;
  701. }
  702. }
  703. .arrow {
  704. font-size: 28rpx;
  705. color: #c0c4cc;
  706. }
  707. .contact-row {
  708. display: flex;
  709. gap: 16rpx;
  710. }
  711. .contact-input {
  712. flex: 1;
  713. }
  714. .upload-tip {
  715. font-size: 26rpx;
  716. color: #888;
  717. line-height: 1.6;
  718. margin-bottom: 20rpx;
  719. }
  720. .upload-row {
  721. display: flex;
  722. gap: 16rpx;
  723. flex-wrap: wrap;
  724. }
  725. .upload-item {
  726. position: relative;
  727. width: 180rpx;
  728. height: 180rpx;
  729. }
  730. .upload-img {
  731. width: 180rpx;
  732. height: 180rpx;
  733. border-radius: 8rpx;
  734. border: 1rpx solid #eee;
  735. }
  736. .upload-del {
  737. position: absolute;
  738. top: -10rpx;
  739. right: -10rpx;
  740. width: 40rpx;
  741. height: 40rpx;
  742. background: rgba(0, 0, 0, 0.5);
  743. color: #fff;
  744. border-radius: 50%;
  745. font-size: 24rpx;
  746. display: flex;
  747. align-items: center;
  748. justify-content: center;
  749. }
  750. .upload-box {
  751. width: 180rpx;
  752. height: 180rpx;
  753. border: 2rpx dashed #ccc;
  754. border-radius: 8rpx;
  755. display: flex;
  756. flex-direction: column;
  757. align-items: center;
  758. justify-content: center;
  759. background: #fafafa;
  760. }
  761. .upload-icon {
  762. font-size: 56rpx;
  763. color: #ccc;
  764. }
  765. .upload-text {
  766. font-size: 22rpx;
  767. color: #999;
  768. margin-top: 4rpx;
  769. }
  770. .sign-item {
  771. display: flex;
  772. align-items: center;
  773. justify-content: space-between;
  774. padding: 28rpx 0;
  775. border-bottom: 1rpx solid #f0f0f0;
  776. &:last-child {
  777. border-bottom: none;
  778. }
  779. }
  780. .sign-left {
  781. display: flex;
  782. flex-direction: column;
  783. gap: 8rpx;
  784. }
  785. .sign-name {
  786. font-size: 28rpx;
  787. color: #333;
  788. }
  789. .sign-status {
  790. font-size: 24rpx;
  791. color: #f5222d;
  792. &.signed {
  793. color: #52c41a;
  794. }
  795. }
  796. .sign-btns {
  797. display: flex;
  798. gap: 12rpx;
  799. flex-shrink: 0;
  800. }
  801. .sign-btn {
  802. padding: 10rpx 28rpx;
  803. border-radius: 28rpx;
  804. font-size: 24rpx;
  805. text-align: center;
  806. flex-shrink: 0;
  807. &.primary {
  808. background: #0e63e3;
  809. color: #fff;
  810. }
  811. &.view {
  812. background: #f0f5ff;
  813. color: #0e63e3;
  814. border: 1rpx solid #d0e0ff;
  815. }
  816. &.resign {
  817. background: #fff7e6;
  818. color: #fa8c16;
  819. border: 1rpx solid #ffe7ba;
  820. }
  821. &:active {
  822. opacity: 0.7;
  823. }
  824. }
  825. .btn-wrap {
  826. position: fixed;
  827. bottom: 0;
  828. left: 0;
  829. right: 0;
  830. background: #fff;
  831. padding: 20rpx 32rpx;
  832. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  833. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.06);
  834. z-index: 100;
  835. }
  836. .agree-row {
  837. display: flex;
  838. align-items: center;
  839. margin-bottom: 16rpx;
  840. padding-left: 4rpx;
  841. }
  842. .agree-checkbox {
  843. width: 36rpx;
  844. height: 36rpx;
  845. border: 2rpx solid #dcdfe6;
  846. border-radius: 50%;
  847. box-sizing: border-box;
  848. display: flex;
  849. align-items: center;
  850. justify-content: center;
  851. flex-shrink: 0;
  852. background: #fff;
  853. }
  854. .agree-checkbox.checked {
  855. border-color: #0e63e3;
  856. background: #0e63e3;
  857. }
  858. .agree-check-icon {
  859. font-size: 24rpx;
  860. line-height: 1;
  861. color: #fff;
  862. font-weight: 600;
  863. }
  864. .agree-text {
  865. font-size: 24rpx;
  866. color: #666;
  867. margin-left: 8rpx;
  868. }
  869. .agree-link {
  870. font-size: 24rpx;
  871. color: #0e63e3;
  872. }
  873. .confirm-popup {
  874. padding: 48rpx 40rpx 40rpx;
  875. width: 560rpx;
  876. }
  877. .confirm-title {
  878. font-size: 34rpx;
  879. font-weight: 600;
  880. color: #333;
  881. text-align: center;
  882. margin-bottom: 28rpx;
  883. }
  884. .confirm-content {
  885. font-size: 28rpx;
  886. color: #666;
  887. line-height: 1.7;
  888. text-align: center;
  889. margin-bottom: 40rpx;
  890. }
  891. .confirm-btns {
  892. display: flex;
  893. gap: 24rpx;
  894. }
  895. </style>