q-map地图组件
常用地图坐标系

项目里的坐标系转换
EPSG:4326(84坐标系)和EPSG:4490(大地2000)互转:


WGS84(EPSG:4326)和GCJ02(高德)互转
javascript
gcoord.transform(coordinate, gcoord.WGS84, gcoord.GCJ02);

gcoord对标准投影坐标系支持没proj4丰富
EPSG4326转EPSG3857(web墨卡托)

无缝切换在线/离线地图引擎的原理
通过监听prop.mapParams.online,销毁地图容器(amap封装了destroy,openlayer需要手动移除各个图层控件并抹除DOM元素),并重新生成地图容器,并重新绘画点线以还原切换前的状态


地图源上的xyz代表什么
XYZ 瓦片模式
X: 瓦片的横向索引(经度方向),从最左边(西经180°)为0,向右递增。
Y: 瓦片的纵向索引(纬度方向),从最上面(北纬85.0511°,Web墨卡托投影的最大纬度)为0,向下递增(注意:这与常规的数学坐标系Y轴向上不同)。
Z: 缩放级别(Zoom Level),通常从0(全球视野的一个瓦片)开始,数字越大,缩放级别越高,地图越详细。
矢量瓦片模式
矢量瓦片是一种将地理空间数据编码为矢量格式并组织成瓦片金字塔结构的地图数据格式。与传统栅格瓦片不同,矢量瓦片存储的是几何图形(如点、线、多边形)和属性信息,而不是预渲染的位图。以下是示例,但是实际上并不是json数据结构,传输用的是二进制数据,转换后是pbf文件(类似小面结构)

传输:只传输矢量几何和属性数据,不传输预渲染图像
客户端:接收数据后实时渲染为地图可视化,,根据一套预先定义好的样式文件(Style JSON),实时地将这些数据绘制成地图。
矢量瓦片本身不是图片,而是纯数据。最终的视觉效果完全取决于客户端的样式配置:
静态渲染(无论地图做了多少操作都长一个样)

动态渲染,接收到数据后根据地图的层级或旋转之类的操作动态改变视图

矢量瓦片缩放是无损的!!!瓦片是有损的!(其实很好理解,矢量瓦片缩放后依然还是用样式工具画出来的线点面)
GeoJSON模式
JSON格式的地理数据 + 完整的地理要素集合
特点:
- 不分瓦片,一次加载全部数据
- 易读易写,人类可读
- 适合小数据量
- 客户端渲染
- 完全可交互
与矢量瓦片地图源的区别:



项目组合模式
在实际应用中,它们经常组合使用:
- XYZ瓦片作为底图
- 矢量瓦片作为道路、建筑等可交互图层
- GeoJSON作为业务标注点、自定义区域等
openlayer和高德在画单点renderPoint上有什么实现差异?
高德直接创建 AMap.Marker 对象,通过marker.setMap(this.map)直接添加到地图实例。高德的事件可以直接绑定到点对象上。
openlayer通过新建Feature要素对象,并把它添加在点图层pointLayer.getSource().addFeatures([feature])。而且他的事件是通过统一注册进行事件分发:

点击判断机制详解
- 像素级别的要素检测
hasFeatureAtPixel(ev.pixel): 这是核心的判断方法
检查指定像素坐标 (ev.pixel) 位置是否存在任何地图要素
返回布尔值:true 表示该像素位置有要素,false 表示空白区域
这是 OpenLayers 提供的方法,会遍历所有可见图层来检测
- 获取点击位置的要素信息
getFeaturesAtPixel(ev.pixel): 获取指定像素位置的所有要素
返回一个要素数组,因为一个像素位置可能有多个重叠的要素
要素按照渲染顺序排列(后渲染的要素在数组前面)
- 要素过滤和处理
过滤特殊要素: 排除 ID 为 -1 和 -2 的要素(可能是特殊标记点)
提取属性: 将要素对象转换为属性对象,便于回调函数使用
- 几何类型判断
几何类型识别: 根据要素的几何类型进行不同处理
类型包括: Point(点)、LineString(线)、Polygon(面)等
卫星地图和瓦片地图怎么切换的。瓦片地图里和矢量地图的区分和应用?
高德:

openlayer通过显隐图层:

项目里openlayer的设计是两类图层:


javascript
// 把feature添加到点图层的资源集合中
this.pointLayer.getSource()?.addFeatures([feature]);
在我的项目中混合了许多图层:

海量点怎么绘制
高德






不需要动的点就用layer装起来,其他需要经常动的用marker,混合策略
ol

优化实现方式:VectorLayer + Features

注册完之后再统一用addFeatures进行添加。可以避免多次渲染

深圳管道项目怎么把深圳这个省市挖出来
只在高德上实现。
步骤一:实例化行政区域服务
、
步骤二:用行政区域服务搜搜行政区的信息,获取边界坐标数组



