Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 

385 rader
15 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="file-text" size="18" color="#fa541c" />
  31. <text>送检信息</text>
  32. </view>
  33. <view class="form-group">
  34. <text class="form-label">送检样本类型{{ sampleRequired ? '' : '(选填)' }}</text>
  35. <u-checkbox-group v-model="form.sample_types" placement="row" :wrap="true" @change="onSampleTypesChange">
  36. <u-checkbox v-for="st in sampleTypeList" :key="st.id" :label="st.name" :name="st.name"
  37. activeColor="#0E63E3" :customStyle="{ marginRight: '24rpx', marginBottom: '16rpx' }" />
  38. </u-checkbox-group>
  39. </view>
  40. <view class="form-group" v-if="showWaxReturn">
  41. <text class="form-label">{{ needReturnNames }}是否需寄回</text>
  42. <u-radio-group v-model="form.wax_return" placement="row">
  43. <u-radio label="是" :name="1" activeColor="#0E63E3" :customStyle="{ marginRight: '40rpx' }" />
  44. <u-radio label="否" :name="0" activeColor="#0E63E3" />
  45. </u-radio-group>
  46. </view>
  47. <template v-if="form.wax_return === 1 && showWaxReturn">
  48. <view class="form-group">
  49. <view style="display:flex;align-items:center;justify-content:space-between;">
  50. <text class="form-label" style="margin-bottom:0;">收件人姓名</text>
  51. <text class="fill-self-btn" @tap="fillSelfReturn">本人接收</text>
  52. </view>
  53. <u-input v-model="form.return_name" placeholder="请输入收件人姓名" border="surround" :customStyle="{ marginTop: '16rpx' }" />
  54. </view>
  55. <view class="form-group">
  56. <text class="form-label">收件人电话</text>
  57. <u-input v-model="form.return_phone" type="number" placeholder="请输入收件人电话" border="surround" maxlength="11" />
  58. </view>
  59. <view class="form-group">
  60. <text class="form-label">收件地址</text>
  61. <view class="region-row" @tap="showRegionPicker = true">
  62. <text :class="['region-text', returnRegionText ? '' : 'placeholder']">{{ returnRegionText || '请选择省/市/区' }}</text>
  63. <text class="arrow">›</text>
  64. </view>
  65. <u-input v-model="form.return_address" placeholder="详细门牌号" border="surround"
  66. :customStyle="{ marginTop: '16rpx' }" />
  67. </view>
  68. </template>
  69. <template v-if="(form.sample_types && form.sample_types.length) || sampleRequired">
  70. <view class="form-group">
  71. <text class="form-label">报告接收邮箱</text>
  72. <u-input v-model="form.report_email" placeholder="请输入邮箱地址" border="surround" />
  73. </view>
  74. <view class="form-group">
  75. <text class="form-label">送检样本物流单号</text>
  76. <u-input v-model="form.sample_tracking_no" placeholder="请输入物流单号" border="surround" />
  77. </view>
  78. <view class="form-group">
  79. <text class="form-label">送检单照片(可上传多张)</text>
  80. <view class="upload-row">
  81. <view class="upload-item" v-for="(photo, idx) in form.sample_photos" :key="'sp'+idx">
  82. <image class="upload-img" :src="photo" mode="aspectFill" @tap="previewSamplePhoto(idx)" />
  83. <view class="upload-del" @tap="form.sample_photos.splice(idx, 1)">×</view>
  84. </view>
  85. <view class="upload-box" @tap="chooseSamplePhoto">
  86. <text class="upload-icon">+</text>
  87. <text class="upload-text">上传图片</text>
  88. </view>
  89. </view>
  90. </view>
  91. </template>
  92. </view>
  93. <!-- 提交按钮 -->
  94. <view class="btn-wrap">
  95. <view class="agree-row" @tap="agreed = !agreed">
  96. <u-checkbox-group>
  97. <u-checkbox :checked="agreed" shape="circle" activeColor="#0E63E3" size="18" @change="agreed = !agreed" />
  98. </u-checkbox-group>
  99. <text class="agree-text">请阅读并同意</text>
  100. <text class="agree-link" @tap.stop="openNotice">《患者告知书》</text>
  101. </view>
  102. <u-button text="提交送检信息" :loading="submitting" @click="handleSubmit" color="#0E63E3" size="large" />
  103. </view>
  104. <!-- 地区选择器 -->
  105. <u-picker v-if="regionColumns[0].length" :show="showRegionPicker" :columns="regionColumns" @confirm="onRegionConfirm"
  106. @cancel="showRegionPicker = false" @change="onRegionChange" :defaultIndex="regionDefaultIndex" />
  107. </view>
  108. </template>
  109. <script setup>
  110. import { ref, reactive, computed } from 'vue'
  111. import { onLoad } from '@dcloudio/uni-app'
  112. import { get, post, upload } from '@/utils/request.js'
  113. const patient = ref({})
  114. const form = reactive({
  115. sample_types: [],
  116. wax_return: 0,
  117. return_name: '',
  118. return_phone: '',
  119. return_province_code: '',
  120. return_city_code: '',
  121. return_district_code: '',
  122. return_address: '',
  123. report_email: '',
  124. sample_tracking_no: '',
  125. sample_photos: []
  126. })
  127. const submitting = ref(false)
  128. const agreed = ref(false)
  129. const sampleTypeList = ref([])
  130. const sampleRequired = ref(false)
  131. // 地区数据
  132. const allRegions = ref([])
  133. const regionColumns = ref([[], [], []])
  134. const regionDefaultIndex = ref([0, 0, 0])
  135. const showRegionPicker = ref(false)
  136. const maskedIdCard = computed(() => {
  137. const v = patient.value.id_card || ''
  138. if (v.length === 18) return v.slice(0, 3) + '****' + v.slice(-4)
  139. return v
  140. })
  141. const maskedPhone = computed(() => {
  142. const v = patient.value.phone || ''
  143. if (v.length === 11) return v.slice(0, 3) + '****' + v.slice(-4)
  144. return v
  145. })
  146. const showWaxReturn = computed(() => {
  147. if (!form.sample_types || !form.sample_types.length) return false
  148. return form.sample_types.some(name => {
  149. const st = sampleTypeList.value.find(s => s.name === name)
  150. return st && st.need_return
  151. })
  152. })
  153. const needReturnNames = computed(() => {
  154. if (!form.sample_types || !form.sample_types.length) return ''
  155. const names = form.sample_types.filter(name => {
  156. const st = sampleTypeList.value.find(s => s.name === name)
  157. return st && st.need_return
  158. })
  159. return names.join('、')
  160. })
  161. const returnRegionText = computed(() => {
  162. const parts = []
  163. if (form.return_province_code) {
  164. const p = allRegions.value.find(r => r.code === form.return_province_code)
  165. if (p) parts.push(p.name)
  166. }
  167. if (form.return_city_code) {
  168. const prov = allRegions.value.find(r => r.code === form.return_province_code)
  169. if (prov && prov.children) {
  170. const c = prov.children.find(r => r.code === form.return_city_code)
  171. if (c) parts.push(c.name)
  172. }
  173. }
  174. if (form.return_district_code) {
  175. const prov = allRegions.value.find(r => r.code === form.return_province_code)
  176. if (prov && prov.children) {
  177. const city = prov.children.find(r => r.code === form.return_city_code)
  178. if (city && city.children) {
  179. const d = city.children.find(r => r.code === form.return_district_code)
  180. if (d) parts.push(d.name)
  181. }
  182. }
  183. }
  184. return parts.join(' ')
  185. })
  186. const onSampleTypesChange = () => {
  187. if (!showWaxReturn.value) {
  188. form.wax_return = 0
  189. }
  190. if (!form.sample_types || form.sample_types.length === 0) {
  191. form.report_email = ''
  192. form.sample_tracking_no = ''
  193. form.sample_photos = []
  194. form.wax_return = 0
  195. form.return_name = ''
  196. form.return_phone = ''
  197. form.return_province_code = ''
  198. form.return_city_code = ''
  199. form.return_district_code = ''
  200. form.return_address = ''
  201. }
  202. }
  203. const fillSelfReturn = () => {
  204. form.return_name = patient.value.name || ''
  205. form.return_phone = patient.value.phone || ''
  206. form.return_province_code = patient.value.province_code || ''
  207. form.return_city_code = patient.value.city_code || ''
  208. form.return_district_code = patient.value.district_code || ''
  209. form.return_address = patient.value.address || ''
  210. }
  211. onLoad(async () => {
  212. await loadRegions()
  213. await loadPatientInfo()
  214. await loadSampleTypes()
  215. })
  216. const loadPatientInfo = async () => {
  217. try {
  218. const res = await get('/api/mp/sampleInfo')
  219. if (res.data) {
  220. patient.value = res.data.patient || {}
  221. // 回显送检信息
  222. form.sample_types = res.data.sample_types || []
  223. form.wax_return = res.data.wax_return || 0
  224. form.return_name = res.data.return_name || ''
  225. form.return_phone = res.data.return_phone || ''
  226. form.return_province_code = res.data.return_province_code || ''
  227. form.return_city_code = res.data.return_city_code || ''
  228. form.return_district_code = res.data.return_district_code || ''
  229. form.return_address = res.data.return_address || ''
  230. form.report_email = res.data.report_email || ''
  231. form.sample_tracking_no = res.data.sample_tracking_no || ''
  232. form.sample_photos = res.data.sample_photos || []
  233. }
  234. } catch (e) {}
  235. }
  236. const loadSampleTypes = async () => {
  237. try {
  238. const res = await get('/common/sampleTypes')
  239. sampleTypeList.value = (res.data && res.data.list) || []
  240. sampleRequired.value = (res.data && res.data.required) || false
  241. } catch (e) {}
  242. }
  243. const loadRegions = async () => {
  244. try {
  245. const res = await get('/common/regions')
  246. allRegions.value = res.data || []
  247. buildRegionColumns()
  248. } catch (e) {}
  249. }
  250. const buildRegionColumns = (pIdx = 0, cIdx = 0) => {
  251. const provinces = allRegions.value
  252. const col0 = provinces.map(p => p.name)
  253. const cities = (provinces[pIdx] && provinces[pIdx].children) || []
  254. const col1 = cities.map(c => c.name)
  255. const districts = (cities[cIdx] && cities[cIdx].children) || []
  256. const col2 = districts.map(d => d.name)
  257. regionColumns.value = [col0, col1, col2]
  258. }
  259. const onRegionChange = (e) => {
  260. const { columnIndex, index } = e
  261. if (columnIndex === 0) {
  262. buildRegionColumns(index, 0)
  263. regionDefaultIndex.value = [index, 0, 0]
  264. } else if (columnIndex === 1) {
  265. const pIdx = regionDefaultIndex.value[0]
  266. buildRegionColumns(pIdx, index)
  267. regionDefaultIndex.value = [pIdx, index, 0]
  268. }
  269. }
  270. const onRegionConfirm = (e) => {
  271. const idxs = e.indexs || e.index || [0, 0, 0]
  272. const provinces = allRegions.value
  273. const prov = provinces[idxs[0]]
  274. const city = prov && prov.children ? prov.children[idxs[1]] : null
  275. const dist = city && city.children ? city.children[idxs[2]] : null
  276. form.return_province_code = prov ? prov.code : ''
  277. form.return_city_code = city ? city.code : ''
  278. form.return_district_code = dist ? dist.code : ''
  279. showRegionPicker.value = false
  280. }
  281. const chooseSamplePhoto = () => {
  282. uni.chooseImage({
  283. count: 9 - form.sample_photos.length,
  284. sizeType: ['compressed'],
  285. sourceType: ['album', 'camera'],
  286. success: async (res) => {
  287. for (const filePath of res.tempFilePaths) {
  288. try {
  289. const uploadRes = await upload('/api/mp/upload', { filePath, name: 'file' })
  290. if (uploadRes.data && uploadRes.data.url) {
  291. form.sample_photos.push(uploadRes.data.url)
  292. }
  293. } catch (e) {}
  294. }
  295. }
  296. })
  297. }
  298. const previewSamplePhoto = (idx) => {
  299. uni.previewImage({ urls: form.sample_photos, current: idx })
  300. }
  301. const openNotice = () => {
  302. uni.navigateTo({ url: '/pages/content/content?key=patient_information_sheet' })
  303. }
  304. const handleSubmit = async () => {
  305. if (!agreed.value) {
  306. return uni.showToast({ title: '请阅读并同意《患者告知书》', icon: 'none' })
  307. }
  308. if (sampleRequired.value && (!form.sample_types || form.sample_types.length === 0)) {
  309. return uni.showToast({ title: '请选择送检样本类型', icon: 'none' })
  310. }
  311. if (form.wax_return === 1 && showWaxReturn.value) {
  312. if (!form.return_name) return uni.showToast({ title: '请输入收件人姓名', icon: 'none' })
  313. if (!form.return_phone) return uni.showToast({ title: '请输入收件人电话', icon: 'none' })
  314. if (!form.return_province_code) return uni.showToast({ title: '请选择收件地址', icon: 'none' })
  315. if (!form.return_address) return uni.showToast({ title: '请输入收件详细地址', icon: 'none' })
  316. }
  317. if (form.sample_types && form.sample_types.length > 0) {
  318. if (!form.report_email) return uni.showToast({ title: '请输入报告接收邮箱', icon: 'none' })
  319. if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.report_email)) return uni.showToast({ title: '邮箱格式不正确', icon: 'none' })
  320. if (!form.sample_tracking_no) return uni.showToast({ title: '请输入送检样本物流单号', icon: 'none' })
  321. if (!form.sample_photos || form.sample_photos.length === 0) return uni.showToast({ title: '请上传送检单照片', icon: 'none' })
  322. }
  323. submitting.value = true
  324. try {
  325. await post('/api/mp/saveSampleInfo', { ...form })
  326. uni.showToast({ title: '提交成功', icon: 'success' })
  327. setTimeout(() => uni.navigateBack(), 1500)
  328. } catch (e) {
  329. if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' })
  330. } finally {
  331. submitting.value = false
  332. }
  333. }
  334. </script>
  335. <style lang="scss" scoped>
  336. .page { min-height: 100vh; background: #f4f4f5; padding: 24rpx; padding-bottom: 260rpx; }
  337. .section { background: #fff; border-radius: 10rpx; padding: 32rpx; margin-bottom: 24rpx; border: 1rpx solid #ebeef5; }
  338. .section-title { display: flex; align-items: center; gap: 12rpx; font-size: 32rpx; font-weight: 600; color: #333; margin-bottom: 28rpx; }
  339. .info-compact { padding-bottom: 8rpx; }
  340. .info-compact-row { display: flex; gap: 32rpx; margin-bottom: 12rpx; &:last-child { margin-bottom: 0; } }
  341. .info-compact-item { font-size: 26rpx; color: #666; }
  342. .form-group { padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: none; } }
  343. .form-label { font-size: 28rpx; color: #555; margin-bottom: 16rpx; display: block; }
  344. .region-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border: 1rpx solid #ddd; border-radius: 8rpx; }
  345. .region-text { font-size: 28rpx; color: #333; &.placeholder { color: #c0c4cc; } }
  346. .arrow { font-size: 28rpx; color: #c0c4cc; }
  347. .fill-self-btn { font-size: 24rpx; color: #0e63e3; padding: 6rpx 16rpx; border: 1rpx solid #d0e0ff; border-radius: 20rpx; background: #f0f5ff; }
  348. .upload-row { display: flex; gap: 16rpx; flex-wrap: wrap; }
  349. .upload-item { position: relative; width: 180rpx; height: 180rpx; }
  350. .upload-img { width: 180rpx; height: 180rpx; border-radius: 8rpx; border: 1rpx solid #eee; }
  351. .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; }
  352. .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; }
  353. .upload-icon { font-size: 56rpx; color: #ccc; }
  354. .upload-text { font-size: 22rpx; color: #999; margin-top: 4rpx; }
  355. .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; }
  356. .agree-row { display: flex; align-items: center; margin-bottom: 16rpx; padding-left: 4rpx; }
  357. .agree-text { font-size: 24rpx; color: #666; margin-left: 8rpx; }
  358. .agree-link { font-size: 24rpx; color: #0e63e3; }
  359. </style>