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.
 
 
 
 
 

648 regels
27 KiB

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