挖孔后,定义可操作的地图块(其实就是城市),生成城市多边形,并用地图api setFitView调整视野。

聚合点怎么处理
高德
光缆点聚合:

将每条光缆的点归类出来并生成一个MarkerClusterer聚合器,聚合器包括5个级别。聚合器效果如下:

离散点聚合:
跟光缆聚合差不多,只不过没有根据光缆个数生成聚合器,而是一个聚合器把点都丢进去
对标签那些进行层级选择性展示


openlayer
生成要素点,放到基础向量源,再把向量源放到聚合源

创建图层,用来放聚合源

style类似于aMap的_renderMarker


图层搞好了,创建地图:



浙江移动的云GIS怎么做
浙江移动的地图是离线地图。且电子地图源是EPSG:3857

javascript
import { get as olProjGet } from 'ol/proj';

此时我们获得了自定义投影对象,并设置了投影边界,紧接着我们来生成瓦片图层



卫星地图图层同理,但是设置为不可见

图层添加

生成地图

view和layer的关系:


看到上面的new Map的view是defaultMapView,这个view是电子地图的视图,所以卫星layer是无法正常显示的。但是我们已经把卫星layer给隐藏了。也就是说如果我们需要正常显示卫星layer,我们需要建立一个卫星view

如果要切换卫星视图,那我们需要设置卫星的view并隐藏电子图层
javascript
this.map.setView(new View(this.defaultStaMapView));
this.localSatLayer.setVisible(!_type);
this.localLiteLayer.setVisible(_type);
画点和画线跟往常一样,注意转换坐标系就行

但是卫星和电子图层的基准坐标系不一样,所以还是做每种类型的图层各做两个比较好,切换的时候把另外一种类型的图层全部屏蔽掉。
海量画线逻辑
高德地图
正常画线逻辑应该是:

缺点是无法加动画、性能和数量上限过于低下。
所以换成Loca库的线图层linelayer。
将业务层的线段数据转换为 Loca 可视化库能够识别的标准 GeoJSON 格式(Loca 可视化库使用 GeoJSON 作为标准的数据交换格式)
线的坐标列表 rr.coordinate:number[][];


点击事件绑定,就是通过坐标找线

openlayer
跟海量点一样,直接把线当做资源注册到线面

