三维地图的创建和三维地图的模型上传,保存和删除功能
This commit is contained in:
parent
3cd4ab510c
commit
6cdc46fedc
@ -2,13 +2,13 @@ export default {
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/f': {
|
'/api/f': {
|
||||||
target: 'http://192.168.1.58:8100',
|
target: 'http://192.168.1.20:8100',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: path => path.replace(/^\/api\/f/, 'de2api')
|
rewrite: path => path.replace(/^\/api\/f/, 'de2api')
|
||||||
},
|
},
|
||||||
// 使用 proxy 实例
|
// 使用 proxy 实例
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://192.168.1.58:8100',
|
target: 'http://192.168.1.20:8100',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: path => path.replace(/^\/api/, 'de2api')
|
rewrite: path => path.replace(/^\/api/, 'de2api')
|
||||||
}
|
}
|
||||||
|
@ -386,7 +386,8 @@ const boardMoveActive = computed(() => {
|
|||||||
'symbolic-map',
|
'symbolic-map',
|
||||||
'heat-map',
|
'heat-map',
|
||||||
't-heatmap',
|
't-heatmap',
|
||||||
'circle-packing'
|
'circle-packing',
|
||||||
|
'three-map'
|
||||||
]
|
]
|
||||||
return element.value.isPlugin || CHARTS.includes(element.value.innerType)
|
return element.value.isPlugin || CHARTS.includes(element.value.innerType)
|
||||||
})
|
})
|
||||||
|
273
core/core-frontend/src/components/three-display/index.vue
Normal file
273
core/core-frontend/src/components/three-display/index.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<div v-loading="loading">
|
||||||
|
<div class="all_conter">
|
||||||
|
<el-button type="primary" @click="importclick()" style="width: 200px;">导入三维模型</el-button>
|
||||||
|
<input type="file" id="avatar" style="display:none" @change="changeFiless">
|
||||||
|
</div>
|
||||||
|
<el-scrollbar height="59vh">
|
||||||
|
<div class="model-container">
|
||||||
|
<div v-for="(model, index) in modelAll" :key="index" class="model-wrapper">
|
||||||
|
<div class="positionbutton" @click="deleteModel(index)">删除</div>
|
||||||
|
<div :ref="(el) => setCanvasRef(el, index)" class="model-canvas"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||||
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
|
||||||
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||||
|
import { uploadFileResult } from '@/api/staticResource'
|
||||||
|
//保存 1.拿数据
|
||||||
|
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||||
|
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { imgUrlTrans } from '@/utils/imgUtils'
|
||||||
|
const dvMainStore = dvMainStoreWithOut()
|
||||||
|
const { curComponent } = storeToRefs(dvMainStore)
|
||||||
|
const snapshotStore = snapshotStoreWithOut()
|
||||||
|
// 模型路径
|
||||||
|
let threeModel: any = ref([])
|
||||||
|
let modelAll: any = ref([])
|
||||||
|
// 用于保存每个模型容器的引用
|
||||||
|
const canvasRefs = ref([])
|
||||||
|
|
||||||
|
// 存储每个模型对应的 renderer / controls / scene 等实例
|
||||||
|
const scenes = []
|
||||||
|
const cameras = []
|
||||||
|
const renderers = []
|
||||||
|
const controlsList = []
|
||||||
|
let loading = ref(true)
|
||||||
|
// 设置 canvas 引用
|
||||||
|
const setCanvasRef = (el, index) => {
|
||||||
|
if (el) {
|
||||||
|
canvasRefs.value[index] = el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化场景
|
||||||
|
const initScene = (container, modelPath, index) => {
|
||||||
|
const scene = new THREE.Scene()
|
||||||
|
scenes.push(scene)
|
||||||
|
|
||||||
|
const camera = new THREE.PerspectiveCamera(20, 198 / 148, 0.1, 1000)
|
||||||
|
cameras.push(camera)
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
container.appendChild(canvas)
|
||||||
|
|
||||||
|
const renderer = new THREE.WebGLRenderer({ canvas })
|
||||||
|
renderer.setSize(198, 148)
|
||||||
|
renderer.setPixelRatio(Math.min(1.5, window.devicePixelRatio)) // 降低像素比
|
||||||
|
renderers.push(renderer)
|
||||||
|
|
||||||
|
// 添加光源
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 5)
|
||||||
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
// 创建相机初始位置
|
||||||
|
camera.position.z = 5
|
||||||
|
|
||||||
|
// 创建控制器
|
||||||
|
const controls = new OrbitControls(camera, canvas)
|
||||||
|
controls.dampingFactor = 0.05
|
||||||
|
controls.enableDamping = true
|
||||||
|
controls.update()
|
||||||
|
|
||||||
|
controls.addEventListener('change', () => {
|
||||||
|
if (!renderers[index].autoClear) {
|
||||||
|
renderers[index].clear()
|
||||||
|
}
|
||||||
|
renderers[index].render(scene, camera)
|
||||||
|
})
|
||||||
|
|
||||||
|
controlsList.push(controls)
|
||||||
|
|
||||||
|
// 加载模型
|
||||||
|
const dracoLoader = new DRACOLoader()
|
||||||
|
dracoLoader.setDecoderPath('./static/js/')
|
||||||
|
dracoLoader.setDecoderConfig({ type: 'js' })
|
||||||
|
|
||||||
|
const loader = new GLTFLoader()
|
||||||
|
loader.setDRACOLoader(dracoLoader)
|
||||||
|
|
||||||
|
loader.load(modelPath, (gltf) => {
|
||||||
|
const model = gltf.scene
|
||||||
|
scene.add(model)
|
||||||
|
|
||||||
|
// 计算模型包围盒的中心
|
||||||
|
const box = new THREE.Box3().setFromObject(model)
|
||||||
|
const center = box.getCenter(new THREE.Vector3())
|
||||||
|
|
||||||
|
// 将模型中心设为本地坐标系原点
|
||||||
|
model.position.sub(center)
|
||||||
|
|
||||||
|
// 设置 OrbitControls 的目标为中心点
|
||||||
|
controls.target.copy(center)
|
||||||
|
controls.update()
|
||||||
|
|
||||||
|
// 初始渲染一次
|
||||||
|
renderer.render(scene, camera)
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error('Error loading model:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//上传文件
|
||||||
|
function importclick() {
|
||||||
|
const avatar = document.getElementById('avatar');
|
||||||
|
avatar?.click();
|
||||||
|
}
|
||||||
|
function changeFiless(e: any) {
|
||||||
|
loading.value = true
|
||||||
|
const files = new FormData()
|
||||||
|
files.append('file', e.target.files[0])
|
||||||
|
uploadFileResult(e.target.files[0], fileUrl => {
|
||||||
|
threeModel.value.push({ path: fileUrl })
|
||||||
|
modelAll.value = getmodelurl(threeModel.value)
|
||||||
|
curComponent.value.threeModel = threeModel.value
|
||||||
|
snapshotStore.recordSnapshotCacheToMobile('threeModel')
|
||||||
|
batchOptChange('threeModel')
|
||||||
|
nextTick(() => {
|
||||||
|
// 手动重新渲染新添加的模型
|
||||||
|
const newIndex = modelAll.value.length - 1
|
||||||
|
const container = canvasRefs.value[newIndex]
|
||||||
|
if (container) {
|
||||||
|
initScene(container, modelAll.value[newIndex].path, newIndex)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var file: any = document.getElementById('avatar');
|
||||||
|
file.value = '';
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function getmodelurl(modelPath: any) {
|
||||||
|
let urlArr = JSON.parse(JSON.stringify(modelPath))
|
||||||
|
urlArr.map((item: any) => {
|
||||||
|
item.path = imgUrlTrans(item.path)
|
||||||
|
})
|
||||||
|
return urlArr
|
||||||
|
}
|
||||||
|
const batchOptChange = (custom, property, value, subProp?) => {
|
||||||
|
dvMainStore.setChangeProperties({
|
||||||
|
custom: custom,
|
||||||
|
property: property,
|
||||||
|
value: value,
|
||||||
|
subProp: subProp
|
||||||
|
})
|
||||||
|
snapshotStore.recordSnapshotCache('renderChart')
|
||||||
|
}
|
||||||
|
const deleteModel = (index: number) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要删除该三维模型吗?',
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// 清理 THREE 渲染资源
|
||||||
|
const scene = scenes[index]
|
||||||
|
const renderer = renderers[index]
|
||||||
|
const control = controlsList[index]
|
||||||
|
|
||||||
|
scene.clear()
|
||||||
|
renderer.dispose()
|
||||||
|
control.dispose()
|
||||||
|
|
||||||
|
// 删除数组中的对应项
|
||||||
|
scenes.splice(index, 1)
|
||||||
|
cameras.splice(index, 1)
|
||||||
|
renderers.splice(index, 1)
|
||||||
|
controlsList.splice(index, 1)
|
||||||
|
|
||||||
|
// 删除数据源中的模型
|
||||||
|
threeModel.value.splice(index, 1)
|
||||||
|
modelAll.value.splice(index, 1)
|
||||||
|
|
||||||
|
// 同步保存到 Pinia store
|
||||||
|
curComponent.value.threeModel = threeModel.value
|
||||||
|
snapshotStore.recordSnapshotCacheToMobile('threeModel')
|
||||||
|
batchOptChange('threeModel')
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
// 监听 threeModel 的变化
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
threeModel.value = curComponent.value.threeModel ? curComponent.value.threeModel : []
|
||||||
|
modelAll.value = getmodelurl(threeModel.value)
|
||||||
|
nextTick(() => {
|
||||||
|
canvasRefs.value.forEach((container, index) => {
|
||||||
|
if (container) {
|
||||||
|
initScene(container, modelAll.value[index].path, index)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
scenes.forEach(scene => {
|
||||||
|
scene.clear()
|
||||||
|
})
|
||||||
|
renderers.forEach(renderer => {
|
||||||
|
renderer.dispose()
|
||||||
|
})
|
||||||
|
controlsList.forEach(control => control.dispose())
|
||||||
|
|
||||||
|
scenes.length = 0
|
||||||
|
cameras.length = 0
|
||||||
|
renderers.length = 0
|
||||||
|
controlsList.length = 0
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.model-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-wrapper {
|
||||||
|
width: 200px;
|
||||||
|
height: 150px;
|
||||||
|
background-color: #00000050;
|
||||||
|
margin: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
will-change: transform; // 提升合成层
|
||||||
|
border: 1px solid #ffffff00;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.positionbutton {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positionbutton:hover {
|
||||||
|
color: #1989fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-wrapper:hover {
|
||||||
|
border: 1px solid #1989fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all_conter {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -176,7 +176,8 @@ const state = reactive({
|
|||||||
'word-cloud',
|
'word-cloud',
|
||||||
'flow-map',
|
'flow-map',
|
||||||
'bidirectional-bar',
|
'bidirectional-bar',
|
||||||
'symbolic-map'
|
'symbolic-map',
|
||||||
|
'three-map'
|
||||||
],
|
],
|
||||||
linkageExcludeViewType: [
|
linkageExcludeViewType: [
|
||||||
'richTextView',
|
'richTextView',
|
||||||
@ -187,7 +188,8 @@ const state = reactive({
|
|||||||
'word-cloud',
|
'word-cloud',
|
||||||
'flow-map',
|
'flow-map',
|
||||||
'bidirectional-bar',
|
'bidirectional-bar',
|
||||||
'symbolic-map'
|
'symbolic-map',
|
||||||
|
'three-map'
|
||||||
],
|
],
|
||||||
copyData: null,
|
copyData: null,
|
||||||
hyperlinksSetVisible: false,
|
hyperlinksSetVisible: false,
|
||||||
|
@ -422,7 +422,7 @@ const openMessageLoading = cb => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 地图
|
// 地图
|
||||||
const mapChartTypes = ['bubble-map', 'flow-map', 'heat-map', 'map', 'symbolic-map']
|
const mapChartTypes = ['bubble-map', 'flow-map', 'heat-map', 'map', 'symbolic-map','three-map']
|
||||||
const htmlToImage = () => {
|
const htmlToImage = () => {
|
||||||
downLoading.value = mapChartTypes.includes(viewInfo.value.type) ? false : true
|
downLoading.value = mapChartTypes.includes(viewInfo.value.type) ? false : true
|
||||||
useEmitt().emitter.emit('renderChart-' + viewInfo.value.id)
|
useEmitt().emitter.emit('renderChart-' + viewInfo.value.id)
|
||||||
|
@ -1,26 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-dropdown
|
<el-dropdown :id="'view-track-bar-' + chartId" :teleported="false" trigger="click" @visible-change="visibleChange">
|
||||||
:id="'view-track-bar-' + chartId"
|
|
||||||
:teleported="false"
|
|
||||||
trigger="click"
|
|
||||||
@visible-change="visibleChange"
|
|
||||||
>
|
|
||||||
<input id="input" ref="trackButton" type="button" hidden />
|
<input id="input" ref="trackButton" type="button" hidden />
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<div :class="{ 'data-mobile': isDataVMobile }">
|
<div :class="{ 'data-mobile': isDataVMobile }">
|
||||||
<el-dropdown-menu
|
<el-dropdown-menu class="track-menu" :style="{ 'font-family': fontFamily }" :append-to-body="false">
|
||||||
class="track-menu"
|
<el-dropdown-item v-for="(item, key) in trackMenu" :key="key" @mousedown.stop
|
||||||
:style="{ 'font-family': fontFamily }"
|
@click="trackMenuClick(item)"><span class="menu-item">{{ state.i18n_map[item] }}</span></el-dropdown-item>
|
||||||
:append-to-body="false"
|
|
||||||
>
|
|
||||||
<el-dropdown-item
|
|
||||||
v-for="(item, key) in trackMenu"
|
|
||||||
:key="key"
|
|
||||||
@mousedown.stop
|
|
||||||
@click="trackMenuClick(item)"
|
|
||||||
><span class="menu-item">{{ state.i18n_map[item] }}</span></el-dropdown-item
|
|
||||||
>
|
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -2003,6 +2003,7 @@ export default {
|
|||||||
export_raw_details: 'Raw Details',
|
export_raw_details: 'Raw Details',
|
||||||
field_is_empty_export_error: 'No fields available, unable to export',
|
field_is_empty_export_error: 'No fields available, unable to export',
|
||||||
chart_symbolic_map: 'Symbolic map',
|
chart_symbolic_map: 'Symbolic map',
|
||||||
|
three_map: 'Three map',
|
||||||
symbolic: 'Symbolic',
|
symbolic: 'Symbolic',
|
||||||
symbolic_shape: 'Symbolic Shape',
|
symbolic_shape: 'Symbolic Shape',
|
||||||
symbolic_upload_hint: 'Supports SVG, JPG, JPEG, PNG files within 1MB',
|
symbolic_upload_hint: 'Supports SVG, JPG, JPEG, PNG files within 1MB',
|
||||||
|
@ -1950,6 +1950,7 @@ export default {
|
|||||||
export_raw_details: '導出原始明細',
|
export_raw_details: '導出原始明細',
|
||||||
field_is_empty_export_error: '目前無欄位,無法匯出',
|
field_is_empty_export_error: '目前無欄位,無法匯出',
|
||||||
chart_symbolic_map: '符號地圖',
|
chart_symbolic_map: '符號地圖',
|
||||||
|
three_map: '三維地圖',
|
||||||
symbolic: '符號',
|
symbolic: '符號',
|
||||||
symbolic_shape: '符號形狀',
|
symbolic_shape: '符號形狀',
|
||||||
symbolic_upload_hint: '支持 1MB 以內的 SVG, JPG, JPEG, PNG 文件',
|
symbolic_upload_hint: '支持 1MB 以內的 SVG, JPG, JPEG, PNG 文件',
|
||||||
|
@ -1956,6 +1956,7 @@ export default {
|
|||||||
field_is_empty_export_error: '当前无字段,无法导出',
|
field_is_empty_export_error: '当前无字段,无法导出',
|
||||||
chart_symbolic_map: '符号地图',
|
chart_symbolic_map: '符号地图',
|
||||||
symbolic: '符号',
|
symbolic: '符号',
|
||||||
|
three_map: '三维地图',
|
||||||
symbolic_shape: '符号形状',
|
symbolic_shape: '符号形状',
|
||||||
symbolic_upload_hint: '支持 1MB 以内的 SVG, JPG, JPEG, PNG 文件',
|
symbolic_upload_hint: '支持 1MB 以内的 SVG, JPG, JPEG, PNG 文件',
|
||||||
symbolic_range: '区间',
|
symbolic_range: '区间',
|
||||||
|
@ -9,6 +9,7 @@ import icon_refresh_outlined from '@/assets/svg/icon_refresh_outlined.svg'
|
|||||||
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
|
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
|
||||||
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
|
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
|
||||||
import icon_copy_outlined from '@/assets/svg/icon_copy_outlined.svg'
|
import icon_copy_outlined from '@/assets/svg/icon_copy_outlined.svg'
|
||||||
|
import threeDisplay from '@/components/three-display/index.vue'
|
||||||
import {
|
import {
|
||||||
PropType,
|
PropType,
|
||||||
reactive,
|
reactive,
|
||||||
@ -321,6 +322,8 @@ const chartStyleShow = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const chartViewInstance = computed(() => {
|
const chartViewInstance = computed(() => {
|
||||||
|
// console.log(chartViewManager.getChartView(view.value.render, view.value.type))
|
||||||
|
// debugger
|
||||||
return chartViewManager.getChartView(view.value.render, view.value.type)
|
return chartViewManager.getChartView(view.value.render, view.value.type)
|
||||||
})
|
})
|
||||||
const showAxis = (axis: AxisType) => chartViewInstance.value?.axis?.includes(axis)
|
const showAxis = (axis: AxisType) => chartViewInstance.value?.axis?.includes(axis)
|
||||||
@ -3131,9 +3134,9 @@ const deleteChartFieldItem = id => {
|
|||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!--filter-->
|
<!--filter-->
|
||||||
<el-row class="padding-lr drag-data no-top-border no-top-padding">
|
<el-row v-if="view.type != 'three-map'" class="padding-lr drag-data no-top-border no-top-padding">
|
||||||
<div class="form-draggable-title">
|
<div class="form-draggable-title">
|
||||||
<span>
|
<span >
|
||||||
{{ t('chart.result_filter') }}
|
{{ t('chart.result_filter') }}
|
||||||
</span>
|
</span>
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
@ -3180,7 +3183,9 @@ const deleteChartFieldItem = id => {
|
|||||||
<span>{{ $t('chart.filter') }}</span>
|
<span>{{ $t('chart.filter') }}</span>
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<div v-if="view.type == 'three-map'">
|
||||||
|
<threeDisplay :view="view" />
|
||||||
|
</div>
|
||||||
<el-row v-if="showAggregate" class="refresh-area">
|
<el-row v-if="showAggregate" class="refresh-area">
|
||||||
<el-form-item
|
<el-form-item
|
||||||
class="form-item no-margin-bottom"
|
class="form-item no-margin-bottom"
|
||||||
@ -3306,7 +3311,7 @@ const deleteChartFieldItem = id => {
|
|||||||
</el-row>
|
</el-row>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane
|
<el-tab-pane
|
||||||
name="style"
|
name="style"
|
||||||
|
@ -1536,6 +1536,13 @@ export const CHART_TYPE_CONFIGS = [
|
|||||||
value: 'symbolic-map',
|
value: 'symbolic-map',
|
||||||
title: t('chart.chart_symbolic_map'),
|
title: t('chart.chart_symbolic_map'),
|
||||||
icon: 'symbolic-map'
|
icon: 'symbolic-map'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
render: 'antv',
|
||||||
|
category: 'map',
|
||||||
|
value: 'three-map',
|
||||||
|
title: t('chart.three_map'),
|
||||||
|
icon: 'three-map'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
L7ChartView,
|
||||||
|
L7Config,
|
||||||
|
L7DrawConfig,
|
||||||
|
L7Wrapper
|
||||||
|
} from '@/views/chart/components/js/panel/types/impl/l7'
|
||||||
|
import { MAP_EDITOR_PROPERTY_INNER } from '@/views/chart/components/js/panel/charts/map/common'
|
||||||
|
import {
|
||||||
|
parseJson,
|
||||||
|
} from '@/views/chart/components/js/util'
|
||||||
|
import { deepCopy } from '@/utils/utils'
|
||||||
|
import { Scene } from '@antv/l7-scene'
|
||||||
|
import {
|
||||||
|
getMapCenter,
|
||||||
|
getMapScene,
|
||||||
|
getMapStyle,
|
||||||
|
mapRendered
|
||||||
|
} from '@/views/chart/components/js/panel/common/common_antv'
|
||||||
|
import { HeatmapLayer } from '@antv/l7-layers'
|
||||||
|
import { DEFAULT_BASIC_STYLE } from '@/views/chart/components/editor/util/chart'
|
||||||
|
/**
|
||||||
|
* 符号地图
|
||||||
|
*/
|
||||||
|
export class ThreeMap extends L7ChartView<Scene, L7Config> {
|
||||||
|
properties: EditorProperty[] = [
|
||||||
|
'background-overall-component',
|
||||||
|
'border-style',
|
||||||
|
'basic-style-selector',
|
||||||
|
'title-selector',
|
||||||
|
'label-selector',
|
||||||
|
'tooltip-selector',
|
||||||
|
'threshold'
|
||||||
|
]
|
||||||
|
propertyInner: EditorPropertyInner = {
|
||||||
|
...MAP_EDITOR_PROPERTY_INNER,
|
||||||
|
'basic-style-selector': [
|
||||||
|
'colors',
|
||||||
|
'alpha',
|
||||||
|
'mapBaseStyle',
|
||||||
|
'zoom',
|
||||||
|
'showLabel',
|
||||||
|
'autoFit',
|
||||||
|
'mapCenter',
|
||||||
|
'zoomLevel'
|
||||||
|
],
|
||||||
|
|
||||||
|
'label-selector': ['color', 'fontSize', 'showFields', 'customContent'],
|
||||||
|
'tooltip-selector': [
|
||||||
|
'color',
|
||||||
|
'fontSize',
|
||||||
|
'showFields',
|
||||||
|
'customContent',
|
||||||
|
'show',
|
||||||
|
'backgroundColor',
|
||||||
|
'carousel'
|
||||||
|
],
|
||||||
|
threshold: ['lineThreshold']
|
||||||
|
}
|
||||||
|
axis: AxisType[] = ['extLabel', 'extTooltip']
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('three-map', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
async drawChart(drawOption: L7DrawConfig<L7Config>) {
|
||||||
|
// console.log('111111111')
|
||||||
|
// debugger
|
||||||
|
const { chart, container, action } = drawOption
|
||||||
|
const containerDom = document.getElementById(container)
|
||||||
|
const rect = containerDom?.getBoundingClientRect()
|
||||||
|
// 修改后
|
||||||
|
if (rect?.height <= 0) {
|
||||||
|
// 容器不可见时,监听尺寸变化并重试绘制
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
observer.disconnect()
|
||||||
|
this.drawChart(drawOption) // 重新绘制
|
||||||
|
})
|
||||||
|
observer.observe(containerDom)
|
||||||
|
return new L7Wrapper(drawOption.chartObj?.getScene(), [])
|
||||||
|
}
|
||||||
|
const xAxis = deepCopy(chart.xAxis)
|
||||||
|
let basicStyle
|
||||||
|
let miscStyle
|
||||||
|
if (chart.customAttr) {
|
||||||
|
basicStyle = parseJson(chart.customAttr).basicStyle
|
||||||
|
miscStyle = parseJson(chart.customAttr).misc
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapKey = await this.getMapKey()
|
||||||
|
const mapStyle = getMapStyle(mapKey, basicStyle)
|
||||||
|
|
||||||
|
let center = getMapCenter(basicStyle)
|
||||||
|
// 联动时,聚焦到数据点,多个取第一个
|
||||||
|
if (
|
||||||
|
chart.chartExtRequest?.linkageFilters?.length &&
|
||||||
|
xAxis?.length === 2 &&
|
||||||
|
chart.data?.tableRow.length
|
||||||
|
) {
|
||||||
|
// 经度
|
||||||
|
const lng = chart.data?.tableRow?.[0][chart.xAxis[0].gisbiName]
|
||||||
|
// 纬度
|
||||||
|
const lat = chart.data?.tableRow?.[0][chart.xAxis[1].gisbiName]
|
||||||
|
center = [lng, lat]
|
||||||
|
}
|
||||||
|
const chartObj = drawOption.chartObj as unknown as L7Wrapper<L7Config, Scene>
|
||||||
|
let scene = chartObj?.getScene()
|
||||||
|
scene = await getMapScene(
|
||||||
|
chart,
|
||||||
|
scene,
|
||||||
|
container,
|
||||||
|
mapKey,
|
||||||
|
basicStyle,
|
||||||
|
miscStyle,
|
||||||
|
mapStyle,
|
||||||
|
center
|
||||||
|
)
|
||||||
|
this.configZoomButton(chart, scene, mapKey)
|
||||||
|
const config: L7Config = new HeatmapLayer({
|
||||||
|
name: 'line',
|
||||||
|
blend: 'normal',
|
||||||
|
autoFit: !(basicStyle.autoFit === false)
|
||||||
|
})
|
||||||
|
.source(chart.data?.data, {
|
||||||
|
parser: {
|
||||||
|
type: 'json',
|
||||||
|
x: 'x',
|
||||||
|
y: 'y'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.size('value', [0, 1.0]) // weight映射通道
|
||||||
|
.shape(basicStyle.heatMapType ?? DEFAULT_BASIC_STYLE.heatMapType)
|
||||||
|
|
||||||
|
config.style({
|
||||||
|
intensity: basicStyle.heatMapIntensity ?? DEFAULT_BASIC_STYLE.heatMapIntensity,
|
||||||
|
radius: basicStyle.heatMapRadius ?? DEFAULT_BASIC_STYLE.heatMapRadius,
|
||||||
|
rampColors: {
|
||||||
|
colors: basicStyle.colors.reverse(),
|
||||||
|
positions: [0, 0.11, 0.22, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 1.0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// const configList: L7Config[] = []
|
||||||
|
scene.render()
|
||||||
|
return new L7Wrapper(scene, config)
|
||||||
|
}
|
||||||
|
}
|
@ -241,7 +241,7 @@ export function getRemark(chart) {
|
|||||||
|
|
||||||
export const quotaViews = ['label', 'richTextView', 'indicator', 'gauge', 'liquid']
|
export const quotaViews = ['label', 'richTextView', 'indicator', 'gauge', 'liquid']
|
||||||
// 地图
|
// 地图
|
||||||
const mapChartTypes = ['bubble-map', 'flow-map', 'heat-map', 'map', 'symbolic-map']
|
const mapChartTypes = ['bubble-map', 'flow-map', 'heat-map', 'map', 'symbolic-map','three-map']
|
||||||
// 分布图
|
// 分布图
|
||||||
const distributionChartTypes = [
|
const distributionChartTypes = [
|
||||||
'pie',
|
'pie',
|
||||||
|
@ -28,15 +28,7 @@ import { isDashboard, trackBarStyleCheck } from '@/utils/canvasUtils'
|
|||||||
import { useEmitt } from '@/hooks/web/useEmitt'
|
import { useEmitt } from '@/hooks/web/useEmitt'
|
||||||
import { L7ChartView } from '@/views/chart/components/js/panel/types/impl/l7'
|
import { L7ChartView } from '@/views/chart/components/js/panel/types/impl/l7'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { ExportImage, Scale, Fullscreen, Control, Scene, TileLayer } from '@antv/l7'
|
import { ExportImage, Scale, Fullscreen, Control} from '@antv/l7'
|
||||||
import { GaodeMap } from '@antv/l7-maps';
|
|
||||||
//三维导入
|
|
||||||
import * as THREE from 'three';
|
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
|
|
||||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' // 控制器
|
|
||||||
import { ThreeLayer, ThreeRender } from '@antv/l7-three';
|
|
||||||
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const dvMainStore = dvMainStoreWithOut()
|
const dvMainStore = dvMainStoreWithOut()
|
||||||
const { nowPanelTrackInfo, nowPanelJumpInfo, mobileInPc, embeddedCallBack, inMobile } =
|
const { nowPanelTrackInfo, nowPanelJumpInfo, mobileInPc, embeddedCallBack, inMobile } =
|
||||||
@ -241,7 +233,7 @@ const calcData = async (view, callback) => {
|
|||||||
callback?.()
|
callback?.()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (['bubble-map', 'map', 'flow-map', 'heat-map'].includes(view.type)) {
|
if (['bubble-map', 'map', 'flow-map', 'heat-map','three-map'].includes(view.type)) {
|
||||||
await renderChart(view, callback)
|
await renderChart(view, callback)
|
||||||
}
|
}
|
||||||
callback?.()
|
callback?.()
|
||||||
@ -249,6 +241,7 @@ const calcData = async (view, callback) => {
|
|||||||
}
|
}
|
||||||
let curView
|
let curView
|
||||||
const renderChart = async (view, callback?) => {
|
const renderChart = async (view, callback?) => {
|
||||||
|
console.log('renderChart',view.type, callback)
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -263,6 +256,8 @@ const renderChart = async (view, callback?) => {
|
|||||||
const chartView = chartViewManager.getChartView(view.render, view.type)
|
const chartView = chartViewManager.getChartView(view.render, view.type)
|
||||||
recursionTransObj(customAttrTrans, chart.customAttr, scale.value, terminal.value)
|
recursionTransObj(customAttrTrans, chart.customAttr, scale.value, terminal.value)
|
||||||
recursionTransObj(customStyleTrans, chart.customStyle, scale.value, terminal.value)
|
recursionTransObj(customStyleTrans, chart.customStyle, scale.value, terminal.value)
|
||||||
|
console.log('chart', chart)
|
||||||
|
console.log( chartView)
|
||||||
switch (chartView.library) {
|
switch (chartView.library) {
|
||||||
case ChartLibraryType.L7_PLOT:
|
case ChartLibraryType.L7_PLOT:
|
||||||
await renderL7Plot(chart, chartView as L7PlotChartView<any, any>, callback)
|
await renderL7Plot(chart, chartView as L7PlotChartView<any, any>, callback)
|
||||||
@ -344,7 +339,6 @@ const renderL7Plot = async (chart: ChartObj, chartView: L7PlotChartView<any, any
|
|||||||
|
|
||||||
class SatelliteControl extends Control {
|
class SatelliteControl extends Control {
|
||||||
protected onAdd() {
|
protected onAdd() {
|
||||||
// debugger
|
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'l7-control-button l7-satellite-control';
|
btn.className = 'l7-control-button l7-satellite-control';
|
||||||
btn.innerHTML = '卫星';
|
btn.innerHTML = '卫星';
|
||||||
@ -398,7 +392,6 @@ const renderL7 = async (chart: ChartObj, chartView: L7ChartView<any, any>, callb
|
|||||||
chart: chart,
|
chart: chart,
|
||||||
action
|
action
|
||||||
});
|
});
|
||||||
|
|
||||||
// 清除已有比例尺
|
// 清除已有比例尺
|
||||||
if (!scaleControl) {
|
if (!scaleControl) {
|
||||||
scaleControl = new Scale({
|
scaleControl = new Scale({
|
||||||
@ -425,188 +418,12 @@ const renderL7 = async (chart: ChartObj, chartView: L7ChartView<any, any>, callb
|
|||||||
myChart.getScene()?.addControl(satelliteControlInstance);
|
myChart.getScene()?.addControl(satelliteControlInstance);
|
||||||
}
|
}
|
||||||
// ====== 修复完成 ======
|
// ====== 修复完成 ======
|
||||||
threel7()
|
|
||||||
myChart?.render();
|
myChart?.render();
|
||||||
callback?.();
|
callback?.();
|
||||||
emit('resetLoading');
|
emit('resetLoading');
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
//三维模型集成
|
|
||||||
let threeJSLayerRef:any = null
|
|
||||||
const threel7 = () => {
|
|
||||||
// ====== 三维模型集成 ======
|
|
||||||
const scene = myChart.getScene(); // 获取场景
|
|
||||||
scene.registerRenderService(ThreeRender);
|
|
||||||
threeJSLayerRef = new ThreeLayer({
|
|
||||||
enableMultiPassRenderer: false,
|
|
||||||
onAddMeshes: (threeScene: any, layer: any) => {
|
|
||||||
// 添加相机初始化代码
|
|
||||||
const camera = new THREE.PerspectiveCamera(
|
|
||||||
75,
|
|
||||||
scene.width / scene.height,
|
|
||||||
0.1,
|
|
||||||
1000000000
|
|
||||||
);
|
|
||||||
camera.position.set(0, 0, 10000000);
|
|
||||||
threeScene.add(camera);
|
|
||||||
layer.camera = camera; // 关键:将相机绑定到图层
|
|
||||||
threeScene.add(new THREE.AmbientLight(0xffffff));
|
|
||||||
const sunlight: any = new THREE.DirectionalLight(0xffffff, 0.25);
|
|
||||||
sunlight.position.set(0, 80000000, 100000000);
|
|
||||||
sunlight.matrixWorldNeedsUpdate = true;
|
|
||||||
threeScene.add(sunlight);
|
|
||||||
// 使用 Three.js glTFLoader 加载模型
|
|
||||||
const manager: any = new THREE.LoadingManager()
|
|
||||||
manager.setURLModifier((url: any, path: any) => {
|
|
||||||
return (path || '') + url
|
|
||||||
})
|
|
||||||
const loader = new GLTFLoader(manager);
|
|
||||||
const dracoLoader = new DRACOLoader();
|
|
||||||
dracoLoader.setDecoderPath('./static/js/');
|
|
||||||
dracoLoader.setDecoderConfig({ type: 'js' });
|
|
||||||
loader.setDRACOLoader(dracoLoader);
|
|
||||||
loader.load(
|
|
||||||
`./static/3DModel/scene.glb`,
|
|
||||||
(gltf) => {
|
|
||||||
// debugger
|
|
||||||
const gltfScene: any = gltf.scene
|
|
||||||
// setDouble(gltfScene);
|
|
||||||
content = gltfScene
|
|
||||||
layer.adjustMeshToMap(gltfScene);
|
|
||||||
// gltfScene.scale.set(1000, 1000, 1000)
|
|
||||||
layer.setMeshScale(gltfScene, 10, 10, 10);
|
|
||||||
layer.setObjectLngLat(gltfScene, [116.397428, 39.91923], 0);
|
|
||||||
// 添加环境光
|
|
||||||
threeScene.add(new THREE.AmbientLight(0xffffff, 0.3));
|
|
||||||
// 动态调整 UV Mapping
|
|
||||||
gltfScene.traverse((node) => {
|
|
||||||
if (node.isMesh) {
|
|
||||||
const material = node.material;
|
|
||||||
if (material.map) {
|
|
||||||
const uv = node.geometry.attributes.uv;
|
|
||||||
for (let i = 0; i < uv.count; i++) {
|
|
||||||
uv.setX(i, /* 新的 U 值 */);
|
|
||||||
uv.setY(i, /* 新的 V 值 */);
|
|
||||||
}
|
|
||||||
uv.needsUpdate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 调整材质属性
|
|
||||||
gltfScene.traverse((node) => {
|
|
||||||
if (node.isMesh) {
|
|
||||||
const material = node.material;
|
|
||||||
if (material.map) {
|
|
||||||
material.map.repeat.set(1, 1);
|
|
||||||
material.map.offset.set(0, 0);
|
|
||||||
material.map.wrapS = THREE.ClampToEdgeWrapping;
|
|
||||||
material.map.wrapT = THREE.ClampToEdgeWrapping;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 添加方向光
|
|
||||||
const sunlight = new THREE.DirectionalLight(0xffffff, 0.4);
|
|
||||||
sunlight.position.set(50, 100, 50);
|
|
||||||
sunlight.target.position.set(0, 0, 0);
|
|
||||||
sunlight.matrixWorldNeedsUpdate = true;
|
|
||||||
threeScene.add(sunlight);
|
|
||||||
|
|
||||||
// 添加点光源
|
|
||||||
const pointLight = new THREE.PointLight(0xffffff, 0.6, 1000);
|
|
||||||
pointLight.position.set(10, 15, -5);
|
|
||||||
threeScene.add(pointLight);
|
|
||||||
|
|
||||||
// 添加聚光灯
|
|
||||||
const spotLight = new THREE.SpotLight(0xffffff, 0.8);
|
|
||||||
spotLight.position.set(10, 25, 15);
|
|
||||||
spotLight.angle = Math.PI / 4;
|
|
||||||
threeScene.add(spotLight);
|
|
||||||
// 添加模型点击事件监听
|
|
||||||
scene.on('click', handleModelClick);
|
|
||||||
// 向场景中添加模型
|
|
||||||
threeScene.add(gltfScene);
|
|
||||||
// // 重绘图层
|
|
||||||
layer.render();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}).animate(true);
|
|
||||||
scene.addLayer(threeJSLayerRef);
|
|
||||||
|
|
||||||
// ====== 三维模型集成完成 ======
|
|
||||||
}
|
|
||||||
// 模型点击事件处理函数
|
|
||||||
const handleModelClick = (e: any) => {
|
|
||||||
// debugger
|
|
||||||
// 添加安全校验
|
|
||||||
if (!threeJSLayerRef || !threeJSLayerRef.camera) {
|
|
||||||
console.warn('Camera not initialized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 获取正确的鼠标坐标
|
|
||||||
const point = e.containerPoint || { x: e.originEvent.x, y: e.originEvent.y }; // 兼容不同事件结构
|
|
||||||
const scene = myChart.getScene();
|
|
||||||
const container = scene.getContainer();
|
|
||||||
const width = container.clientWidth;
|
|
||||||
const height = container.clientHeight;
|
|
||||||
// 2. 计算归一化设备坐标
|
|
||||||
const mouse = new THREE.Vector2();
|
|
||||||
mouse.x = (point.x / width) * 2 - 1;
|
|
||||||
mouse.y = -(point.y / height) * 2 + 1;
|
|
||||||
|
|
||||||
// 3. 优化射线检测
|
|
||||||
const raycaster = new THREE.Raycaster();
|
|
||||||
raycaster.setFromCamera(mouse, threeJSLayerRef.camera);
|
|
||||||
|
|
||||||
// 4. 检测所有可交互对象(包括嵌套结构)
|
|
||||||
const allObjects = [];
|
|
||||||
content.traverse(child => {
|
|
||||||
if (child.isMesh && child.visible) {
|
|
||||||
allObjects.push(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const intersects = raycaster.intersectObjects(allObjects);
|
|
||||||
|
|
||||||
// 5. 处理点击结果
|
|
||||||
if (intersects.length > 0) {
|
|
||||||
const closest = intersects[0];
|
|
||||||
console.log('模型被点击:', closest.object);
|
|
||||||
|
|
||||||
// 6. 添加业务逻辑(示例:高亮选中对象)
|
|
||||||
highlightSelectedObject(closest.object);
|
|
||||||
} else {
|
|
||||||
// 点击空白区域处理
|
|
||||||
clearSelection();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 高亮选中对象
|
|
||||||
const highlightSelectedObject = (object) => {
|
|
||||||
// 保存原始材质
|
|
||||||
if (!object.userData.originalMaterial) {
|
|
||||||
object.userData.originalMaterial = object.material;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用高亮材质
|
|
||||||
object.material = new THREE.MeshBasicMaterial({
|
|
||||||
color: 0xff0000,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.7
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清除选中状态
|
|
||||||
const clearSelection = () => {
|
|
||||||
content.traverse(child => {
|
|
||||||
if (child.userData.originalMaterial) {
|
|
||||||
child.material = child.userData.originalMaterial;
|
|
||||||
delete child.userData.originalMaterial;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const pointClickTrans = () => {
|
const pointClickTrans = () => {
|
||||||
@ -917,33 +734,53 @@ defineExpose({
|
|||||||
|
|
||||||
let resizeObserver
|
let resizeObserver
|
||||||
const TOLERANCE = 0.01
|
const TOLERANCE = 0.01
|
||||||
const RESIZE_MONITOR_CHARTS = ['map', 'bubble-map', 'flow-map', 'heat-map']
|
const RESIZE_MONITOR_CHARTS = ['map', 'bubble-map', 'flow-map', 'heat-map','three-map']
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('Three.js实际版本:', THREE.REVISION);
|
// 1. 获取当前图表容器的 DOM 元素
|
||||||
const containerDom = document.getElementById(containerId)
|
const containerDom = document.getElementById(containerId)
|
||||||
|
// 2. 记录容器初始尺寸 [宽度, 高度]
|
||||||
const { offsetWidth, offsetHeight } = containerDom
|
const { offsetWidth, offsetHeight } = containerDom
|
||||||
const preSize = [offsetWidth, offsetHeight]
|
const preSize = [offsetWidth, offsetHeight]
|
||||||
|
|
||||||
|
// 3. 创建 ResizeObserver 实例,监听容器尺寸变化
|
||||||
resizeObserver = new ResizeObserver(([entry] = []) => {
|
resizeObserver = new ResizeObserver(([entry] = []) => {
|
||||||
|
// 3.1 仅对指定图表类型(如 map、three-map)生效
|
||||||
if (!RESIZE_MONITOR_CHARTS.includes(view.value.type)) {
|
if (!RESIZE_MONITOR_CHARTS.includes(view.value.type)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3.2 获取当前容器尺寸
|
||||||
const [size] = entry.borderBoxSize || []
|
const [size] = entry.borderBoxSize || []
|
||||||
|
// 3.3 计算尺寸变化率(相对于初始尺寸)
|
||||||
const widthOffsetPercent = (size.inlineSize - preSize[0]) / preSize[0]
|
const widthOffsetPercent = (size.inlineSize - preSize[0]) / preSize[0]
|
||||||
const heightOffsetPercent = (size.blockSize - preSize[1]) / preSize[1]
|
const heightOffsetPercent = (size.blockSize - preSize[1]) / preSize[1]
|
||||||
|
|
||||||
|
// 3.4 若变化率小于容差值(TOLERANCE=0.01),避免频繁触发重绘
|
||||||
if (Math.abs(widthOffsetPercent) < TOLERANCE && Math.abs(heightOffsetPercent) < TOLERANCE) {
|
if (Math.abs(widthOffsetPercent) < TOLERANCE && Math.abs(heightOffsetPercent) < TOLERANCE) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3.5 尺寸变化超过阈值且图表已初始化时,重新渲染图表
|
||||||
if (myChart && preSize[1] > 1) {
|
if (myChart && preSize[1] > 1) {
|
||||||
renderChart(curView)
|
renderChart(curView) // 调用 [renderChart](file://e:\2项目\1gis_bi\gis-bi\core\core-frontend\src\views\chart\components\views\components\ChartComponentG2Plot.vue#L241-L273) 重绘图表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3.6 更新记录的尺寸为当前值
|
||||||
preSize[0] = size.inlineSize
|
preSize[0] = size.inlineSize
|
||||||
preSize[1] = size.blockSize
|
preSize[1] = size.blockSize
|
||||||
})
|
})
|
||||||
resizeObserver.observe(containerDom)
|
|
||||||
useEmitt({ name: 'l7-prepare-picture', callback: preparePicture })
|
|
||||||
useEmitt({ name: 'l7-unprepare-picture', callback: unPreparePicture })
|
|
||||||
// 初始化 Three.js
|
|
||||||
|
|
||||||
|
// 4. 开始监听图表容器尺寸变化
|
||||||
|
resizeObserver.observe(containerDom)
|
||||||
|
|
||||||
|
// 5. 绑定全局事件监听器
|
||||||
|
// 5.1 监听 `l7-prepare-picture` 事件:触发图表转图片操作
|
||||||
|
useEmitt({ name: 'l7-prepare-picture', callback: preparePicture })
|
||||||
|
// 5.2 监听 `l7-unprepare-picture` 事件:还原图表内容
|
||||||
|
useEmitt({ name: 'l7-unprepare-picture', callback: unPreparePicture })
|
||||||
|
|
||||||
|
// 6. 预留 Three.js 初始化逻辑(当前未启用)
|
||||||
|
// 示例:this.initThreeJs()
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
try {
|
try {
|
||||||
|
@ -598,7 +598,7 @@ const calcData = params => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showChartView = (...libs: ChartLibraryType[]) => {
|
const showChartView = (...libs: ChartLibraryType[]) => {
|
||||||
if (view.value?.render && view.value?.type) {
|
if (view.value?.render && view.value?.type) {
|
||||||
const chartView = chartViewManager.getChartView(view.value.render, view.value.type)
|
const chartView = chartViewManager.getChartView(view.value.render, view.value.type)
|
||||||
return chartView && libs?.includes(chartView.library)
|
return chartView && libs?.includes(chartView.library)
|
||||||
} else {
|
} else {
|
||||||
@ -1049,7 +1049,7 @@ const loadPluginCategory = data => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allEmptyCheck = computed(() => {
|
const allEmptyCheck = computed(() => {
|
||||||
return ['rich-text', 'picture-group'].includes(element.value.innerType)
|
return ['rich-text', 'picture-group','three-map'].includes(element.value.innerType)
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* 标题提示的最大宽度
|
* 标题提示的最大宽度
|
||||||
@ -1160,6 +1160,7 @@ const clearG2Tooltip = () => {
|
|||||||
</div>
|
</div>
|
||||||
<!--这里去渲染不同图库的图表-->
|
<!--这里去渲染不同图库的图表-->
|
||||||
<div v-if="allEmptyCheck || (chartAreaShow && !showEmpty)" style="flex: 1; overflow: hidden">
|
<div v-if="allEmptyCheck || (chartAreaShow && !showEmpty)" style="flex: 1; overflow: hidden">
|
||||||
|
|
||||||
<plugin-component
|
<plugin-component
|
||||||
v-if="view.plugin?.isPlugin && loadPlugin"
|
v-if="view.plugin?.isPlugin && loadPlugin"
|
||||||
:jsname="view.plugin.staticMap['index']"
|
:jsname="view.plugin.staticMap['index']"
|
||||||
@ -1230,7 +1231,7 @@ const clearG2Tooltip = () => {
|
|||||||
:font-family="fontFamily"
|
:font-family="fontFamily"
|
||||||
:active="active"
|
:active="active"
|
||||||
v-else-if="
|
v-else-if="
|
||||||
showChartView(ChartLibraryType.G2_PLOT, ChartLibraryType.L7_PLOT, ChartLibraryType.L7)
|
showChartView(ChartLibraryType.G2_PLOT, ChartLibraryType.L7_PLOT, ChartLibraryType.L7) || view.type == 'three-map'
|
||||||
"
|
"
|
||||||
ref="chartComponent"
|
ref="chartComponent"
|
||||||
@onChartClick="chartClick"
|
@onChartClick="chartClick"
|
||||||
|
@ -124,7 +124,7 @@ const loadCanvasData = (dvId, weight?) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 地图类图表,需要预先准备图片
|
// 地图类图表,需要预先准备图片
|
||||||
const mapChartTypes = ['bubble-map', 'flow-map', 'heat-map', 'map', 'symbolic-map']
|
const mapChartTypes = ['bubble-map', 'flow-map', 'heat-map', 'map', 'symbolic-map','three-map']
|
||||||
const downloadH2 = type => {
|
const downloadH2 = type => {
|
||||||
downloadStatus.value = true
|
downloadStatus.value = true
|
||||||
const mapElementIds =
|
const mapElementIds =
|
||||||
|
@ -175,10 +175,10 @@ const handleNew = newComponentInfo => {
|
|||||||
const { componentName, innerType, staticMap } = newComponentInfo
|
const { componentName, innerType, staticMap } = newComponentInfo
|
||||||
if (componentName) {
|
if (componentName) {
|
||||||
const { width, height, scale } = canvasStyleData.value
|
const { width, height, scale } = canvasStyleData.value
|
||||||
const component = findNewComponent(componentName, innerType, staticMap)
|
const component = findNewComponent(componentName, innerType, staticMap)//动态创建组件实例
|
||||||
component.style.top = ((height - component.style.height) * scale) / 200
|
component.style.top = ((height - component.style.height) * scale) / 200
|
||||||
component.style.left = ((width - component.style.width) * scale) / 200
|
component.style.left = ((width - component.style.width) * scale) / 200
|
||||||
component.id = guid()
|
component.id = guid() //生成唯一的随机id
|
||||||
const popComponents = componentData.value.filter(
|
const popComponents = componentData.value.filter(
|
||||||
ele => ele.category && ele.category === 'hidden'
|
ele => ele.category && ele.category === 'hidden'
|
||||||
)
|
)
|
||||||
@ -191,9 +191,9 @@ const handleNew = newComponentInfo => {
|
|||||||
component.category = canvasState.value.curPointArea
|
component.category = canvasState.value.curPointArea
|
||||||
component.commonBackground.backgroundColor = 'rgba(41, 41, 41, 1)'
|
component.commonBackground.backgroundColor = 'rgba(41, 41, 41, 1)'
|
||||||
}
|
}
|
||||||
changeComponentSizeWithScale(component)
|
changeComponentSizeWithScale(component) //确保组件在不同比例下保持视觉比例协调
|
||||||
dvMainStore.addComponent({ component: component, index: undefined })
|
dvMainStore.addComponent({ component: component, index: undefined })
|
||||||
adaptCurThemeCommonStyle(component)
|
adaptCurThemeCommonStyle(component)//用户切换亮/暗色模式时,自动更新所有组件的样式(如文字颜色、背景色)。
|
||||||
snapshotStore.recordSnapshotCache('renderChart', component.id)
|
snapshotStore.recordSnapshotCache('renderChart', component.id)
|
||||||
if (state.countTime > 10) {
|
if (state.countTime > 10) {
|
||||||
state.sideShow = false
|
state.sideShow = false
|
||||||
|
@ -169,7 +169,7 @@ const contentStyle = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 通过实时监听的方式直接添加组件
|
// 通过实时监听的方式直接添加组件 symbolic-map
|
||||||
const handleNew = newComponentInfo => {
|
const handleNew = newComponentInfo => {
|
||||||
state.countTime++
|
state.countTime++
|
||||||
const { componentName, innerType, staticMap } = newComponentInfo
|
const { componentName, innerType, staticMap } = newComponentInfo
|
||||||
|
Loading…
Reference in New Issue
Block a user