Procházet zdrojové kódy

feat : 新增医院城市

master
leiyun před 2 týdny
rodič
revize
c82de83c4f
3 změnil soubory, kde provedl 328 přidání a 156 odebrání
  1. +289
    -0
      components/hospital-picker/hospital-picker.vue
  2. +2
    -2
      config/env.js
  3. +37
    -154
      pages/myinfo/myinfo.vue

+ 289
- 0
components/hospital-picker/hospital-picker.vue Zobrazit soubor

@@ -0,0 +1,289 @@
<template>
<u-popup :show="visible" mode="bottom" round="16" :closeOnClickOverlay="true" :safeAreaInsetBottom="true" @close="handleClose">
<view class="picker-wrap">
<view class="picker-header">
<text class="picker-title">选择就诊医院</text>
<u-icon name="close" size="36rpx" color="#999" @click="handleClose" />
</view>

<!-- 全量医院搜索 -->
<view class="search-bar">
<u--input v-model="hospitalKeyword" placeholder="搜索医院名称" prefixIcon="search" prefixIconStyle="color: #999" border="surround" shape="circle" clearable />
</view>

<!-- Tab 切换:省 → 市 → 区 → 医院 -->
<view class="tab-bar">
<view class="tab-item" :class="{ active: currentTab === 'province' }" @click="switchTab('province')">
{{ selectedProvince ? selectedProvince.provinceName : '请选择省份' }}
</view>
<view v-if="selectedProvince" class="tab-item" :class="{ active: currentTab === 'city' }" @click="switchTab('city')">
{{ selectedCity ? selectedCity.cityName : '请选择城市' }}
</view>
<view v-if="selectedCity" class="tab-item" :class="{ active: currentTab === 'district' }" @click="switchTab('district')">
{{ selectedDistrict ? selectedDistrict.districtName : '请选择区县' }}
</view>
<view v-if="selectedDistrict" class="tab-item" :class="{ active: currentTab === 'hospital' }" @click="switchTab('hospital')">
{{ selectedHospital ? selectedHospital.name : '请选择医院' }}
</view>
</view>

<!-- 列表区域 -->
<scroll-view class="list-wrap" scroll-y>
<!-- 全量搜索结果 -->
<template v-if="hasSearchKeyword">
<view v-for="item in globalFilteredHospitals" :key="item.id" class="list-item"
:class="{ selected: selectedHospital && selectedHospital.id === item.id }" @click="onSelectHospital(item)">
<view class="hospital-result">
<text class="hospital-name">{{ item.name }}</text>
<text class="hospital-region">{{ item.regionText || '地区未设置' }}</text>
</view>
<u-icon v-if="selectedHospital && selectedHospital.id === item.id" name="checkmark" color="#FF7700" size="28rpx" />
</view>
</template>
<!-- 省份 -->
<template v-else-if="currentTab === 'province'">
<view v-for="item in provinces" :key="item.provinceCode || item.provinceName" class="list-item"
:class="{ selected: selectedProvince === item }" @click="onSelectProvince(item)">
<text>{{ item.provinceName }}</text>
<u-icon v-if="selectedProvince === item" name="checkmark" color="#FF7700" size="28rpx" />
</view>
</template>
<!-- 城市 -->
<template v-if="currentTab === 'city'">
<view v-for="item in cities" :key="item.cityCode || item.cityName" class="list-item"
:class="{ selected: selectedCity === item }" @click="onSelectCity(item)">
<text>{{ item.cityName }}</text>
<u-icon v-if="selectedCity === item" name="checkmark" color="#FF7700" size="28rpx" />
</view>
</template>
<!-- 区县 -->
<template v-if="currentTab === 'district'">
<view v-for="item in districts" :key="item.districtCode || item.districtName" class="list-item"
:class="{ selected: selectedDistrict === item }" @click="onSelectDistrict(item)">
<text>{{ item.districtName }}</text>
<u-icon v-if="selectedDistrict === item" name="checkmark" color="#FF7700" size="28rpx" />
</view>
</template>
<!-- 医院 -->
<template v-if="currentTab === 'hospital'">
<view v-for="item in filteredHospitals" :key="item.id" class="list-item"
:class="{ selected: selectedHospital && selectedHospital.id === item.id }" @click="onSelectHospital(item)">
<text>{{ item.name }}</text>
<u-icon v-if="selectedHospital && selectedHospital.id === item.id" name="checkmark" color="#FF7700" size="28rpx" />
</view>
</template>
<u-empty v-if="currentEmpty" mode="data" text="暂无数据" marginTop="60" />
</scroll-view>
</view>
</u-popup>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
hospitalData: { type: Array, default: () => [] }
})
const emit = defineEmits(['confirm'])

