123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733 |
- <template>
- <el-dialog
- v-model="visible"
- title="选择地图位置"
- width="1200px"
- :before-close="handleClose"
- destroy-on-close
- class="map-fire-dialog"
- >
- <div class="map-dialog-content">
- <!-- 搜索框 -->
- <div id="panel" class="scrollbar1">
- <div id="searchBar">
- <el-select class="search-select" v-model="selectedSearchAdress" placeholder="地址">
- <el-option label="地址" value="1"></el-option>
- <el-option label="经纬度" value="2"></el-option>
- </el-select>
- <input class="search-input-latlng" v-if="selectedSearchAdress === '2'" @input="handleSearch" autocomplete="off" @keydown="handleKeyDown" placeholder="输入经纬度搜索,如23.117661,113.281272" />
- <input class="search-input-address" v-else id="searchInput" @input="handleSearch" autocomplete="off" placeholder="输入地址搜索" />
- <!-- <input id="searchInput" @input="handleSearch" autocomplete="off" @keydown="handleKeyDown" :placeholder="selectedSearchAdress === '1' ? '输入地址' : '输入经纬度'" /> -->
- </div>
- <div id="searchResults">暂无数据</div>
- </div>
- <!-- 地图容器 -->
- <div class="map-container" v-if="visible">
- <div id="container" class="map" style="width: 800px; height: 600px" tabindex="0"></div>
- </div>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button @click="handleClose">取消</el-button>
- <el-button type="primary" @click="handleConfirm">
- 确定
- </el-button>
- </div>
- </template>
- </el-dialog>
- </template>
- <script setup lang="ts">
- import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue'
- import { ElDialog, ElInput, ElButton, ElDescriptions, ElDescriptionsItem, ElIcon, ElMessage } from 'element-plus'
- import { Search } from '@element-plus/icons-vue'
- import AMapLoader from '@amap/amap-jsapi-loader'
- import html2canvas from 'html2canvas'
- import { uploadFileToServer, canvasToBlob, blobToFile } from '@/util/upload-utils'
- import { axios } from '@/request'
- import { addOrUpdateCaseTabulation } from '@/request/urls'
- import { user } from "@/store/user";
- // 添加高德地图类型声明
- declare const AMap: any
- declare const AMapUI: any
- // 定义组件props和emits
- interface Props {
- modelValue: boolean
- caseId?: string | number
- }
- interface LocationInfo {
- name?: string
- address?: string
- lat?: number
- lng?: number
- adcode?: string
- citycode?: string
- district?: string
- screenshotUrl?: string
- screenshotFileName?: string
- }
- const props = defineProps<Props>()
- const mapInput = ref('')
- const emit = defineEmits<{
- 'update:modelValue': [value: boolean]
- 'confirm': [location: LocationInfo]
- }>()
- // 响应式变量
- const visible = ref(false)
- const keyword = ref('')
- const selectedLocation = ref<LocationInfo | null>(null)
- const mapContainer = ref<HTMLDivElement>()
- // 高德地图相关变量
- let map: any = null
- let placeSearch: any = null
- let geocoder: any = null
- let currentMarker: any = null
- let poiPicker: any = null
- // 高德地图配置
- const AMAP_KEY = '2ae5a7713612a8d5a65cfd54c989c969'
- const selectedSearchAdress = ref('1')
- const searchInputValue = ref('')
- // 根基app打开不同地址
- const appId = import.meta.env.VITE_APP_APP || 'fire'
- const url = 'http://test-mix3d.4dkankan.com'
- // 防抖定时器
- let searchDebounceTimer: number | null = null
- // 监听visible变化
- watch(() => props.modelValue, (newVal) => {
- visible.value = newVal
- if (newVal) {
- nextTick(() => {
- initMap()
- })
- }
- }, { immediate: true })
- watch(visible, (newVal) => {
- selectedSearchAdress.value = '1'
- emit('update:modelValue', newVal)
- })
- // 监听搜索类型变化,切换时清空输入框和重置搜索
- watch(selectedSearchAdress, (newVal, oldVal) => {
- if (newVal !== oldVal) {
- // 清空输入框
- searchInputValue.value = ''
- const searchInput = document.getElementById('searchInput') as HTMLInputElement
- if (searchInput) {
- searchInput.value = ''
- }
-
- // 重置搜索结果
- const searchResults = document.getElementById('searchResults')
- if (searchResults) {
- searchResults.innerHTML = '暂无数据'
- }
-
- // 清空地图标记
- if (map) {
- map.clearMap()
- }
-
- // 根据搜索类型调整POI选择器的显示
- if (poiPicker) {
- if (newVal === '2') {
- // 经纬度搜索模式,隐藏选择列表
- poiPicker.hideSearchResults()
- } else {
- // 地址搜索模式,可以显示选择列表
- poiPicker.searchByKeyword('')
- }
- }
- }
- })
- // 初始化地图
- const initMap = async () => {
- map = new AMap.Map('container', {
- zoom: 11,
- key: AMAP_KEY, // 替换为你的API密钥
- center: [116.397513, 39.908739],
- WebGLParams: {
- preserveDrawingBuffer: true
- }
- });
- AMapUI.loadUI(['misc/PoiPicker'], function(PoiPicker) {
- poiPicker = new PoiPicker({
- input: 'searchInput',
- placeSearchOptions: {
- map: map,
- pageSize: 7,
- pageIndex: 1,
- },
- searchResultsContainer: 'searchResults'
- });
- poiPicker.on('poiPicked', function(poiResult) {
- poiPicker.hideSearchResults();
- map.clearMap();
- var source = poiResult.source, poi = poiResult.item;
- if (source !== 'search') {
- //suggest来源的,同样调用搜索
- poiPicker.searchByKeyword(poi.name);
- } else {
- // 舒琪说不需要marker
- // addMarker(poi.location.lng, poi.location.lat)
- // console.log(poi);
- }
- });
- poiPicker.onCityReady(function() {
- poiPicker.searchByKeyword('');
- });
- });
- }
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Enter') {
- if(selectedSearchAdress.value === '2'){
- console.log(11111)
- e.preventDefault()
- e.stopPropagation()
- return
- }
- }
- }
- // 验证经纬度格式的函数
- const validateCoordinates = (input: string): { isValid: boolean; lat?: number; lng?: number } => {
- // 移除空格
- const trimmed = input.trim()
-
- // 检查是否包含逗号
- if (!trimmed.includes(',')) {
- return { isValid: false }
- }
-
- // 分割经纬度
- const parts = trimmed.split(',')
- if (parts.length !== 2) {
- return { isValid: false }
- }
-
- // 转换为数字并验证
- const lat = parseFloat(parts[0].trim())
- const lng = parseFloat(parts[1].trim())
-
- // 验证是否为有效数字
- if (isNaN(lat) || isNaN(lng)) {
- return { isValid: false }
- }
-
- // 验证纬度范围 (-90 到 90)
- if (lat < -90 || lat > 90) {
- return { isValid: false }
- }
-
- // 验证经度范围 (-180 到 180)
- if (lng < -180 || lng > 180) {
- return { isValid: false }
- }
-
- return { isValid: true, lat, lng }
- }
- // 根据经纬度定位地图
- const locateByCoordinates = (lat: number, lng: number) => {
- if (!map) {
- ElMessage.error('地图未初始化')
- return
- }
-
- try {
- // 创建经纬度点
- const lngLat = new AMap.LngLat(lng, lat)
-
- // 设置地图中心点并调整缩放级别
- map.setZoomAndCenter(15, lngLat)
-
- // 清除之前的标记
- map.clearMap()
-
- // 添加标记
- const marker = new AMap.Marker({
- position: lngLat,
- content: '<div class="amap_lib_placeSearch_poi"></div>',
- offset: new AMap.Pixel(-10, -31)
- })
- map.add(marker)
-
- // 清空搜索结果显示
- const searchResults = document.getElementById('searchResults')
- if (searchResults) {
- searchResults.innerHTML = `
- <div style="padding: 16px; text-align: center; color: #606266;width:345px;height: 106px;border: 1px solid #D9D9D9;">
- <div style="font-weight: 500; margin-bottom: 8px;color:#67C23A;">经纬度定位成功</div>
- <div style="font-size: 14px;color: #A7A7A7;">
- 纬度: ${lat}<br/>
- 经度: ${lng}
- </div>
- </div>
- `
- }
-
- ElMessage.success('经纬度定位成功')
- } catch (error) {
- console.error('经纬度定位失败:', error)
- ElMessage.error('经纬度定位失败')
- }
- }
- // 清除防抖定时器
- const clearSearchDebounce = () => {
- if (searchDebounceTimer) {
- clearTimeout(searchDebounceTimer)
- searchDebounceTimer = null
- }
- }
- // 防抖搜索函数
- const debouncedSearch = (inputValue: string, searchType: string) => {
- clearSearchDebounce()
-
- searchDebounceTimer = setTimeout(() => {
- if (searchType === '2') {
- // 经纬度搜索
- if (!inputValue.trim()) {
- // 输入为空时,清空搜索结果
- const searchResults = document.getElementById('searchResults')
- if (searchResults) {
- searchResults.innerHTML = '暂无数据'
- }
- return
- }
-
- const validation = validateCoordinates(inputValue)
-
- if (validation.isValid && validation.lat !== undefined && validation.lng !== undefined) {
- // 经纬度格式正确,进行定位
- locateByCoordinates(validation.lat, validation.lng)
- } else {
- // 经纬度格式错误,只在搜索结果区域显示提示,不弹窗
- const searchResults = document.getElementById('searchResults')
- if (searchResults) {
- searchResults.innerHTML = `
- <div style="padding: 16px; text-align: center; width:345px;height: 106px;border: 1px solid #D9D9D9;">
- <div style="font-weight: 600; margin-bottom: 8px;color: #B3261E;">经纬度格式错误</div>
- <div style="font-size: 12px; line-height: 1.5;color:#A7A7A7;">
- 请输入正确的经纬度格式:<br/>
- 纬度,经度(例如:23.11766,113.28122)<br/>
- 纬度范围:-90 到 90<br/>
- 经度范围:-180 到 180
- </div>
- </div>
- `
- }
- }
- } else {
- console.log(22222)
- // 地址搜索,保持原有逻辑
- if (poiPicker) {
- poiPicker.placeSearch.opt.pageIndex = 1;
- poiPicker.searchByKeyword(inputValue)
- }
- }
- }, 500) // 500ms防抖延迟
- }
- const handleSearch = (e) => {
- console.log('handleSearch', e.target.value)
- const inputValue = e.target.value
- searchInputValue.value = inputValue
-
- // 对于经纬度搜索,强制隐藏POI选择列表
- if (selectedSearchAdress.value === '2' && poiPicker) {
- poiPicker.hideSearchResults()
- }
-
- // 使用防抖搜索
- debouncedSearch(inputValue, selectedSearchAdress.value)
- }
- // 添加标记
- const addMarker = (lng: number, lat: number) => {
- // 添加新标记
- var marker = new AMap.Marker({
- position: new AMap.LngLat(lng, lat),
- content: '<div class="amap_lib_placeSearch_poi"></div>',
- offset: new AMap.Pixel(-10, -31)
- });
- map.add(marker)
- }
- // 处理弹窗关闭
- const handleClose = () => {
- visible.value = false
- selectedLocation.value = null
- keyword.value = ''
-
- // 清理地图
- if (map) {
- map.destroy()
- map = null
- }
- }
- const getMapSize = () => {
- if (!map) {
- ElMessage.error('地图未初始化')
- return
- }
- try {
- // 获取当前地图的可视区域边界
- const bounds = map.getBounds()
- const southwest = bounds.getSouthWest() // 西南角
- const northeast = bounds.getNorthEast() // 东北角
- const northwest = new (window as any).AMap.LngLat(southwest.lng, northeast.lat) // 西北角
- const southeast = new (window as any).AMap.LngLat(northeast.lng, southwest.lat) // 东南角
-
- // 使用高德地图的几何工具计算距离(单位:米)
- const AMap = (window as any).AMap
-
- // 计算宽度(东西方向距离)
- const width = AMap.GeometryUtil.distance(southwest, southeast)
-
- // 计算高度(南北方向距离)
- const height = AMap.GeometryUtil.distance(southwest, northwest)
-
- // 计算面积(平方米)
- const area = width * height
-
- // 获取当前缩放级别
- const zoom = map.getZoom()
-
- // 获取地图中心点
- const center = map.getCenter()
-
- const viewportInfo = {
- width: width, // 宽度(米)
- height: height, // 高度(米)
- area: Math.round(area), // 面积(平方米)
- zoom: zoom, // 缩放级别
- center: {
- lat: center.lat,
- lng: center.lng
- },
- bounds: {
- southwest: { lat: southwest.lat, lng: southwest.lng },
- northeast: { lat: northeast.lat, lng: northeast.lng }
- }
- }
-
- console.log('当前可视区域信息:', viewportInfo)
-
- // ElMessage.success(`
- // 可视区域信息:
- // 宽度:${viewportInfo.width.toLocaleString()} 米
- // 高度:${viewportInfo.height.toLocaleString()} 米
- // 面积:${viewportInfo.area.toLocaleString()} 平方米
- // 缩放级别:${viewportInfo.zoom.toFixed(2)}
- // `)
- return {
- width: viewportInfo.width,
- height: viewportInfo.height,
- }
- } catch (error) {
- console.error('计算可视区域失败:', error)
- ElMessage.error('计算可视区域失败')
- }
- }
- const getCanvasImage = async (): Promise<{url: string, fileName: string} | null> => {
- try {
- // 使用html2canvas截图高德地图可视化区域
- const mapElement = document.getElementById('container')
- if (!mapElement) {
- ElMessage.error('地图容器未找到')
- return null
- }
- // ElMessage.info('正在生成地图截图...')
-
- // 配置html2canvas选项
- const canvas = await html2canvas(mapElement, {
- useCORS: true,
- allowTaint: true,
- scale: 1,
- logging: false,
- imageTimeout: 15000,
- // 忽略某些元素(比如控件)
- ignoreElements: (element) => {
- return element.classList.contains('amap-logo') ||
- element.classList.contains('amap-copyright') ||
- element.classList.contains('amap-controls')
- }
- })
- // 将canvas转换为Blob
- const blob = await canvasToBlob(canvas, 0.8, 'image/jpeg')
-
- // 生成文件名
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
- const fileName = `map-screenshot-${timestamp}.jpg`
-
- // 转换为File对象
- const file = blobToFile(blob, fileName)
-
- // 上传文件
- const uploadResult = await uploadFileToServer(file, (progressEvent) => {
- // 可选:显示上传进度
- if (progressEvent.lengthComputable) {
- const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
- console.log(`上传进度: ${percentCompleted}%`)
- }
- })
- console.log('上传成功:', uploadResult)
- return uploadResult
- } catch (error) {
- console.error('截图或上传失败:', error)
- ElMessage.error('截图或上传失败,请重试')
- return null
- }
- }
- // 处理确认选择
- const handleConfirm = async () => {
- try {
- if (!props.caseId) {
- ElMessage.error('缺少案件ID参数')
- return
- }
- // 获取地图尺寸信息
- const mapSizeInfo = getMapSize()
- if (!mapSizeInfo) {
- ElMessage.error('获取地图尺寸失败')
- return
- }
- // 获取截图并上传
- const uploadResult = await getCanvasImage()
- if (!uploadResult) {
- ElMessage.error('截图上传失败')
- return
- }
- // ElMessage.info('正在保存方位图信息...')
- // 调用案件制表接口
- const response = await axios({
- url: addOrUpdateCaseTabulation,
- method: 'POST',
- data: {
- caseId: props.caseId,
- width: mapSizeInfo.width,
- high: mapSizeInfo.height,
- listCover: '', // 封面图不传,等那边确定才会有
- mapUrl: uploadResult.url, // 地图URL
- title: '方位图'
- }
- })
- console.log('方位图保存成功:', response)
- if(appId === 'fire'){
- window.open(`${url}/draw/fire/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`, '_blank')
- } else if(appId === 'criminal'){
- window.open(`${url}/draw/criminal/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`, '_blank')
- } else if(appId === 'cjzfire'){
- window.open(`${url}/draw/cjzfire/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`, '_blank')
- } else if(appId === 'xmfire'){
- window.open(`${url}/draw/xmfire/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`, '_blank')
- } else{
- window.open(`${url}/draw/fire/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`, '_blank')
- }
- // console.log(`http://test-mix3d.4dkankan.com/draw/index.html#/tabulation?caseId=${props.caseId}&tabulationId=${response.data.id}&token=${user.value.token}`)
- handleClose()
- } catch (error) {
- console.error('保存方位图失败:', error)
- // ElMessage.error('保存方位图失败,请重试')
- }
- }
- // 组件卸载时清理
- onUnmounted(() => {
- if (map) {
- map.destroy()
- }
- // 清除防抖定时器
- clearSearchDebounce()
- })
- </script>
- <style lang="scss">
- .map-fire-dialog{
- .el-dialog__header{
- text-align: left;
- border-bottom: 1px solid #dcdfe6;
- margin-bottom: 16px;
- }
- // https://a.amap.com/jsapi/static/image/plugin/marker_red.png
- .amap-marker{
- display: none!important;
- }
- .amap-info-contentContainer{
- display: none!important;
- }
- }
- </style>
- <style scoped lang="scss">
- .map-dialog-content {
- height: 600px;
- display: flex;
- // flex-direction: column;
- }
- #panel {
- // position: absolute;
- // top: 24px;
- // left: 24px;
- height: 100%;
- overflow: auto;
- width: 400px;
- padding-right: 16px;
- // z-index: 999;
- background: #fff;
- :deep(.amap-ui-poi-picker-sugg-container) {
- display: none;
- }
- #searchBar{
- display: flex;
- width: 380px;
- text-align: left;
- margin-bottom: 16px;
- }
- .search-select{
- width: 100px;
- height: 40px!important;
-
- :deep(.el-select__wrapper){
- border-radius: 4px 0 0 4px;
- height: 40px;
- }
- }
- .search-input-latlng{
- width: 260px;
- height: 38px;
- border-radius: 0 4px 4px 0;
- border: 1px solid #dcdfe6;
- border-left: 0;
- padding: 0 10px;
- font-size: 14px;
- color: #303133;
- background: #fff;
- outline: none;
- }
- :deep(#searchInput) {
- width: 260px;
- height: 38px;
- border-radius: 0 4px 4px 0;
- border: 1px solid #dcdfe6;
- border-left: 0;
- padding: 0 10px;
- font-size: 14px;
- color: #303133;
- background: #fff;
- outline: none;
- }
- }
- #searchResults {
- width: 100%;
- height: 540px;
- overflow: auto;
- }
- .search-section {
- position: relative;
- margin-bottom: 16px;
- z-index: 1000;
- }
- .search-results {
- position: absolute;
- top: 100%;
- left: 0;
- right: 0;
- background: white;
- border: 1px solid #dcdfe6;
- border-top: none;
- border-radius: 0 0 4px 4px;
- max-height: 200px;
- overflow-y: auto;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- }
- .search-item {
- padding: 12px 16px;
- cursor: pointer;
- border-bottom: 1px solid #f0f0f0;
- transition: background-color 0.2s;
- &:hover {
- background-color: #f5f7fa;
- }
- &:last-child {
- border-bottom: none;
- }
- }
- .item-name {
- font-weight: 500;
- color: #303133;
- margin-bottom: 4px;
- }
- .item-address {
- font-size: 12px;
- color: #909399;
- }
- .map-container {
- width: 800px;
- height: 600px;
- position: relative;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- overflow: hidden;
- }
- .amap-wrapper {
- width: 100%;
- height: 100%;
- }
- .location-info {
- margin-top: 16px;
- }
- .dialog-footer {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- }
- // 覆盖高德地图控件样式
- :deep(.amap-logo) {
- display: none !important;
- }
- :deep(.amap-copyright) {
- display: none !important;
- }
- </style>
|