address-picker.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <template>
  2. <view class="address-picker">
  3. <view class="picker-row" @click="showProvincePicker">
  4. <text class="picker-label">省份:</text>
  5. <text class="picker-value">{{ selectedProvince ? selectedProvince.province : '请选择' }}</text>
  6. <text class="picker-arrow">▶</text>
  7. </view>
  8. <view class="picker-row" @click="showCityPicker" v-if="selectedProvince">
  9. <text class="picker-label">城市:</text>
  10. <text class="picker-value">{{ selectedCity ? selectedCity.city : '请选择' }}</text>
  11. <text class="picker-arrow">▶</text>
  12. </view>
  13. <view class="picker-row" @click="showDistrictPicker" v-if="selectedCity">
  14. <text class="picker-label">区县:</text>
  15. <text class="picker-value">{{ selectedDistrict ? selectedDistrict.district : '请选择' }}</text>
  16. <text class="picker-arrow">▶</text>
  17. </view>
  18. <view class="picker-row" @click="showStreetPicker" v-if="selectedDistrict">
  19. <text class="picker-label">街道:</text>
  20. <text class="picker-value">{{ selectedStreet ? selectedStreet.street : '请选择' }}</text>
  21. <text class="picker-arrow">▶</text>
  22. </view>
  23. <view class="address-display" v-if="fullAddress">
  24. <text class="address-label">完整地址:</text>
  25. <text class="address-text">{{ fullAddress }}</text>
  26. </view>
  27. </view>
  28. </template>
  29. <script>
  30. export default {
  31. name: 'AddressPicker',
  32. props: {
  33. value: {
  34. type: Number,
  35. default: null
  36. }
  37. },
  38. data() {
  39. return {
  40. provinces: [],
  41. cities: [],
  42. districts: [],
  43. streets: [],
  44. selectedProvince: null,
  45. selectedCity: null,
  46. selectedDistrict: null,
  47. selectedStreet: null
  48. }
  49. },
  50. computed: {
  51. fullAddress() {
  52. if (this.selectedStreet) {
  53. return this.selectedStreet.fullName
  54. }
  55. return ''
  56. }
  57. },
  58. watch: {
  59. value: {
  60. immediate: true,
  61. handler(newVal) {
  62. if (newVal) {
  63. this.loadAddressById(newVal)
  64. }
  65. }
  66. }
  67. },
  68. mounted() {
  69. this.loadProvinces()
  70. },
  71. methods: {
  72. async loadProvinces() {
  73. try {
  74. const res = await this.$http.get('/api/streets/provinces')
  75. this.provinces = res.data || []
  76. } catch (e) {
  77. console.error('加载省份失败', e)
  78. }
  79. },
  80. async loadCities(provinceId) {
  81. try {
  82. const res = await this.$http.get(`/api/streets/children/${provinceId}`)
  83. this.cities = res.data || []
  84. } catch (e) {
  85. console.error('加载城市失败', e)
  86. }
  87. },
  88. async loadDistricts(cityId) {
  89. try {
  90. const res = await this.$http.get(`/api/streets/children/${cityId}`)
  91. this.districts = res.data || []
  92. } catch (e) {
  93. console.error('加载区县失败', e)
  94. }
  95. },
  96. async loadStreets(districtId) {
  97. try {
  98. const res = await this.$http.get(`/api/streets/children/${districtId}`)
  99. this.streets = res.data || []
  100. } catch (e) {
  101. console.error('加载街道失败', e)
  102. }
  103. },
  104. async loadAddressById(streetId) {
  105. try {
  106. const res = await this.$http.get(`/api/streets/${streetId}/path`)
  107. const path = res.data || []
  108. if (path.length >= 1) this.selectedProvince = path[0]
  109. if (path.length >= 2) this.selectedCity = path[1]
  110. if (path.length >= 3) this.selectedDistrict = path[2]
  111. if (path.length >= 4) this.selectedStreet = path[3]
  112. // 加载下级选项
  113. if (this.selectedProvince) await this.loadCities(this.selectedProvince.id)
  114. if (this.selectedCity) await this.loadDistricts(this.selectedCity.id)
  115. if (this.selectedDistrict) await this.loadStreets(this.selectedDistrict.id)
  116. } catch (e) {
  117. console.error('加载地址路径失败', e)
  118. }
  119. },
  120. showProvincePicker() {
  121. uni.showActionSheet({
  122. itemList: this.provinces.map(p => p.province),
  123. success: async (res) => {
  124. this.selectedProvince = this.provinces[res.tapIndex]
  125. this.selectedCity = null
  126. this.selectedDistrict = null
  127. this.selectedStreet = null
  128. this.cities = []
  129. this.districts = []
  130. this.streets = []
  131. await this.loadCities(this.selectedProvince.id)
  132. this.emitChange()
  133. }
  134. })
  135. },
  136. showCityPicker() {
  137. if (this.cities.length === 0) {
  138. uni.showToast({ title: '请先选择省份', icon: 'none' })
  139. return
  140. }
  141. uni.showActionSheet({
  142. itemList: this.cities.map(c => c.city),
  143. success: async (res) => {
  144. this.selectedCity = this.cities[res.tapIndex]
  145. this.selectedDistrict = null
  146. this.selectedStreet = null
  147. this.districts = []
  148. this.streets = []
  149. await this.loadDistricts(this.selectedCity.id)
  150. this.emitChange()
  151. }
  152. })
  153. },
  154. showDistrictPicker() {
  155. if (this.districts.length === 0) {
  156. uni.showToast({ title: '请先选择城市', icon: 'none' })
  157. return
  158. }
  159. uni.showActionSheet({
  160. itemList: this.districts.map(d => d.district),
  161. success: async (res) => {
  162. this.selectedDistrict = this.districts[res.tapIndex]
  163. this.selectedStreet = null
  164. this.streets = []
  165. await this.loadStreets(this.selectedDistrict.id)
  166. this.emitChange()
  167. }
  168. })
  169. },
  170. showStreetPicker() {
  171. if (this.streets.length === 0) {
  172. uni.showToast({ title: '请先选择区县', icon: 'none' })
  173. return
  174. }
  175. uni.showActionSheet({
  176. itemList: this.streets.map(s => s.street),
  177. success: (res) => {
  178. this.selectedStreet = this.streets[res.tapIndex]
  179. this.emitChange()
  180. }
  181. })
  182. },
  183. emitChange() {
  184. const streetId = this.selectedStreet ? this.selectedStreet.id : null
  185. const addressData = {
  186. streetId: streetId,
  187. province: this.selectedProvince ? this.selectedProvince.province : null,
  188. city: this.selectedCity ? this.selectedCity.city : null,
  189. district: this.selectedDistrict ? this.selectedDistrict.district : null,
  190. street: this.selectedStreet ? this.selectedStreet.street : null,
  191. fullName: this.fullAddress
  192. }
  193. this.$emit('input', streetId)
  194. this.$emit('change', addressData)
  195. },
  196. reset() {
  197. this.selectedProvince = null
  198. this.selectedCity = null
  199. this.selectedDistrict = null
  200. this.selectedStreet = null
  201. this.cities = []
  202. this.districts = []
  203. this.streets = []
  204. this.emitChange()
  205. }
  206. }
  207. }
  208. </script>
  209. <style scoped>
  210. .address-picker {
  211. background: #fff;
  212. padding: 20rpx;
  213. }
  214. .picker-row {
  215. display: flex;
  216. align-items: center;
  217. padding: 20rpx 0;
  218. border-bottom: 1px solid #eee;
  219. }
  220. .picker-label {
  221. font-size: 28rpx;
  222. color: #666;
  223. width: 140rpx;
  224. }
  225. .picker-value {
  226. flex: 1;
  227. font-size: 28rpx;
  228. color: #333;
  229. }
  230. .picker-arrow {
  231. font-size: 24rpx;
  232. color: #999;
  233. margin-left: 10rpx;
  234. }
  235. .address-display {
  236. margin-top: 20rpx;
  237. padding: 20rpx;
  238. background: #f5f5f5;
  239. border-radius: 8rpx;
  240. }
  241. .address-label {
  242. font-size: 24rpx;
  243. color: #999;
  244. display: block;
  245. margin-bottom: 10rpx;
  246. }
  247. .address-text {
  248. font-size: 28rpx;
  249. color: #333;
  250. }
  251. </style>