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.
 
 
 
 
 

290 line
11 KiB

  1. <template>
  2. <u-popup :show="visible" mode="bottom" round="16" :closeOnClickOverlay="true" :safeAreaInsetBottom="true" @close="handleClose">
  3. <view class="picker-wrap">
  4. <view class="picker-header">
  5. <text class="picker-title">选择就诊医院</text>
  6. <u-icon name="close" size="36rpx" color="#999" @click="handleClose" />
  7. </view>
  8. <!-- 全量医院搜索 -->
  9. <view class="search-bar">
  10. <u--input v-model="hospitalKeyword" placeholder="搜索医院名称" prefixIcon="search" prefixIconStyle="color: #999" border="surround" shape="circle" clearable />
  11. </view>
  12. <!-- Tab 切换:省 → 市 → 区 → 医院 -->
  13. <view class="tab-bar">
  14. <view class="tab-item" :class="{ active: currentTab === 'province' }" @click="switchTab('province')">
  15. {{ selectedProvince ? selectedProvince.provinceName : '请选择省份' }}
  16. </view>
  17. <view v-if="selectedProvince" class="tab-item" :class="{ active: currentTab === 'city' }" @click="switchTab('city')">
  18. {{ selectedCity ? selectedCity.cityName : '请选择城市' }}
  19. </view>
  20. <view v-if="selectedCity" class="tab-item" :class="{ active: currentTab === 'district' }" @click="switchTab('district')">
  21. {{ selectedDistrict ? selectedDistrict.districtName : '请选择区县' }}
  22. </view>
  23. <view v-if="selectedDistrict" class="tab-item" :class="{ active: currentTab === 'hospital' }" @click="switchTab('hospital')">
  24. {{ selectedHospital ? selectedHospital.name : '请选择医院' }}
  25. </view>
  26. </view>
  27. <!-- 列表区域 -->
  28. <scroll-view class="list-wrap" scroll-y>
  29. <!-- 全量搜索结果 -->
  30. <template v-if="hasSearchKeyword">
  31. <view v-for="item in globalFilteredHospitals" :key="item.id" class="list-item"
  32. :class="{ selected: selectedHospital && selectedHospital.id === item.id }" @click="onSelectHospital(item)">
  33. <view class="hospital-result">
  34. <text class="hospital-name">{{ item.name }}</text>
  35. <text class="hospital-region">{{ item.regionText || '地区未设置' }}</text>
  36. </view>
  37. <u-icon v-if="selectedHospital && selectedHospital.id === item.id" name="checkmark" color="#FF7700" size="28rpx" />
  38. </view>
  39. </template>
  40. <!-- 省份 -->
  41. <template v-else-if="currentTab === 'province'">
  42. <view v-for="item in provinces" :key="item.provinceCode || item.provinceName" class="list-item"
  43. :class="{ selected: selectedProvince === item }" @click="onSelectProvince(item)">
  44. <text>{{ item.provinceName }}</text>
  45. <u-icon v-if="selectedProvince === item" name="checkmark" color="#FF7700" size="28rpx" />
  46. </view>
  47. </template>
  48. <!-- 城市 -->
  49. <template v-if="currentTab === 'city'">
  50. <view v-for="item in cities" :key="item.cityCode || item.cityName" class="list-item"
  51. :class="{ selected: selectedCity === item }" @click="onSelectCity(item)">
  52. <text>{{ item.cityName }}</text>
  53. <u-icon v-if="selectedCity === item" name="checkmark" color="#FF7700" size="28rpx" />
  54. </view>
  55. </template>
  56. <!-- 区县 -->
  57. <template v-if="currentTab === 'district'">
  58. <view v-for="item in districts" :key="item.districtCode || item.districtName" class="list-item"
  59. :class="{ selected: selectedDistrict === item }" @click="onSelectDistrict(item)">
  60. <text>{{ item.districtName }}</text>
  61. <u-icon v-if="selectedDistrict === item" name="checkmark" color="#FF7700" size="28rpx" />
  62. </view>
  63. </template>
  64. <!-- 医院 -->
  65. <template v-if="currentTab === 'hospital'">
  66. <view v-for="item in filteredHospitals" :key="item.id" class="list-item"
  67. :class="{ selected: selectedHospital && selectedHospital.id === item.id }" @click="onSelectHospital(item)">
  68. <text>{{ item.name }}</text>
  69. <u-icon v-if="selectedHospital && selectedHospital.id === item.id" name="checkmark" color="#FF7700" size="28rpx" />
  70. </view>
  71. </template>
  72. <u-empty v-if="currentEmpty" mode="data" text="暂无数据" marginTop="60" />
  73. </scroll-view>
  74. </view>
  75. </u-popup>
  76. </template>
  77. <script setup>
  78. import { ref, computed } from 'vue'
  79. const props = defineProps({
  80. hospitalData: { type: Array, default: () => [] }
  81. })
  82. const emit = defineEmits(['confirm'])
  83. const visible = ref(false)
  84. const currentTab = ref('province')
  85. const selectedProvince = ref(null)
  86. const selectedCity = ref(null)
  87. const selectedDistrict = ref(null)
  88. const selectedHospital = ref(null)
  89. const hospitalKeyword = ref('')
  90. const provinces = computed(() => props.hospitalData || [])
  91. const cities = computed(() => (selectedProvince.value && selectedProvince.value.cities) || [])
  92. const districts = computed(() => (selectedCity.value && selectedCity.value.districts) || [])
  93. const hospitals = computed(() => (selectedDistrict.value && selectedDistrict.value.hospitals) || [])
  94. const hasSearchKeyword = computed(() => !!hospitalKeyword.value.trim())
  95. const globalHospitals = computed(() => {
  96. const list = []
  97. provinces.value.forEach(prov => {
  98. ;(prov.cities || []).forEach(city => {
  99. ;(city.districts || []).forEach(dist => {
  100. ;(dist.hospitals || []).forEach(h => {
  101. list.push({
  102. ...h,
  103. _province: prov,
  104. _city: city,
  105. _district: dist,
  106. regionText: [prov.provinceName, city.cityName, dist.districtName].filter(Boolean).join(' ')
  107. })
  108. })
  109. })
  110. })
  111. })
  112. return list
  113. })
  114. const globalFilteredHospitals = computed(() => {
  115. const kw = hospitalKeyword.value.trim()
  116. if (!kw) return []
  117. return globalHospitals.value.filter(h => h.name.includes(kw) || (h.regionText && h.regionText.includes(kw)))
  118. })
  119. const filteredHospitals = computed(() => {
  120. return hospitals.value
  121. })
  122. const currentEmpty = computed(() => {
  123. if (hasSearchKeyword.value) return globalFilteredHospitals.value.length === 0
  124. if (currentTab.value === 'province') return provinces.value.length === 0
  125. if (currentTab.value === 'city') return cities.value.length === 0
  126. if (currentTab.value === 'district') return districts.value.length === 0
  127. return filteredHospitals.value.length === 0
  128. })
  129. const switchTab = (tab) => { currentTab.value = tab }
  130. const onSelectProvince = (item) => {
  131. selectedProvince.value = item
  132. selectedCity.value = null
  133. selectedDistrict.value = null
  134. selectedHospital.value = null
  135. if (item.cities && item.cities.length === 1) {
  136. selectedCity.value = item.cities[0]
  137. if (selectedCity.value.districts && selectedCity.value.districts.length === 1) {
  138. selectedDistrict.value = selectedCity.value.districts[0]
  139. currentTab.value = 'hospital'
  140. } else {
  141. currentTab.value = 'district'
  142. }
  143. } else {
  144. currentTab.value = 'city'
  145. }
  146. }
  147. const onSelectCity = (item) => {
  148. selectedCity.value = item
  149. selectedDistrict.value = null
  150. selectedHospital.value = null
  151. if (item.districts && item.districts.length === 1) {
  152. selectedDistrict.value = item.districts[0]
  153. currentTab.value = 'hospital'
  154. } else {
  155. currentTab.value = 'district'
  156. }
  157. }
  158. const onSelectDistrict = (item) => {
  159. selectedDistrict.value = item
  160. selectedHospital.value = null
  161. hospitalKeyword.value = ''
  162. currentTab.value = 'hospital'
  163. }
  164. const onSelectHospital = (item) => {
  165. if (item._province) selectedProvince.value = item._province
  166. if (item._city) selectedCity.value = item._city
  167. if (item._district) selectedDistrict.value = item._district
  168. selectedHospital.value = item
  169. emitData()
  170. handleClose()
  171. }
  172. const emitData = () => {
  173. if (!selectedHospital.value) return
  174. emit('confirm', {
  175. hospitalId: selectedHospital.value.id,
  176. hospitalName: selectedHospital.value.name,
  177. province_code: selectedHospital.value.province_code || (selectedProvince.value && selectedProvince.value.provinceCode) || '',
  178. city_code: selectedHospital.value.city_code || (selectedCity.value && selectedCity.value.cityCode) || '',
  179. district_code: selectedHospital.value.district_code || (selectedDistrict.value && selectedDistrict.value.districtCode) || ''
  180. })
  181. }
  182. const handleClose = () => {
  183. visible.value = false
  184. }
  185. const open = (initial = {}) => {
  186. selectedProvince.value = null
  187. selectedCity.value = null
  188. selectedDistrict.value = null
  189. selectedHospital.value = null
  190. hospitalKeyword.value = ''
  191. currentTab.value = 'province'
  192. const provinceCode = initial.province_code || ''
  193. const cityCode = initial.city_code || ''
  194. const districtCode = initial.district_code || ''
  195. const hospitalName = initial.hospitalName || initial.hospital || ''
  196. const hospitalId = initial.hospitalId || ''
  197. if (provinceCode) {
  198. selectedProvince.value = provinces.value.find(p => p.provinceCode === provinceCode) || null
  199. if (selectedProvince.value && cityCode) {
  200. selectedCity.value = (selectedProvince.value.cities || []).find(c => c.cityCode === cityCode) || null
  201. if (selectedCity.value && districtCode) {
  202. selectedDistrict.value = (selectedCity.value.districts || []).find(d => d.districtCode === districtCode) || null
  203. }
  204. }
  205. }
  206. if (!selectedDistrict.value && (hospitalName || hospitalId)) {
  207. findHospitalPath(hospitalName, hospitalId)
  208. }
  209. if (selectedDistrict.value) {
  210. selectedHospital.value = (selectedDistrict.value.hospitals || []).find(h => {
  211. return (hospitalId && h.id === hospitalId) || (hospitalName && h.name === hospitalName)
  212. }) || null
  213. currentTab.value = 'hospital'
  214. } else if (selectedCity.value) {
  215. currentTab.value = 'district'
  216. } else if (selectedProvince.value) {
  217. currentTab.value = 'city'
  218. }
  219. visible.value = true
  220. }
  221. const findHospitalPath = (hospitalName, hospitalId) => {
  222. for (const prov of provinces.value) {
  223. for (const city of (prov.cities || [])) {
  224. for (const dist of (city.districts || [])) {
  225. const hospital = (dist.hospitals || []).find(h => {
  226. return (hospitalId && h.id === hospitalId) || (hospitalName && h.name === hospitalName)
  227. })
  228. if (hospital) {
  229. selectedProvince.value = prov
  230. selectedCity.value = city
  231. selectedDistrict.value = dist
  232. selectedHospital.value = hospital
  233. return
  234. }
  235. }
  236. }
  237. }
  238. }
  239. defineExpose({ open })
  240. </script>
  241. <style lang="scss" scoped>
  242. .picker-wrap { height: 70vh; display: flex; flex-direction: column; }
  243. .picker-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 1rpx solid #f0f0f0; }
  244. .picker-title { font-size: 32rpx; font-weight: 600; color: #333; }
  245. .tab-bar { display: flex; padding: 20rpx 32rpx; gap: 24rpx; border-bottom: 1rpx solid #f0f0f0; flex-wrap: wrap; }
  246. .tab-item {
  247. font-size: 26rpx; color: #999; padding-bottom: 12rpx; position: relative;
  248. max-width: 240rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  249. &.active {
  250. color: #FF7700; font-weight: 500;
  251. &::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 4rpx; background: #FF7700; border-radius: 2rpx; }
  252. }
  253. }
  254. .search-bar { padding: 16rpx 32rpx; border-bottom: 1rpx solid #f0f0f0; }
  255. .list-wrap { flex: 1; overflow: hidden; }
  256. .list-item {
  257. display: flex; align-items: center; justify-content: space-between;
  258. padding: 28rpx 32rpx; font-size: 28rpx; color: #333; border-bottom: 1rpx solid #f8f8f8;
  259. &.selected { color: #FF7700; background: #FFF8F0; }
  260. }
  261. .hospital-result { display: flex; flex-direction: column; gap: 8rpx; min-width: 0; }
  262. .hospital-name { font-size: 28rpx; color: #333; }
  263. .hospital-region { font-size: 22rpx; color: #999; }
  264. </style>