first commit

This commit is contained in:
zhangtao 2025-07-31 16:12:17 +08:00
commit 3efb239b23
22 changed files with 3494 additions and 0 deletions

0
.gitignore vendored Normal file
View File

0
.idea/.gitignore vendored Normal file
View File

12
.idea/geo.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,5 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/geo.iml" filepath="$PROJECT_DIR$/.idea/geo.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

52
.idea/workspace.xml Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="7bd3c14e-34ef-4905-8343-493f8e7480c1" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 7
}]]></component>
<component name="ProjectId" id="30dF6TjQdioNRSFt2AwM8C8kXXK" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
<option name="showMembers" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"git-widget-placeholder": "main",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-WS-251.27812.50" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="7bd3c14e-34ef-4905-8343-493f8e7480c1" name="Changes" comment="" />
<created>1753949367505</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1753949367505</updated>
<workItem from="1753949368756" duration="155000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

186
README.md Normal file
View File

@ -0,0 +1,186 @@
# Vue 3 + ArcGIS + 天地图分屏对比项目
这是一个基于Vue 3和ArcGIS API for JavaScript的天地图分屏对比项目。项目实现了同时显示矢量地图和影像地图的分屏功能支持多种交互和动画效果。
## ✨ 功能特点
### 🗺️ 地图功能
- ✅ 天地图矢量地图和影像地图加载
- ✅ 普通地图模式:支持图层切换
- ✅ **分屏对比模式**:同时显示两种地图类型
- ✅ 垂直/水平分割方向切换
- ✅ 可拖拽调整分割比例
### 🎮 交互控制
- ✅ 分割线位置实时显示
- ✅ 一键重置到中间位置
- ✅ 预设布局快速切换
- ✅ 平滑动画过渡效果
- ✅ 摆动演示动画
### 🎨 界面设计
- ✅ 响应式设计,适配移动端
- ✅ 现代化UI控件
- ✅ 实时状态显示
- ✅ 直观的操作反馈
## 🚀 快速开始
### 1. 安装依赖
```bash
npm install
```
### 2. 配置天地图API Key
**重要需要天地图API Key才能正常显示地图**
编辑 `src/config/tianditu.js`
```javascript
export const TIANDITU_CONFIG = {
API_KEY: '你的天地图API_KEY', // 替换这里
// ... 其他配置
}
```
获取API Key
1. 访问 [天地图开发者控制台](https://console.tianditu.gov.cn/)
2. 注册并创建应用
3. 获取API Key
详细配置说明:[TIANDITU_SETUP.md](./TIANDITU_SETUP.md)
### 3. 启动开发服务器
```bash
npm run dev
```
访问 http://localhost:5173
### 4. 构建生产版本
```bash
npm run build
```
## 📁 项目结构
```
geo/
├── src/
│ ├── components/
│ │ ├── TiandituMap.vue # 普通天地图组件
│ │ ├── SplitScreenMap.vue # 分屏对比组件
│ │ └── HelloWorld.vue # 示例组件
│ ├── config/
│ │ └── tianditu.js # 天地图配置
│ ├── utils/
│ │ └── splitScreen.js # 分屏工具类 ⭐
│ ├── App.vue # 主应用(模式切换)
│ └── main.js # 应用入口
├── TIANDITU_SETUP.md # 天地图配置指南
├── SPLIT_SCREEN_GUIDE.md # 分屏功能使用指南 ⭐
├── vite.config.js # Vite配置ArcGIS支持
└── package.json
```
## 🎯 核心功能说明
### 普通地图模式
- 矢量地图:道路、建筑、行政区划
- 影像地图:高清卫星影像
- 图层切换:右上角按钮切换
### 分屏对比模式 ⭐
- **同时显示**:左侧矢量,右侧影像(或上下分布)
- **实时对比**:相同区域不同图层的直观对比
- **交互同步**:缩放、平移操作同步到两个图层
- **分割线控制**:拖拽调整显示比例
### 高级功能
- **动画过渡**:平滑的位置变化动画
- **预设布局**:影像主导/矢量主导快速切换
- **摆动演示**:分割线摆动效果展示
- **实时反馈**:显示当前分割位置和方向
## 🛠️ 技术栈
- **前端框架**Vue 3 (Composition API)
- **构建工具**Vite
- **地图引擎**ArcGIS API for JavaScript
- **地图服务**:天地图 (国家地理信息公共服务平台)
- **核心功能**ArcGIS Swipe Widget
## 📖 使用指南
### 基础操作
1. 页面右上角切换"普通地图"/"分屏对比"
2. 分屏模式下,左上角控制面板进行操作
3. 拖拽分割线手动调整比例
### 控制面板功能
- **重置分割线**动画回到50%中间位置
- **方向切换**:垂直分割 ↔ 水平分割
- **影像主导**影像占70%显示区域
- **矢量主导**矢量占70%显示区域
- **摆动演示**:分割线摆动动画效果
### 高级定制
详细的自定义配置和扩展功能请查看:
- [分屏功能使用指南](./SPLIT_SCREEN_GUIDE.md)
- [天地图配置指南](./TIANDITU_SETUP.md)
## 🎨 界面预览
### 普通地图模式
- 右上角图层切换按钮
- 支持矢量/影像地图切换
### 分屏对比模式
- 左上角功能丰富的控制面板
- 实时显示分割状态信息
- 多种预设布局和动画效果
## 🔧 开发说明
### 核心文件
- `splitScreen.js`:分屏功能核心工具类
- `SplitScreenMap.vue`:分屏地图组件
- `tianditu.js`:天地图服务配置
### 自定义开发
```javascript
// 使用分屏管理器
import { SplitScreenManager } from './utils/splitScreen.js'
const manager = new SplitScreenManager(view)
manager.createSwipe(leadingLayers, trailingLayers)
// 使用动画器
import { SplitScreenAnimator } from './utils/splitScreen.js'
const animator = new SplitScreenAnimator(manager)
await animator.animateToPosition(75, 1000)
```
## 🐛 常见问题
### Q: 地图显示空白?
A: 检查天地图API Key是否正确配置
### Q: 分屏功能不工作?
A: 确认ArcGIS API版本支持Swipe小部件
### Q: 动画效果卡顿?
A: 检查浏览器性能,可调整动画时长
详细问题解答请查看 [SPLIT_SCREEN_GUIDE.md](./SPLIT_SCREEN_GUIDE.md)
## 📚 相关文档
- [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/latest/)
- [ArcGIS Swipe Widget](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Swipe.html)
- [天地图官网](https://www.tianditu.gov.cn/)
- [Vue 3 官方文档](https://v3.vuejs.org/)
## 📄 许可证
MIT License

205
SPLIT_SCREEN_GUIDE.md Normal file
View File

@ -0,0 +1,205 @@
# 分屏地图使用指南
## 概述
本项目实现了基于ArcGIS API for JavaScript的分屏地图功能可以同时显示天地图的矢量地图和影像地图支持多种交互和动画效果。
## 功能特点
### 🎯 核心功能
- **分屏对比**:同时显示矢量地图和影像地图
- **方向切换**:支持垂直分割和水平分割
- **位置调节**:可拖拽分割线调整显示比例
- **动画效果**:平滑的过渡动画和演示效果
### 🎮 交互控制
- **重置分割线**一键回到50%中间位置
- **方向切换**:在垂直和水平分割间切换
- **预设布局**:快速切换到预定义布局
- **动画演示**:摆动效果展示分屏功能
## 使用方法
### 1. 启动项目
```bash
npm run dev
```
### 2. 切换到分屏模式
在页面右上角点击"分屏对比"按钮
### 3. 控制面板操作
分屏模式下,左上角会显示控制面板,包含以下功能:
#### 基础控制
- **重置分割线**将分割线动画回到中间位置50%
- **水平分割/垂直分割**:切换分割方向
#### 预设布局
- **影像主导**影像地图占70%矢量地图占30%
- **矢量主导**矢量地图占70%影像地图占30%
#### 动画演示
- **摆动演示**:分割线摆动动画,展示分屏效果
### 4. 手动调节
- 拖拽分割线可以手动调整两种地图的显示比例
- 实时显示当前分割位置百分比
## 技术实现
### splitScreen.js 工具类
#### SplitScreenManager
分屏管理器,负责创建和管理分屏小部件:
```javascript
import { SplitScreenManager } from './utils/splitScreen.js'
// 创建管理器
const manager = new SplitScreenManager(view)
// 创建分屏
manager.createSwipe(leadingLayers, trailingLayers, options)
// 切换方向
manager.toggleDirection()
// 设置位置
manager.setPosition(75) // 75%
```
#### SplitScreenAnimator
动画控制器,提供平滑的动画效果:
```javascript
import { SplitScreenAnimator } from './utils/splitScreen.js'
// 创建动画器
const animator = new SplitScreenAnimator(manager)
// 动画到指定位置
await animator.animateToPosition(75, 1000) // 1秒动画到75%
// 摆动动画
await animator.wiggle(15, 3, 2000) // 摆动幅度153个周期2秒
```
#### 预设配置
```javascript
import { SPLIT_PRESETS } from './utils/splitScreen.js'
// 可用预设
SPLIT_PRESETS.VERTICAL_SATELLITE_RIGHT // 垂直分割,影像在右
SPLIT_PRESETS.HORIZONTAL_SATELLITE_BOTTOM // 水平分割,影像在下
SPLIT_PRESETS.SATELLITE_DOMINANT // 影像占主导70%
SPLIT_PRESETS.VECTOR_DOMINANT // 矢量占主导70%
```
## 自定义配置
### 修改默认分割方向
`SplitScreenMap.vue` 中:
```javascript
const isVertical = ref(false) // 改为false使用水平分割
```
### 修改默认分割位置
```javascript
const currentPosition = ref(30) // 改为30%
```
### 添加新的预设
`splitScreen.js` 中添加:
```javascript
export const SPLIT_PRESETS = {
// 现有预设...
// 新预设
CUSTOM_LAYOUT: {
direction: 'vertical',
position: 25
}
}
```
### 自定义动画效果
```javascript
// 修改动画时长
await animator.animateToPosition(50, 2000) // 2秒动画
// 修改摆动参数
await animator.wiggle(20, 4, 3000) // 摆动幅度204个周期3秒
```
## 样式定制
### 分割线样式
`SplitScreenMap.vue` 的全局样式中:
```css
:global(.esri-swipe__divider) {
background-color: #ff0000 !important; /* 红色分割线 */
width: 6px !important; /* 更粗的分割线 */
}
```
### 控制面板样式
修改 `.control-panel` 类的样式:
```css
.control-panel {
background: rgba(0, 0, 0, 0.8); /* 深色背景 */
color: white;
/* 其他样式... */
}
```
## 性能优化
### 图层管理
- 分屏时只加载必要的图层
- 避免重复创建相同的图层
- 及时销毁不需要的小部件
### 动画优化
- 使用 `requestAnimationFrame` 确保流畅动画
- 缓动函数提供自然的动画效果
- 避免在动画过程中创建新的分屏小部件
## 常见问题
### Q: 分屏功能不显示?
A: 检查ArcGIS API版本确保支持Swipe小部件
### Q: 动画卡顿?
A: 检查浏览器性能,降低动画复杂度或时长
### Q: 自定义样式不生效?
A: 使用 `:global()` 选择器覆盖ArcGIS默认样式
### Q: 如何添加更多地图类型?
A: 在天地图配置中添加新的服务URL然后在分屏组件中使用
## 扩展功能
### 三分屏对比
可以扩展为三个或更多地图的对比:
```javascript
// 创建多个分屏小部件
const swipe1 = new Swipe({ /* 配置1 */ })
const swipe2 = new Swipe({ /* 配置2 */ })
```
### 时间轴对比
结合时间滑块,对比不同时间的地图数据
### 测量工具
在分屏模式下添加距离和面积测量功能
### 图层透明度
添加图层透明度控制,实现更细致的对比效果
## 相关资源
- [ArcGIS Swipe Widget 文档](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Swipe.html)
- [天地图API文档](https://lbs.tianditu.gov.cn/server/MapService.html)
- [Vue 3 Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html)

67
TIANDITU_SETUP.md Normal file
View File

@ -0,0 +1,67 @@
# 天地图配置指南
## 1. 申请天地图API Key
要使用天地图服务你需要先申请API Key
1. 访问天地图官网https://www.tianditu.gov.cn/
2. 点击右上角"控制台"进入开发者控制台https://console.tianditu.gov.cn/
3. 注册账号并登录
4. 在控制台中创建应用并获取API Key
## 2. 配置API Key
获取API Key后需要在项目中配置
### 方法1修改配置文件
编辑 `src/config/tianditu.js` 文件,将 `API_KEY` 替换为你的API Key
```javascript
export const TIANDITU_CONFIG = {
API_KEY: '你的API_KEY', // 替换这里
// ... 其他配置
}
```
### 方法2直接修改组件
编辑 `src/components/TiandituMap.vue` 文件,找到 `apiKey` 字段并替换:
```javascript
const tiandituConfig = {
apiKey: '你的API_KEY', // 替换这里
// ... 其他配置
}
```
## 3. 启动项目
配置完成后,启动项目:
```bash
npm run dev
```
## 4. 功能说明
- **矢量地图**:显示道路、建筑等矢量信息
- **影像地图**:显示卫星影像
- **图层切换**:点击右上角按钮可以切换不同图层
- **地图交互**:支持缩放、平移等常用地图操作
## 5. 常见问题
### Q: 地图显示空白或加载失败
A: 请确认API Key是否正确配置并检查网络连接
### Q: 如何修改默认地图中心点?
A: 在 `TiandituMap.vue` 组件中找到 `center` 属性并修改坐标
### Q: 如何添加更多图层?
A: 天地图还提供地形图等其他图层,可以参考官方文档进行配置
## 6. 相关链接
- [天地图官网](https://www.tianditu.gov.cn/)
- [天地图开发者控制台](https://console.tianditu.gov.cn/)
- [天地图API文档](https://lbs.tianditu.gov.cn/server/MapService.html)
- [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/latest/)

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1911
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "geo-vue-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@arcgis/core": "^4.33.11",
"vue": "^3.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

120
src/App.vue Normal file
View File

@ -0,0 +1,120 @@
<template>
<div id="app">
<!-- 地图模式切换按钮 -->
<div class="map-mode-selector">
<button
@click="switchMapMode('normal')"
:class="{ active: currentMode === 'normal' }"
class="mode-btn"
>
普通地图
</button>
<button
@click="switchMapMode('split')"
:class="{ active: currentMode === 'split' }"
class="mode-btn"
>
分屏对比
</button>
</div>
<!-- 根据模式显示不同的地图组件 -->
<TiandituMap v-if="currentMode === 'normal'" />
<SplitScreenMap v-if="currentMode === 'split'" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import TiandituMap from './components/TiandituMap.vue'
import SplitScreenMap from './components/SplitScreenMap.vue'
const currentMode = ref('normal')
const switchMapMode = (mode) => {
currentMode.value = mode
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
.map-mode-selector {
position: absolute;
top: 20px;
right: 125px;
z-index: 2000;
display: flex;
gap: 5px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
}
.mode-btn {
padding: 10px 16px;
background: transparent;
color: #333;
border: 2px solid transparent;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
white-space: nowrap;
}
.mode-btn:hover {
background: rgba(0, 122, 200, 0.1);
border-color: #007ac8;
color: #007ac8;
}
.mode-btn.active {
background: linear-gradient(135deg, #007ac8, #005a96);
color: white;
border-color: #005a96;
box-shadow: 0 2px 8px rgba(0, 122, 200, 0.3);
}
.mode-btn.active:hover {
background: linear-gradient(135deg, #005a96, #004070);
}
/* 响应式设计 */
@media (max-width: 768px) {
.map-mode-selector {
top: 10px;
right: 125px;
left: 10px;
justify-content: center;
}
.mode-btn {
flex: 1;
padding: 8px 12px;
font-size: 13px;
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div class="map-container">
<div id="mapDiv" class="map-div"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
// props
const props = defineProps({
msg: String
})
//
const mapInstance = ref(null)
onMounted(() => {
// API
if (typeof T !== 'undefined') {
mapInstance.value = new T.Map('mapDiv')
//
let ctrl = new T.Control.MapType([
{
title: "卫星混合",
icon: "https://api.tianditu.gov.cn/v4.0/image/map/maptype/satellitepoi.png",
layer: TMAP_HYBRID_MAP,
},
{
title: "地图",
icon: "https://api.tianditu.gov.cn/v4.0/image/map/maptype/vector.png",
layer: TMAP_NORMAL_MAP,
},
]);
//
ctrl.setPosition(window.T_ANCHOR_TOP_RIGHT);
mapInstance.value.addControl(ctrl);
//
mapInstance.value.centerAndZoom(new T.LngLat(121.4737, 31.2304), 10);
console.log('地图初始化成功')
} else {
console.error('天地图 API 未加载')
}
})
</script>
<style scoped>
.map-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 1000;
}
.map-div {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,331 @@
<template>
<div class="split-screen-container">
<div ref="mapDiv" class="split-map-view"></div>
<div class="split-controls">
<div class="control-panel">
<h3>分屏地图对比</h3>
<div class="control-buttons">
<button @click="resetSplit" class="control-btn">
重置分割线
</button>
<button @click="toggleSplitDirection" class="control-btn">
{{ isVertical ? '水平分割' : '垂直分割' }}
</button>
<button @click="animateToPreset('SATELLITE_DOMINANT')" class="control-btn preset-btn">
影像主导
</button>
<button @click="animateToPreset('VECTOR_DOMINANT')" class="control-btn preset-btn">
矢量主导
</button>
<button @click="wiggleAnimation" class="control-btn animation-btn">
摆动演示
</button>
</div>
<div class="map-info">
<div class="info-item">
<span class="label">左侧/上方:</span>
<span class="value">矢量地图</span>
</div>
<div class="info-item">
<span class="label">右侧/下方:</span>
<span class="value">影像地图</span>
</div>
<div class="info-item">
<span class="label">分割位置:</span>
<span class="value">{{ Math.round(currentPosition) }}%</span>
</div>
<div class="info-item">
<span class="label">分割方向:</span>
<span class="value">{{ isVertical ? '垂直' : '水平' }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Map from '@arcgis/core/Map'
import MapView from '@arcgis/core/views/MapView'
import WebTileLayer from '@arcgis/core/layers/WebTileLayer'
import { TIANDITU_CONFIG } from '../config/tianditu.js'
import { SplitScreenManager, SplitScreenAnimator, SPLIT_PRESETS } from '../utils/splitScreen.js'
const mapDiv = ref()
let view = null
let splitManager = null
let animator = null
const isVertical = ref(true)
const currentPosition = ref(50)
//
const createTiandituLayer = (serviceUrl, id) => {
return new WebTileLayer({
id: id,
urlTemplate: serviceUrl.replace('{key}', TIANDITU_CONFIG.API_KEY),
subDomains: TIANDITU_CONFIG.SUB_DOMAINS,
copyright: '天地图'
})
}
//
const initSplitScreenMap = () => {
//
const vectorLayer = createTiandituLayer(TIANDITU_CONFIG.SERVICES.VECTOR, 'vector')
const vectorAnnotation = createTiandituLayer(TIANDITU_CONFIG.SERVICES.VECTOR_ANNOTATION, 'vector-annotation')
//
const satelliteLayer = createTiandituLayer(TIANDITU_CONFIG.SERVICES.SATELLITE, 'satellite')
const satelliteAnnotation = createTiandituLayer(TIANDITU_CONFIG.SERVICES.SATELLITE_ANNOTATION, 'satellite-annotation')
//
const map = new Map({
layers: [
vectorLayer,
vectorAnnotation,
satelliteLayer,
satelliteAnnotation
]
})
//
view = new MapView({
container: mapDiv.value,
map: map,
center: TIANDITU_CONFIG.DEFAULT_CENTER,
zoom: TIANDITU_CONFIG.DEFAULT_ZOOM
})
//
view.when(() => {
//
splitManager = new SplitScreenManager(view)
animator = new SplitScreenAnimator(splitManager)
//
const leadingLayers = [satelliteLayer, satelliteAnnotation]
const trailingLayers = [vectorLayer, vectorAnnotation]
splitManager.createSwipe(leadingLayers, trailingLayers, {
direction: isVertical.value ? 'vertical' : 'horizontal',
position: currentPosition.value
})
//
if (splitManager.swipeWidget) {
splitManager.swipeWidget.watch('position', (newPosition) => {
currentPosition.value = newPosition
})
}
})
}
// 线
const resetSplit = async () => {
if (splitManager) {
await animator.animateToPosition(50, 800)
}
}
//
const toggleSplitDirection = () => {
isVertical.value = !isVertical.value
if (splitManager) {
splitManager.toggleDirection()
}
}
//
const animateToPreset = async (presetName) => {
if (!splitManager || !animator) return
const preset = SPLIT_PRESETS[presetName]
if (preset) {
//
if ((preset.direction === 'vertical') !== isVertical.value) {
toggleSplitDirection()
}
//
await animator.animateToPosition(preset.position, 1000)
}
}
//
const wiggleAnimation = async () => {
if (animator) {
await animator.wiggle(15, 2, 1500)
}
}
onMounted(() => {
initSplitScreenMap()
})
onUnmounted(() => {
if (splitManager) {
splitManager.destroySwipe()
}
if (view) {
view.destroy()
}
})
</script>
<style scoped>
.split-screen-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.split-map-view {
width: 100%;
height: 100%;
}
.split-controls {
position: absolute;
top: 20px;
left: 20px;
z-index: 1000;
}
.control-panel {
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 250px;
backdrop-filter: blur(10px);
}
.control-panel h3 {
margin: 0 0 15px 0;
color: #333;
font-size: 16px;
font-weight: bold;
text-align: center;
border-bottom: 2px solid #007ac8;
padding-bottom: 8px;
}
.control-buttons {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.control-btn {
padding: 10px 15px;
background: linear-gradient(135deg, #007ac8, #005a96);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 122, 200, 0.3);
}
.control-btn:hover {
background: linear-gradient(135deg, #005a96, #004070);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 122, 200, 0.4);
}
.control-btn:active {
transform: translateY(0);
}
.preset-btn {
background: linear-gradient(135deg, #28a745, #20c997);
border-color: #1e7e34;
}
.preset-btn:hover {
background: linear-gradient(135deg, #1e7e34, #17a2b8);
}
.animation-btn {
background: linear-gradient(135deg, #ffc107, #fd7e14);
border-color: #e0a800;
color: #212529;
}
.animation-btn:hover {
background: linear-gradient(135deg, #e0a800, #dc3545);
color: white;
}
.map-info {
border-top: 1px solid #e0e0e0;
padding-top: 15px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
}
.label {
color: #666;
font-weight: 500;
}
.value {
color: #333;
font-weight: bold;
background: #f0f8ff;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid #007ac8;
}
/* 响应式设计 */
@media (max-width: 768px) {
.split-controls {
top: 10px;
left: 10px;
right: 10px;
}
.control-panel {
padding: 15px;
min-width: auto;
}
.control-panel h3 {
font-size: 14px;
}
.control-btn {
padding: 8px 12px;
font-size: 13px;
}
}
/* 分屏小部件样式定制 */
:global(.esri-swipe__container) {
border: 2px solid #007ac8 !important;
}
:global(.esri-swipe__divider) {
background-color: #007ac8 !important;
width: 4px !important;
}
:global(.esri-swipe__divider::before) {
background-color: #005a96 !important;
border: 2px solid white !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="map-container">
<div ref="mapDiv" class="map-view"></div>
<div class="map-controls">
<button
@click="switchLayer('vector')"
:class="{ active: currentLayer === 'vector' }"
class="layer-btn"
>
矢量地图
</button>
<button
@click="switchLayer('satellite')"
:class="{ active: currentLayer === 'satellite' }"
class="layer-btn"
>
影像地图
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Map from '@arcgis/core/Map'
import MapView from '@arcgis/core/views/MapView'
import WebTileLayer from '@arcgis/core/layers/WebTileLayer'
import { TIANDITU_CONFIG } from '../config/tianditu.js'
const mapDiv = ref()
let view = null
const currentLayer = ref('vector')
//
const createTiandituLayer = (serviceUrl, id) => {
return new WebTileLayer({
id: id,
urlTemplate: serviceUrl.replace('{key}', TIANDITU_CONFIG.API_KEY),
subDomains: TIANDITU_CONFIG.SUB_DOMAINS,
copyright: '天地图'
})
}
//
const initMap = () => {
//
const vectorLayer = createTiandituLayer(TIANDITU_CONFIG.SERVICES.VECTOR, 'vector')
const vectorAnnotation = createTiandituLayer(TIANDITU_CONFIG.SERVICES.VECTOR_ANNOTATION, 'vector-annotation')
const map = new Map({
layers: [vectorLayer, vectorAnnotation]
})
view = new MapView({
container: mapDiv.value,
map: map,
center: TIANDITU_CONFIG.DEFAULT_CENTER,
zoom: TIANDITU_CONFIG.DEFAULT_ZOOM
})
}
//
const switchLayer = (layerType) => {
if (!view) return
currentLayer.value = layerType
view.map.removeAll()
if (layerType === 'vector') {
//
const vectorLayer = createTiandituLayer(TIANDITU_CONFIG.SERVICES.VECTOR, 'vector')
const vectorAnnotation = createTiandituLayer(TIANDITU_CONFIG.SERVICES.VECTOR_ANNOTATION, 'vector-annotation')
view.map.add(vectorLayer)
view.map.add(vectorAnnotation)
} else if (layerType === 'satellite') {
//
const satelliteLayer = createTiandituLayer(TIANDITU_CONFIG.SERVICES.SATELLITE, 'satellite')
const satelliteAnnotation = createTiandituLayer(TIANDITU_CONFIG.SERVICES.SATELLITE_ANNOTATION, 'satellite-annotation')
view.map.add(satelliteLayer)
view.map.add(satelliteAnnotation)
}
}
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (view) {
view.destroy()
}
})
</script>
<style scoped>
.map-container {
position: relative;
width: 100%;
height: 100vh;
}
.map-view {
width: 100%;
height: 100%;
}
.map-controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.layer-btn {
padding: 10px 15px;
background: rgba(255, 255, 255, 0.9);
border: 2px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
color: #333;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.layer-btn:hover {
background: rgba(255, 255, 255, 1);
border-color: #007ac8;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.layer-btn.active {
background: #007ac8;
color: white;
border-color: #005a96;
}
.layer-btn.active:hover {
background: #005a96;
}
</style>

33
src/config/tianditu.js Normal file
View File

@ -0,0 +1,33 @@
// 天地图配置文件
// 请在这里配置你的天地图API Key
export const TIANDITU_CONFIG = {
// 请将下面的字符串替换为你从天地图官网申请的API Key
// 申请地址https://console.tianditu.gov.cn/
// 如果没有API Key地图将无法正常显示
API_KEY: '4559200347e75489c4474f5b11846368',
// 天地图服务URL配置
SERVICES: {
// 矢量底图
VECTOR: 'http://t{subDomain}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}&tk={key}',
// 矢量注记
VECTOR_ANNOTATION: 'http://t{subDomain}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}&tk={key}',
// 影像底图
SATELLITE: 'http://t{subDomain}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}&tk={key}',
// 影像注记
SATELLITE_ANNOTATION: 'http://t{subDomain}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={level}&TILEROW={row}&TILECOL={col}&tk={key}'
},
// 服务器子域名
SUB_DOMAINS: ['0', '1', '2', '3', '4', '5', '6', '7'],
// 默认地图中心点(北京)
DEFAULT_CENTER: [116.4074, 39.9042],
// 默认缩放级别
DEFAULT_ZOOM: 10
}

4
src/main.js Normal file
View File

@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

289
src/utils/splitScreen.js Normal file
View File

@ -0,0 +1,289 @@
// splitScreen.js - 分屏地图工具类
// 提供分屏地图的创建、管理和控制功能
import Swipe from '@arcgis/core/widgets/Swipe'
/**
* 分屏地图管理器
*/
export class SplitScreenManager {
constructor(view) {
this.view = view
this.swipeWidget = null
this.isVertical = true
this.position = 50
this.leadingLayers = []
this.trailingLayers = []
}
/**
* 创建分屏小部件
* @param {Array} leadingLayers - 前景图层右侧/下方显示
* @param {Array} trailingLayers - 背景图层左侧/上方显示
* @param {Object} options - 配置选项
*/
createSwipe(leadingLayers = [], trailingLayers = [], options = {}) {
// 如果已存在分屏小部件,先销毁
this.destroySwipe()
// 保存图层引用
this.leadingLayers = leadingLayers
this.trailingLayers = trailingLayers
// 合并默认配置
const config = {
view: this.view,
leadingLayers: leadingLayers,
trailingLayers: trailingLayers,
direction: options.direction || (this.isVertical ? 'vertical' : 'horizontal'),
position: options.position || this.position,
...options
}
// 创建分屏小部件
this.swipeWidget = new Swipe(config)
// 添加到视图
this.view.ui.add(this.swipeWidget)
// 监听位置变化
this.swipeWidget.watch('position', (newPosition) => {
this.position = newPosition
})
return this.swipeWidget
}
/**
* 销毁分屏小部件
*/
destroySwipe() {
if (this.swipeWidget) {
this.view.ui.remove(this.swipeWidget)
this.swipeWidget.destroy()
this.swipeWidget = null
}
}
/**
* 切换分割方向
*/
toggleDirection() {
this.isVertical = !this.isVertical
if (this.swipeWidget) {
// 重新创建分屏小部件
this.createSwipe(this.leadingLayers, this.trailingLayers, {
direction: this.isVertical ? 'vertical' : 'horizontal',
position: this.position
})
}
return this.isVertical
}
/**
* 设置分割线位置
* @param {number} position - 位置百分比 (0-100)
*/
setPosition(position) {
this.position = Math.max(0, Math.min(100, position))
if (this.swipeWidget) {
this.swipeWidget.position = this.position
}
return this.position
}
/**
* 重置分割线到中间位置
*/
resetPosition() {
return this.setPosition(50)
}
/**
* 获取当前分割方向
*/
getDirection() {
return this.isVertical ? 'vertical' : 'horizontal'
}
/**
* 获取当前位置
*/
getPosition() {
return this.position
}
/**
* 更新前景图层
* @param {Array} layers - 新的前景图层
*/
updateLeadingLayers(layers) {
this.leadingLayers = layers
if (this.swipeWidget) {
this.swipeWidget.leadingLayers = layers
}
}
/**
* 更新背景图层
* @param {Array} layers - 新的背景图层
*/
updateTrailingLayers(layers) {
this.trailingLayers = layers
if (this.swipeWidget) {
this.swipeWidget.trailingLayers = layers
}
}
/**
* 检查分屏小部件是否存在
*/
isActive() {
return this.swipeWidget !== null
}
}
/**
* 创建天地图分屏对比
* @param {MapView} view - 地图视图
* @param {Object} vectorLayers - 矢量图层 {base, annotation}
* @param {Object} satelliteLayers - 影像图层 {base, annotation}
* @param {Object} options - 配置选项
*/
export function createTiandituSplitScreen(view, vectorLayers, satelliteLayers, options = {}) {
const manager = new SplitScreenManager(view)
// 设置影像图层为前景(右侧/下方)
const leadingLayers = [satelliteLayers.base, satelliteLayers.annotation].filter(Boolean)
// 矢量图层作为背景(左侧/上方)
const trailingLayers = [vectorLayers.base, vectorLayers.annotation].filter(Boolean)
// 创建分屏
manager.createSwipe(leadingLayers, trailingLayers, options)
return manager
}
/**
* 分屏预设配置
*/
export const SPLIT_PRESETS = {
// 垂直分割,影像在右
VERTICAL_SATELLITE_RIGHT: {
direction: 'vertical',
position: 50
},
// 水平分割,影像在下
HORIZONTAL_SATELLITE_BOTTOM: {
direction: 'horizontal',
position: 50
},
// 影像占大部分70%
SATELLITE_DOMINANT: {
direction: 'vertical',
position: 30
},
// 矢量占大部分70%
VECTOR_DOMINANT: {
direction: 'vertical',
position: 70
}
}
/**
* 分屏动画效果
*/
export class SplitScreenAnimator {
constructor(manager) {
this.manager = manager
this.isAnimating = false
}
/**
* 动画移动分割线
* @param {number} targetPosition - 目标位置
* @param {number} duration - 动画时长(ms)
*/
async animateToPosition(targetPosition, duration = 1000) {
if (this.isAnimating || !this.manager.isActive()) {
return false
}
this.isAnimating = true
const startPosition = this.manager.getPosition()
const distance = targetPosition - startPosition
const startTime = Date.now()
return new Promise((resolve) => {
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
// 使用缓动函数
const easeProgress = this.easeInOutCubic(progress)
const currentPosition = startPosition + (distance * easeProgress)
this.manager.setPosition(currentPosition)
if (progress < 1) {
requestAnimationFrame(animate)
} else {
this.isAnimating = false
resolve(true)
}
}
animate()
})
}
/**
* 缓动函数
*/
easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
}
/**
* 摆动动画
*/
async wiggle(amplitude = 10, cycles = 3, duration = 1000) {
if (this.isAnimating || !this.manager.isActive()) {
return false
}
this.isAnimating = true
const centerPosition = this.manager.getPosition()
const startTime = Date.now()
return new Promise((resolve) => {
const animate = () => {
const elapsed = Date.now() - startTime
const progress = elapsed / duration
if (progress >= 1) {
this.manager.setPosition(centerPosition)
this.isAnimating = false
resolve(true)
return
}
// 正弦波摆动
const wiggleOffset = Math.sin(progress * Math.PI * 2 * cycles) * amplitude * (1 - progress)
this.manager.setPosition(centerPosition + wiggleOffset)
requestAnimationFrame(animate)
}
animate()
})
}
}

19
vite.config.js Normal file
View File

@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
define: {
// 解决ArcGIS API中的process.env问题
'process.env': {}
},
optimizeDeps: {
// 排除ArcGIS模块从预构建中因为它们已经是优化的
exclude: ['@arcgis/core']
},
server: {
// 允许从外部域加载资源
cors: true
}
})