const visible = ref(false)
const currentTab = ref('province')
const selectedProvince = ref(null)
const selectedCity = ref(null)
const selectedDistrict = ref(null)
const selectedHospital = ref(null)
const hospitalKeyword = ref('')

const provinces = computed(() => props.hospitalData || [])
const cities = computed(() => (selectedProvince.value && selectedProvince.value.cities) || [])
const districts = computed(() => (selectedCity.value && selectedCity.value.districts) || [])
const hospitals = computed(() => (selectedDistrict.value && selectedDistrict.value.hospitals) || [])

const hasSearchKeyword = computed(() => !!hospitalKeyword.value.trim())
const globalHospitals = computed(() => {
const list = []
provinces.value.forEach(prov => {
;(prov.cities || []).forEach(city => {
;(city.districts || []).forEach(dist => {
;(dist.hospitals || []).forEach(h => {
list.push({
...h,
_province: prov,
_city: city,
_district: dist,
regionText: [prov.provinceName, city.cityName, dist.districtName].filter(Boolean).join(' ')
})
})
})
})
})
return list
})
const globalFilteredHospitals = computed(() => {
const kw = hospitalKeyword.value.trim()
if (!kw) return []
return globalHospitals.value.filter(h => h.name.includes(kw) || (h.regionText && h.regionText.includes(kw)))
})

const filteredHospitals = computed(() => {
return hospitals.value
})

const currentEmpty = computed(() => {
if (hasSearchKeyword.value) return globalFilteredHospitals.value.length === 0
if (currentTab.value === 'province') return provinces.value.length === 0
if (currentTab.value === 'city') return cities.value.length === 0
if (currentTab.value === 'district') return districts.value.length === 0
return filteredHospitals.value.length === 0
})

const switchTab = (tab) => { currentTab.value = tab }

const onSelectProvince = (item) => {
selectedProvince.value = item
selectedCity.value = null
selectedDistrict.value = null
selectedHospital.value = null
if (item.cities && item.cities.length === 1) {
selectedCity.value = item.cities[0]
if (selectedCity.value.districts && selectedCity.value.districts.length === 1) {
selectedDistrict.value = selectedCity.value.districts[0]
currentTab.value = 'hospital'
} else {
currentTab.value = 'district'
}
} else {
currentTab.value = 'city'
}
}

const onSelectCity = (item) => {
selectedCity.value = item
selectedDistrict.value = null
selectedHospital.value = null
if (item.districts && item.districts.length === 1) {
selectedDistrict.value = item.districts[0]
currentTab.value = 'hospital'
} else {
currentTab.value = 'district'
}
}

const onSelectDistrict = (item) => {
selectedDistrict.value = item
selectedHospital.value = null
hospitalKeyword.value = ''
currentTab.value = 'hospital'
}

const onSelectHospital = (item) => {
if (item._province) selectedProvince.value = item._province
if (item._city) selectedCity.value = item._city
if (item._district) selectedDistrict.value = item._district
selectedHospital.value = item
emitData()
handleClose()
}

const emitData = () => {
if (!selectedHospital.value) return
emit('confirm', {
hospitalId: selectedHospital.value.id,
hospitalName: selectedHospital.value.name,
province_code: selectedHospital.value.province_code || (selectedProvince.value && selectedProvince.value.provinceCode) || '',
city_code: selectedHospital.value.city_code || (selectedCity.value && selectedCity.value.cityCode) || '',
district_code: selectedHospital.value.district_code || (selectedDistrict.value && selectedDistrict.value.districtCode) || ''
})
}

