地图初步

main
Lufaneng 1 year ago
parent 0b09d5e17f
commit 2a02861d93

@ -7,6 +7,7 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside'] ElAside: typeof import('element-plus/es')['ElAside']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
@ -41,9 +42,17 @@ declare module 'vue' {
IEpMessageBox: typeof import('~icons/ep/message-box')['default'] IEpMessageBox: typeof import('~icons/ep/message-box')['default']
IEpPlus: typeof import('~icons/ep/plus')['default'] IEpPlus: typeof import('~icons/ep/plus')['default']
IEpScaleToOriginal: typeof import('~icons/ep/scale-to-original')['default'] IEpScaleToOriginal: typeof import('~icons/ep/scale-to-original')['default']
MapSelector: typeof import('./components/MapSelector.vue')['default']
MapSelectorFinal: typeof import('./components/MapSelectorFinal.vue')['default']
MapSelectorFixed: typeof import('./components/MapSelectorFixed.vue')['default']
MapSelectorOptimized: typeof import('./components/MapSelectorOptimized.vue')['default']
MapSelectorSimple: typeof import('./components/MapSelectorSimple.vue')['default']
MapSelectorWithMarker: typeof import('./components/MapSelectorWithMarker.vue')['default']
MapTest: typeof import('./components/MapTest.vue')['default']
Pagination: typeof import('./components/Pagination/index.vue')['default'] Pagination: typeof import('./components/Pagination/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SimpleMapTest: typeof import('./components/SimpleMapTest.vue')['default']
} }
export interface ComponentCustomProperties { export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']

@ -0,0 +1,89 @@
# 腾讯地图坐标选择器使用说明 (基于 tlbs-map-vue)
## 功能说明
MapSelector 是一个基于 `tlbs-map-vue` 的坐标选择器组件,用于在地图上选择经纬度坐标。
## 技术栈
- Vue 3
- tlbs-map-vue (腾讯地图官方 Vue 组件库)
- Element Plus
## 安装依赖
```bash
npm install tlbs-map-vue
```
## 使用方法
### 1. 配置腾讯地图API密钥
`src/config/map.js` 文件中,将 `TENCENT_MAP_KEY` 替换为你的实际API密钥
```javascript
export const MAP_CONFIG = {
TENCENT_MAP_KEY: '你的腾讯地图API密钥',
// ...其他配置
}
```
### 2. 获取腾讯地图API密钥
1. 访问腾讯位置服务官网https://lbs.qq.com/
2. 注册并登录账号
3. 创建应用并获取API密钥WebServiceAPI密钥
4. 在控制台中配置域名白名单
### 3. 在组件中使用
```vue
<template>
<MapSelector
v-model="showMapSelector"
:longitude="formField.longitude"
:latitude="formField.latitude"
@confirm="handleCoordinateConfirm"
/>
</template>
<script setup>
import MapSelector from '@/components/MapSelector.vue'
const showMapSelector = ref(false)
const formField = ref({
longitude: null,
latitude: null
})
const handleCoordinateConfirm = (coordinates) => {
formField.value.longitude = coordinates.longitude
formField.value.latitude = coordinates.latitude
}
</script>
```
## Props
- `modelValue`: Boolean - 控制对话框显示/隐藏
- `longitude`: Number - 初始经度值
- `latitude`: Number - 初始纬度值
## Events
- `update:modelValue`: 更新对话框显示状态
- `confirm`: 确认选择坐标时触发,返回 `{longitude, latitude}` 对象
## 特性
- ✅ 基于腾讯地图官方 Vue 组件库
- ✅ 支持点击地图选择坐标
- ✅ 实时显示选择的经纬度
- ✅ 支持初始坐标显示
- ✅ 响应式设计
- ✅ 自动处理地图加载
## 注意事项
1. 确保已正确配置腾讯地图API密钥
2. 需要在腾讯地图控制台中配置域名白名单
3. 在生产环境中建议将API密钥配置在环境变量中
4. 使用的是 WebServiceAPI 密钥,不是 JavaScript API 密钥
## 故障排除
如果地图无法显示:
1. 检查 API 密钥是否正确
2. 确认域名已添加到白名单
3. 检查网络连接
4. 查看浏览器控制台错误信息

@ -0,0 +1,522 @@
<template>
<el-dialog
v-model="visible"
title="选择坐标"
width="800px"
:close-on-click-modal="false"
@closed="handleClose"
@opened="handleOpened"
>
<div class="map-container">
<div
ref="mapContainer"
style="width: 100%; height: 400px; border: 1px solid #ccc; position: relative; z-index: 1"
v-loading="mapLoading || isGettingLocation"
:element-loading-text="isGettingLocation ? '正在获取当前位置...' : '正在加载地图...'"
>
<!-- 自定义标记 -->
<div v-if="markerPosition && map" class="custom-marker" :style="markerPosition">
<div class="marker-pin"></div>
<div class="marker-shadow"></div>
</div>
</div>
<!-- 错误提示 -->
<div v-if="mapError" class="error-info">
<el-alert :title="mapError" type="error" :closable="false" show-icon />
</div>
<!-- 坐标信息 -->
<div v-else class="coordinate-info">
<p>当前选择坐标经度 {{ selectedLng || '未选择' }}, 纬度 {{ selectedLat || '未选择' }}</p>
<p class="tip">点击地图选择坐标位置</p>
<p
v-if="selectedLng && selectedLat && !props.longitude && !props.latitude"
class="current-location-info"
>
📍 已自动定位到当前位置
</p>
<p v-else-if="selectedLng && selectedLat" class="selected-info"> 已选择坐标点</p>
</div>
<!-- 使用原生按钮 -->
<div class="native-button-area">
<button
type="button"
class="location-btn"
@click="getCurrentLocationManually"
:disabled="isGettingLocation"
>
{{ isGettingLocation ? '定位中...' : '📍 获取当前位置' }}
</button>
<button type="button" class="cancel-btn" @click="handleClose"></button>
<button
type="button"
class="confirm-btn"
:disabled="!selectedLng || !selectedLat"
@click="confirmSelection"
>
确定
</button>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { MAP_CONFIG } from '@/config/map'
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
longitude: {
type: Number,
default: null,
},
latitude: {
type: Number,
default: null,
},
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
const mapContainer = ref(null)
const selectedLng = ref(props.longitude || null)
const selectedLat = ref(props.latitude || null)
const mapLoading = ref(false)
const mapError = ref('')
const markerPosition = ref(null)
const isGettingLocation = ref(false)
let map = null
let isMapInitialized = false
//
const getCurrentLocation = () => {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('浏览器不支持地理位置获取'))
return
}
isGettingLocation.value = true
console.log('开始获取当前位置...')
navigator.geolocation.getCurrentPosition(
(position) => {
const { longitude, latitude } = position.coords
console.log('获取到当前位置:', { longitude, latitude })
resolve({ longitude, latitude })
},
(error) => {
console.error('获取位置失败:', error)
let errorMessage = '获取位置失败'
switch (error.code) {
case error.PERMISSION_DENIED:
errorMessage = '用户拒绝了位置请求'
break
case error.POSITION_UNAVAILABLE:
errorMessage = '位置信息不可用'
break
case error.TIMEOUT:
errorMessage = '获取位置超时'
break
}
reject(new Error(errorMessage))
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000,
}
)
}).finally(() => {
isGettingLocation.value = false
})
}
// API
const loadTencentMapAPI = () => {
return new Promise((resolve, reject) => {
if (window.TMap) {
resolve()
return
}
const script = document.createElement('script')
script.src = `https://map.qq.com/api/gljs?v=1.exp&key=${MAP_CONFIG.TENCENT_MAP_KEY}`
script.onload = () => {
if (window.TMap) {
resolve()
} else {
reject(new Error('腾讯地图API加载失败'))
}
}
script.onerror = () => reject(new Error('腾讯地图API加载失败'))
document.head.appendChild(script)
})
}
//
const latLngToPixel = (lat, lng) => {
if (!map) return null
try {
const point = map.projectToContainer(new window.TMap.LatLng(lat, lng))
return {
left: point.getX() + 'px',
top: point.getY() + 'px',
}
} catch (error) {
console.error('坐标转换失败:', error)
return null
}
}
//
const updateMarkerPosition = () => {
if (selectedLng.value && selectedLat.value && map) {
const position = latLngToPixel(selectedLat.value, selectedLng.value)
markerPosition.value = position
} else {
markerPosition.value = null
}
}
//
const initMap = async () => {
if (!mapContainer.value || isMapInitialized) return
mapLoading.value = true
mapError.value = ''
try {
await loadTencentMapAPI()
//
let centerLat = props.latitude || MAP_CONFIG.DEFAULT_CENTER.latitude
let centerLng = props.longitude || MAP_CONFIG.DEFAULT_CENTER.longitude
//
if (!props.latitude && !props.longitude) {
try {
const currentLocation = await getCurrentLocation()
centerLat = currentLocation.latitude
centerLng = currentLocation.longitude
//
selectedLat.value = currentLocation.latitude
selectedLng.value = currentLocation.longitude
ElMessage.success('已获取到当前位置')
console.log('使用当前位置作为地图中心:', { centerLat, centerLng })
} catch (locationError) {
console.warn('获取当前位置失败,使用默认位置:', locationError.message)
ElMessage.warning('获取当前位置失败,使用默认位置')
}
}
//
map = new window.TMap.Map(mapContainer.value, {
center: new window.TMap.LatLng(centerLat, centerLng),
zoom: MAP_CONFIG.DEFAULT_ZOOM,
})
//
map.on('click', (event) => {
const lat = event.latLng.getLat()
const lng = event.latLng.getLng()
selectedLat.value = lat
selectedLng.value = lng
console.log('选择坐标:', { lng, lat })
//
updateMarkerPosition()
//
map.setCenter(new window.TMap.LatLng(lat, lng))
//
ElMessage.success(`已选择坐标: ${lng.toFixed(6)}, ${lat.toFixed(6)}`)
})
//
map.on('moveend', updateMarkerPosition)
map.on('zoomend', updateMarkerPosition)
//
if (props.longitude && props.latitude) {
nextTick(() => {
updateMarkerPosition()
})
}
isMapInitialized = true
console.log('腾讯地图初始化成功')
} catch (error) {
console.error('地图初始化失败:', error)
mapError.value = '地图加载失败: ' + error.message
} finally {
mapLoading.value = false
}
}
//
const updateMapCenter = () => {
if (map && (props.longitude || props.latitude)) {
const center = new window.TMap.LatLng(
props.latitude || MAP_CONFIG.DEFAULT_CENTER.latitude,
props.longitude || MAP_CONFIG.DEFAULT_CENTER.longitude
)
map.setCenter(center)
//
nextTick(() => {
updateMarkerPosition()
})
}
}
//
const getCurrentLocationManually = async () => {
try {
const currentLocation = await getCurrentLocation()
selectedLat.value = currentLocation.latitude
selectedLng.value = currentLocation.longitude
//
if (map) {
const center = new window.TMap.LatLng(currentLocation.latitude, currentLocation.longitude)
map.setCenter(center)
nextTick(() => {
updateMarkerPosition()
})
}
ElMessage.success('已获取到当前位置')
} catch (error) {
console.error('手动获取位置失败:', error)
ElMessage.error('获取当前位置失败: ' + error.message)
}
}
//
const confirmSelection = () => {
console.log('确认选择按钮被点击')
console.log('当前坐标:', { lng: selectedLng.value, lat: selectedLat.value })
if (selectedLng.value && selectedLat.value) {
console.log('发送确认事件:', {
longitude: selectedLng.value,
latitude: selectedLat.value,
})
emit('confirm', {
longitude: selectedLng.value,
latitude: selectedLat.value,
})
ElMessage.success('坐标确认成功!')
handleClose()
} else {
console.log('坐标未选择,无法确认')
ElMessage.warning('请先选择坐标位置')
}
}
//
const handleOpened = () => {
console.log('对话框已打开')
nextTick(() => {
if (!isMapInitialized) {
initMap()
} else {
updateMapCenter()
}
})
}
//
const handleClose = () => {
console.log('关闭对话框')
markerPosition.value = null
visible.value = false
}
// props
watch([() => props.longitude, () => props.latitude], ([newLng, newLat]) => {
selectedLng.value = newLng
selectedLat.value = newLat
updateMapCenter()
})
//
onUnmounted(() => {
if (map) {
try {
map.destroy()
map = null
isMapInitialized = false
console.log('地图实例已清理')
} catch (error) {
console.warn('清理地图实例失败:', error)
}
}
})
</script>
<style lang="scss" scoped>
.map-container {
position: relative;
.custom-marker {
position: absolute;
z-index: 1000;
pointer-events: none;
transform: translate(-50%, -100%);
.marker-pin {
width: 20px;
height: 20px;
background: #ff4444;
border: 3px solid #ffffff;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
position: relative;
&::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid #ff4444;
}
}
.marker-shadow {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
width: 16px;
height: 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 50%;
margin-top: 2px;
}
}
.coordinate-info {
margin-top: 16px;
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
p {
margin: 0;
color: #606266;
font-size: 14px;
&.tip {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
&.selected-info {
color: #67c23a;
font-weight: 500;
margin-top: 8px;
}
&.current-location-info {
color: #409eff;
font-weight: 500;
margin-top: 8px;
}
}
}
.error-info {
margin-top: 16px;
}
.native-button-area {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e4e7ed;
text-align: right;
position: relative;
z-index: 9999;
button {
padding: 8px 16px;
border-radius: 4px;
border: 1px solid #dcdfe6;
background-color: #fff;
color: #606266;
cursor: pointer;
font-size: 14px;
margin-left: 8px;
position: relative;
z-index: 10000;
&:hover {
background-color: #f5f7fa;
}
&.confirm-btn {
background-color: #409eff;
border-color: #409eff;
color: #fff;
&:hover:not(:disabled) {
background-color: #66b1ff;
border-color: #66b1ff;
}
&:disabled {
background-color: #a0cfff;
border-color: #a0cfff;
cursor: not-allowed;
}
}
&.location-btn {
background-color: #67c23a;
border-color: #67c23a;
color: #fff;
&:hover:not(:disabled) {
background-color: #85ce61;
border-color: #85ce61;
}
&:disabled {
background-color: #b3e19d;
border-color: #b3e19d;
cursor: not-allowed;
}
}
}
}
}
</style>

