Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

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