const handleClose = () => {
visible.value = false
}

const open = (initial = {}) => {
selectedProvince.value = null
selectedCity.value = null
selectedDistrict.value = null
selectedHospital.value = null
hospitalKeyword.value = ''
currentTab.value = 'province'

const provinceCode = initial.province_code || ''
const cityCode = initial.city_code || ''
const districtCode = initial.district_code || ''
const hospitalName = initial.hospitalName || initial.hospital || ''
const hospitalId = initial.hospitalId || ''

if (provinceCode) {
selectedProvince.value = provinces.value.find(p => p.provinceCode === provinceCode) || null
if (selectedProvince.value && cityCode) {
selectedCity.value = (selectedProvince.value.cities || []).find(c => c.cityCode === cityCode) || null
if (selectedCity.value && districtCode) {
selectedDistrict.value = (selectedCity.value.districts || []).find(d => d.districtCode === districtCode) || null
}
}
}

if (!selectedDistrict.value && (hospitalName || hospitalId)) {
findHospitalPath(hospitalName, hospitalId)
}

if (selectedDistrict.value) {
selectedHospital.value = (selectedDistrict.value.hospitals || []).find(h => {
return (hospitalId && h.id === hospitalId) || (hospitalName && h.name === hospitalName)
}) || null
currentTab.value = 'hospital'
} else if (selectedCity.value) {
currentTab.value = 'district'
} else if (selectedProvince.value) {
currentTab.value = 'city'
}
visible.value = true
}

const findHospitalPath = (hospitalName, hospitalId) => {
for (const prov of provinces.value) {
for (const city of (prov.cities || [])) {
for (const dist of (city.districts || [])) {
const hospital = (dist.hospitals || []).find(h => {
return (hospitalId && h.id === hospitalId) || (hospitalName && h.name === hospitalName)
})
if (hospital) {
selectedProvince.value = prov
selectedCity.value = city
selectedDistrict.value = dist
selectedHospital.value = hospital
return
}
}
}
}
}

defineExpose({ open })
</script>

