No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 

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