771 lines
28 KiB
Vue
771 lines
28 KiB
Vue
<template>
|
||
<div class="app">
|
||
<!-- 地图(全屏底层) -->
|
||
<PoiMap ref="mapRef" @marker-click="onMarkerClick" @map-click="onMapClick" />
|
||
|
||
<!-- 顶部搜索栏 -->
|
||
<div class="top-bar">
|
||
<div class="search-form">
|
||
<div class="input-wrap">
|
||
<input
|
||
v-model="keywords"
|
||
class="input"
|
||
placeholder="关键词(选填,如:火锅店)"
|
||
@input="onKeywordInput"
|
||
@compositionend="onKeywordInput"
|
||
@keydown.enter="doSearch(); hideSuggestions()"
|
||
@keydown.down.prevent="onSuggestArrow(1)"
|
||
@keydown.up.prevent="onSuggestArrow(-1)"
|
||
@keydown.esc="hideSuggestions"
|
||
@blur="onSuggestBlur"
|
||
autocomplete="off"
|
||
/>
|
||
</div>
|
||
<ul v-if="suggestions.length > 0" class="suggest-dropdown">
|
||
<li
|
||
v-for="(item, i) in suggestions"
|
||
:key="item.poiId || i"
|
||
class="suggest-item"
|
||
:class="{ active: i === suggestIndex }"
|
||
@mousedown.prevent="selectSuggestion(item)"
|
||
@mouseenter="suggestIndex = i"
|
||
>
|
||
<span class="suggest-name">{{ item.name }}</span>
|
||
<span v-if="item.city" class="suggest-city">{{ item.city }}</span>
|
||
</li>
|
||
</ul>
|
||
<select v-model="province" class="select province-select" @change="provinceChanged">
|
||
<option value="">所有省份</option>
|
||
<option v-for="p in provinces" :key="p" :value="p">{{ p }}</option>
|
||
</select>
|
||
<select v-model="city" class="select city-select" :disabled="!province">
|
||
<option value="">{{ province ? '所有城市' : '请先选省份' }}</option>
|
||
<option v-for="c in cities" :key="c" :value="c">{{ c }}</option>
|
||
</select>
|
||
<select v-model="radius" class="select radius-select">
|
||
<option v-for="r in radiusOptions" :key="r.value" :value="r.value">{{ r.label }}</option>
|
||
</select>
|
||
<button class="btn" :disabled="loading" @click="doSearch">
|
||
{{ loading ? '搜索中...' : '搜索' }}
|
||
</button>
|
||
</div>
|
||
<!-- 已选中心点 -->
|
||
<div v-if="centerLat || centerLng" class="center-badge">
|
||
<span>中心点:{{ centerLng.toFixed(4) }}, {{ centerLat.toFixed(4) }}</span>
|
||
<button class="clear-center" @click="clearCenter">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧结果面板 -->
|
||
<div v-if="searched" class="side-panel" :class="{ 'has-results': results.length > 0 }">
|
||
<!-- 加载中 -->
|
||
<div v-if="loading" class="panel-loading">
|
||
<div class="spinner"></div>
|
||
<span>正在查询...</span>
|
||
</div>
|
||
|
||
<!-- 错误 -->
|
||
<div v-else-if="error" class="panel-error">{{ error }}</div>
|
||
|
||
<!-- 空结果 -->
|
||
<div v-else-if="results.length === 0" class="panel-empty">
|
||
未找到相关商户<br /><small>请尝试其他关键词</small>
|
||
</div>
|
||
|
||
<!-- 结果列表 -->
|
||
<template v-else>
|
||
<div class="panel-header">
|
||
<span class="panel-title">共 {{ total }} 条结果</span>
|
||
<span class="panel-page">第 {{ page }}/{{ maxPage }} 页</span>
|
||
</div>
|
||
|
||
<div class="panel-list">
|
||
<div
|
||
v-for="item in results"
|
||
:key="item.poiId"
|
||
class="result-card"
|
||
:class="{ active: activePoi && activePoi.poiId === item.poiId }"
|
||
@click="focusItem(item)"
|
||
>
|
||
<div class="card-name">{{ item.name }}</div>
|
||
<div class="card-phone">{{ item.phone || '暂无电话' }}</div>
|
||
<div class="card-addr">{{ item.address }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel-footer">
|
||
<button class="page-btn" :disabled="page <= 1" @click="page--; doSearch()">上一页</button>
|
||
<button class="page-btn" :disabled="page >= maxPage" @click="page++; doSearch()">下一页</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 操作提示 -->
|
||
<div class="hint-bar">
|
||
<span>点击地图设置搜索中心点</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue'
|
||
import { searchPoi, suggestPoi } from './api/poi.js'
|
||
import PoiMap from './components/PoiMap.vue'
|
||
|
||
const mapRef = ref(null)
|
||
const keywords = ref('')
|
||
const city = ref('')
|
||
const radius = ref(3000)
|
||
const centerLat = ref(0)
|
||
const centerLng = ref(0)
|
||
const results = ref([])
|
||
const total = ref(0)
|
||
const loading = ref(false)
|
||
const searched = ref(false)
|
||
const error = ref('')
|
||
const page = ref(1)
|
||
const maxPage = ref(1)
|
||
const activePoi = ref(null)
|
||
const suggestions = ref([])
|
||
const suggestIndex = ref(-1)
|
||
let suggestTimer = null
|
||
|
||
const province = ref('')
|
||
const provinces = [
|
||
'北京', '天津', '上海', '重庆',
|
||
'广东', '浙江', '江苏', '福建', '山东', '四川', '湖北', '湖南',
|
||
'河南', '河北', '安徽', '江西', '陕西', '山西', '辽宁', '吉林',
|
||
'黑龙江', '广西', '内蒙古', '西藏', '新疆', '宁夏', '青海',
|
||
'甘肃', '贵州', '云南', '海南',
|
||
]
|
||
|
||
const regions = {
|
||
'北京': ['北京'],
|
||
'天津': ['天津'],
|
||
'上海': ['上海'],
|
||
'重庆': ['重庆'],
|
||
'广东': ['广州', '深圳', '东莞', '佛山', '珠海', '中山', '惠州', '汕头', '江门', '湛江', '肇庆', '茂名', '揭阳', '梅州', '清远', '韶关', '阳江', '河源', '汕尾', '潮州', '云浮'],
|
||
'浙江': ['杭州', '宁波', '温州', '绍兴', '嘉兴', '金华', '台州', '湖州', '衢州', '舟山', '丽水'],
|
||
'江苏': ['南京', '苏州', '无锡', '常州', '南通', '徐州', '扬州', '盐城', '淮安', '连云港', '镇江', '泰州', '宿迁'],
|
||
'福建': ['福州', '厦门', '泉州', '漳州', '莆田', '宁德', '龙岩', '三明', '南平'],
|
||
'山东': ['济南', '青岛', '烟台', '潍坊', '临沂', '淄博', '济宁', '威海', '泰安', '德州', '聊城', '东营', '枣庄', '日照', '滨州', '菏泽'],
|
||
'四川': ['成都', '绵阳', '德阳', '宜宾', '南充', '泸州', '自贡', '乐山', '眉山', '达州', '内江', '遂宁', '广安', '攀枝花', '资阳', '广元', '雅安', '巴中'],
|
||
'湖北': ['武汉', '襄阳', '宜昌', '荆州', '黄冈', '十堰', '孝感', '荆门', '鄂州', '黄石', '咸宁', '随州'],
|
||
'湖南': ['长沙', '株洲', '湘潭', '衡阳', '岳阳', '常德', '益阳', '邵阳', '郴州', '怀化', '永州', '娄底', '湘西'],
|
||
'河南': ['郑州', '洛阳', '南阳', '许昌', '周口', '新乡', '安阳', '开封', '商丘', '信阳', '驻马店', '平顶山', '焦作', '濮阳', '三门峡', '漯河', '鹤壁'],
|
||
'河北': ['石家庄', '唐山', '保定', '邯郸', '沧州', '廊坊', '秦皇岛', '张家口', '衡水', '邢台', '承德'],
|
||
'安徽': ['合肥', '芜湖', '安庆', '阜阳', '滁州', '马鞍山', '蚌埠', '宿州', '六安', '宣城', '铜陵', '池州', '淮南', '淮北', '黄山'],
|
||
'江西': ['南昌', '赣州', '九江', '宜春', '吉安', '上饶', '抚州', '萍乡', '景德镇', '新余', '鹰潭'],
|
||
'陕西': ['西安', '咸阳', '宝鸡', '渭南', '汉中', '延安', '榆林', '安康', '商洛', '铜川'],
|
||
'山西': ['太原', '大同', '长治', '临汾', '晋中', '运城', '晋城', '吕梁', '忻州', '阳泉', '朔州'],
|
||
'辽宁': ['沈阳', '大连', '鞍山', '抚顺', '锦州', '营口', '盘锦', '丹东', '本溪', '辽阳', '朝阳', '阜新', '葫芦岛', '铁岭'],
|
||
'吉林': ['长春', '吉林', '延边', '四平', '通化', '松原', '白城', '辽源', '白山'],
|
||
'黑龙江': ['哈尔滨', '大庆', '齐齐哈尔', '牡丹江', '佳木斯', '绥化', '鸡西', '鹤岗', '双鸭山', '伊春', '七台河', '黑河', '大兴安岭'],
|
||
'广西': ['南宁', '桂林', '柳州', '北海', '玉林', '百色', '梧州', '贵港', '钦州', '河池', '防城港', '崇左', '来宾', '贺州'],
|
||
'内蒙古': ['呼和浩特', '包头', '赤峰', '鄂尔多斯', '通辽', '呼伦贝尔', '乌兰察布', '巴彦淖尔', '兴安', '锡林郭勒', '阿拉善'],
|
||
'西藏': ['拉萨', '日喀则', '昌都', '林芝', '山南', '那曲', '阿里'],
|
||
'新疆': ['乌鲁木齐', '克拉玛依', '吐鲁番', '哈密', '昌吉', '伊犁', '喀什', '阿克苏', '和田', '塔城', '阿勒泰', '博尔塔拉', '巴音郭楞', '克孜勒苏'],
|
||
'宁夏': ['银川', '石嘴山', '吴忠', '固原', '中卫'],
|
||
'青海': ['西宁', '海东', '海西', '海南', '海北', '黄南', '玉树', '果洛'],
|
||
'甘肃': ['兰州', '天水', '白银', '金昌', '嘉峪关', '武威', '张掖', '平凉', '酒泉', '庆阳', '定西', '陇南', '临夏', '甘南'],
|
||
'贵州': ['贵阳', '遵义', '六盘水', '安顺', '毕节', '铜仁', '黔东南', '黔南', '黔西南'],
|
||
'云南': ['昆明', '大理', '丽江', '曲靖', '玉溪', '红河', '楚雄', '普洱', '昭通', '文山', '西双版纳', '保山', '德宏', '临沧', '怒江', '迪庆'],
|
||
'海南': ['海口', '三亚', '儋州', '三沙', '琼海', '文昌', '万宁', '陵水', '五指山'],
|
||
}
|
||
|
||
const cities = computed(() => regions[province.value] || [])
|
||
|
||
// 城市中心坐标(用于选中城市时地图跳转)
|
||
const cityCoords = {
|
||
'北京': [39.9042, 116.4074], '天津': [39.1252, 117.1908], '上海': [31.2304, 121.4737], '重庆': [29.4316, 106.9123],
|
||
'广州': [23.1291, 113.2644], '深圳': [22.5431, 114.0579], '东莞': [23.0208, 113.7518], '佛山': [23.0219, 113.1219],
|
||
'珠海': [22.2710, 113.5767], '中山': [22.5150, 113.3926], '惠州': [23.1110, 114.4162], '汕头': [23.3545, 116.6822],
|
||
'江门': [22.5786, 113.0945], '湛江': [21.2713, 110.3594], '肇庆': [23.0469, 112.4653], '茂名': [21.6630, 110.9252],
|
||
'揭阳': [23.5499, 116.3727], '梅州': [24.2884, 116.1176], '清远': [23.6810, 113.0560], '韶关': [24.8104, 113.5975],
|
||
'阳江': [21.8583, 111.9822], '河源': [23.7437, 114.7009], '汕尾': [22.7870, 115.3753], '潮州': [23.6564, 116.6300],
|
||
'云浮': [22.9150, 112.0445],
|
||
'杭州': [30.2741, 120.1551], '宁波': [29.8683, 121.5440], '温州': [28.0000, 120.6994], '绍兴': [30.0300, 120.5800],
|
||
'嘉兴': [30.7711, 120.7551], '金华': [29.0792, 119.6474], '台州': [28.6562, 121.4208], '湖州': [30.8931, 120.0875],
|
||
'衢州': [28.9359, 118.8742], '舟山': [29.9855, 122.2076], '丽水': [28.4680, 119.9228],
|
||
'南京': [32.0603, 118.7969], '苏州': [31.2990, 120.5853], '无锡': [31.4910, 120.3119], '常州': [31.8107, 119.9737],
|
||
'南通': [31.9798, 120.8938], '徐州': [34.2044, 117.2858], '扬州': [32.3937, 119.4126], '盐城': [33.3876, 120.1633],
|
||
'淮安': [33.5516, 119.1131], '连云港': [34.5969, 119.2215], '镇江': [32.1898, 119.4251], '泰州': [32.4555, 119.9227],
|
||
'宿迁': [33.9630, 118.2755],
|
||
'福州': [26.0745, 119.2965], '厦门': [24.4798, 118.0895], '泉州': [24.8741, 118.6756], '漳州': [24.5134, 117.6470],
|
||
'莆田': [25.4540, 119.0077], '宁德': [26.6657, 119.5479], '龙岩': [25.0751, 117.0170], '三明': [26.2634, 117.6392],
|
||
'南平': [26.6417, 118.1779],
|
||
'济南': [36.6512, 116.9972], '青岛': [36.0671, 120.3826], '烟台': [37.4645, 121.4479], '潍坊': [36.7068, 119.1618],
|
||
'临沂': [35.1046, 118.3564], '淄博': [36.8135, 118.0550], '济宁': [35.4148, 116.5872], '威海': [37.5133, 122.1210],
|
||
'泰安': [36.1994, 117.0875], '德州': [37.4363, 116.3594], '聊城': [36.4568, 115.9854], '东营': [37.4341, 118.6746],
|
||
'枣庄': [34.8105, 117.3211], '日照': [35.4204, 119.5272], '滨州': [37.3819, 117.9724], '菏泽': [35.2337, 115.4812],
|
||
'成都': [30.5728, 104.0668], '绵阳': [31.4675, 104.6791], '德阳': [31.1268, 104.3980], '宜宾': [28.7519, 104.6417],
|
||
'南充': [30.7991, 106.0827], '泸州': [28.8717, 105.4430], '自贡': [29.3390, 104.7784], '乐山': [29.5527, 103.7654],
|
||
'眉山': [30.0595, 103.8265], '达州': [31.2091, 107.4677], '内江': [29.5802, 105.0584], '遂宁': [30.5329, 105.5929],
|
||
'广安': [30.4560, 106.6332], '攀枝花': [26.5823, 101.7186], '资阳': [30.1286, 104.6276], '广元': [32.4355, 105.8450],
|
||
'雅安': [29.9765, 103.0094], '巴中': [31.8674, 106.7477],
|
||
'武汉': [30.5928, 114.3055], '襄阳': [32.0090, 112.1226], '宜昌': [30.6920, 111.2864], '荆州': [30.3345, 112.2407],
|
||
'黄冈': [30.4544, 114.8724], '十堰': [32.6319, 110.7939], '孝感': [30.9178, 113.9168], '荆门': [31.0354, 112.1992],
|
||
'鄂州': [30.3908, 114.8950], '黄石': [30.2010, 115.0769], '咸宁': [29.8327, 114.3222], '随州': [31.6902, 113.3826],
|
||
'长沙': [28.2282, 112.9388], '株洲': [27.8278, 113.1339], '湘潭': [27.8298, 112.9442], '衡阳': [26.8932, 112.5720],
|
||
'岳阳': [29.3572, 113.1288], '常德': [29.0316, 111.6985], '益阳': [28.5889, 112.3551], '邵阳': [27.2389, 111.4673],
|
||
'郴州': [25.7705, 113.0145], '怀化': [27.5697, 109.9985], '永州': [26.4207, 111.6132], '娄底': [27.7000, 112.0004],
|
||
'湘西': [28.3118, 109.7394],
|
||
'郑州': [34.7470, 113.6247], '洛阳': [34.6202, 112.4545], '南阳': [32.9908, 112.5286], '许昌': [34.0355, 113.8523],
|
||
'周口': [33.6260, 114.6473], '新乡': [35.3036, 113.9269], '安阳': [36.0969, 114.3931], '开封': [34.7973, 114.3073],
|
||
'商丘': [34.4140, 115.6554], '信阳': [32.1471, 114.0932], '驻马店': [32.9815, 114.0220], '平顶山': [33.7662, 113.1924],
|
||
'焦作': [35.2159, 113.2418], '濮阳': [35.7624, 115.0297], '三门峡': [34.7731, 111.1999], '漯河': [33.5814, 114.0165],
|
||
'鹤壁': [35.8995, 114.2975],
|
||
'石家庄': [38.0423, 114.5145], '唐山': [39.6303, 118.1824], '保定': [38.8739, 115.4646], '邯郸': [36.6256, 114.5391],
|
||
'沧州': [38.3044, 116.8387], '廊坊': [39.5379, 116.6838], '秦皇岛': [39.8891, 119.5997], '张家口': [40.7680, 114.8862],
|
||
'衡水': [37.7389, 115.6707], '邢台': [37.0736, 114.5047], '承德': [40.9495, 117.9593],
|
||
'合肥': [31.8206, 117.2272], '芜湖': [31.3525, 118.4329], '安庆': [30.5429, 117.0635], '阜阳': [32.8901, 115.8141],
|
||
'滁州': [32.3019, 118.3071], '马鞍山': [31.6714, 118.5071], '蚌埠': [32.9188, 117.3894], '宿州': [33.6476, 116.9639],
|
||
'六安': [31.7331, 116.5199], '宣城': [30.9404, 118.7587], '铜陵': [30.8789, 117.8107], '池州': [30.6648, 117.4916],
|
||
'淮南': [32.6261, 116.9997], '淮北': [33.9653, 116.7985], '黄山': [29.7153, 118.3370],
|
||
'南昌': [28.6829, 115.8582], '赣州': [25.8310, 114.9356], '九江': [29.6619, 115.9973], '宜春': [27.8143, 114.4161],
|
||
'吉安': [27.1130, 114.9934], '上饶': [28.4546, 117.9435], '抚州': [27.9479, 116.3581], '萍乡': [27.6229, 113.8548],
|
||
'景德镇': [29.2686, 117.1784], '新余': [27.8178, 114.9171], '鹰潭': [28.2588, 117.0692],
|
||
'西安': [34.3416, 108.9398], '咸阳': [34.3293, 108.7092], '宝鸡': [34.3621, 107.2373], '渭南': [34.5205, 109.5103],
|
||
'汉中': [33.0677, 107.0236], '延安': [36.5854, 109.4888], '榆林': [38.2855, 109.7345], '安康': [32.6849, 109.0291],
|
||
'商洛': [33.8683, 109.9202], '铜川': [34.8970, 108.9451],
|
||
'太原': [37.8706, 112.5499], '大同': [40.0769, 113.3001], '长治': [36.1954, 113.1164], '临汾': [36.0880, 111.5193],
|
||
'晋中': [37.6875, 112.7528], '运城': [35.0265, 111.0070], '晋城': [35.4905, 112.8513], '吕梁': [37.5193, 111.1446],
|
||
'忻州': [38.4170, 112.7340], '阳泉': [37.8568, 113.5803], '朔州': [39.3315, 112.4327],
|
||
'沈阳': [41.8057, 123.4315], '大连': [38.9140, 121.6148], '鞍山': [41.1078, 122.9945], '抚顺': [41.8796, 123.9572],
|
||
'锦州': [41.0952, 121.1270], '营口': [40.6241, 122.2351], '盘锦': [41.1198, 122.0708], '丹东': [40.0024, 124.3567],
|
||
'本溪': [41.4870, 123.7716], '辽阳': [41.2691, 123.2378], '朝阳': [41.5737, 120.4508], '阜新': [42.0210, 121.6703],
|
||
'葫芦岛': [40.7109, 120.8372], '铁岭': [42.2860, 123.8426],
|
||
'长春': [43.8960, 125.3265], '吉林': [43.8379, 126.5493], '延边': [42.8907, 129.5094], '四平': [43.1639, 124.3504],
|
||
'通化': [41.7278, 125.9395], '松原': [45.1410, 124.8255], '白城': [45.6193, 122.8389], '辽源': [42.8877, 125.1437],
|
||
'白山': [41.9406, 126.4244],
|
||
'哈尔滨': [45.8038, 126.5350], '大庆': [46.5967, 125.1041], '齐齐哈尔': [47.3543, 123.9180], '牡丹江': [44.5528, 129.6186],
|
||
'佳木斯': [46.7998, 130.3194], '绥化': [46.6368, 126.9692], '鸡西': [45.2953, 130.9661], '鹤岗': [47.3499, 130.2978],
|
||
'双鸭山': [46.6467, 131.1585], '伊春': [47.7277, 128.8404], '七台河': [45.7708, 131.0153], '黑河': [50.2454, 127.5284],
|
||
'大兴安岭': [50.5506, 126.2050],
|
||
'南宁': [22.8170, 108.3665], '桂林': [25.2344, 110.1798], '柳州': [24.3249, 109.4280], '北海': [21.4679, 109.1197],
|
||
'玉林': [22.6529, 110.1411], '百色': [23.9020, 106.6184], '梧州': [23.4769, 111.2786], '贵港': [23.1131, 109.5980],
|
||
'钦州': [21.9799, 108.6541], '河池': [24.6929, 108.0854], '防城港': [21.6869, 108.3783], '崇左': [22.3795, 107.3652],
|
||
'来宾': [23.7332, 109.2213], '贺州': [24.4035, 111.5667],
|
||
'呼和浩特': [40.8422, 111.7498], '包头': [40.6571, 109.8402], '赤峰': [42.2576, 118.8888], '鄂尔多斯': [39.6083, 109.7809],
|
||
'通辽': [43.6530, 122.2445], '呼伦贝尔': [49.2000, 119.7600], '乌兰察布': [40.9935, 113.1310], '巴彦淖尔': [40.7274, 107.3862],
|
||
'兴安': [46.0820, 122.0380], '锡林郭勒': [43.9335, 116.0480], '阿拉善': [38.8483, 105.6878],
|
||
'拉萨': [29.6500, 91.1000], '日喀则': [29.2670, 88.8810], '昌都': [31.1400, 97.1700], '林芝': [29.6500, 94.3600],
|
||
'山南': [29.2300, 91.7700], '那曲': [31.4700, 92.0600], '阿里': [30.4000, 81.1400],
|
||
'乌鲁木齐': [43.8256, 87.6168], '克拉玛依': [45.5750, 84.8820], '吐鲁番': [42.9400, 89.1900], '哈密': [42.8200, 93.5100],
|
||
'昌吉': [44.0130, 87.3050], '伊犁': [43.9200, 81.3200], '喀什': [39.4700, 75.9900], '阿克苏': [41.1700, 80.2600],
|
||
'和田': [37.1100, 79.9200], '塔城': [46.7500, 82.9800], '阿勒泰': [47.8700, 88.1400],
|
||
'博尔塔拉': [44.9000, 82.0700], '巴音郭楞': [41.7700, 86.1500], '克孜勒苏': [39.7100, 76.1700],
|
||
'银川': [38.4710, 106.2590], '石嘴山': [38.9860, 106.3850], '吴忠': [37.9990, 106.2010], '固原': [36.0170, 106.2860],
|
||
'中卫': [37.5170, 105.1960],
|
||
'西宁': [36.6230, 101.7800], '海东': [36.5020, 102.1040], '海西': [37.3730, 97.3710],
|
||
'海南': [36.2800, 100.6200], '海北': [36.9600, 100.9000], '黄南': [35.5200, 102.0200], '玉树': [33.0000, 97.0100],
|
||
'果洛': [34.4800, 100.2400],
|
||
'兰州': [36.0611, 103.8343], '天水': [34.5811, 105.7250], '白银': [36.5447, 104.1382], '金昌': [38.5210, 102.1880],
|
||
'嘉峪关': [39.7730, 98.2900], '武威': [37.9277, 102.6379], '张掖': [38.9349, 100.4599], '平凉': [35.5434, 106.6682],
|
||
'酒泉': [39.7368, 98.4940], '庆阳': [35.7100, 107.6440], '定西': [35.5800, 104.6250], '陇南': [33.3700, 104.9200],
|
||
'临夏': [35.6000, 103.2100], '甘南': [34.9900, 102.9100],
|
||
'贵阳': [26.6470, 106.6302], '遵义': [27.7254, 106.9270], '六盘水': [26.5927, 104.8300], '安顺': [26.2537, 105.9462],
|
||
'毕节': [27.3020, 105.2900], '铜仁': [27.7300, 109.1800],
|
||
'黔东南': [26.5800, 107.9700], '黔南': [26.2600, 107.5200], '黔西南': [25.0900, 104.9000],
|
||
'昆明': [25.0389, 102.7183], '大理': [25.6065, 100.2280], '丽江': [26.8720, 100.2300], '曲靖': [25.4900, 103.8000],
|
||
'玉溪': [24.3520, 102.5470], '红河': [23.3700, 103.3800], '楚雄': [25.0400, 101.5400], '普洱': [22.8300, 100.9700],
|
||
'昭通': [27.3400, 103.7200], '文山': [23.3800, 104.2400], '西双版纳': [22.0100, 100.8000],
|
||
'保山': [25.1200, 99.1700], '德宏': [24.4400, 98.5900], '临沧': [23.8900, 100.0900], '怒江': [25.8500, 98.8600],
|
||
'迪庆': [27.8200, 99.7100],
|
||
'海口': [20.0440, 110.3500], '三亚': [18.2540, 109.5020], '儋州': [19.5210, 109.5810], '三沙': [16.8300, 112.3400],
|
||
'琼海': [19.2590, 110.4660], '文昌': [19.5440, 110.7980], '万宁': [18.7970, 110.3890], '陵水': [18.5060, 110.0370],
|
||
'五指山': [18.7750, 109.5170],
|
||
}
|
||
|
||
const radiusOptions = [
|
||
{ value: 1000, label: '1km' },
|
||
{ value: 3000, label: '3km' },
|
||
{ value: 5000, label: '5km' },
|
||
{ value: 10000, label: '10km' },
|
||
{ value: 20000, label: '20km' },
|
||
{ value: 50000, label: '50km' },
|
||
]
|
||
|
||
// 选中城市时地图跳转
|
||
watch(city, (val, oldVal) => {
|
||
if (!val || val === oldVal || !mapRef.value) return
|
||
const coord = cityCoords[val]
|
||
if (!coord) return
|
||
centerLat.value = 0
|
||
centerLng.value = 0
|
||
mapRef.value.clearRadiusCircle()
|
||
})
|
||
|
||
// 范围变更时只重绘圆圈大小,不改变地图视角
|
||
watch(radius, (val) => {
|
||
if (centerLng.value && centerLat.value && mapRef.value) {
|
||
mapRef.value.updateRadiusCircle(val)
|
||
}
|
||
})
|
||
|
||
async function fetchSuggestions(kw) {
|
||
if (!kw.trim()) {
|
||
suggestions.value = []
|
||
return
|
||
}
|
||
try {
|
||
const data = await suggestPoi(kw, city.value)
|
||
suggestions.value = (data.tips || []).slice(0, 8)
|
||
suggestIndex.value = -1
|
||
} catch {
|
||
suggestions.value = []
|
||
}
|
||
}
|
||
|
||
function onKeywordInput() {
|
||
clearTimeout(suggestTimer)
|
||
suggestTimer = setTimeout(() => fetchSuggestions(keywords.value), 200)
|
||
}
|
||
|
||
function hideSuggestions() {
|
||
suggestions.value = []
|
||
suggestIndex.value = -1
|
||
}
|
||
|
||
function onSuggestBlur() {
|
||
setTimeout(hideSuggestions, 150)
|
||
}
|
||
|
||
function onSuggestArrow(dir) {
|
||
const len = suggestions.value.length
|
||
if (len === 0) return
|
||
suggestIndex.value = (suggestIndex.value + dir + len) % len
|
||
}
|
||
|
||
function selectSuggestion(item) {
|
||
keywords.value = item.name
|
||
hideSuggestions()
|
||
doSearch()
|
||
}
|
||
|
||
async function doSearch() {
|
||
error.value = ''
|
||
|
||
// 关键词和坐标必须至少有一个
|
||
const kw = keywords.value.trim()
|
||
const hasCoord = centerLng.value && centerLat.value
|
||
if (!kw && !hasCoord) {
|
||
error.value = '请输入关键词或点击地图选择搜索范围'
|
||
searched.value = true
|
||
return
|
||
}
|
||
|
||
loading.value = true
|
||
searched.value = true
|
||
activePoi.value = null
|
||
try {
|
||
const data = await searchPoi(
|
||
kw,
|
||
city.value,
|
||
page.value,
|
||
centerLng.value,
|
||
centerLat.value,
|
||
radius.value,
|
||
)
|
||
results.value = data.list || []
|
||
total.value = data.total || 0
|
||
maxPage.value = data.maxPage || 1
|
||
page.value = data.page || 1
|
||
|
||
// 在地图上标注
|
||
if (mapRef.value && results.value.length > 0) {
|
||
mapRef.value.addMarkers(results.value)
|
||
}
|
||
} catch (e) {
|
||
error.value = e.response?.data?.message || e.message || '请求失败'
|
||
results.value = []
|
||
total.value = 0
|
||
maxPage.value = 1
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function focusItem(item) {
|
||
activePoi.value = item
|
||
if (mapRef.value) {
|
||
mapRef.value.focusMarker(item)
|
||
}
|
||
}
|
||
|
||
function onMarkerClick(poi) {
|
||
activePoi.value = poi
|
||
}
|
||
|
||
function onMapClick({ lat, lng }) {
|
||
centerLat.value = lat
|
||
centerLng.value = lng
|
||
// 切换为周边搜索时清除城市条件
|
||
province.value = ''
|
||
city.value = ''
|
||
// 地图上画圆圈
|
||
if (mapRef.value) {
|
||
mapRef.value.drawRadiusCircle(lat, lng, radius.value)
|
||
}
|
||
}
|
||
|
||
function clearCenter() {
|
||
centerLat.value = 0
|
||
centerLng.value = 0
|
||
if (mapRef.value) {
|
||
mapRef.value.clearRadiusCircle()
|
||
}
|
||
}
|
||
|
||
function provinceChanged() {
|
||
city.value = ''
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
/* === Global reset === */
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html, body, #app {
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
color: #333;
|
||
}
|
||
|
||
/* === App layout === */
|
||
.app {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
/* === Top search bar === */
|
||
.top-bar {
|
||
position: absolute;
|
||
top: 16px;
|
||
left: 16px;
|
||
right: 380px;
|
||
z-index: 1000;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.search-form {
|
||
display: flex;
|
||
gap: 10px;
|
||
background: #fff;
|
||
padding: 12px 16px;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
|
||
pointer-events: auto;
|
||
max-width: 700px;
|
||
position: relative;
|
||
}
|
||
|
||
.input {
|
||
flex: 1;
|
||
padding: 9px 14px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.input:focus {
|
||
border-color: #1a73e8;
|
||
}
|
||
|
||
.input-wrap {
|
||
position: relative;
|
||
flex: 1;
|
||
}
|
||
|
||
.input-wrap .input {
|
||
width: 100%;
|
||
}
|
||
|
||
.suggest-dropdown {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: -16px;
|
||
right: -16px;
|
||
margin: 4px 0 0;
|
||
padding: 4px 0;
|
||
list-style: none;
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 0 0 10px 10px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||
z-index: 2000;
|
||
max-height: 320px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.suggest-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 14px;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: background 0.1s;
|
||
}
|
||
|
||
.suggest-item.active,
|
||
.suggest-item:hover {
|
||
background: #f0f5ff;
|
||
}
|
||
|
||
.suggest-name {
|
||
color: #333;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.suggest-city {
|
||
flex-shrink: 0;
|
||
margin-left: 8px;
|
||
font-size: 11px;
|
||
color: #999;
|
||
}
|
||
|
||
.select {
|
||
padding: 9px 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
outline: none;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.select:focus {
|
||
border-color: #1a73e8;
|
||
}
|
||
|
||
.province-select {
|
||
max-width: 110px;
|
||
}
|
||
|
||
.city-select {
|
||
max-width: 110px;
|
||
}
|
||
|
||
.radius-select {
|
||
max-width: 90px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 9px 24px;
|
||
background: #1a73e8;
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.btn:hover:not(:disabled) {
|
||
background: #1557b0;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.center-badge {
|
||
margin-top: 8px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
padding: 5px 12px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
color: #1a73e8;
|
||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.clear-center {
|
||
background: none;
|
||
border: none;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
color: #999;
|
||
line-height: 1;
|
||
}
|
||
|
||
.clear-center:hover {
|
||
color: #dc2626;
|
||
}
|
||
|
||
/* === Side panel === */
|
||
.side-panel {
|
||
position: absolute;
|
||
top: 80px;
|
||
right: 16px;
|
||
width: 350px;
|
||
max-height: calc(100% - 100px);
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
||
z-index: 1000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 14px 16px 10px;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.panel-page {
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.panel-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
}
|
||
|
||
.result-card {
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.result-card:hover {
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.result-card.active {
|
||
background: #e8f0fe;
|
||
border-left: 3px solid #1a73e8;
|
||
}
|
||
|
||
.card-name {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.card-phone {
|
||
font-size: 15px;
|
||
color: #1a73e8;
|
||
font-weight: 600;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.card-addr {
|
||
font-size: 12px;
|
||
color: #888;
|
||
}
|
||
|
||
.panel-footer {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: center;
|
||
padding: 10px 16px 14px;
|
||
border-top: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.page-btn {
|
||
flex: 1;
|
||
padding: 7px 0;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.page-btn:hover:not(:disabled) {
|
||
border-color: #1a73e8;
|
||
color: #1a73e8;
|
||
}
|
||
|
||
.page-btn:disabled {
|
||
opacity: 0.35;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.panel-loading,
|
||
.panel-error,
|
||
.panel-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 48px 16px;
|
||
text-align: center;
|
||
color: #888;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.panel-error {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.panel-empty small {
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.spinner {
|
||
width: 22px;
|
||
height: 22px;
|
||
border: 3px solid #e5e7eb;
|
||
border-top-color: #1a73e8;
|
||
border-radius: 50%;
|
||
animation: spin 0.7s linear infinite;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* === Hint bar === */
|
||
.hint-bar {
|
||
position: absolute;
|
||
bottom: 24px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 1000;
|
||
background: rgba(0, 0, 0, 0.55);
|
||
color: #fff;
|
||
padding: 6px 18px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
white-space: nowrap;
|
||
}
|
||
</style>
|