You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

663 regels
27 KiB

  1. <template>
  2. <view class="page">
  3. <u-loading-page :loading="pageLoading" loading-text="加载中..." />
  4. <!-- 患者基本信息(只读) -->
  5. <view class="section">
  6. <view class="section-title">
  7. <u-icon name="account-fill" size="18" color="#0e63e3" />
  8. <text>基本信息</text>
  9. </view>
  10. <view class="info-compact">
  11. <view class="info-compact-row">
  12. <text class="info-compact-item">姓名:{{ patient.name }}</text>
  13. <text class="info-compact-item">性别:{{ patient.gender }}</text>
  14. </view>
  15. <view class="info-compact-row">
  16. <text class="info-compact-item">身份证:{{ maskedIdCard }}</text>
  17. <text class="info-compact-item">手机号:{{ maskedPhone }}</text>
  18. </view>
  19. <view class="info-compact-row">
  20. <text class="info-compact-item">联系地址:{{ patient.region_text }} {{ patient.address }}</text>
  21. </view>
  22. <view class="info-compact-row">
  23. <text class="info-compact-item">医院:{{ patient.hospital || '—' }}</text>
  24. <text class="info-compact-item">癌种:{{ patient.tag || '—' }}</text>
  25. </view>
  26. </view>
  27. </view>
  28. <!-- 平台收件信息 -->
  29. <view class="section">
  30. <view class="section-title">
  31. <u-icon name="order" size="18" color="#0e63e3" />
  32. <text>平台收件信息</text>
  33. </view>
  34. <view class="receiver-info">
  35. <view class="receiver-row">
  36. <text class="receiver-label">收件地址</text>
  37. <text class="receiver-value">{{ sampleReceiverInfo.address || '—' }}</text>
  38. </view>
  39. <view class="receiver-row">
  40. <text class="receiver-label">收件人</text>
  41. <text class="receiver-value">{{ sampleReceiverInfo.receiver || '—' }}</text>
  42. </view>
  43. <view class="receiver-row">
  44. <text class="receiver-label">电话</text>
  45. <text class="receiver-value">{{ sampleReceiverInfo.phone || '—' }}</text>
  46. </view>
  47. </view>
  48. <view class="copy-receiver" @tap="copySampleReceiverInfo">复制送检信息</view>
  49. </view>
  50. <!-- 送检信息 -->
  51. <view class="section">
  52. <view class="section-title">
  53. <u-icon name="file-text" size="18" color="#fa541c" />
  54. <text>送检信息</text>
  55. <text v-if="sampleInfoStatusText" class="status-pill" :class="sampleInfoStatusClass">{{ sampleInfoStatusText }}</text>
  56. </view>
  57. <view v-if="isSampleReturned" class="return-info">
  58. <text class="return-title">样品已经寄回</text>
  59. <text class="return-text">回寄物流单号:{{ returnTrackingNo || '—' }}</text>
  60. <text v-if="returnTime" class="return-time">回寄时间:{{ returnTime }}</text>
  61. <text class="copy-return-no" @tap="copyReturnTrackingNo">复制单号</text>
  62. </view>
  63. <view v-if="sampleEditRejectReason" class="reject-info">
  64. <text class="reject-title">修改申请未通过</text>
  65. <text class="reject-text">原因:{{ sampleEditRejectReason }}</text>
  66. </view>
  67. <view class="form-group">
  68. <text class="form-label">送检样本类型{{ sampleRequired ? '' : '(选填)' }}</text>
  69. <u-checkbox-group v-model="form.sample_types" placement="row" :wrap="true" @change="onSampleTypesChange">
  70. <u-checkbox v-for="st in sampleTypeList" :key="st.id" :label="st.name" :name="st.name"
  71. :disabled="isSampleInfoLocked" activeColor="#0E63E3" :customStyle="{ marginRight: '24rpx', marginBottom: '16rpx' }" />
  72. </u-checkbox-group>
  73. </view>
  74. <view class="form-group" v-if="showWaxReturn">
  75. <text class="form-label">{{ needReturnNames }}是否需寄回</text>
  76. <u-radio-group v-model="form.wax_return" placement="row">
  77. <u-radio label="是" :name="1" :disabled="isSampleInfoLocked" activeColor="#0E63E3" :customStyle="{ marginRight: '40rpx' }" />
  78. <u-radio label="否" :name="0" :disabled="isSampleInfoLocked" activeColor="#0E63E3" />
  79. </u-radio-group>
  80. </view>
  81. <template v-if="form.wax_return === 1 && showWaxReturn">
  82. <view class="form-group">
  83. <view style="display:flex;align-items:center;justify-content:space-between;">
  84. <text class="form-label" style="margin-bottom:0;">收件人姓名</text>
  85. <text :class="['fill-self-btn', isSampleInfoLocked ? 'disabled' : '']" @tap="fillSelfReturn">本人接收</text>
  86. </view>
  87. <u-input v-model="form.return_name" :disabled="isSampleInfoLocked" placeholder="请输入收件人姓名" border="surround" :customStyle="{ marginTop: '16rpx' }" />
  88. </view>
  89. <view class="form-group">
  90. <text class="form-label">收件人电话</text>
  91. <u-input v-model="form.return_phone" :disabled="isSampleInfoLocked" type="number" placeholder="请输入收件人电话" border="surround" maxlength="11" />
  92. </view>
  93. <view class="form-group">
  94. <text class="form-label">收件地址</text>
  95. <view :class="['region-row', isSampleInfoLocked ? 'disabled' : '']" @tap="openRegionPicker">
  96. <text :class="['region-text', returnRegionText ? '' : 'placeholder']">{{ returnRegionText || '请选择省/市/区' }}</text>
  97. <text class="arrow">›</text>
  98. </view>
  99. <u-input v-model="form.return_address" :disabled="isSampleInfoLocked" placeholder="详细门牌号" border="surround"
  100. :customStyle="{ marginTop: '16rpx' }" />
  101. </view>
  102. </template>
  103. <template v-if="(form.sample_types && form.sample_types.length) || sampleRequired">
  104. <view class="form-group">
  105. <text class="form-label">报告接收邮箱</text>
  106. <u-input v-model="form.report_email" :disabled="isSampleInfoLocked" placeholder="请输入邮箱地址" border="surround" />
  107. </view>
  108. <view class="form-group">
  109. <text class="form-label">送检样本物流单号</text>
  110. <u-input v-model="form.sample_tracking_no" :disabled="isSampleInfoLocked" placeholder="请输入物流单号" border="surround" />
  111. </view>
  112. <view class="form-group">
  113. <text class="form-label">送检单照片(可上传多张)</text>
  114. <view class="upload-row">
  115. <view class="upload-item" v-for="(photo, idx) in form.sample_photos" :key="'sp'+idx">
  116. <image class="upload-img" :src="photo" mode="aspectFill" @tap="previewSamplePhoto(idx)" />
  117. <view v-if="!isSampleInfoLocked" class="upload-del" @tap="form.sample_photos.splice(idx, 1)">×</view>
  118. </view>
  119. <view v-if="!isSampleInfoLocked" class="upload-box" @tap="chooseSamplePhoto">
  120. <text class="upload-icon">+</text>
  121. <text class="upload-text">上传图片</text>
  122. </view>
  123. </view>
  124. </view>
  125. </template>
  126. </view>
  127. <!-- 提交按钮 -->
  128. <view class="btn-wrap">
  129. <view class="agree-row" @tap="toggleAgree">
  130. <u-checkbox-group>
  131. <u-checkbox :checked="agreed" :disabled="isSampleInfoLocked" shape="circle" activeColor="#0E63E3" size="18" />
  132. </u-checkbox-group>
  133. <text class="agree-text">请阅读并同意</text>
  134. <text class="agree-link" @tap.stop="openNotice">《患者告知书》</text>
  135. </view>
  136. <u-button v-if="canApplySampleEdit" text="申请修改送检信息" :loading="applyEditSubmitting" @click="showApplyEditDialog"
  137. color="#0E63E3" plain size="large" />
  138. <u-button v-if="!canApplySampleEdit" :text="submitButtonText" :loading="submitting" @click="handleSubmit" color="#0E63E3" size="large" />
  139. </view>
  140. <!-- 修改申请弹窗 -->
  141. <u-popup :show="applyEditVisible" mode="bottom" round="12" :safeAreaInsetBottom="true" @close="closeApplyEditDialog">
  142. <view class="apply-popup">
  143. <view class="apply-title">申请修改送检信息</view>
  144. <view class="apply-desc">请填写需要修改送检信息的原因,平台审核通过后即可重新编辑。</view>
  145. <textarea v-model="applyEditReason" class="apply-textarea" maxlength="500" placeholder="请输入申请原因"
  146. :adjust-position="true" :cursor-spacing="40" />
  147. <view class="apply-actions">
  148. <u-button text="取消" @click="closeApplyEditDialog" plain color="#909399" />
  149. <u-button text="提交申请" :loading="applyEditSubmitting" @click="submitApplyEdit" color="#0E63E3" />
  150. </view>
  151. </view>
  152. </u-popup>
  153. <!-- 地区选择器 -->
  154. <u-picker v-if="regionColumns[0].length" :show="showRegionPicker" :columns="regionColumns" @confirm="onRegionConfirm"
  155. @cancel="showRegionPicker = false" @change="onRegionChange" :defaultIndex="regionDefaultIndex" />
  156. </view>
  157. </template>
  158. <script setup>
  159. import { ref, reactive, computed } from 'vue'
  160. import { onLoad } from '@dcloudio/uni-app'
  161. import { get, post, upload } from '@/utils/request.js'
  162. const patient = ref({})
  163. const form = reactive({
  164. sample_types: [],
  165. wax_return: 0,
  166. return_name: '',
  167. return_phone: '',
  168. return_province_code: '',
  169. return_city_code: '',
  170. return_district_code: '',
  171. return_address: '',
  172. report_email: '',
  173. sample_tracking_no: '',
  174. sample_photos: []
  175. })
  176. const submitting = ref(false)
  177. const pageLoading = ref(true)
  178. const agreed = ref(false)
  179. const sampleTypeList = ref([])
  180. const sampleRequired = ref(false)
  181. const sampleReceiverInfo = ref({ address: '', receiver: '', phone: '', contact_phone: '' })
  182. const sampleInfoStatus = ref(0)
  183. const returnTrackingNo = ref('')
  184. const returnTime = ref('')
  185. const sampleEditReason = ref('')
  186. const sampleEditRejectReason = ref('')
  187. const sampleEditApplyTime = ref('')
  188. const pendingLockedTip = ref(false)
  189. const applyEditVisible = ref(false)
  190. const applyEditReason = ref('')
  191. const applyEditSubmitting = ref(false)
  192. const sampleEditAuditTmplId = ref('')
  193. const isSampleReturned = computed(() => sampleInfoStatus.value === 2)
  194. const isSampleEditApplying = computed(() => sampleInfoStatus.value === 3)
  195. const isSampleInfoLocked = computed(() => sampleInfoStatus.value === 1 || sampleInfoStatus.value === 2 || sampleInfoStatus.value === 3)
  196. const canApplySampleEdit = computed(() => sampleInfoStatus.value === 1)
  197. const sampleInfoStatusText = computed(() => {
  198. if (sampleInfoStatus.value === 3) return '申请审核中'
  199. if (sampleInfoStatus.value === 2) return '已寄回'
  200. if (sampleInfoStatus.value === 1) return '已生效'
  201. return ''
  202. })
  203. const sampleInfoStatusClass = computed(() => {
  204. if (sampleInfoStatus.value === 3) return 'applying'
  205. return sampleInfoStatus.value === 2 ? 'returned' : 'effective'
  206. })
  207. const submitButtonText = computed(() => {
  208. if (sampleInfoStatus.value === 3) return '修改申请审核中'
  209. if (sampleInfoStatus.value === 2) return '样品已寄回'
  210. if (sampleInfoStatus.value === 1) return '送检信息已生效'
  211. return '提交送检信息'
  212. })
  213. // 地区数据
  214. const allRegions = ref([])
  215. const regionColumns = ref([[], [], []])
  216. const regionDefaultIndex = ref([0, 0, 0])
  217. const showRegionPicker = ref(false)
  218. const maskedIdCard = computed(() => {
  219. const v = patient.value.id_card || ''
  220. if (v.length === 18) return v.slice(0, 3) + '****' + v.slice(-4)
  221. return v
  222. })
  223. const maskedPhone = computed(() => {
  224. const v = patient.value.phone || ''
  225. if (v.length === 11) return v.slice(0, 3) + '****' + v.slice(-4)
  226. return v
  227. })
  228. const showWaxReturn = computed(() => {
  229. if (!form.sample_types || !form.sample_types.length) return false
  230. return form.sample_types.some(name => {
  231. const st = sampleTypeList.value.find(s => s.name === name)
  232. return st && st.need_return
  233. })
  234. })
  235. const needReturnNames = computed(() => {
  236. if (!form.sample_types || !form.sample_types.length) return ''
  237. const names = form.sample_types.filter(name => {
  238. const st = sampleTypeList.value.find(s => s.name === name)
  239. return st && st.need_return
  240. })
  241. return names.join('、')
  242. })
  243. const returnRegionText = computed(() => {
  244. const parts = []
  245. if (form.return_province_code) {
  246. const p = allRegions.value.find(r => r.code === form.return_province_code)
  247. if (p) parts.push(p.name)
  248. }
  249. if (form.return_city_code) {
  250. const prov = allRegions.value.find(r => r.code === form.return_province_code)
  251. if (prov && prov.children) {
  252. const c = prov.children.find(r => r.code === form.return_city_code)
  253. if (c) parts.push(c.name)
  254. }
  255. }
  256. if (form.return_district_code) {
  257. const prov = allRegions.value.find(r => r.code === form.return_province_code)
  258. if (prov && prov.children) {
  259. const city = prov.children.find(r => r.code === form.return_city_code)
  260. if (city && city.children) {
  261. const d = city.children.find(r => r.code === form.return_district_code)
  262. if (d) parts.push(d.name)
  263. }
  264. }
  265. }
  266. return parts.join(' ')
  267. })
  268. const onSampleTypesChange = () => {
  269. if (isSampleInfoLocked.value) return
  270. if (!showWaxReturn.value) {
  271. form.wax_return = 0
  272. }
  273. if (!form.sample_types || form.sample_types.length === 0) {
  274. form.report_email = ''
  275. form.sample_tracking_no = ''
  276. form.sample_photos = []
  277. form.wax_return = 0
  278. form.return_name = ''
  279. form.return_phone = ''
  280. form.return_province_code = ''
  281. form.return_city_code = ''
  282. form.return_district_code = ''
  283. form.return_address = ''
  284. }
  285. }
  286. const fillSelfReturn = () => {
  287. if (isSampleInfoLocked.value) return showLockedTip()
  288. form.return_name = patient.value.name || ''
  289. form.return_phone = patient.value.phone || ''
  290. form.return_province_code = patient.value.province_code || ''
  291. form.return_city_code = patient.value.city_code || ''
  292. form.return_district_code = patient.value.district_code || ''
  293. form.return_address = patient.value.address || ''
  294. }
  295. const openRegionPicker = () => {
  296. if (isSampleInfoLocked.value) return showLockedTip()
  297. showRegionPicker.value = true
  298. }
  299. onLoad(async () => {
  300. pageLoading.value = true
  301. try {
  302. await loadRegions()
  303. await loadPatientInfo()
  304. await loadSampleTypes()
  305. await loadSubscribeConfig()
  306. } finally {
  307. pageLoading.value = false
  308. if (pendingLockedTip.value) {
  309. pendingLockedTip.value = false
  310. setTimeout(() => showLockedTip(), 100)
  311. }
  312. }
  313. })
  314. const loadSubscribeConfig = async () => {
  315. try {
  316. const res = await get('/api/mp/subscribeConfig')
  317. if (res.data && res.data.sample_edit_audit) {
  318. sampleEditAuditTmplId.value = res.data.sample_edit_audit
  319. }
  320. } catch (e) {}
  321. }
  322. const requestSampleEditSubscribe = () => {
  323. return new Promise((resolve) => {
  324. if (!sampleEditAuditTmplId.value) return resolve(false)
  325. // #ifdef MP-WEIXIN
  326. wx.requestSubscribeMessage({
  327. tmplIds: [sampleEditAuditTmplId.value],
  328. success: () => resolve(true),
  329. fail: () => resolve(false)
  330. })
  331. // #endif
  332. // #ifndef MP-WEIXIN
  333. resolve(false)
  334. // #endif
  335. })
  336. }
  337. const loadPatientInfo = async () => {
  338. try {
  339. const res = await get('/api/mp/sampleInfo')
  340. if (res.data) {
  341. patient.value = res.data.patient || {}
  342. // 回显送检信息
  343. form.sample_types = res.data.sample_types || []
  344. form.wax_return = res.data.wax_return || 0
  345. form.return_name = res.data.return_name || ''
  346. form.return_phone = res.data.return_phone || ''
  347. form.return_province_code = res.data.return_province_code || ''
  348. form.return_city_code = res.data.return_city_code || ''
  349. form.return_district_code = res.data.return_district_code || ''
  350. form.return_address = res.data.return_address || ''
  351. form.report_email = res.data.report_email || ''
  352. form.sample_tracking_no = res.data.sample_tracking_no || ''
  353. form.sample_photos = res.data.sample_photos || []
  354. sampleInfoStatus.value = Number(res.data.sample_info_status) || 0
  355. returnTrackingNo.value = res.data.return_tracking_no || ''
  356. returnTime.value = res.data.return_time || ''
  357. sampleEditReason.value = res.data.sample_edit_reason || ''
  358. sampleEditRejectReason.value = res.data.sample_edit_reject_reason || ''
  359. sampleEditApplyTime.value = res.data.sample_edit_apply_time || ''
  360. sampleReceiverInfo.value = res.data.sample_receiver_info || { address: '', receiver: '', phone: '', contact_phone: '' }
  361. pendingLockedTip.value = (sampleInfoStatus.value === 1 && !sampleEditRejectReason.value) || sampleInfoStatus.value === 3
  362. }
  363. } catch (e) {}
  364. }
  365. const loadSampleTypes = async () => {
  366. try {
  367. const res = await get('/common/sampleTypes')
  368. sampleTypeList.value = (res.data && res.data.list) || []
  369. sampleRequired.value = (res.data && res.data.required) || false
  370. } catch (e) {}
  371. }
  372. const loadRegions = async () => {
  373. try {
  374. const res = await get('/common/regions')
  375. allRegions.value = res.data || []
  376. buildRegionColumns()
  377. } catch (e) {}
  378. }
  379. const buildRegionColumns = (pIdx = 0, cIdx = 0) => {
  380. const provinces = allRegions.value
  381. const col0 = provinces.map(p => p.name)
  382. const cities = (provinces[pIdx] && provinces[pIdx].children) || []
  383. const col1 = cities.map(c => c.name)
  384. const districts = (cities[cIdx] && cities[cIdx].children) || []
  385. const col2 = districts.map(d => d.name)
  386. regionColumns.value = [col0, col1, col2]
  387. }
  388. const onRegionChange = (e) => {
  389. const { columnIndex, index } = e
  390. if (columnIndex === 0) {
  391. buildRegionColumns(index, 0)
  392. regionDefaultIndex.value = [index, 0, 0]
  393. } else if (columnIndex === 1) {
  394. const pIdx = regionDefaultIndex.value[0]
  395. buildRegionColumns(pIdx, index)
  396. regionDefaultIndex.value = [pIdx, index, 0]
  397. }
  398. }
  399. const onRegionConfirm = (e) => {
  400. if (isSampleInfoLocked.value) return
  401. const idxs = e.indexs || e.index || [0, 0, 0]
  402. const provinces = allRegions.value
  403. const prov = provinces[idxs[0]]
  404. const city = prov && prov.children ? prov.children[idxs[1]] : null
  405. const dist = city && city.children ? city.children[idxs[2]] : null
  406. form.return_province_code = prov ? prov.code : ''
  407. form.return_city_code = city ? city.code : ''
  408. form.return_district_code = dist ? dist.code : ''
  409. showRegionPicker.value = false
  410. }
  411. const chooseSamplePhoto = () => {
  412. if (isSampleInfoLocked.value) return showLockedTip()
  413. uni.chooseImage({
  414. count: 9 - form.sample_photos.length,
  415. sizeType: ['compressed'],
  416. sourceType: ['album', 'camera'],
  417. success: async (res) => {
  418. for (const filePath of res.tempFilePaths) {
  419. try {
  420. const uploadRes = await upload('/api/mp/upload', { filePath, name: 'file' })
  421. if (uploadRes.data && uploadRes.data.url) {
  422. form.sample_photos.push(uploadRes.data.url)
  423. }
  424. } catch (e) {}
  425. }
  426. }
  427. })
  428. }
  429. const previewSamplePhoto = (idx) => {
  430. uni.previewImage({ urls: form.sample_photos, current: idx })
  431. }
  432. const openNotice = () => {
  433. uni.navigateTo({ url: '/pages/content/content?key=patient_information_sheet' })
  434. }
  435. const getContactPhone = () => sampleReceiverInfo.value.contact_phone || ''
  436. const getCleanPhone = () => getContactPhone().replace(/[^\d+]/g, '')
  437. const contactPlatform = () => {
  438. const phone = getContactPhone()
  439. if (!phone) return uni.showToast({ title: '暂无联系电话', icon: 'none' })
  440. // #ifdef H5
  441. uni.setClipboardData({
  442. data: phone,
  443. success: () => uni.showToast({ title: '电话已复制', icon: 'success' })
  444. })
  445. // #endif
  446. // #ifndef H5
  447. uni.makePhoneCall({ phoneNumber: getCleanPhone() })
  448. // #endif
  449. }
  450. const showLockedTip = () => {
  451. if (isSampleEditApplying.value) {
  452. uni.showModal({
  453. title: '温馨提示',
  454. content: '送检信息修改申请正在审核中,请等待平台处理。',
  455. showCancel: false,
  456. confirmText: '知道了'
  457. })
  458. return
  459. }
  460. if (isSampleReturned.value) {
  461. uni.showModal({
  462. title: '温馨提示',
  463. content: `样品已经寄回,回寄物流单号:${returnTrackingNo.value || '—'}。`,
  464. cancelText: '知道了',
  465. confirmText: '复制单号',
  466. success: (res) => {
  467. if (res.confirm) copyReturnTrackingNo()
  468. }
  469. })
  470. return
  471. }
  472. const phone = getContactPhone()
  473. let content = '送检信息已生效,如需修改请点击页面的【申请修改送检信息】按钮申请,通过后可重新提交送检信息。'
  474. if (phone) content += `详情咨询${phone}。`
  475. uni.showModal({
  476. title: '温馨提示',
  477. content,
  478. showCancel: false,
  479. confirmText: '知道了'
  480. })
  481. }
  482. const showApplyEditDialog = () => {
  483. if (!canApplySampleEdit.value) return showLockedTip()
  484. applyEditReason.value = ''
  485. applyEditVisible.value = true
  486. }
  487. const closeApplyEditDialog = () => {
  488. applyEditVisible.value = false
  489. }
  490. const submitApplyEdit = async () => {
  491. if (!applyEditReason.value.trim()) {
  492. return uni.showToast({ title: '请填写申请原因', icon: 'none' })
  493. }
  494. applyEditSubmitting.value = true
  495. try {
  496. await requestSampleEditSubscribe()
  497. const params = { reason: applyEditReason.value.trim() }
  498. // #ifdef MP-WEIXIN
  499. params.mp_env_version = uni.getAccountInfoSync().miniProgram.envVersion || 'release'
  500. // #endif
  501. await post('/api/mp/applySampleInfoEdit', params)
  502. sampleInfoStatus.value = 3
  503. sampleEditReason.value = applyEditReason.value.trim()
  504. sampleEditApplyTime.value = ''
  505. applyEditVisible.value = false
  506. uni.showToast({ title: '申请已提交', icon: 'success' })
  507. } catch (e) {
  508. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  509. } finally {
  510. applyEditSubmitting.value = false
  511. }
  512. }
  513. const copyReturnTrackingNo = () => {
  514. if (!returnTrackingNo.value) return uni.showToast({ title: '暂无单号', icon: 'none' })
  515. uni.setClipboardData({
  516. data: returnTrackingNo.value,
  517. success: () => uni.showToast({ title: '单号已复制', icon: 'success' })
  518. })
  519. }
  520. const copySampleReceiverInfo = () => {
  521. const info = sampleReceiverInfo.value
  522. const text = [
  523. `收件地址:${info.address || ''}`,
  524. `收件人:${info.receiver || ''}`,
  525. `电话:${info.phone || ''}`
  526. ].join('\n')
  527. uni.setClipboardData({
  528. data: text,
  529. success: () => uni.showToast({ title: '已复制', icon: 'success' })
  530. })
  531. }
  532. const toggleAgree = () => {
  533. if (isSampleInfoLocked.value) return showLockedTip()
  534. agreed.value = !agreed.value
  535. }
  536. const handleSubmit = async () => {
  537. if (isSampleInfoLocked.value) {
  538. return showLockedTip()
  539. }
  540. if (!agreed.value) {
  541. return uni.showToast({ title: '请阅读并同意《患者告知书》', icon: 'none' })
  542. }
  543. if (sampleRequired.value && (!form.sample_types || form.sample_types.length === 0)) {
  544. return uni.showToast({ title: '请选择送检样本类型', icon: 'none' })
  545. }
  546. if (form.wax_return === 1 && showWaxReturn.value) {
  547. if (!form.return_name) return uni.showToast({ title: '请输入收件人姓名', icon: 'none' })
  548. if (!form.return_phone) return uni.showToast({ title: '请输入收件人电话', icon: 'none' })
  549. if (!form.return_province_code) return uni.showToast({ title: '请选择收件地址', icon: 'none' })
  550. if (!form.return_address) return uni.showToast({ title: '请输入收件详细地址', icon: 'none' })
  551. }
  552. if (form.sample_types && form.sample_types.length > 0) {
  553. if (!form.report_email) return uni.showToast({ title: '请输入报告接收邮箱', icon: 'none' })
  554. if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.report_email)) return uni.showToast({ title: '邮箱格式不正确', icon: 'none' })
  555. if (!form.sample_tracking_no) return uni.showToast({ title: '请输入送检样本物流单号', icon: 'none' })
  556. if (!form.sample_photos || form.sample_photos.length === 0) return uni.showToast({ title: '请上传送检单照片', icon: 'none' })
  557. }
  558. submitting.value = true
  559. try {
  560. await post('/api/mp/saveSampleInfo', { ...form })
  561. sampleInfoStatus.value = 1
  562. uni.showToast({ title: '提交成功', icon: 'success' })
  563. setTimeout(() => uni.navigateBack(), 1500)
  564. } catch (e) {
  565. if (e && e.data && [1, 2, 3].includes(Number(e.data.sample_info_status))) {
  566. sampleInfoStatus.value = Number(e.data.sample_info_status)
  567. returnTrackingNo.value = e.data.return_tracking_no || returnTrackingNo.value
  568. returnTime.value = e.data.return_time || returnTime.value
  569. sampleEditReason.value = e.data.sample_edit_reason || sampleEditReason.value
  570. sampleEditApplyTime.value = e.data.sample_edit_apply_time || sampleEditApplyTime.value
  571. if (e.data.sample_receiver_info) sampleReceiverInfo.value = e.data.sample_receiver_info
  572. return showLockedTip()
  573. }
  574. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  575. } finally {
  576. submitting.value = false
  577. }
  578. }
  579. </script>
  580. <style lang="scss" scoped>
  581. .page { min-height: 100vh; background: #f4f4f5; padding: 24rpx; padding-bottom: 260rpx; }
  582. .section { background: #fff; border-radius: 10rpx; padding: 32rpx; margin-bottom: 24rpx; border: 1rpx solid #ebeef5; }
  583. .section-title { display: flex; align-items: center; gap: 12rpx; font-size: 32rpx; font-weight: 600; color: #333; margin-bottom: 28rpx; }
  584. .status-pill { margin-left: auto; font-size: 22rpx; font-weight: 500; padding: 4rpx 14rpx; border-radius: 6rpx; &.effective { color: #52c41a; background: #f6ffed; border: 1rpx solid #b7eb8f; } &.returned { color: #0e63e3; background: #f0f5ff; border: 1rpx solid #adc6ff; } &.applying { color: #d46b08; background: #fff7e6; border: 1rpx solid #ffd591; } }
  585. .info-compact { padding-bottom: 8rpx; }
  586. .info-compact-row { display: flex; gap: 32rpx; margin-bottom: 12rpx; &:last-child { margin-bottom: 0; } }
  587. .info-compact-item { font-size: 26rpx; color: #666; }
  588. .return-info { background: #f0f5ff; border-left: 6rpx solid #0e63e3; border-radius: 8rpx; padding: 24rpx; margin-bottom: 20rpx; }
  589. .return-title { display: block; font-size: 30rpx; font-weight: 600; color: #303133; margin-bottom: 12rpx; }
  590. .return-text { display: block; font-size: 28rpx; color: #0e63e3; font-weight: 600; line-height: 1.5; }
  591. .return-time { display: block; font-size: 24rpx; color: #909399; margin-top: 8rpx; }
  592. .copy-return-no { display: block; text-align: right; font-size: 26rpx; color: #0e63e3; margin-top: 12rpx; }
  593. .reject-info { background: #fff2f0; border-left: 6rpx solid #ff4d4f; border-radius: 8rpx; padding: 24rpx; margin-bottom: 20rpx; }
  594. .reject-title { display: block; font-size: 28rpx; font-weight: 600; color: #a8071a; margin-bottom: 10rpx; }
  595. .reject-text { display: block; font-size: 26rpx; color: #606266; line-height: 1.6; }
  596. .receiver-info { padding-bottom: 8rpx; }
  597. .receiver-row { display: flex; gap: 20rpx; margin-bottom: 14rpx; &:last-child { margin-bottom: 0; } }
  598. .receiver-label { width: 120rpx; flex-shrink: 0; font-size: 26rpx; color: #909399; }
  599. .receiver-value { flex: 1; font-size: 26rpx; color: #303133; line-height: 1.6; }
  600. .copy-receiver { margin-top: 20rpx; color: #0e63e3; font-size: 26rpx; text-align: right; }
  601. .form-group { padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: none; } }
  602. .form-label { font-size: 28rpx; color: #555; margin-bottom: 16rpx; display: block; }
  603. .region-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border: 1rpx solid #ddd; border-radius: 8rpx; &.disabled { background: #f7f8fa; } }
  604. .region-text { font-size: 28rpx; color: #333; &.placeholder { color: #c0c4cc; } }
  605. .arrow { font-size: 28rpx; color: #c0c4cc; }
  606. .fill-self-btn { font-size: 24rpx; color: #0e63e3; padding: 6rpx 16rpx; border: 1rpx solid #d0e0ff; border-radius: 20rpx; background: #f0f5ff; &.disabled { color: #c0c4cc; border-color: #e4e7ed; background: #f7f8fa; } }
  607. .upload-row { display: flex; gap: 16rpx; flex-wrap: wrap; }
  608. .upload-item { position: relative; width: 180rpx; height: 180rpx; }
  609. .upload-img { width: 180rpx; height: 180rpx; border-radius: 8rpx; border: 1rpx solid #eee; }
  610. .upload-del { position: absolute; top: -10rpx; right: -10rpx; width: 40rpx; height: 40rpx; background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%; font-size: 24rpx; display: flex; align-items: center; justify-content: center; }
  611. .upload-box { width: 180rpx; height: 180rpx; border: 2rpx dashed #ccc; border-radius: 8rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; background: #fafafa; }
  612. .upload-icon { font-size: 56rpx; color: #ccc; }
  613. .upload-text { font-size: 22rpx; color: #999; margin-top: 4rpx; }
  614. .btn-wrap { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 20rpx 32rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.06); z-index: 100; }
  615. .agree-row { display: flex; align-items: center; margin-bottom: 16rpx; padding-left: 4rpx; }
  616. .agree-text { font-size: 24rpx; color: #666; margin-left: 8rpx; }
  617. .agree-link { font-size: 24rpx; color: #0e63e3; }
  618. .apply-popup { width: 100%; box-sizing: border-box; background: #fff; border-radius: 24rpx 24rpx 0 0; padding: 36rpx 32rpx 24rpx; }
  619. .apply-title { font-size: 32rpx; font-weight: 600; color: #303133; margin-bottom: 12rpx; }
  620. .apply-desc { font-size: 26rpx; color: #606266; line-height: 1.6; margin-bottom: 20rpx; }
  621. .apply-textarea { width: 100%; height: 150rpx; box-sizing: border-box; border: 1rpx solid #dcdfe6; border-radius: 8rpx; padding: 20rpx; font-size: 28rpx; color: #303133; background: #fff; }
  622. .apply-actions { display: flex; gap: 20rpx; margin-top: 24rpx; }
  623. </style>