1
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
# Go
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
# Node
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,5 @@
|
||||
package consts
|
||||
|
||||
const (
|
||||
TableNamePoiMerchant = "poi_merchant"
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"pointly-tel/amap/model/dto"
|
||||
"pointly-tel/amap/service"
|
||||
)
|
||||
|
||||
type poi struct{}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"pointly-tel/amap/consts"
|
||||
"pointly-tel/amap/model/entity"
|
||||
"pointly-tel/common"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var PoiMerchant = &poiMerchantDao{}
|
||||
|
||||
type poiMerchantDao struct{}
|
||||
|
||||
func init() {
|
||||
ctx := context.Background()
|
||||
_, err := g.DB().Exec(ctx, `CREATE TABLE IF NOT EXISTS `+consts.TableNamePoiMerchant+` (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
poi_id VARCHAR(64) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
address VARCHAR(500) NOT NULL DEFAULT '',
|
||||
phone VARCHAR(100) NOT NULL DEFAULT '',
|
||||
category VARCHAR(255) NOT NULL DEFAULT '',
|
||||
category_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
city VARCHAR(64) NOT NULL DEFAULT '',
|
||||
city_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
longitude REAL DEFAULT 0,
|
||||
latitude REAL DEFAULT 0,
|
||||
business_area VARCHAR(255) NOT NULL DEFAULT '',
|
||||
website VARCHAR(500) NOT NULL DEFAULT '',
|
||||
raw_data TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
updated_at DATETIME DEFAULT (datetime('now','localtime')),
|
||||
deleted_at DATETIME
|
||||
)`)
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "create poi_merchant table failed: %v", err)
|
||||
}
|
||||
_, err = g.DB().Exec(ctx, "CREATE INDEX IF NOT EXISTS idx_poi_merchant_poi_id ON "+consts.TableNamePoiMerchant+"(poi_id)")
|
||||
if err != nil {
|
||||
g.Log().Warningf(ctx, "create poi_merchant index failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *poiMerchantDao) Insert(ctx context.Context, data *entity.PoiMerchant) (int64, error) {
|
||||
return common.InsertAndReturnId(ctx, consts.TableNamePoiMerchant, data)
|
||||
}
|
||||
|
||||
func (d *poiMerchantDao) GetByPoiId(ctx context.Context, poiId string) (*entity.PoiMerchant, error) {
|
||||
var data *entity.PoiMerchant
|
||||
err := g.DB().Model(consts.TableNamePoiMerchant).Ctx(ctx).Where("poi_id", poiId).Scan(&data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (d *poiMerchantDao) GetListByKeywords(ctx context.Context, keywords string) ([]*entity.PoiMerchant, error) {
|
||||
var list []*entity.PoiMerchant
|
||||
err := g.DB().Model(consts.TableNamePoiMerchant).Ctx(ctx).
|
||||
Where("name LIKE ?", "%"+keywords+"%").
|
||||
Limit(100).
|
||||
Scan(&list)
|
||||
return list, err
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package dto
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
type SearchPoiReq struct {
|
||||
g.Meta `path:"/search" method:"get" tags:"POI检索" summary:"POI关键词搜索"`
|
||||
Keywords string `v:"required" json:"keywords" dc:"搜索关键词"`
|
||||
City string `json:"city" dc:"城市名称或代码"`
|
||||
Offset int `d:"20" json:"offset" dc:"每页记录数"`
|
||||
Page int `d:"1" json:"page" dc:"页码"`
|
||||
}
|
||||
|
||||
type PoiItem struct {
|
||||
PoiId string `json:"poiId" dc:"POI ID"`
|
||||
Name string `json:"name" dc:"商户名称"`
|
||||
Address string `json:"address" dc:"地址"`
|
||||
Phone string `json:"phone" dc:"联系电话"`
|
||||
Category string `json:"category" dc:"分类"`
|
||||
City string `json:"city" dc:"城市"`
|
||||
Longitude string `json:"longitude" dc:"经度"`
|
||||
Latitude string `json:"latitude" dc:"纬度"`
|
||||
BusinessArea string `json:"businessArea" dc:"商圈"`
|
||||
Website string `json:"website" dc:"网址"`
|
||||
}
|
||||
|
||||
type SearchPoiRes struct {
|
||||
List []*PoiItem `json:"list" dc:"POI列表"`
|
||||
Total int `json:"total" dc:"匹配总数"`
|
||||
Count int `json:"count" dc:"当前返回数量"`
|
||||
Page int `json:"page" dc:"当前页码"`
|
||||
Offset int `json:"offset" dc:"每页数量"`
|
||||
MaxPage int `json:"maxPage" dc:"最大可查询页码(高德限制100页)"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type PoiMerchant struct {
|
||||
Id int64 `orm:"id" json:"id" dc:"主键ID"`
|
||||
PoiId string `orm:"poi_id" json:"poiId" dc:"高德POI ID"`
|
||||
Name string `orm:"name" json:"name" dc:"商户名称"`
|
||||
Address string `orm:"address" json:"address" dc:"地址"`
|
||||
Phone string `orm:"phone" json:"phone" dc:"联系电话"`
|
||||
Category string `orm:"category" json:"category" dc:"分类"`
|
||||
CategoryCode string `orm:"category_code" json:"categoryCode" dc:"分类代码"`
|
||||
City string `orm:"city" json:"city" dc:"城市"`
|
||||
CityCode string `orm:"city_code" json:"cityCode" dc:"城市代码"`
|
||||
Longitude float64 `orm:"longitude" json:"longitude" dc:"经度"`
|
||||
Latitude float64 `orm:"latitude" json:"latitude" dc:"纬度"`
|
||||
BusinessArea string `orm:"business_area" json:"businessArea" dc:"商圈"`
|
||||
Website string `orm:"website" json:"website" dc:"网址"`
|
||||
RawData string `orm:"raw_data" json:"rawData" dc:"原始数据"`
|
||||
CreatedAt *gtime.Time `orm:"created_at" json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `orm:"updated_at" json:"updatedAt" dc:"更新时间"`
|
||||
DeletedAt *gtime.Time `orm:"deleted_at" json:"deletedAt" dc:"删除时间"`
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"pointly-tel/amap/dao"
|
||||
"pointly-tel/amap/model/dto"
|
||||
"pointly-tel/amap/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type poiService struct{}
|
||||
|
||||
var PoiService = new(poiService)
|
||||
|
||||
// amapString 处理高德API中字段有时返回string有时返回[]的情况
|
||||
type amapString string
|
||||
|
||||
func (s *amapString) UnmarshalJSON(data []byte) error {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
*s = amapString(str)
|
||||
return nil
|
||||
}
|
||||
*s = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// Amap API 响应结构
|
||||
type amapResponse struct {
|
||||
Status string `json:"status"`
|
||||
Info string `json:"info"`
|
||||
Infocode string `json:"infocode"`
|
||||
Count string `json:"count"`
|
||||
Pois []*amapPoi `json:"pois"`
|
||||
}
|
||||
|
||||
type amapPoi struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Typecode string `json:"typecode"`
|
||||
Address string `json:"address"`
|
||||
Tel amapString `json:"tel"`
|
||||
Location string `json:"location"`
|
||||
BusinessArea amapString `json:"business_area"`
|
||||
City amapString `json:"city"`
|
||||
CityCode amapString `json:"citycode"`
|
||||
Website amapString `json:"website"`
|
||||
}
|
||||
|
||||
func (s *poiService) Search(ctx context.Context, req *dto.SearchPoiReq) (res *dto.SearchPoiRes, err 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(config.yml 中的 amap.api_key)")
|
||||
}
|
||||
|
||||
apiUrlVar, _ := g.Cfg().Get(ctx, "amap.api_url")
|
||||
baseUrl := apiUrlVar.String()
|
||||
if baseUrl == "" {
|
||||
baseUrl = "https://restapi.amap.com/v3/place/text"
|
||||
}
|
||||
|
||||
// 高德POI分页限制:offset 最大 25,page 最大 100
|
||||
const (
|
||||
maxOffset = 25
|
||||
maxPage = 100
|
||||
)
|
||||
offset := req.Offset
|
||||
if offset < 1 {
|
||||
offset = 1
|
||||
} else if offset > maxOffset {
|
||||
offset = maxOffset
|
||||
}
|
||||
page := req.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
} else if page > maxPage {
|
||||
page = maxPage
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("key", apiKeyVar.String())
|
||||
params.Set("keywords", req.Keywords)
|
||||
params.Set("offset", gconv.String(offset))
|
||||
params.Set("page", gconv.String(page))
|
||||
params.Set("extensions", "all")
|
||||
if req.City != "" {
|
||||
params.Set("city", req.City)
|
||||
}
|
||||
|
||||
requestUrl := baseUrl + "?" + params.Encode()
|
||||
g.Log().Info(ctx, "调用高德POI搜索", "url", requestUrl)
|
||||
|
||||
response, err := g.Client().Get(ctx, requestUrl)
|
||||
if err != nil {
|
||||
return nil, gerror.Newf("高德API请求失败: %v", err)
|
||||
}
|
||||
defer response.Close()
|
||||
|
||||
var amapResp amapResponse
|
||||
if err := json.Unmarshal(response.ReadAll(), &amapResp); err != nil {
|
||||
return nil, gerror.Newf("响应解析失败: %v", err)
|
||||
}
|
||||
|
||||
if amapResp.Status != "1" {
|
||||
return nil, gerror.Newf("高德API返回错误: %s", amapResp.Info)
|
||||
}
|
||||
|
||||
total := gconv.Int(amapResp.Count)
|
||||
items := make([]*dto.PoiItem, 0, len(amapResp.Pois))
|
||||
|
||||
for _, p := range amapResp.Pois {
|
||||
lng, lat := parseLocation(p.Location)
|
||||
item := &dto.PoiItem{
|
||||
PoiId: p.Id,
|
||||
Name: p.Name,
|
||||
Address: p.Address,
|
||||
Phone: string(p.Tel),
|
||||
Category: p.Type,
|
||||
City: string(p.City),
|
||||
Longitude: gconv.String(lng),
|
||||
Latitude: gconv.String(lat),
|
||||
BusinessArea: string(p.BusinessArea),
|
||||
Website: string(p.Website),
|
||||
}
|
||||
items = append(items, item)
|
||||
|
||||
go s.cachePoi(ctx, p)
|
||||
}
|
||||
|
||||
// 计算最大可查页数(高德限制最多查 100 页)
|
||||
maxPageCalculated := maxPage
|
||||
if total > 0 {
|
||||
if maxPageCalculated*offset > total {
|
||||
maxPageCalculated = (total + offset - 1) / offset
|
||||
}
|
||||
}
|
||||
|
||||
return &dto.SearchPoiRes{
|
||||
List: items,
|
||||
Total: total,
|
||||
Count: len(items),
|
||||
Page: page,
|
||||
Offset: offset,
|
||||
MaxPage: maxPageCalculated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// cachePoi 将POI结果异步缓存到数据库
|
||||
func (s *poiService) cachePoi(ctx context.Context, p *amapPoi) {
|
||||
existing, _ := dao.PoiMerchant.GetByPoiId(ctx, p.Id)
|
||||
if existing != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lng, lat := parseLocation(p.Location)
|
||||
rawBytes, _ := json.Marshal(p)
|
||||
|
||||
_, err := dao.PoiMerchant.Insert(ctx, &entity.PoiMerchant{
|
||||
PoiId: p.Id,
|
||||
Name: p.Name,
|
||||
Address: p.Address,
|
||||
Phone: string(p.Tel),
|
||||
Category: p.Type,
|
||||
CategoryCode: p.Typecode,
|
||||
City: string(p.City),
|
||||
CityCode: string(p.CityCode),
|
||||
Longitude: lng,
|
||||
Latitude: lat,
|
||||
BusinessArea: string(p.BusinessArea),
|
||||
Website: string(p.Website),
|
||||
RawData: string(rawBytes),
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Warning(ctx, "缓存POI数据失败", "poiId", p.Id, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseLocation(location string) (float64, float64) {
|
||||
if location == "" {
|
||||
return 0, 0
|
||||
}
|
||||
parts := strings.Split(location, ",")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0
|
||||
}
|
||||
return gconv.Float64(parts[0]), gconv.Float64(parts[1])
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func prepareInsertData(data any) map[string]any {
|
||||
m := gconv.Map(data, gconv.MapOption{Tags: []string{"orm"}})
|
||||
delete(m, "id")
|
||||
m["created_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
m["updated_at"] = gtime.Now().Format("Y-m-d H:i:s")
|
||||
delete(m, "deleted_at")
|
||||
return m
|
||||
}
|
||||
|
||||
func InsertAndReturnId(ctx context.Context, table string, data any) (id int64, err error) {
|
||||
m := prepareInsertData(data)
|
||||
r, err := g.DB().Model(table).Ctx(ctx).Data(m).Insert()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if r == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return r.LastInsertId()
|
||||
}
|
||||
|
||||
func GetOneByPk[T any](ctx context.Context, table string, pk int64) (res *T, err error) {
|
||||
r, err := g.DB().Model(table).Ctx(ctx).
|
||||
Where("id", pk).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r == nil {
|
||||
return nil, nil
|
||||
}
|
||||
err = r.Struct(&res)
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateByPk(ctx context.Context, table string, pk int64, data any) error {
|
||||
_, err := g.DB().Model(table).Ctx(ctx).Data(data).Where("id", pk).Update()
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteByPk(ctx context.Context, table string, pk int64) error {
|
||||
_, err := g.DB().Model(table).Ctx(ctx).Unscoped().Where("id", pk).Delete()
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
var Httpserver = g.Server()
|
||||
|
||||
func init() {
|
||||
err := gtime.SetTimeZone("Asia/Shanghai")
|
||||
if err != nil {
|
||||
panic("设置时区失败")
|
||||
}
|
||||
Httpserver.SetOpenApiPath("/api.json")
|
||||
Httpserver.BindMiddlewareDefault(ghttp.MiddlewareHandlerResponse)
|
||||
Httpserver.BindMiddlewareDefault(func(r *ghttp.Request) {
|
||||
r.Response.CORS(r.Response.DefaultCORSOptions())
|
||||
r.Middleware.Next()
|
||||
})
|
||||
}
|
||||
|
||||
// RouteRegister 根据控制器结构体名称自动注册路由
|
||||
func RouteRegister(controllers []interface{}) {
|
||||
re := regexp.MustCompile("[A-Z]")
|
||||
for _, t := range controllers {
|
||||
sName := reflect.ValueOf(t).Elem().Type().Name()
|
||||
convertedStr := re.ReplaceAllStringFunc(sName, func(s string) string {
|
||||
return fmt.Sprintf("-%s", strings.ToLower(s))
|
||||
})
|
||||
if len(convertedStr) > 0 && convertedStr[0] == '-' {
|
||||
convertedStr = convertedStr[1:]
|
||||
}
|
||||
Httpserver.Group("/"+convertedStr, func(group *ghttp.RouterGroup) {
|
||||
group.Bind(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ServeFrontend 配置前端静态文件服务
|
||||
func ServeFrontend(distDir string) {
|
||||
absDir, err := filepath.Abs(distDir)
|
||||
if err != nil {
|
||||
g.Log().Fatalf(nil, "前端目录路径无效: %v", err)
|
||||
}
|
||||
|
||||
// 静态资源(JS/CSS/图片)
|
||||
Httpserver.AddStaticPath("/assets", absDir+"/assets")
|
||||
|
||||
// SPA 兜底:非API路由返回 index.html
|
||||
Httpserver.BindHandler("/*", func(r *ghttp.Request) {
|
||||
indexFile := filepath.Join(absDir, "index.html")
|
||||
if fileExists(indexFile) {
|
||||
r.Response.ServeFile(indexFile)
|
||||
} else {
|
||||
r.Response.WriteStatus(404)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
database:
|
||||
default:
|
||||
name: pointly_tel.db
|
||||
type: sqlite
|
||||
debug: false
|
||||
server:
|
||||
address: :3000
|
||||
name: pointly-tel
|
||||
amap:
|
||||
api_key: "884d33aef9842e60b840e2b2b6aedce9"
|
||||
api_url: "https://restapi.amap.com/v3/place/text"
|
||||
@@ -0,0 +1,45 @@
|
||||
module pointly-tel
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2
|
||||
github.com/gogf/gf/v2 v2.10.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2 h1:KLS68SWS2W749x7e+eCCOO3UD2Sbw+bIbLEPR8o1FXw=
|
||||
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.2/go.mod h1:uLcsu73PfpyhRc0Jq0gGAWQjN1tyGU9iBRrYgt/lu7g=
|
||||
github.com/gogf/gf/v2 v2.10.2 h1:46IO0Uc8e85/FqdftJFskfDejJLBL0JBnGS5qOftUu8=
|
||||
github.com/gogf/gf/v2 v2.10.2/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"pointly-tel/amap/controller"
|
||||
commonHttp "pointly-tel/common"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 注册 API 路由
|
||||
commonHttp.RouteRegister([]interface{}{
|
||||
controller.Poi,
|
||||
})
|
||||
|
||||
// 前端静态文件服务(需先执行 cd web && npm run build)
|
||||
commonHttp.ServeFrontend("web/dist")
|
||||
|
||||
// 启动 HTTP 服务(监听 config.yml 中 server.address,默认 :3000)
|
||||
commonHttp.Httpserver.Run()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pointly-Tel - 高德POI商户查询</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1693
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "pointly-tel-web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.0",
|
||||
"axios": "^1.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.0",
|
||||
"vite": "^6.3.0"
|
||||
}
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<header class="header">
|
||||
<h1>Pointly-Tel</h1>
|
||||
<p class="subtitle">高德地图 POI 商户信息查询</p>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<div class="search-box">
|
||||
<div class="form-row">
|
||||
<input
|
||||
v-model="keywords"
|
||||
class="input"
|
||||
placeholder="输入关键词(如:火锅店 咖啡厅)"
|
||||
@keyup.enter="doSearch"
|
||||
/>
|
||||
<input
|
||||
v-model="city"
|
||||
class="input city-input"
|
||||
placeholder="城市(可选)"
|
||||
@keyup.enter="doSearch"
|
||||
/>
|
||||
<button class="btn" :disabled="loading" @click="doSearch">
|
||||
{{ loading ? '搜索中...' : '搜索' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
<div class="spinner"></div>
|
||||
<span>正在查询...</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && searched && results.length === 0" class="empty">
|
||||
未找到相关商户,请尝试其他关键词
|
||||
</div>
|
||||
|
||||
<div v-if="results.length > 0" class="results">
|
||||
<div class="result-meta">共 {{ total }} 条结果</div>
|
||||
<div v-for="item in results" :key="item.poiId" class="card">
|
||||
<div class="card-header">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<span v-if="item.category" class="tag">{{ item.category.split('|').pop() }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row">
|
||||
<span class="label">电话</span>
|
||||
<a v-if="item.phone" :href="'tel:' + item.phone" class="phone">{{ item.phone }}</a>
|
||||
<span v-else class="na">暂无</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">地址</span>
|
||||
<span>{{ item.address || '暂无' }}</span>
|
||||
</div>
|
||||
<div class="info-row" v-if="item.businessArea">
|
||||
<span class="label">商圈</span>
|
||||
<span>{{ item.businessArea }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="total > results.length" class="pagination">
|
||||
<button class="btn-outline" :disabled="page <= 1" @click="page--; doSearch()">上一页</button>
|
||||
<span class="page-info">第 {{ page }} / {{ maxPage }} 页(共 {{ total }} 条)</span>
|
||||
<button class="btn-outline" :disabled="page >= maxPage" @click="page++; doSearch()">下一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { searchPoi } from './api/poi.js'
|
||||
|
||||
const keywords = ref('')
|
||||
const city = ref('')
|
||||
const results = ref([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const searched = ref(false)
|
||||
const error = ref('')
|
||||
const page = ref(1)
|
||||
const offset = ref(20)
|
||||
const maxPage = ref(1)
|
||||
|
||||
async function doSearch() {
|
||||
if (!keywords.value.trim()) {
|
||||
error.value = '请输入搜索关键词'
|
||||
return
|
||||
}
|
||||
error.value = ''
|
||||
loading.value = true
|
||||
searched.value = true
|
||||
try {
|
||||
const data = await searchPoi(keywords.value.trim(), city.value.trim(), page.value, offset.value)
|
||||
results.value = data.list || []
|
||||
total.value = data.total || 0
|
||||
maxPage.value = data.maxPage || 1
|
||||
page.value = data.page || 1
|
||||
offset.value = data.offset || 20
|
||||
} catch (e) {
|
||||
error.value = e.response?.data?.message || e.message || '请求失败'
|
||||
results.value = []
|
||||
total.value = 0
|
||||
maxPage.value = 1
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #1a73e8;
|
||||
}
|
||||
|
||||
.city-input {
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 28px;
|
||||
background: #1a73e8;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover:not(:disabled) {
|
||||
background: #1557b0;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 48px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid #e5e7eb;
|
||||
border-top-color: #1a73e8;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 48px 0;
|
||||
color: #999;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.results {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 12px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 16px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 12px;
|
||||
background: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
min-width: 40px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.phone {
|
||||
color: #1a73e8;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.phone:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.na {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
padding: 8px 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-outline:hover:not(:disabled) {
|
||||
border-color: #1a73e8;
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.btn-outline:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.DEV ? '' : '',
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
export async function searchPoi(keywords, city = '', page = 1, offset = 20) {
|
||||
const params = { keywords }
|
||||
if (city) params.city = city
|
||||
params.page = page
|
||||
params.offset = offset
|
||||
|
||||
const res = await api.get('/poi/search', { params })
|
||||
return res.data.data
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/poi': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user