AddressSercher.vue 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <script setup lang="ts">
  2. import { ref, computed, nextTick, watch, type Ref } from 'vue';
  3. import { SearchOutlined } from '@ant-design/icons-vue';
  4. import type { SelectProps } from 'ant-design-vue';
  5. import { waitTimeOut } from '@imengyu/imengyu-utils';
  6. import { ScrollRect } from '@imengyu/vue-scroll-rect';
  7. // 定义Props
  8. const props = defineProps<{
  9. modelValue?: string;
  10. disabled?: boolean;
  11. }>();
  12. // 定义Emits
  13. const emit = defineEmits<{
  14. 'update:modelValue': [value: string];
  15. 'choosedAddress': [address: AddressItem];
  16. }>();
  17. export interface AddressItem {
  18. name: string;
  19. lng: number;
  20. lat: number;
  21. address: string;
  22. }
  23. // 状态管理
  24. const inputValue = ref(props.modelValue);
  25. const addressList = ref<Array<AddressItem>>([]);
  26. const loading = ref(false);
  27. const openList = ref(false);
  28. // 监听modelValue变化
  29. watch(() => props.modelValue, (newValue) => {
  30. inputValue.value = newValue ;
  31. });
  32. // 调用高德API搜索地址
  33. async function searchAddress() {
  34. if (!inputValue.value?.trim()) return;
  35. loading.value = true;
  36. openList.value = true;
  37. try {
  38. const apiKey = '8fd09264c33678141f609588c432df0e';
  39. const url = `https://restapi.amap.com/v3/place/text?key=${apiKey}&keywords=${encodeURIComponent(inputValue.value)}&citylimit=true&offset=10&page=1`;
  40. const response = await fetch(url);
  41. const data = await response.json();
  42. if (data.status === '1' && data.pois && data.pois.length > 0) {
  43. console.log('搜索到的地址:', data.pois);
  44. addressList.value = data.pois.map((item: any) => ({
  45. name: item.name,
  46. lng: parseFloat(item.location.split(',')[0]),
  47. lat: parseFloat(item.location.split(',')[1]),
  48. address: item.address ? `${item.cityname}${item.adname}${item.address} ${item.name}` : item.name
  49. }));
  50. } else {
  51. addressList.value = [];
  52. }
  53. } catch (error) {
  54. console.error('搜索地址失败:', error);
  55. addressList.value = [];
  56. } finally {
  57. loading.value = false;
  58. }
  59. }
  60. // 选择地址
  61. function handleSelectAddress(address: any) {
  62. // 更新输入值
  63. inputValue.value = address.address;
  64. // 关闭下拉列表
  65. openList.value = false;
  66. // 发送详细地址信息
  67. emit('choosedAddress', {
  68. name: address.name,
  69. lng: address.lng,
  70. lat: address.lat,
  71. address: address.address
  72. });
  73. }
  74. watch(inputValue, (newValue) => {
  75. emit('update:modelValue', newValue ?? '');
  76. })
  77. // 处理输入变化
  78. function handleInputChange(value: string) {
  79. inputValue.value = value;
  80. emit('update:modelValue', value);
  81. }
  82. // 处理点击搜索按钮
  83. function handleSearch() {
  84. searchAddress();
  85. }
  86. // 定义select组件的选项
  87. const selectOptions = computed<SelectProps['options']>(() => {
  88. return addressList.value.map(item => ({
  89. label: item.name,
  90. value: item.address,
  91. data: item
  92. }));
  93. });
  94. </script>
  95. <template>
  96. <div class="address-searcher">
  97. <div class="input-wrapper">
  98. <a-input
  99. v-model:value="inputValue"
  100. mode="combobox"
  101. :options="selectOptions"
  102. :disabled="props.disabled"
  103. :show-search="true"
  104. :default-active-first-option="false"
  105. :filter-option="false"
  106. :not-found-content="'暂无匹配的可选地址,可修改关键字扩大搜索范围'"
  107. placeholder="请输入地址"
  108. style="width: 100%;"
  109. @select="(value: string, option: any) => handleSelectAddress(option?.data)"
  110. @search="handleInputChange"
  111. />
  112. </div>
  113. <a-popover placement="bottomRight" trigger="click" v-model:open="openList" >
  114. <template #content>
  115. <ScrollRect scroll="vertical">
  116. <a-list class="list" size="small" itemLayout="vertical" :data-source="selectOptions">
  117. <template #renderItem="{ item }">
  118. <div class="list-item" @click="handleSelectAddress(item.data)">
  119. <div class="list-item-content">
  120. <div class="list-item-title">{{ item.label }}</div>
  121. <div class="list-item-desc">{{ item.value }}</div>
  122. </div>
  123. </div>
  124. </template>
  125. </a-list>
  126. </ScrollRect>
  127. </template>
  128. <template #title>
  129. <span>模糊地址查询</span>
  130. </template>
  131. <a-button :disabled="props.disabled" type="primary" :loading="loading" @click="handleSearch">
  132. <SearchOutlined />
  133. 搜索
  134. </a-button>
  135. </a-popover>
  136. </div>
  137. </template>
  138. <style scoped lang="scss">
  139. .address-searcher {
  140. position: relative;
  141. display: flex;
  142. align-items: center;
  143. gap: 8px;
  144. .input-wrapper {
  145. flex: 1;
  146. }
  147. }
  148. .list {
  149. width: 50vw;
  150. max-width: 600px;
  151. max-height: 70vh;
  152. .list-item {
  153. padding: 8px 12px;
  154. cursor: pointer;
  155. &:hover {
  156. background-color: #f5f5f5;
  157. }
  158. }
  159. .list-item-content {
  160. display: flex;
  161. flex-direction: column;
  162. }
  163. .list-item-title {
  164. font-size: 14px;
  165. font-weight: 500;
  166. }
  167. .list-item-desc {
  168. font-size: 12px;
  169. color: #999;
  170. }
  171. }
  172. </style>