怎么画面和海量画面
openlayer
原理还是与画点一样,生成面要素并添加到面图层的资源中(也是支持海量画面

Amap:

高德

不同坐标系下的地图源的xyz值为什么不一样
XYZ瓦片坐标是基于特定的地图投影计算出来的。投影改变了,计算XYZ的数学公式就完全变了,同一个地理位置在不同投影下的XYZ坐标也截然不同。



矢量地图这么先进,为什么还有那么多企业使用瓦片地图?

二次贝塞尔曲线怎么画
画贝塞尔曲线,前提是需要一个起点,一个终点,还有一个路径数组(防止曲线重叠)



先把中心点找出来,中心点和途经点的距离是四分之一长度。

控制点只有一个,大概是这么算的

找到控制点后,判断这条连接起-控制-控-终的贝塞尔曲线是否已经存在(怕重叠),重叠的话就翻转曲线

拿到点后,高德地图只需要调用AMap.BezierCurve,并传入三个点(起始、控制点、终点)。它会自动计算连接这三个点的离散点(点数越多曲线越平滑)

openlayer只能自己计算离散点

计算点后用LineString装点后添加到图层

项目中ol涉及到的图层、要素汇总

点:

清除点:

增加线:

增加圆面:
请看上面画面章节
增加多边形面:

点怎么动态根据层级显示或隐藏部分信息
高德
建立一个zoomMarkers(Set集合),存储所有需要响应缩放的标记

通过datazoom事件进行监听

openlayers
ol就不用说了获取所有点的feature然后遍历修改

q-dailog弹窗组件
设计结构
Matlab
q-dailog
|---el-dailog
|---插槽header
|---直接提供具名插槽给用户自定义
|---插槽default
|---el-from
|---form-item
|---el-form-item
|---插槽footer
|---按钮

可以看到fromItem其实就是一个封装了elformItem的组件。其中最重要的思路是多层插槽传递(可在任意层级自定义内容)

可以在q-dailog层级直接使用#prop插槽替换掉每个fromItem的内容

作用域透传


gkDailog和q-dailog


GkDialog 类是一个数据模型,q-dialog 是视图组件。

比如open方法通过响应式属性控制dialog显隐

form item里的表单组件
最主要是封装的gkInput组件(q-input)。它是一个动态组件加载、统一接口、灵活插槽和引用收集等技术,实现了一个高度可扩展、可配置的表单控件系统。
动态组件预加载:



动态component

组件引用收集机制

示例:

或者通过作用域插槽透传到最外部进行调用

外部调用:
html
<q-dialog v-bind="dialog">
<template #latitudeTip="{item, el}">
<el-icon class="position" @click="locationAction(el)">
</template>
</q-dialog>

表单控件-地图经纬度选择器

地图显示(可切换全屏显示)


只需要动态增删full类,并重新渲染地图视图

拖动地图点变化经纬度
在q-input里的动态component里的mapval添加了点移动响应事件监听(地图绘点后默认会一直上报点位置的变化)



javascript
const draggableEvnt = (center: any) => {
props.change && props.change(center.toString());
}

把传过来的经纬度进行赋值

填入经纬度后地图重新定位


data.el是一个很关键的东西,是作用域插槽传出来的数据

在这个<template #[`${item.prop}Tip`]>动态插槽的作用域数据里,把组件合集传了出来

表单检验
q-dailog接收一个ref(new GkDailog)作为配置项

可以看到这里接收了一组规则rules。这个其实是用了element plus的表单验证。


在项目中我们自定义了经纬度验证器:

紧接着挂载到el-from上就行了,除了通过blur、change等被动方式触发验证外,也可以通过validate主动触发验证

表单控件之间的联动

dialog是通过配置字段然后生成Gkdialog实例

对具体字段的change方法绑定联动

我们先回到组件库看看change的触发逻辑

可以看到是表单字段是挂载的change方法一直向下传递到具体的表单组件。

当某个字段发生变化时调接口,并修改其他两个字段的值,更新某个字段的选项。实现联动。
q-table表格组件
特点
① 使用Element Plus深度定制开发了功能丰富的表格扩展组件
-
在Element Plus el-table基础上进行深度定制,扩展了20+个自定义功能特性
-
开发了智能操作栏布局算法,当操作按钮超过3个时自动转换为下拉菜单形式
② 使用DOM测量和自适应算法实现了表格列宽智能适配
-
实现了文本宽度测量算法,通过创建临时DOM元素精确计算文本渲染宽度
-
开发了响应式列宽适配系统,根据屏幕分辨率动态调整列宽(基于1920px基准进行缩放)
-
实现了操作栏宽度自适应算法,确保按钮文本完整显示同时保持界面美观
③ 使用Vue生命周期和缓存策略优化了表格在复杂场景下的表现
-
实现了keep-alive缓存周期内的滚动条位置保持机制,解决页面切换时的用户体验问题
-
开发了右键菜单功能,支持单元格内容的快速复制操作
④ 使用计算属性和函数式配置实现了灵活的数据处理能力
-
实现了函数式操作权限控制,支持基于行数据的动态按钮显示/隐藏
-
开发了字典映射和选项转换功能,支持复杂数据格式的自动转换显示
-
实现了树形表格支持,满足组织架构和层级数据的展示需求
20+自定义功能特性

列表布局
设计是除了操作栏动态计算宽度外,字段列都是自适应(每列设置一个最小宽度,最小宽度是60 或用户配置的width)。是否显示滚动条是继承了Element Plus 的 el-table 组件内部机制 决定:



element-plus的el-table怎么计算内部是否超过宽度需要滚动条?
需要用到两个css属性概念


智能按钮布局



自动计算按钮占据的宽度

怎么计算按钮文本的宽度呢?

右键事件处理
想要在表格里面(比如行)自定义右键事件,需要
屏蔽表格容器的右键原生事件


给el-table上添加cell-contextmenu单元格响应事件

支持整行选中和radio选择

虚拟滚动
项目中依赖分页进行渲染优化。如果需要虚拟滚动,应该这么实现:
虚拟滚动需要设置
1、行高,以方便计算窗口位置
2、窗口高度(在这个窗口区域里滚动)
javascript
const init = () => {
nextTick(() => {
if (tableRef.value && tableRef.value.$el) {
const tableElement = tableRef.value.$el;
const rect = tableElement.getBoundingClientRect();
containerHeight.value = rect.height || 400;
}
});
};
3、缓冲区(在窗口区域外可视行数:实现渐变的效果)

对滚动事件进行代理,同时处理虚拟滚动的数据
javascript
// 记录table的垂直滚动距离
const scrollTop = ref(0);
// 处理滚动事件
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement;
if (target && target.scrollTop !== undefined) {
scrollTop.value = target.scrollTop;
}
};
用计算属性定义虚拟窗口数据
javascript
const visibleData = computed(() => {
// 行高
const itemHeight = virtualScrollConfig.value.itemHeight;
// 缓冲条数
const buffer = virtualScrollConfig.value.buffer;
// 整个table视窗高度
const containerH = containerHeight.value;
const startIndex = Math.max(0, Math.floor(scrollTop.value / itemHeight) - buffer);
// 可视区域条数
const visibleCount = Math.ceil(containerH / itemHeight) + buffer * 2;
const endIndex = Math.min(props.data.length, startIndex + visibleCount);
return props.data.slice(startIndex, endIndex);
});
排序
前端排序
:sortable="true"


