This commit is contained in:
2026-07-30 15:05:56 +08:00
parent 1c5d50c339
commit f41ec31852
11 changed files with 269 additions and 49 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
/.idea/*
*.db
web/node_modules/
# web/dist/ 已提交至 git,由 Go 服务统一暴露
# web/dist/ 已提交至 git,由 Go 服务统一暴露
.gstack/
+4
View File
@@ -14,3 +14,7 @@ var Poi = new(poi)
func (c *poi) Search(ctx context.Context, req *dto.SearchPoiReq) (res *dto.SearchPoiRes, err error) {
return service.PoiService.Search(ctx, req)
}
func (c *poi) Suggest(ctx context.Context, req *dto.SuggestPoiReq) (res *dto.SuggestPoiRes, err error) {
return service.PoiService.Suggest(ctx, req)
}
+11
View File
@@ -25,6 +25,17 @@ type PoiItem struct {
Website string `json:"website" dc:"网址"`
}
type SuggestPoiReq struct {
g.Meta `path:"/suggest" method:"get" tags:"POI检索" summary:"关键词联想"`
Keywords string `json:"keywords" dc:"搜索关键词"`
City string `json:"city" dc:"城市名称"`
}
type SuggestPoiRes struct {
Keywords string `json:"keywords" dc:"联想关键词"`
Tips []*PoiItem `json:"tips" dc:"联想列表"`
}
type SearchPoiRes struct {
List []*PoiItem `json:"list" dc:"POI列表"`
Total int `json:"total" dc:"匹配总数"`
+65
View File
@@ -258,6 +258,71 @@ func (s *poiService) fallbackPhone(ctx context.Context, apiKey string, noPhone [
}
// fetchTextPage 文本搜索单页
// Suggest 高德输入提示(关键词联想)
func (s *poiService) Suggest(ctx context.Context, req *dto.SuggestPoiReq) (*dto.SuggestPoiRes, error) {
apiKeyVar, err := g.Cfg().Get(ctx, "amap.api_key")
if err != nil {
return nil, gerror.New("读取配置失败")
}
if apiKeyVar.IsEmpty() {
return nil, gerror.New("请先配置高德地图API Key")
}
apiKey := apiKeyVar.String()
params := url.Values{}
params.Set("key", apiKey)
params.Set("keywords", req.Keywords)
if req.City != "" {
params.Set("city", req.City)
}
params.Set("offset", "10")
params.Set("citylimit", "true")
requestUrl := "https://restapi.amap.com/v3/assistant/inputtips?" + params.Encode()
response, err := g.Client().Get(ctx, requestUrl)
if err != nil {
return nil, gerror.New("高德API请求失败")
}
defer response.Close()
bodyBytes := response.ReadAll()
var amapResp struct {
Status string `json:"status"`
Info string `json:"info"`
Tips []struct {
Name amapString `json:"name"`
Address amapString `json:"address"`
Location amapString `json:"location"`
City amapString `json:"city"`
} `json:"tips"`
}
if err := json.Unmarshal(bodyBytes, &amapResp); err != nil {
return nil, gerror.New("高德API响应解析失败")
}
if amapResp.Status != "1" {
return nil, gerror.New(amapResp.Info)
}
tips := make([]*dto.PoiItem, 0, len(amapResp.Tips))
for _, t := range amapResp.Tips {
lng, lat := parseLocation(string(t.Location))
tips = append(tips, &dto.PoiItem{
Name: string(t.Name),
Address: string(t.Address),
City: string(t.City),
Longitude: gconv.String(lng),
Latitude: gconv.String(lat),
})
}
return &dto.SuggestPoiRes{
Keywords: req.Keywords,
Tips: tips,
}, nil
}
func (s *poiService) fetchTextPage(ctx context.Context, apiKey, baseUrl, keywords, city string, page, offset int) ([]*amapPoi, int, error) {
pois, total := s.fetchAmapPage(ctx, apiKey, baseUrl, keywords, city, page, offset)
return pois, total, nil
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pointly-Tel - 高德POI商户查询</title>
<script type="module" crossorigin src="/assets/index-fNB4NbEb.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C6NseWna.css">
<script type="module" crossorigin src="/assets/index-BbrXGaLX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C3fajWoH.css">
</head>
<body>
<div id="app"></div>
+128 -7
View File
@@ -6,12 +6,34 @@
<!-- 顶部搜索栏 -->
<div class="top-bar">
<div class="search-form">
<input
v-model="keywords"
class="input"
placeholder="关键词(选填,如:火锅店)"
@keyup.enter="doSearch"
/>
<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>
@@ -87,7 +109,7 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { searchPoi } from './api/poi.js'
import { searchPoi, suggestPoi } from './api/poi.js'
import PoiMap from './components/PoiMap.vue'
const mapRef = ref(null)
@@ -104,6 +126,9 @@ 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 = [
@@ -278,6 +303,46 @@ watch(radius, (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 = ''
@@ -399,6 +464,7 @@ html, body, #app {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
pointer-events: auto;
max-width: 700px;
position: relative;
}
.input {
@@ -415,6 +481,61 @@ html, body, #app {
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;
+7
View File
@@ -15,3 +15,10 @@ export async function searchPoi(keywords, city = '', page = 1, longitude = 0, la
const res = await api.get('/poi/search', { params })
return res.data.data
}
export async function suggestPoi(keywords, city = '') {
const params = { keywords }
if (city) params.city = city
const res = await api.get('/poi/suggest', { params })
return res.data.data
}
+11
View File
@@ -150,6 +150,17 @@ defineExpose({ addMarkers, clearMarkers, focusMarker, drawRadiusCircle, clearRad
min-height: 400px;
}
/* 缩放控件左侧垂直居中 */
:deep(.leaflet-control-zoom) {
margin-top: 0 !important;
}
:deep(.leaflet-top.leaflet-left) {
top: 50%;
transform: translateY(-50%);
position: absolute;
z-index: 1000;
}
:deep(.map-popup h4) {
margin: 0 0 4px;
font-size: 14px;