<style lang="scss" scoped>
.picker-wrap { height: 70vh; display: flex; flex-direction: column; }
.picker-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 1rpx solid #f0f0f0; }
.picker-title { font-size: 32rpx; font-weight: 600; color: #333; }
.tab-bar { display: flex; padding: 20rpx 32rpx; gap: 24rpx; border-bottom: 1rpx solid #f0f0f0; flex-wrap: wrap; }
.tab-item {
font-size: 26rpx; color: #999; padding-bottom: 12rpx; position: relative;
max-width: 240rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
&.active {
color: #FF7700; font-weight: 500;
&::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 4rpx; background: #FF7700; border-radius: 2rpx; }
}
}
.search-bar { padding: 16rpx 32rpx; border-bottom: 1rpx solid #f0f0f0; }
.list-wrap { flex: 1; overflow: hidden; }
.list-item {
display: flex; align-items: center; justify-content: space-between;
padding: 28rpx 32rpx; font-size: 28rpx; color: #333; border-bottom: 1rpx solid #f8f8f8;
&.selected { color: #FF7700; background: #FFF8F0; }
}
.hospital-result { display: flex; flex-direction: column; gap: 8rpx; min-width: 0; }
.hospital-name { font-size: 28rpx; color: #333; }
.hospital-region { font-size: 22rpx; color: #999; }
</style>

+ 2
- 2
config/env.js Zobrazit soubor

@@ -1,8 +1,8 @@
const envConf = {
// 开发版-本地环境
develop: {
// BASE_URL: 'http://192.168.10.8:8361',
BASE_URL: 'https://cytx.csybhelp.com',
BASE_URL: 'http://192.168.10.8:8361',
// BASE_URL: 'https://cytx.csybhelp.com',
},
// 体验版-测试环境
trial: {


+ 37
- 154
pages/myinfo/myinfo.vue Zobrazit soubor

@@ -51,12 +51,10 @@
</view>
<view class="form-group">
<text class="form-label">医院名称</text>
<!-- 有医院数据:点击弹窗选择;无数据:降级为输入框 -->
<view v-if="hospitalOptions.length" class="region-row" @tap="openHospitalPopup">
<text :class="['region-text', form.hospital ? '' : 'placeholder']">{{ form.hospital || '请选择医院' }}</text>
<view class="region-row" @tap="openHospitalPicker">
<text :class="['region-text', form.hospital ? '' : 'placeholder']">{{ form.hospital || '请选择就诊医院' }}</text>
<text class="arrow">›</text>
</view>
<u-input v-else v-model="form.hospital" placeholder="请输入就诊医院名称" border="surround" />
</view>
<view class="form-group">
<text class="form-label">癌种</text>
@@ -154,42 +152,8 @@
<u-picker v-if="regionColumns[0].length" :show="showRegionPicker" :columns="regionColumns" @confirm="onRegionConfirm"
@cancel="showRegionPicker = false" @change="onRegionChange" :defaultIndex="regionDefaultIndex" />

<!-- 医院搜索弹窗 -->
<u-popup :show="showHospitalPopup" mode="bottom" round="24" :closeable="true" :safeAreaInsetBottom="true" @close="showHospitalPopup = false">
<view class="hospital-popup">
<text class="popup-title">选择医院</text>
<view class="search-bar">
<u--input v-model="hospitalKeywords" placeholder="搜索医院名称" prefixIcon="search" prefixIconStyle="color: #999" border="surround" shape="circle" @change="searchHospital" />
</view>
<scroll-view scroll-y class="hospital-list">
<view v-for="item in filteredHospitals" :key="item.id" class="hospital-item" @click="onSelectHospital(item)">
<text>{{ item.name }}</text>
<u-icon v-if="form.hospital === item.name" name="checkmark" size="16" color="#0E63E3" />
</view>
<view v-if="!filteredHospitals.length" class="empty-tip">
<text>没有找到相关医院</text>
</view>
</scroll-view>
<view class="add-hospital-link" v-if="hospitalKeywords.trim() && filteredHospitals.length <= 5" @click="manualInputHospital">
<text>找不到医院?手动填写</text>
<u-icon name="arrow-right" size="14" color="#0E63E3" />
</view>
</view>
</u-popup>

<!-- 手动输入医院弹窗 -->
<u-popup :show="showManualHospital" mode="center" round="12" :safeAreaInsetBottom="false" @close="showManualHospital = false">
<view class="manual-hospital-popup">
<text class="popup-title">手动填写医院</text>
<view class="manual-input">
<u--input v-model="manualHospitalName" placeholder="请输入医院名称" border="surround" />
</view>
<view class="manual-btns">
<u-button text="取消" size="normal" :plain="true" shape="circle" @click="showManualHospital = false" />
<u-button text="确定" size="normal" color="#0E63E3" shape="circle" @click="confirmManualHospital" />
</view>
</view>
</u-popup>
<!-- 医院选择器组件 -->
<hospital-picker ref="hospitalPickerRef" :hospital-data="hospitalTree" @confirm="onHospitalConfirm" />

<!-- 已通过重新提交确认弹窗 -->
<u-popup :show="showConfirmPopup" mode="center" round="12" :safeAreaInsetBottom="false" @close="showConfirmPopup = false">
@@ -218,6 +182,9 @@ const form = reactive({
district_code: '',
address: '',
hospital: '',
hospital_province_code: '',
hospital_city_code: '',
hospital_district_code: '',
emergency_contact: '',
emergency_phone: '',
tag: '',
@@ -236,51 +203,30 @@ const agreed = ref(false)

// 瘤种选项
const tagOptions = ref([])
const showReturnRegionPicker = ref(false)
const returnRegionDefaultIndex = ref([0, 0, 0])

// 医院选项
const hospitalOptions = ref([])
const showHospitalPopup = ref(false)
const hospitalKeywords = ref('')
const showManualHospital = ref(false)
const manualHospitalName = ref('')

const filteredHospitals = computed(() => {
const kw = hospitalKeywords.value.trim()
if (!kw) return hospitalOptions.value
return hospitalOptions.value.filter(h => h.name.includes(kw))
})

const openHospitalPopup = () => {
hospitalKeywords.value = ''
showHospitalPopup.value = true
}

const searchHospital = () => {
// 本地过滤,filteredHospitals 自动响应
}

const onSelectHospital = (item) => {
form.hospital = item.name
showHospitalPopup.value = false
}

const manualInputHospital = () => {
// 关闭搜索弹窗,打开手动输入弹窗,带入搜索框内容
manualHospitalName.value = hospitalKeywords.value.trim()
showHospitalPopup.value = false
showManualHospital.value = true
}
// 医院选择器
const hospitalTree = ref([])
const hospitalPickerRef = ref(null)

const confirmManualHospital = () => {
const name = manualHospitalName.value.trim()
if (!name) {
uni.showToast({ title: '请输入医院名称', icon: 'none' })
const openHospitalPicker = () => {
if (!hospitalTree.value.length) {
uni.showToast({ title: '医院列表加载中,请稍候', icon: 'none' })
loadHospitalTree()
return
}
form.hospital = name
showManualHospital.value = false
hospitalPickerRef.value && hospitalPickerRef.value.open({
hospital: form.hospital,
province_code: form.hospital_province_code,
city_code: form.hospital_city_code,
district_code: form.hospital_district_code
})
}

const onHospitalConfirm = (data) => {
form.hospital = data.hospitalName || ''
form.hospital_province_code = data.province_code || ''
form.hospital_city_code = data.city_code || ''
form.hospital_district_code = data.district_code || ''
}

// 签署时的额外信息(用于重签回显)
@@ -407,7 +353,7 @@ onLoad(async () => {
await loadInfo()
loadSubscribeConfig()
loadTagOptions()
loadHospitals()
loadHospitalTree()
})

onBeforeUnmount(() => {
@@ -496,6 +442,9 @@ const loadInfo = async () => {
form.district_code = res.data.district_code || ''
form.address = res.data.address || ''
form.hospital = res.data.hospital || ''
form.hospital_province_code = res.data.hospital_province_code || ''
form.hospital_city_code = res.data.hospital_city_code || ''
form.hospital_district_code = res.data.hospital_district_code || ''
form.emergency_contact = res.data.emergency_contact || ''
form.emergency_phone = res.data.emergency_phone || ''
form.tag = res.data.tag || ''
@@ -532,10 +481,10 @@ const loadTagOptions = async () => {
} catch (e) {}
}

const loadHospitals = async () => {
const loadHospitalTree = async () => {
try {
const res = await get('/common/hospitals')
hospitalOptions.value = res.data || []
const res = await get('/common/hospitalTree')
hospitalTree.value = res.data || []
} catch (e) {}
}

@@ -628,6 +577,9 @@ const doSubmit = async () => {
district_code: form.district_code,
address: form.address.trim(),
hospital: form.hospital,
hospital_province_code: form.hospital_province_code,
hospital_city_code: form.hospital_city_code,
hospital_district_code: form.hospital_district_code,
emergency_contact: form.emergency_contact,
emergency_phone: form.emergency_phone,
tag: form.tag,
@@ -984,73 +936,4 @@ const doSubmit = async () => {
gap: 24rpx;
}

.hospital-popup {
padding: 32rpx;
display: flex;
flex-direction: column;
height: 70vh;
}

.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 24rpx;
}

.search-bar {
margin-bottom: 16rpx;
}

.hospital-list {
flex: 1;
overflow: hidden;
}

.hospital-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 16rpx;
border-bottom: 1rpx solid #f0f0f0;
font-size: 28rpx;
color: #333;

&:active {
background: #f5f7fa;
}
}

.empty-tip {
text-align: center;
padding: 60rpx 0;
font-size: 26rpx;
color: #999;
}

.add-hospital-link {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 28rpx 0;
font-size: 28rpx;
color: #0e63e3;
border-top: 1rpx solid #f0f0f0;
}

.manual-hospital-popup {
padding: 48rpx 40rpx 40rpx;
width: 560rpx;
}

.manual-input {
margin-bottom: 40rpx;
}

.manual-btns {
display: flex;
gap: 24rpx;
}
</style>

Načítá se…
Zrušit
Uložit