@ -0,0 +1,19 @@
// 地图配置
export const MAP_CONFIG = {
// 腾讯地图API密钥 - 请替换为你的实际密钥
TENCENT_MAP_KEY: 'HGWBZ-QUB6Z-C7KXZ-TJRCH-IFLLO-AMFIJ',
// 默认地图中心点(北京)
DEFAULT_CENTER: {
latitude: 39.916527,
longitude: 116.397128
},
// 默认缩放级别
DEFAULT_ZOOM: 13
}
// 注意:
// 1. 请到腾讯位置服务官网申请API密钥https://lbs.qq.com/
// 2. 将上面的 YOUR_TENCENT_MAP_API_KEY 替换为你的实际密钥
// 3. 在生产环境中,建议将密钥配置在环境变量中

@ -1,5 +1,7 @@
<script setup> <script setup>
import RPC from '@/utils/rpc'; import RPC from '@/utils/rpc'
import MapSelector from '@/components/MapSelector.vue'
const props = defineProps({ const props = defineProps({
type: { type: {
type: String, type: String,
@ -11,20 +13,20 @@ import { VueDraggable } from 'vue-draggable-plus'
const list = ref([ const list = ref([
{ {
name: 'Joao', name: 'Joao',
id: 1 id: 1,
}, },
{ {
name: 'Jean', name: 'Jean',
id: 2 id: 2,
}, },
{ {
name: 'Johanna', name: 'Johanna',
id: 3 id: 3,
}, },
{ {
name: 'Juan', name: 'Juan',
id: 4 id: 4,
} },
]) ])
const emit = defineEmits(['closed', 'onSuccess']) const emit = defineEmits(['closed', 'onSuccess'])
@ -38,60 +40,82 @@ onMounted(() => {
}) })
const formRef = ref(null) const formRef = ref(null)
const formField = ref({ const formField = ref({})
color_component: [] const formRules = reactive({})
})
const formRules = reactive({
})
const loading = ref(false) const loading = ref(false)
const showMapSelector = ref(false)
function closed() { function closed() {
emit('closed') emit('closed')
} }
const submitForm = () => { const submitForm = () => {}
}
const detailData = ref([])
if (!isAdd.value) { if (!isAdd.value) {
// formField.value = props.row // formField.value = props.row
} }
const chooseCoordinates = () => {
const add = () => { showMapSelector.value = true
formField.value.color_component.push({ num: calcId(formField.value.color_component), color_paste_id: '', remark: '', ratio: 0.01 })
} }
const handleDelete = row => { //
formField.value.color_component = formField.value.color_component.filter(item => item.num !== row.num) const handleCoordinateConfirm = (coordinates) => {
console.log('接收到坐标确认事件:', coordinates)
formField.value.longitude = coordinates.longitude
formField.value.latitude = coordinates.latitude
console.log('表单字段已更新:', formField.value)
ElMessage.success(`坐标选择成功: 经度 ${coordinates.longitude}, 纬度 ${coordinates.latitude}`)
} }
</script> </script>
<template> <template>
<el-dialog align-center :title="isAdd ? '新增' : '修改'" width="774px" v-model="dialogVisible" @closed="closed" :close-on-click-modal="false"> <el-dialog
<el-form ref="formRef" label-position="right" label-suffix=":" :model="formField" :rules="formRules" v-loading="loading"> align-center
<VueDraggable ref="el" v-model="list"> :title="isAdd ? '新增' : '修改'"
width="774px"
v-model="dialogVisible"
@closed="closed"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
label-position="right"
label-suffix=":"
:model="formField"
:rules="formRules"
v-loading="loading"
>
<!-- <VueDraggable ref="el" v-model="list">
<div v-for="item in list" :key="item.id"> <div v-for="item in list" :key="item.id">
{{ item.name }} {{ item.name }}
</div> </div>
</VueDraggable> </VueDraggable> -->
<el-form-item prop="longitude" label="经度">
<el-input-number :controls="false" placeholder="请输入" v-model="formField.longitude" />
</el-form-item>
<el-form-item prop="latitude" label="纬度">
<el-input-number :controls="false" placeholder="请输入" v-model="formField.latitude" />
</el-form-item>
<el-button type="primary" @click="chooseCoordinates"></el-button>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="closed"> <el-button @click="closed"> </el-button>
<el-button type="primary" :loading="loading" @click="submitForm"> </el-button>
</el-button>
<el-button type="primary" :loading="loading" @click="submitForm">
</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
<!-- 地图坐标选择器 -->
<MapSelector
v-model="showMapSelector"
:longitude="formField.longitude"
:latitude="formField.latitude"
@confirm="handleCoordinateConfirm"
/>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

@ -46,7 +46,7 @@
<pagination v-show="total > 0" :total="total" v-model:limit="queryParams.limit" v-model:page="queryParams.page" @pagination="getList" /> <pagination v-show="total > 0" :total="total" v-model:limit="queryParams.limit" v-model:page="queryParams.page" @pagination="getList" />
<HandleDialog v-if="isShowDialog" :type="rowType" :colorPasteList="colorPasteList" :row="currentRow" @closed="isShowDialog = false" @on-success="getList" /> <HandleDialog v-if="isShowDialog" :type="rowType" :row="currentRow" @closed="isShowDialog = false" @on-success="getList" />
</div> </div>
</template> </template>

@ -53,7 +53,7 @@
<pagination v-show="total > 0" :total="total" v-model:limit="queryParams.limit" v-model:page="queryParams.page" @pagination="getList" /> <pagination v-show="total > 0" :total="total" v-model:limit="queryParams.limit" v-model:page="queryParams.page" @pagination="getList" />
<HandleDialog v-if="isShowDialog" :type="rowType" :colorPasteList="colorPasteList" :row="currentRow" @closed="isShowDialog = false" @on-success="getList" /> <HandleDialog v-if="isShowDialog" :type="rowType" :row="currentRow" @closed="isShowDialog = false" @on-success="getList" />
</div> </div>
</template> </template>

Loading…
Cancel
Save