想调整前端排序的逻辑的话:

后端排序
:sortable="custom"

此时通过sort-change自定义排序逻辑


把排序参数传出去,给到外部进行接口调用
分页
拖管在Gktable数据类里,每次发起请求会带上。

架构设计
三层架构:
第一层
GkTable(基础表格类)用于管理表格的状态和数据。管理表格的加载状态、数据、列宽等等配置并提供修改这些状态的方法。
SearchForm(搜索类)记录一些搜索的状态和数据。
第二层
sheetClass 工厂函数。负责整合:表格数据源 + 搜索数据源。
数据源:


把整合管理的响应式状态抛给Qsearch和Qtable。在工厂函数内处理带搜索条件和分页的接口查询。并返回响应式的tableInfo和searchForms给到外面视图同步。提供查询、重置等方法给到外部逻辑调用,允许外部传入表格配置、搜索配置、搜索配置、替换数据(如强行覆盖查询条件、额外的查询条件)
传入配置:

自定义排序参数、自定义查询参数、自定义api等。传入后调用给外部调用的查询函数即可获取数据。
第三层
给外部调用:

查询函数等
数据组件
动态的具名插槽


下载

URL.createObjectURL()用于创建临时的下载链接,用于创建一个指向给定对象的URL字符串。这个URL可以用来引用 File、Blob(二进制数据) 或 MediaSource 对象的内容。

那么后端给出的这个url有什么要求呢?

文档系统vitepress
系统脉络

bash
pnpm add -D vitepress
# 初始化向导,可以在已有的项目里执行
pnpm vitepress init

配置脚本:
配置vitepress的核心设置:

互相影响是因为vitePress就寄生在原项目根目录下,可以通过路径引用原项目里的内容:

主配置文件.vitepress/config.mts(运行在Node.js环境)

VitePress内部执行流程
- Node.js进程启动
- 读取 config.mts 文件
- 使用 esbuild 编译 TypeScript
- 在 Node.js 环境中执行配置
- 基于配置启动 Vite 服务器


上面的页面由下面这块配置组成:

看到指向的是md文件

沿用原项目里的ts规范文件等

页面书写



用preview加载vue文件

其他
字体自适应大小
使用的scss。需要启用js支持功能

字体自适应函数:

思路:
默认以1920px屏幕,根字体大小为16px为基准(蓝湖设计图一般都是这样)
而rootVw就是为了计算根字体大小:

根据这个计算思路可以得到在不同分辨率上html根字体大小为:(小于1200px一律设置10px为根字体大小)

有了根字体大小后我们就可以开始计算动态字体大小,也就是rem函数
rem 是 CSS 中的一个相对长度单位,相对于根元素(root)字体大小。
而em是相当于当前元素的字体大小。
为什么rem适合做动态字体大小匹配?

在不同屏幕尺寸下怎么动态适配字体大小
其实就是通过rootVw动态计算出来的根字体大小保证多种分辨率下字体大小的一致。

打包
为什么build命令可以把q-map里面包括的q-viewer组件打包进来

Vite.config.ts的external作用

打包亮点




打包配置
更改vite的build配置:
javascript
outDir: 'lib', // 打包后输出的文件夹
target: 'modules' // 构建目标为 ES modules
这里的traget为什么是modules? 平台端的是es2015?

无论是modules还是es2015,转译后的产物都是ES模块格式。只是modules是转译为现代浏览器支持的原生es语法,es2015是转译为es2015语法
ES模块格式



javascript
lib: {
entry: resolve(__dirname, './src/output.ts'),
name: 'yzy-base',
fileName: 'yzyBase'
}

一旦配置了lib属性(如果是应用程序则无配置此属性),vite框架就会打包出两种格式的库文件(ES模块格式文件和UMD格式模块文件)。

UMD格式模块可以保证库的最大兼容性,与ES格式模块对比:


此时可以看到打包后ES模块格式是直接引入了vue。而UMD格式是用了Vue.defineComponent这个api。

umd文件在浏览器环境下的使用:

umd文件在Node.js环境下的使用:

umd文件在AMD环境下的使用:

rollupOptions
javascript
rollupOptions: {
output: {
globals: { vue: 'Vue' }
},
external: [
'vue',
'@vueuse/core',
'element-plus',
'@amap/amap-jsapi-loader',
'@element-plus/icons-vue',
'vue-i18n',
'gcoord',
'ol',
'path-browserify'
]
}



UMD模块需要全局Vue变量,受globals配置影响。所以globals属性只是为了让库能够适应现代ES模块环境,也能兼容传统的全局变量环境。
