ScenicAtlas(景区图集)是一个前后端分离的文旅可视化项目,把河南省 18 个地级市、864 个景区的数据呈现在一张可缩放、可下钻的交互地图上。
后端用 Flask 读取本地资源、做聚合/搜索/详情,并对外暴露 JSON API;前端用 Vue 3 + Leaflet 渲染地图、DataV-Vue3 提供大屏组件:景区与城市按真实经纬度定位,地图既可叠加卫星影像底图,也可切换成**矢量省界 + 按景区数分级填色(choropleth)**的数据视图;点击城市会缩放到该市边界并展开其景区点,右侧详情面板联动展示当前景区(5A 景区附实景大图,可点开查看灯箱大图)。
数据本身来自一条独立的离线管线(Data_processing/):Excel 原始表 → DeepSeek 富化(简介/标签/推荐理由/推荐分)→ 百度地理编码(经纬度)→ scenic.json。运行时后端不再依赖该管线,只读取产出的资源文件。
- 双底图模式:地图模式(卫星影像 WebP 铺底)/ 普通模式(矢量省界 + 按景区数分级填色 choropleth,悬停区域高亮并浮出数据气泡),右上角一键切换
- 真实地理定位:所有点位走经纬度,滚轮缩放、拖拽漫游;点击城市
fitBounds到该市轮廓,铺满视区 - 默认去噪:默认只显示 18 个城市气泡;点击某城市才展开该市景区点,避免 864 点糊屏
- 省会标识:郑州以金色 ★ 标注;默认镜头定位省会郑州
- 城市 ↔ 详情联动:点击城市 → 右侧详情切换到该市最高分景区(hero);切城市时镜头、详情、同城列表三者一致
- 选中态:当前选中景区珊瑚色高亮(地图/列表统一);选中城市为珊瑚环,缩放进城后隐藏只留城市名。普通模式下已选区域珊瑚色、悬停区域青色,两态颜色区分清晰
- 同城高分景点:该市景区按分数倒序全量展示(列表内滚动),当前景区高亮,选择景区时自动滚动跟随;切换城市时面板回到顶部
- 关键词搜索(景区 / 城市 / 标签),输入即检索并自动聚焦命中目标
- 等级筛选(5A / 4A / 3A / 2A),命中数为真实总数(后端不截断);筛选时隐藏 0 命中城市,结果中景区组优先于城市组展示;超长结果列表虚拟滚动
- 等级配色一一对应(城市 teal / 5A 金 / 4A 橙 / 3A 蓝 / 2A 灰 / 选中珊瑚),图例与地图一致
- 22 张 5A 景区图片:详情面板顶部大图 + 搜索结果/同城列表缩略图(仅有图的记录显示)
- 点击任意图查看灯箱大图(× / ESC / 点背景关闭)
- 统一规格(详见图片处理标准)
- DataV-Vue3 组件:三栏统一科技边框(
dv-border-box,覆盖层不挡交互)、指标数字翻牌(dv-digital-flop)、标题装饰线(dv-decoration) - 立体感(纯 CSS 滤镜):省界投影阴影、选中城市高光描边、卫星影像对比/饱和增强凸显地形起伏
- 地图氛围层(
MapAmbience+effects/):雷达扫光、同心脉冲环、辉光、科技网格、漂浮粒子;组件化可独立开关,铺在地图后方只填充空白区,普通模式自动减弱
- 响应式:宽屏三栏(侧栏 / 地图 / 详情,
100dvh满屏),窄屏自动堆叠 - 指标数字翻牌、详情切换淡入;选中景区/城市时列表自动滚动跟随,切城市时详情回到顶部
prefers-reduced-motion降级动效、:focus-visible焦点环、提升的文字对比度
浏览器 (Vue 3 SPA + Leaflet)
│ HTTP:fetch /api/* · /resources/*(图片 / GeoJSON)
▼
Flask (backend/app.py)
├─ api/routes.py REST:bootstrap / search / cities / scenic / summary / top
├─ services/scenic_service.py 聚合 · 搜索 · 城市/景区详情 · hero 排序 · 城市中心点
└─ storage/scenic_repository.py 读取 resources/ · 扫描 5A 图片索引 · 由 GeoJSON 计算经纬度范围
▼
resources/ scenic.json(864) · 省界 GeoJSON · 卫星 WebP · 5A 景区 WebP
(离线)Data_processing/ Excel → DeepSeek 富化 → 百度地理编码 → scenic.json
- 底图:地图模式用
L.imageOverlay(卫星WebP, 地理边界)铺底;普通模式隐藏影像、用L.geoJSON画矢量省界,并按各市景区数做分级填色(choropleth),区域可悬停高亮 + 数据气泡(已选珊瑚 / 悬停青色)。两种模式都基于同一份 GeoJSON 与真实经纬度,点位始终对齐。 - 省界范围自适应:后端从 GeoJSON 顶点算出
minLng/maxLng/minLat/maxLat,前端据此fitBounds,不写死。 - 选中城市环的显隐:用
map.getBoundsZoom(省界)算出"概览缩放",当前缩放超过它 0.6 级即判定为"已进城",隐藏珊瑚环只留城市名标签——自适应任意视口,不依赖固定 zoom 阈值。 - 漫游流畅度:去掉了悬浮控件的
backdrop-filter(地图在其后移动会逐帧重算模糊);拖拽期间临时隐藏城市名标签层(避免 18 个常驻 DOM 每帧重定位)。 - 大量点位渲染:景区点用共享
L.canvas()渲染(而非每点一个 SVG 节点),避免进城瞬间上百点同时创建造成的卡顿;城市点/选中态仍用 SVG 以支持标签与 CSS 动画。镜头移动时临时隐藏选中珊瑚环,避免定半径标记随缩放动画"放大再弹回"的突变。
- 城市 hero / 默认选中:每城景区按
recommendScore → 等级 → 坐标置信度 → 名称倒序取第一(评分优先,坐标置信度仅作兜底)。 - 城市中心点:优先用内置城市锚点;否则对该市景区坐标做 IQR 去离群后取中位数,避免被离群坐标带偏。
- 5A 图片注入:
scenic_image_index()扫描5Apic/*.webp,按 id 给记录注入image字段。
ScenicAtlas/
├── start_dev.py # 跨平台一键启动器(后端 5001 + 前端 5173)
├── b_requirements.txt # Data_processing 数据管线的 Python 依赖
├── backend/ # Flask 后端
│ ├── app.py # 应用入口;注册蓝图、托管 /resources/<path>
│ ├── api/routes.py # HTTP 路由(薄适配层)
│ ├── services/scenic_service.py# 聚合/搜索/详情/排序等业务逻辑 + lru_cache
│ ├── storage/scenic_repository.py # 读取 resources/、5A 图片索引、算地理范围
│ └── schemas/scenic.py # TypedDict 数据结构
├── frontend/ # Vue 3 + Vite 前端
│ ├── index.html · vite.config.js · tsconfig.json(开发期 /api、/resources 代理到 :5001)
│ ├── public/favicon.svg # 站点图标
│ └── src/
│ ├── App.vue # 串联搜索/城市联动/镜头聚焦/灯箱
│ ├── components/ # TourismMap / TourismSidebar / TourismDetail / TourismTooltip / TourismLightbox
│ │ ├── MapAmbience.vue # 地图氛围层容器(组合 effects/ 各特效,可独立开关)
│ │ └── effects/ # RadarSweep / PulseRings / TechGrid / ProvinceGlow / FloatingParticles / CornerHud
│ ├── composables/ # useTourismDashboard(状态编排)· useVirtualList(长列表虚拟滚动)
│ ├── api/scenic.ts # 类型化 API 层(与后端 schema 对齐)
│ ├── types/scenic.ts # 前端域/接口 TypeScript 类型
│ ├── assets/logo.svg # 品牌标识
│ └── styles.css # 全局样式
├── resources/ # 运行时资源(后端只读)
│ ├── scenic/scenic.json # 景区数据(864 条)
│ └── mapresource/
│ ├── mapdata/Ali_Data_VGeoAtlas.geojson # 全精度省界(DataV.GeoAtlas)
│ ├── mappic/410000_z10_web.webp # 卫星影像底图(WebP)
│ └── 5Apic/<id>.webp # 5A 景区图片(22 张)
└── Data_processing/ # 离线数据管线(独立维护,运行时不依赖)
└── scenic_pipeline/ # schema/api/io/prompts(含评分算法 scenic_schema.py)
- Python 3.10+(开发于 3.12/3.13)
- Flask 3.1(
json.ensure_ascii=False直出中文;lru_cache缓存聚合结果) - Pillow(图片规格化脚本,把景区原图转为统一 WebP)
- 标准库即可运行;数据管线另需
b_requirements.txt(openai / pandas / openpyxl / httpx 等)
- Vue 3 + Vite 6 SPA
- Leaflet 1.9(地图渲染、imageOverlay、geoJSON、circleMarker;景区点用 canvas 渲染)
- DataV-Vue3 1.7(大屏边框 / 数字翻牌 / 装饰线等组件)
- TypeScript 渐进式打底(
tsconfig.json+ 共享类型types/scenic.ts+ 类型化 APIapi/scenic.ts,与现有.js/.vue共存;npm run typecheck) - 省界矢量来自阿里 DataV.GeoAtlas;组件化 + 单一 composable 管理状态
- Python 3.10+
- Node.js 18+(开发/构建前端)
# 1) 后端运行依赖(仅需 Flask;图片处理脚本另需 Pillow)
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate
pip install flask pillow
# 2) 前端依赖
cd frontend
npm install
cd ..
b_requirements.txt是Data_processing/数据管线(DeepSeek 富化 + 地理编码)的依赖,仅在重新生成scenic.json时才需要安装,运行本应用不需要。
# 方式 A:一键启动器(后端 :5001 + 前端 Vite :5173,自动开浏览器)
python start_dev.py start
# python start_dev.py stop # 仅清理占用端口
# python start_dev.py start --no-browser # 不自动开浏览器
# python start_dev.py start --local-only # 仅绑定 127.0.0.1,不暴露局域网打开 http://localhost:5173 即可使用。
后端(仓库根目录,已激活 venv):
python -m backend.app # 以模块方式启动后端(确保 backend 包可导入),默认 :5001
python start_dev.py start # 同时拉起 后端(:5001) + 前端 Vite(:5173)直接
python backend/app.py会因仓库根不在sys.path而报ModuleNotFoundError: backend;请用python -m backend.app或start_dev.py(后者已自动注入PYTHONPATH)。
前端(frontend/ 目录):
npm install # 安装依赖
npm run dev # Vite 开发服务器(:5173,热更新,代理 /api 与 /resources 到 :5001)
npm run build # 生产构建 → dist/
npm run preview # 本地预览构建产物
npm run typecheck # vue-tsc 类型检查(TS 打底,校验 types/ 与 api/)backend/storage/scenic_repository.py读取resources/中的数据,并从 GeoJSON 计算地图经纬度范围、扫描 5A 图片索引backend/services/scenic_service.py负责聚合、搜索、城市/景区详情与排序backend/api/routes.py暴露 APIfrontend/src/composables/useTourismDashboard.js编排搜索、城市联动、选中与镜头聚焦frontend/src/components/TourismMap.vue用 Leaflet 渲染地图与两种底图模式
景区图片(当前仅 5A,22 张)统一规格后存放于 resources/mapresource/5Apic/,按景区 id 命名。
- 格式:WebP(RGB;源图若带透明通道则展平)
- 尺寸:等比缩放,最长边 ≤ 1000px(仅缩小、不放大)
- 质量:
quality=80、method=6 - 命名:
<4 位景区 id>.webp(如0001.webp),与scenic.json的id对应 - 接入:后端
scenic_image_index()扫描5Apic/*.webp,为对应 id 的记录注入image字段(/resources/mapresource/5Apic/<id>.webp);前端用于详情面板大图 + 搜索结果/同城列表缩略图(仅有图的记录显示) - 更新流程:把原图放任意暂存目录 → 用 Pillow 按上述规格转 WebP、覆盖到
5Apic/对应 id → 文件名不变,无需改后端(浏览器需硬刷新 Ctrl+F5 才能看到新图)
参考转换片段:
from pathlib import Path
from PIL import Image
MAX_EDGE = 1000 # 最长边像素上限
def normalize_scenic_image(src_path: str, scenic_id: str, out_dir: Path) -> None:
"""统一为 5A 详情用的 WebP(RGB、最长边 ≤ 1000、quality 80)。"""
img = Image.open(src_path).convert("RGB")
width, height = img.size
if max(width, height) > MAX_EDGE:
ratio = MAX_EDGE / max(width, height)
size = (round(width * ratio), round(height * ratio))
img = img.resize(size, Image.Resampling.LANCZOS)
img.save(out_dir / f"{scenic_id}.webp", "WEBP", quality=80, method=6)recommendScore 为 0–100 的推荐分,在 Data_processing/scenic_pipeline/scenic_schema.py 的 calc_recommend_score(level, category, tags) 中计算:
- 基础分(按等级):5A=85 / 4A=75 / 3A=65 / 其它=55
- 类别加分:自然风光、历史文化、宗教文化 +5;红色旅游、博物馆展馆 +3;其它 +0
- 标签加分(累计上限 +10):重点标签每个 +2(历史文化 / 自然风光 / 山岳 / 古建筑 / 宗教文化 / 瀑布 / 世界遗产),其余约 32 个常见标签每个 +1
- 组合加分(各 +1):山岳+瀑布;亲子+研学;古镇+古建筑;宗教文化+(寺庙|道观);自然风光+(湖泊|峡谷|森林|湿地|瀑布)
- 标签丰富度:去重后标签数 ≥4 个 +1,≥5 个 +1
- 最终裁剪到 [0, 100]
应用侧排序:城市「默认选中 / 头牌景区(hero)」按
recommendScore → 等级 → 坐标置信度 → 名称倒序取第一;「同城高分景点」为该市景区按分数倒序的全量列表(含当前景区,高亮显示)。
| 资源 | 说明 |
|---|---|
resources/scenic/scenic.json |
景区数据源(864 条) |
resources/mapresource/mapdata/Ali_Data_VGeoAtlas.geojson |
全精度矢量省界(DataV.GeoAtlas);普通模式分级填色 + 区域高亮/气泡 + 算经纬度范围 |
resources/mapresource/mappic/410000_z10_web.webp |
卫星影像底图(由原 97MB PNG 降采样为 ~0.8MB WebP) |
resources/mapresource/5Apic/<id>.webp |
5A 景区图片(22 张,按 id 命名) |
所有接口返回 JSON(Content-Type: application/json,中文不转义)。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/health |
健康检查 |
| GET | /api/bootstrap |
首屏全量数据:stats / assets / bounds / records / cities / featured / top* |
| GET | /api/summary |
概览:stats + topCities + topScenic |
| GET | /api/top?limit= |
热门城市与景区 |
| GET | /api/search?q=&level=&minScore=&limit= |
综合检索(不传 limit 返回全部命中) |
| GET | /api/cities?level=&q= |
城市汇总(含可见计数) |
| GET | /api/cities/<id 或 名称> |
城市详情(该市景区列表);前端统一传 id |
| GET | /api/scenic?city=&level=&q=&minScore=&limit=&offset= |
景区列表查询 |
| GET | /api/scenic/<id> |
单景区详情 + 同城相关 |
| GET | /resources/<path> |
静态资源(图片 / GeoJSON) |
- Windows / macOS / Linux 均支持:
start_dev.py按操作系统分支处理端口清理(Windows PowerShell/netstatvs POSIXlsof)、进程组(CREATE_NEW_PROCESS_GROUPvssetsid)、venv 路径(Scriptsvsbin),并为后端进程注入PYTHONPATH。
- 后端报
ModuleNotFoundError: backend:用python -m backend.app或python start_dev.py start启动,不要直接python backend/app.py。 - 前端地图/图片 404 或空白:确认后端在
:5001运行、Vite 代理生效;首屏依赖/api/bootstrap。 - 更新了 5A 图片但页面没变:URL 未变、内容变了属浏览器缓存,硬刷新(Ctrl+F5)。
- 端口被占用:
python start_dev.py stop清理 5001 / 5173 后重启。
- 后端运行时不再依赖
Data_processing,只读取resources/中的产物;数据管线可独立维护、按需重新生成scenic.json。 - 前端可独立
npm run build,后端可继续扩展查询接口与数据分层。
本项目基于 MIT License 开源,版权 © 2026 ScenicAtlas。