|
|
|
@@ -0,0 +1,383 @@ |
|
|
|
<template> |
|
|
|
<view class="page"> |
|
|
|
<!-- 患者基本信息(只读) --> |
|
|
|
<view class="section"> |
|
|
|
<view class="section-title"> |
|
|
|
<u-icon name="account-fill" size="18" color="#0e63e3" /> |
|
|
|
<text>基本信息</text> |
|
|
|
</view> |
|
|
|
<view class="info-compact"> |
|
|
|
<view class="info-compact-row"> |
|
|
|
<text class="info-compact-item">姓名:{{ patient.name }}</text> |
|
|
|
<text class="info-compact-item">性别:{{ patient.gender }}</text> |
|
|
|
</view> |
|
|
|
<view class="info-compact-row"> |
|
|
|
<text class="info-compact-item">身份证:{{ maskedIdCard }}</text> |
|
|
|
<text class="info-compact-item">手机号:{{ maskedPhone }}</text> |
|
|
|
</view> |
|
|
|
<view class="info-compact-row"> |
|
|
|
<text class="info-compact-item">联系地址:{{ patient.region_text }} {{ patient.address }}</text> |
|
|
|
</view> |
|
|
|
<view class="info-compact-row"> |
|
|
|
<text class="info-compact-item">医院:{{ patient.hospital || '—' }}</text> |
|
|
|
<text class="info-compact-item">癌种:{{ patient.tag || '—' }}</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 送检信息 --> |
|
|
|
<view class="section"> |
|
|
|
<view class="section-title"> |
|
|
|
<u-icon name="file-text" size="18" color="#fa541c" /> |
|
|
|
<text>送检信息</text> |
|
|
|
</view> |
|
|
|
<view class="form-group"> |
|
|
|
<text class="form-label">送检样本类型{{ sampleRequired ? '' : '(选填)' }}</text> |
|
|
|
<u-checkbox-group v-model="form.sample_types" placement="row" :wrap="true" @change="onSampleTypesChange"> |
|
|
|
<u-checkbox v-for="st in sampleTypeList" :key="st.id" :label="st.name" :name="st.name" |
|
|
|
activeColor="#0E63E3" :customStyle="{ marginRight: '24rpx', marginBottom: '16rpx' }" /> |
|
|
|
</u-checkbox-group> |
|
|
|
</view> |
|
|
|
<view class="form-group" v-if="showWaxReturn"> |
|
|
|
<text class="form-label">{{ needReturnNames }}是否需寄回</text> |
|
|
|
<u-radio-group v-model="form.wax_return" placement="row"> |
|
|
|
<u-radio label="是" :name="1" activeColor="#0E63E3" :customStyle="{ marginRight: '40rpx' }" /> |
|
|
|
<u-radio label="否" :name="0" activeColor="#0E63E3" /> |
|
|
|
</u-radio-group> |
|
|
|
</view> |
|
|
|
<template v-if="form.wax_return === 1 && showWaxReturn"> |
|
|
|
<view class="form-group"> |
|
|
|
<view style="display:flex;align-items:center;justify-content:space-between;"> |
|
|
|
<text class="form-label" style="margin-bottom:0;">收件人姓名</text> |
|
|
|
<text class="fill-self-btn" @tap="fillSelfReturn">本人接收</text> |
|
|
|
</view> |
|
|
|
<u-input v-model="form.return_name" placeholder="请输入收件人姓名" border="surround" :customStyle="{ marginTop: '16rpx' }" /> |
|
|
|
</view> |
|
|
|
<view class="form-group"> |
|
|
|
<text class="form-label">收件人电话</text> |
|
|
|
<u-input v-model="form.return_phone" type="number" placeholder="请输入收件人电话" border="surround" maxlength="11" /> |
|
|
|
</view> |
|
|
|
<view class="form-group"> |
|
|
|
<text class="form-label">收件地址</text> |
|
|
|
<view class="region-row" @tap="showRegionPicker = true"> |
|
|
|
<text :class="['region-text', returnRegionText ? '' : 'placeholder']">{{ returnRegionText || '请选择省/市/区' }}</text> |
|
|
|
<text class="arrow">›</text> |
|
|
|
</view> |
|
|
|
<u-input v-model="form.return_address" placeholder="详细门牌号" border="surround" |
|
|
|
:customStyle="{ marginTop: '16rpx' }" /> |
|
|
|
</view> |
|
|
|
</template> |
|
|
|
<template v-if="(form.sample_types && form.sample_types.length) || sampleRequired"> |
|
|
|
<view class="form-group"> |
|
|
|
<text class="form-label">报告接收邮箱</text> |
|
|
|
<u-input v-model="form.report_email" placeholder="请输入邮箱地址" border="surround" /> |
|
|
|
</view> |
|
|
|
<view class="form-group"> |
|
|
|
<text class="form-label">送检样本物流单号</text> |
|
|
|
<u-input v-model="form.sample_tracking_no" placeholder="请输入物流单号" border="surround" /> |
|
|
|
</view> |
|
|
|
<view class="form-group"> |
|
|
|
<text class="form-label">送检单照片(可上传多张)</text> |
|
|
|
<view class="upload-row"> |
|
|
|
<view class="upload-item" v-for="(photo, idx) in form.sample_photos" :key="'sp'+idx"> |
|
|
|
<image class="upload-img" :src="photo" mode="aspectFill" @tap="previewSamplePhoto(idx)" /> |
|
|
|
<view class="upload-del" @tap="form.sample_photos.splice(idx, 1)">×</view> |
|
|
|
</view> |
|
|
|
<view class="upload-box" @tap="chooseSamplePhoto"> |
|
|
|
<text class="upload-icon">+</text> |
|
|
|
<text class="upload-text">上传图片</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</template> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 提交按钮 --> |
|
|
|
<view class="btn-wrap"> |
|
|
|
<view class="agree-row" @tap="agreed = !agreed"> |
|
|
|
<u-checkbox-group> |
|
|
|
<u-checkbox :checked="agreed" shape="circle" activeColor="#0E63E3" size="18" @change="agreed = !agreed" /> |
|
|
|
</u-checkbox-group> |
|
|
|
<text class="agree-text">请阅读并同意</text> |
|
|
|
<text class="agree-link" @tap.stop="openNotice">《患者告知书》</text> |
|
|
|
</view> |
|
|
|
<u-button text="提交送检信息" :loading="submitting" @click="handleSubmit" color="#0E63E3" size="large" /> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 地区选择器 --> |
|
|
|
<u-picker v-if="regionColumns[0].length" :show="showRegionPicker" :columns="regionColumns" @confirm="onRegionConfirm" |
|
|
|
@cancel="showRegionPicker = false" @change="onRegionChange" :defaultIndex="regionDefaultIndex" /> |
|
|
|
</view> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup> |
|
|
|
import { ref, reactive, computed } from 'vue' |
|
|
|
import { onLoad } from '@dcloudio/uni-app' |
|
|
|
import { get, post, upload } from '@/utils/request.js' |
|
|
|
|
|
|
|
const patient = ref({}) |
|
|
|
const form = reactive({ |
|
|
|
sample_types: [], |
|
|
|
wax_return: 0, |
|
|
|
return_name: '', |
|
|
|
return_phone: '', |
|
|
|
return_province_code: '', |
|
|
|
return_city_code: '', |
|
|
|
return_district_code: '', |
|
|
|
return_address: '', |
|
|
|
report_email: '', |
|
|
|
sample_tracking_no: '', |
|
|
|
sample_photos: [] |
|
|
|
}) |
|
|
|
const submitting = ref(false) |
|
|
|
const agreed = ref(false) |
|
|
|
const sampleTypeList = ref([]) |
|
|
|
const sampleRequired = ref(false) |
|
|
|
|
|
|
|
// 地区数据 |
|
|
|
const allRegions = ref([]) |
|
|
|
const regionColumns = ref([[], [], []]) |
|
|
|
const regionDefaultIndex = ref([0, 0, 0]) |
|
|
|
const showRegionPicker = ref(false) |
|
|
|
|
|
|
|
const maskedIdCard = computed(() => { |
|
|
|
const v = patient.value.id_card || '' |
|
|
|
if (v.length === 18) return v.slice(0, 3) + '****' + v.slice(-4) |
|
|
|
return v |
|
|
|
}) |
|
|
|
|
|
|
|
const maskedPhone = computed(() => { |
|
|
|
const v = patient.value.phone || '' |
|
|
|
if (v.length === 11) return v.slice(0, 3) + '****' + v.slice(-4) |
|
|
|
return v |
|
|
|
}) |
|
|
|
|
|
|
|
const showWaxReturn = computed(() => { |
|
|
|
if (!form.sample_types || !form.sample_types.length) return false |
|
|
|
return form.sample_types.some(name => { |
|
|
|
const st = sampleTypeList.value.find(s => s.name === name) |
|
|
|
return st && st.need_return |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
const needReturnNames = computed(() => { |
|
|
|
if (!form.sample_types || !form.sample_types.length) return '' |
|
|
|
const names = form.sample_types.filter(name => { |
|
|
|
const st = sampleTypeList.value.find(s => s.name === name) |
|
|
|
return st && st.need_return |
|
|
|
}) |
|
|
|
return names.join('、') |
|
|
|
}) |
|
|
|
|
|
|
|
const returnRegionText = computed(() => { |
|
|
|
const parts = [] |
|
|
|
if (form.return_province_code) { |
|
|
|
const p = allRegions.value.find(r => r.code === form.return_province_code) |
|
|
|
if (p) parts.push(p.name) |
|
|
|
} |
|
|
|
if (form.return_city_code) { |
|
|
|
const prov = allRegions.value.find(r => r.code === form.return_province_code) |
|
|
|
if (prov && prov.children) { |
|
|
|
const c = prov.children.find(r => r.code === form.return_city_code) |
|
|
|
if (c) parts.push(c.name) |
|
|
|
} |
|
|
|
} |
|
|
|
if (form.return_district_code) { |
|
|
|
const prov = allRegions.value.find(r => r.code === form.return_province_code) |
|
|
|
if (prov && prov.children) { |
|
|
|
const city = prov.children.find(r => r.code === form.return_city_code) |
|
|
|
if (city && city.children) { |
|
|
|
const d = city.children.find(r => r.code === form.return_district_code) |
|
|
|
if (d) parts.push(d.name) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return parts.join(' ') |
|
|
|
}) |
|
|
|
|
|
|
|
const onSampleTypesChange = () => { |
|
|
|
if (!showWaxReturn.value) { |
|
|
|
form.wax_return = 0 |
|
|
|
} |
|
|
|
if (!form.sample_types || form.sample_types.length === 0) { |
|
|
|
form.report_email = '' |
|
|
|
form.sample_tracking_no = '' |
|
|
|
form.sample_photos = [] |
|
|
|
form.wax_return = 0 |
|
|
|
form.return_name = '' |
|
|
|
form.return_phone = '' |
|
|
|
form.return_province_code = '' |
|
|
|
form.return_city_code = '' |
|
|
|
form.return_district_code = '' |
|
|
|
form.return_address = '' |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const fillSelfReturn = () => { |
|
|
|
form.return_name = patient.value.name || '' |
|
|
|
form.return_phone = patient.value.phone || '' |
|
|
|
form.return_province_code = patient.value.province_code || '' |
|
|
|
form.return_city_code = patient.value.city_code || '' |
|
|
|
form.return_district_code = patient.value.district_code || '' |
|
|
|
form.return_address = patient.value.address || '' |
|
|
|
} |
|
|
|
|
|
|
|
onLoad(async () => { |
|
|
|
await loadRegions() |
|
|
|
await loadPatientInfo() |
|
|
|
await loadSampleTypes() |
|
|
|
}) |
|
|
|
|
|
|
|
const loadPatientInfo = async () => { |
|
|
|
try { |
|
|
|
const res = await get('/api/mp/sampleInfo') |
|
|
|
if (res.data) { |
|
|
|
patient.value = res.data.patient || {} |
|
|
|
// 回显送检信息 |
|
|
|
form.sample_types = res.data.sample_types || [] |
|
|
|
form.wax_return = res.data.wax_return || 0 |
|
|
|
form.return_name = res.data.return_name || '' |
|
|
|
form.return_phone = res.data.return_phone || '' |
|
|
|
form.return_province_code = res.data.return_province_code || '' |
|
|
|
form.return_city_code = res.data.return_city_code || '' |
|
|
|
form.return_district_code = res.data.return_district_code || '' |
|
|
|
form.return_address = res.data.return_address || '' |
|
|
|
form.report_email = res.data.report_email || '' |
|
|
|
form.sample_tracking_no = res.data.sample_tracking_no || '' |
|
|
|
form.sample_photos = res.data.sample_photos || [] |
|
|
|
} |
|
|
|
} catch (e) {} |
|
|
|
} |
|
|
|
|
|
|
|
const loadSampleTypes = async () => { |
|
|
|
try { |
|
|
|
const res = await get('/common/sampleTypes') |
|
|
|
sampleTypeList.value = (res.data && res.data.list) || [] |
|
|
|
sampleRequired.value = (res.data && res.data.required) || false |
|
|
|
} catch (e) {} |
|
|
|
} |
|
|
|
|
|
|
|
const loadRegions = async () => { |
|
|
|
try { |
|
|
|
const res = await get('/common/regions') |
|
|
|
allRegions.value = res.data || [] |
|
|
|
buildRegionColumns() |
|
|
|
} catch (e) {} |
|
|
|
} |
|
|
|
|
|
|
|
const buildRegionColumns = (pIdx = 0, cIdx = 0) => { |
|
|
|
const provinces = allRegions.value |
|
|
|
const col0 = provinces.map(p => p.name) |
|
|
|
const cities = (provinces[pIdx] && provinces[pIdx].children) || [] |
|
|
|
const col1 = cities.map(c => c.name) |
|
|
|
const districts = (cities[cIdx] && cities[cIdx].children) || [] |
|
|
|
const col2 = districts.map(d => d.name) |
|
|
|
regionColumns.value = [col0, col1, col2] |
|
|
|
} |
|
|
|
|
|
|
|
const onRegionChange = (e) => { |
|
|
|
const { columnIndex, index } = e |
|
|
|
if (columnIndex === 0) { |
|
|
|
buildRegionColumns(index, 0) |
|
|
|
regionDefaultIndex.value = [index, 0, 0] |
|
|
|
} else if (columnIndex === 1) { |
|
|
|
const pIdx = regionDefaultIndex.value[0] |
|
|
|
buildRegionColumns(pIdx, index) |
|
|
|
regionDefaultIndex.value = [pIdx, index, 0] |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const onRegionConfirm = (e) => { |
|
|
|
const idxs = e.indexs || e.index || [0, 0, 0] |
|
|
|
const provinces = allRegions.value |
|
|
|
const prov = provinces[idxs[0]] |
|
|
|
const city = prov && prov.children ? prov.children[idxs[1]] : null |
|
|
|
const dist = city && city.children ? city.children[idxs[2]] : null |
|
|
|
form.return_province_code = prov ? prov.code : '' |
|
|
|
form.return_city_code = city ? city.code : '' |
|
|
|
form.return_district_code = dist ? dist.code : '' |
|
|
|
showRegionPicker.value = false |
|
|
|
} |
|
|
|
|
|
|
|
const chooseSamplePhoto = () => { |
|
|
|
uni.chooseImage({ |
|
|
|
count: 9 - form.sample_photos.length, |
|
|
|
sizeType: ['compressed'], |
|
|
|
sourceType: ['album', 'camera'], |
|
|
|
success: async (res) => { |
|
|
|
for (const filePath of res.tempFilePaths) { |
|
|
|
try { |
|
|
|
const uploadRes = await upload('/api/mp/upload', { filePath, name: 'file' }) |
|
|
|
if (uploadRes.data && uploadRes.data.url) { |
|
|
|
form.sample_photos.push(uploadRes.data.url) |
|
|
|
} |
|
|
|
} catch (e) {} |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
const previewSamplePhoto = (idx) => { |
|
|
|
uni.previewImage({ urls: form.sample_photos, current: idx }) |
|
|
|
} |
|
|
|
|
|
|
|
const openNotice = () => { |
|
|
|
uni.navigateTo({ url: '/pages/content/content?key=patient_information_sheet' }) |
|
|
|
} |
|
|
|
|
|
|
|
const handleSubmit = async () => { |
|
|
|
if (!agreed.value) { |
|
|
|
return uni.showToast({ title: '请阅读并同意《患者告知书》', icon: 'none' }) |
|
|
|
} |
|
|
|
if (sampleRequired.value && (!form.sample_types || form.sample_types.length === 0)) { |
|
|
|
return uni.showToast({ title: '请选择送检样本类型', icon: 'none' }) |
|
|
|
} |
|
|
|
if (form.wax_return === 1 && showWaxReturn.value) { |
|
|
|
if (!form.return_name) return uni.showToast({ title: '请输入收件人姓名', icon: 'none' }) |
|
|
|
if (!form.return_phone) return uni.showToast({ title: '请输入收件人电话', icon: 'none' }) |
|
|
|
if (!form.return_province_code) return uni.showToast({ title: '请选择收件地址', icon: 'none' }) |
|
|
|
if (!form.return_address) return uni.showToast({ title: '请输入收件详细地址', icon: 'none' }) |
|
|
|
} |
|
|
|
if (form.sample_types && form.sample_types.length > 0) { |
|
|
|
if (!form.report_email) return uni.showToast({ title: '请输入报告接收邮箱', icon: 'none' }) |
|
|
|
if (!form.sample_tracking_no) return uni.showToast({ title: '请输入送检样本物流单号', icon: 'none' }) |
|
|
|
if (!form.sample_photos || form.sample_photos.length === 0) return uni.showToast({ title: '请上传送检单照片', icon: 'none' }) |
|
|
|
} |
|
|
|
submitting.value = true |
|
|
|
try { |
|
|
|
await post('/api/mp/saveSampleInfo', { ...form }) |
|
|
|
uni.showToast({ title: '提交成功', icon: 'success' }) |
|
|
|
setTimeout(() => uni.navigateBack(), 1500) |
|
|
|
} catch (e) { |
|
|
|
if (e && e.msg) uni.showToast({ title: e.msg, icon: 'none' }) |
|
|
|
} finally { |
|
|
|
submitting.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
|
.page { min-height: 100vh; background: #f4f4f5; padding: 24rpx; padding-bottom: 260rpx; } |
|
|
|
.section { background: #fff; border-radius: 10rpx; padding: 32rpx; margin-bottom: 24rpx; border: 1rpx solid #ebeef5; } |
|
|
|
.section-title { display: flex; align-items: center; gap: 12rpx; font-size: 32rpx; font-weight: 600; color: #333; margin-bottom: 28rpx; } |
|
|
|
.info-compact { padding-bottom: 8rpx; } |
|
|
|
.info-compact-row { display: flex; gap: 32rpx; margin-bottom: 12rpx; &:last-child { margin-bottom: 0; } } |
|
|
|
.info-compact-item { font-size: 26rpx; color: #666; } |
|
|
|
.form-group { padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0; &:last-child { border-bottom: none; } } |
|
|
|
.form-label { font-size: 28rpx; color: #555; margin-bottom: 16rpx; display: block; } |
|
|
|
.region-row { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border: 1rpx solid #ddd; border-radius: 8rpx; } |
|
|
|
.region-text { font-size: 28rpx; color: #333; &.placeholder { color: #c0c4cc; } } |
|
|
|
.arrow { font-size: 28rpx; color: #c0c4cc; } |
|
|
|
.fill-self-btn { font-size: 24rpx; color: #0e63e3; padding: 6rpx 16rpx; border: 1rpx solid #d0e0ff; border-radius: 20rpx; background: #f0f5ff; } |
|
|
|
.upload-row { display: flex; gap: 16rpx; flex-wrap: wrap; } |
|
|
|
.upload-item { position: relative; width: 180rpx; height: 180rpx; } |
|
|
|
.upload-img { width: 180rpx; height: 180rpx; border-radius: 8rpx; border: 1rpx solid #eee; } |
|
|
|
.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; } |
|
|
|
.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; } |
|
|
|
.upload-icon { font-size: 56rpx; color: #ccc; } |
|
|
|
.upload-text { font-size: 22rpx; color: #999; margin-top: 4rpx; } |
|
|
|
.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; } |
|
|
|
.agree-row { display: flex; align-items: center; margin-bottom: 16rpx; padding-left: 4rpx; } |
|
|
|
.agree-text { font-size: 24rpx; color: #666; margin-left: 8rpx; } |
|
|
|
.agree-link { font-size: 24rpx; color: #0e63e3; } |
|
|
|
</style> |