diff --git a/.idea/fileTemplates/[项目专用]常规组件.vue b/.idea/fileTemplates/[项目专用]常规组件.vue
new file mode 100644
index 0000000..38c3ef2
--- /dev/null
+++ b/.idea/fileTemplates/[项目专用]常规组件.vue
@@ -0,0 +1,14 @@
+
+
+
+
+ #[[$END$]]#
+
+
+
+
\ No newline at end of file
diff --git a/src/assets/login_bg.png b/src/assets/login_bg.png
new file mode 100644
index 0000000..313c873
Binary files /dev/null and b/src/assets/login_bg.png differ
diff --git a/src/components/BasePanel.vue b/src/components/BasePanel.vue
new file mode 100644
index 0000000..32d4973
--- /dev/null
+++ b/src/components/BasePanel.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
diff --git a/src/components/ImageUploader.vue b/src/components/ImageUploader.vue
new file mode 100644
index 0000000..3f870e9
--- /dev/null
+++ b/src/components/ImageUploader.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
diff --git a/src/config/urls.js b/src/config/urls.js
index 6244c5f..5a3e97a 100644
--- a/src/config/urls.js
+++ b/src/config/urls.js
@@ -2,8 +2,35 @@
* 接口地址列表
*/
+import { buildURL } from '@/utils/helpers';
+
const { APP_DEVELOPMENT_BASE_URL, APP_PRODUCTION_BASE_URL, MODE } = import.meta.env; // 获取环境变量
export const { [MODE]: BASE_URL = APP_DEVELOPMENT_BASE_URL } = { development: APP_DEVELOPMENT_BASE_URL, production: APP_PRODUCTION_BASE_URL }; // 声明变量 BASE_URL 并赋值
+// 上传文件
+export const REQUEST_UPLOAD_FILE = (subject) => buildURL(`${BASE_URL}/fileServ/v1/files/{fileType}`, subject);
+
// 登录
-export const EXAMPLE_API = `${BASE_URL}xxxxxxxx`;
+export const LOGIN_WITH_PASSWORD = `${BASE_URL}/userServ/v1/login`;
+
+// 禁飞区管理
+export const GET_NO_FLY_ZONE_LIST = `${BASE_URL}/mainServ/v1/noFlyZones`;
+export const GET_NO_FLY_ZONE_DETAIL = `${BASE_URL}/mainServ/v1/noFlyZones/{id}`;
+export const CREATE_NO_FLY_ZONE = `${BASE_URL}/mainServ/v1/noFlyZones`;
+export const UPDATE_NO_FLY_ZONE = `${BASE_URL}/mainServ/v1/noFlyZones/{id}`;
+export const DELETE_NO_FLY_ZONE = `${BASE_URL}/mainServ/v1/noFlyZones/{id}`;
+export const UPDATE_NO_FLY_ZONE_DISABLE = `${BASE_URL}/mainServ/v1/noFlyZones/{id}/disable`;
+
+// 制造商管理
+export const GET_MANUFACTURER_LIST = `${BASE_URL}/mainServ/v1/manufacturers`;
+export const CREATE_MANUFACTURER = `${BASE_URL}/mainServ/v1/manufacturers`;
+export const UPDATE_MANUFACTURER = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
+export const DELETE_MANUFACTURER = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
+export const UPDATE_MANUFACTURER_DISABLE = `${BASE_URL}/mainServ/v1/manufacturers/{id}/disable`;
+
+// 制造商管理
+export const GET_REGULATOR_LIST = `${BASE_URL}/userServ/v1/regulators`;
+export const CREATE_REGULATOR = `${BASE_URL}/userServ/v1/regulator`;
+// export const UPDATE_REGULATOR = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
+// export const DELETE_REGULATOR = `${BASE_URL}/mainServ/v1/manufacturers/{id}`;
+// export const UPDATE_MANUFACTURER_DISABLE = `${BASE_URL}/mainServ/v1/manufacturers/{id}/disable`;
diff --git a/src/core/EventDispatcher.js b/src/core/EventDispatcher.js
new file mode 100644
index 0000000..15168bf
--- /dev/null
+++ b/src/core/EventDispatcher.js
@@ -0,0 +1,66 @@
+/**
+ * 事件调度类
+ */
+
+class EventDispatcher {
+ // 支持的事件名
+ _supportedEventNames = ['click'];
+
+ // 事件池
+ _eventPool = {};
+
+ constructor(supportedEventNames = []) {
+ this._supportedEventNames = [
+ ...this._supportedEventNames,
+ ...supportedEventNames,
+ ];
+ }
+
+ on(eventName, eventHandler) {
+ if (!this._supportedEventNames.includes(eventName)) return;
+
+ if (eventName in this._eventPool) {
+ if (!this._eventPool[eventName].includes(eventHandler)) {
+ this._eventPool[eventName].push(eventHandler);
+ }
+ } else {
+ this._eventPool[eventName] = [eventHandler];
+ }
+ }
+
+ once(eventName, eventHandler) {
+ if (!this._supportedEventNames.includes(eventName)) return;
+
+ const handler = payload => {
+ eventHandler(payload);
+ this.off(eventName, handler);
+ };
+ this.on(eventName, handler);
+ }
+
+ off(eventName, eventHandler) {
+ if (!this._supportedEventNames.includes(eventName)) return;
+
+ if (eventName in this._eventPool) {
+ if (!eventHandler) {
+ delete this._eventPool[eventName];
+ return;
+ }
+ const index = this._eventPool[eventName].indexOf(eventHandler);
+ if (index >= 0) {
+ this._eventPool[eventName].splice(index, 1);
+ }
+ }
+ }
+
+ _trigger(eventName, payload = {}) {
+ if (!this._supportedEventNames.includes(eventName)) return;
+ if (!(eventName in this._eventPool)) return;
+
+ (this._eventPool[eventName] || []).forEach(eventHandler => {
+ eventHandler(payload);
+ });
+ }
+}
+
+export default EventDispatcher;
diff --git a/src/core/ShapeCreator.js b/src/core/ShapeCreator.js
new file mode 100644
index 0000000..1e0dc8b
--- /dev/null
+++ b/src/core/ShapeCreator.js
@@ -0,0 +1,401 @@
+/**
+ * 绘制多边形
+ */
+import { action, computed, makeObservable, observable, reaction, values } from 'mobx';
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+import EventDispatcher from './EventDispatcher';
+
+// eslint-disable-next-line max-len
+const aimCursor = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsSAAALEgHS3X78AAADcElEQVRYhc1XTUgbURD+sgaDGEjWQ7EUQaFnydVLSW49SaC5SWgCKvRU9SxUoWdNT4UqpCJBChbFkxTqhl68Rs+CopRKwayr1RAxu+V7fa/YzebHWIwDE3b3zXwzb2bevInPcRy0SCkAL6XqEoCPrcDcxQEjl8td82FkZMQPIHbvDsRif2wahoFWHdBatf6/6DYOBAEkTNNc4aY91g25lpCyTVFTKbAsazwQCCSurq782WzWt76+jkKhgNPTU7EeDocRjUYFp9Npp7Oz87pcLq+GQqEPjbAbORC8uLh4b9v2k7m5OV8mkxFG+/v7hbF8Pi+E1PPBwYFwZmJiAlNTU46mad+7u7tfAfhV0wIdqMFBy7Jyh4eHW5FIhCE3UqmUUSgUDMdxBEejUcHqnWuUoSx1qEsMYtWyU7MGuHPLsh4PDg76uDPuMJvNoq+v7xuAMQ+VMa5RRkWDusQgVi07ng4w5wz78PCwj+8EHBoaKgOY7OnpeQNgDzL3ZEl7cm2Ssio9xCAWMZtNQbBUKn2ZmZkRoczn80a5XN50h7FSqXw6Pz//SuazG4M61CUGsYjplQovBxKWZW2Fw2GRT5nfiIdc7+Xl5TSZzx7r1BEYxCImsRs6UCwWVzKZjPCcRXVycjJbp1DrMnWJQSxiEtst73UMRYtlEe3v70MW3F6tImpATwEsDAwMIBKJYG1tDe6W7XfdaoLYZOLxuHpd4I9t2z81TXsN4LiB0V7btt9pmvZIfWCfYPNSG7whu8QIiFttcXHRr76ygtlsyJDVvry8bHd0dBhdXV1v61kvlUrTlUollkwmNdUpGU0yHVE0Ojp6zVu07ZcRI1CVAl3XRQrYVBTdJQXpdFqkwDRNt+xS24uwKgWmaR5z93RgZ2eHxzLZonGhSwyVf2JXCT24RvQQWjHv/PGzszNxDdN7djPpxM1IGPF4XLCM0t+dU5Y61CUGsYjZ9HXMSYbDxMbGhqhQ5m97ezsAYL5YLM7K4hLDiTrr/CbX5imrzjwxiFVrOqrZBzjJhEKhH7u7u46agHicjo6Onqnu6KIFrlGGstShLjHkVORJbR/JWhpKaYx8H0PpP9EA8Nw0zRe6rvfyg+uPiTjnuq5/BrBZdxC9Qf5mhCQRcFXX9VX57v5vEGMLvy21/TK6TQSqiFfqXXHa+/ccwG/SPyXk58r5VgAAAABJRU5ErkJggg==") 16 16';
+
+class ShapeCreator extends EventDispatcher {
+ _topic = null;
+
+ // 地图实例
+ _map = null;
+
+ // 构成形状的点坐标
+ _coordinates = [];
+
+ // 操作控制点
+ _markers = [];
+
+ // 形状是否扭结
+ _isKinked = false;
+
+ // 当前激活的marker索引
+ _activeMarkerIndex = -1;
+
+ // 操控点提示
+ _markerTooltip = (new mapbox.Popup({ closeButton: false, offset: 6, className: 'shape-editor-popup' })).setHTML('按住可拖拽控制点
点击可微调控制点
点击第一个控制点可闭合形状');
+
+ // 闭合形状提示
+ _completeTooltip = (new mapbox.Popup({ closeButton: false, offset: 6, className: 'shape-editor-popup' })).setHTML('按住可拖拽控制点
点击可微调控制点,或闭合形状');
+
+ // 坐标编辑器
+ _coordinatePopupEditor = new mapbox.Popup({ offset: 6, className: 'shape-editor-popup' });
+
+ // 鼠标指针
+ _cursor = `${aimCursor}, crosshair`;
+
+ // 视觉选项
+ _defaultStyle = {
+ fillColor: 'rgba(0, 0, 255, 0.2)',
+ edgeColor: 'rgba(0, 0, 255, 0.5)',
+ markerFillColor: 'rgba(0, 0, 255, 1)',
+ markerEdgeColor: 'rgba(255, 255, 255, 1)',
+ };
+
+ _style = {};
+
+ get _sourceId() {
+ return {
+ FILL: `${this._topic}-shape-creator-fill-source`,
+ EDGE: `${this._topic}-shape-creator-edge-source`,
+ };
+ }
+
+ get _layerId() {
+ return {
+ FILL: `${this._topic}-shape-creator-fill-layer`,
+ EDGE: `${this._topic}-shape-creator-edge-layer`,
+ };
+ }
+
+ // 形状边线
+ get _shapeEdgeFeature() {
+ if (this._coordinates.length < 2) {
+ return turf.feature({});
+ }
+ return turf.lineString(this._coordinates);
+ }
+
+ // 形状填充
+ get _shapeFillFeature() {
+ if (this._coordinates.length < 3) {
+ return turf.feature({});
+ }
+ return turf.lineToPolygon(turf.lineString(this._coordinates));
+ }
+
+ constructor(topic, mapInstance = null) {
+ super(['shape_kinked', 'kink_recovered', 'completed']);
+ this._topic = topic;
+ if (mapInstance) this.setMap(mapInstance);
+
+ this.initStyle();
+ this._initCoordinateEditor();
+
+ makeObservable(this, {
+ _topic: observable,
+ _coordinates: observable,
+ _sourceId: computed,
+ _layerId: computed,
+ _shapeEdgeFeature: computed,
+ _shapeFillFeature: computed,
+ _appendToCoordinates: action,
+ _updateCoordinates: action,
+ clear: action,
+ });
+
+ reaction(() => this._coordinates, () => {
+ this._render();
+ this._updateCoordinateEditor();
+ });
+ }
+
+ // 初始化视觉样式
+ initStyle(style = {}) {
+ this._style = {
+ ...this._defaultStyle,
+ ...style,
+ };
+ }
+
+ setMap(mapInstance) {
+ if (this._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._map = mapInstance;
+ }
+
+ // 开始创建
+ start() {
+ this._map.on('mousemove', this._onHoverMap);
+ this._map.on('click', this._onClickMap);
+ }
+
+ // 结束创建
+ end() {
+ this._map.off('mousemove', this._onHoverMap);
+ this._map.off('click', this._onClickMap);
+ this._map.getCanvas().style.cursor = '';
+ }
+
+ // 提交结果
+ commit() {
+ if (!this._coordinates.length) return;
+ this._trigger('completed', this._coordinates.map(coordinate => values(coordinate)));
+ this.clear();
+ }
+
+ // 追加新的坐标点
+ _appendToCoordinates(coordinate) {
+ const newCoordinates = [...this._coordinates];
+ newCoordinates.push(coordinate);
+ this._coordinates = newCoordinates;
+ }
+
+ // 更新坐标点
+ _updateCoordinates(index, coordinate) {
+ const newCoordinates = [...this._coordinates];
+ newCoordinates[index] = coordinate;
+ this._coordinates = newCoordinates;
+ }
+
+ _initCoordinateEditor() {
+ const node = document.createElement('div');
+ node.className = 'form';
+ node.innerHTML = `
+
+
+
+
+
+
+ `;
+ this._coordinatePopupEditor.setDOMContent(node);
+ this._coordinatePopupEditor.on('close', () => {
+ this._activeMarkerIndex = -1;
+ });
+
+ node.querySelector('.confirm').addEventListener('click', this._onConfirmCoordinate);
+ node.querySelector('.close-shape').addEventListener('click', this._onCommitShape);
+ }
+
+ _updateCoordinateEditor(toggleButton = false) {
+ if (this._activeMarkerIndex < 0) return;
+ const [lng, lat] = this._coordinates[this._activeMarkerIndex];
+ const node = this._coordinatePopupEditor.getElement();
+ if (!node) return;
+ node.querySelector('.lng').value = lng.toFixed(7);
+ node.querySelector('.lat').value = lat.toFixed(7);
+ if (toggleButton) {
+ const btn = node.querySelector('.close-shape');
+ btn.style.display = this._activeMarkerIndex === 0 ? 'inline-block' : 'none';
+ }
+ }
+
+ _onConfirmCoordinate = () => {
+ const node = this._coordinatePopupEditor.getElement();
+ if (!node || this._activeMarkerIndex < 0) return;
+ const newLng = node.querySelector('.lng').value - 0;
+ const newLat = node.querySelector('.lat').value - 0;
+ const marker = this._markers[this._activeMarkerIndex];
+ marker.setLngLat([newLng, newLat]);
+ this._updateCoordinates(this._activeMarkerIndex, [newLng, newLat]);
+ this._hidePopup();
+ };
+
+ _onCommitShape = () => {
+ if (this._coordinates.length < 3) return;
+ if (this._checkKinked()) {
+ this._trigger('shape_kinked', { action: 'close' });
+ return;
+ }
+ this._hidePopup();
+ this.commit();
+ };
+
+ _hidePopup() {
+ this._markers.forEach(marker => marker.setPopup(null));
+ this._activeMarkerIndex = -1;
+ }
+
+ // 检测形状是否扭结(判断新坐标加入后,或者对原有坐标进行判断)
+ _checkKinked(newCoordinate) {
+ if (this._coordinates.length < 3) return false;
+ if (newCoordinate) {
+ const lineFeature = turf.lineString([...this._coordinates, newCoordinate]);
+ const kinks = turf.kinks(lineFeature);
+ return !!kinks.features.length;
+ }
+ const kinks = turf.kinks(this._shapeFillFeature);
+ return !!kinks.features.length;
+ }
+
+ // 生成操作点
+ _genMarkerNode() {
+ const node = document.createElement('div');
+ node.style.cursor = 'pointer';
+ node.style.width = '12px';
+ node.style.height = '12px';
+ node.style.borderRadius = '50%';
+ node.style.background = this._style.markerFillColor;
+ node.style.boxSizing = 'border-box';
+ node.style.border = `2px solid ${this._style.markerEdgeColor}`;
+ node.addEventListener('click', this._onClickMarker, false);
+ node.addEventListener('mousemove', this._onMouseMoveMarker);
+ node.addEventListener('mouseleave', this._onMouseLeaveMarker);
+ return node;
+ }
+
+ _onClickMarker = e => {
+ e.stopPropagation();
+ const { markerIndex } = e.target.dataset;
+ this._markers.forEach(marker => marker.setPopup(null));
+ if (markerIndex >= 0) {
+ this._activeMarkerIndex = markerIndex - 0;
+ const marker = this._markers[markerIndex];
+ marker.setPopup(this._coordinatePopupEditor);
+ if (!marker.getPopup().isOpen()) {
+ marker.togglePopup();
+ }
+ this._updateCoordinateEditor(true);
+ }
+ };
+
+ _onMouseMoveMarker = e => {
+ const { markerIndex } = e.target.dataset;
+ if (markerIndex === undefined) return;
+ const marker = this._markers[markerIndex];
+ if (this._activeMarkerIndex >= 0) return;
+ marker.setPopup(!(markerIndex - 0) ? this._completeTooltip : this._markerTooltip);
+ marker.togglePopup();
+ };
+
+ _onMouseLeaveMarker = e => {
+ const { markerIndex } = e.target.dataset;
+ if (markerIndex === undefined) return;
+ const marker = this._markers[markerIndex];
+ const popup = marker.getPopup();
+ if (!popup || popup === this._coordinatePopupEditor) return;
+ if (popup.isOpen()) {
+ marker.togglePopup();
+ }
+ };
+
+ _onDragMarker = e => {
+ const marker = e.target;
+ const index = this._markers.indexOf(marker);
+ const { lng: newLng, lat: newLat } = marker.getLngLat();
+ this._updateCoordinates(index, [newLng, newLat]);
+ if (this._checkKinked()) {
+ this._isKinked = true;
+ this._trigger('shape_kinked', { action: 'drag' });
+ } else {
+ // 从扭结复原了
+ if (this._isKinked) {
+ this._trigger('kink_recovered');
+ }
+ this._isKinked = false;
+ }
+ };
+
+ _onClickMap = e => {
+ const { lng, lat } = e.lngLat;
+ // 判断新坐标是否会造成扭结(即:新的边线是否会跟其他边线交叉)
+ if (this._checkKinked([lng, lat])) {
+ this._trigger('shape_kinked', { action: 'click' });
+ return;
+ }
+
+ const element = this._genMarkerNode();
+ const marker = new mapbox.Marker({
+ element,
+ draggable: true,
+ }).setLngLat(e.lngLat).addTo(this._map);
+ this._markers.push(marker);
+ element.dataset.markerIndex = `${this._markers.length - 1}`;
+ this._appendToCoordinates([lng, lat]);
+
+ marker.on('drag', this._onDragMarker);
+ };
+
+ _onHoverMap = () => {
+ this._map.getCanvas().style.cursor = this._cursor;
+ };
+
+ _render() {
+ if (!this._map) return;
+ const { length: count } = this._coordinates;
+ if (count < 2) return;
+ this._renderEdge();
+ if (count < 3) return;
+ this._renderFill();
+ }
+
+ _renderEdge() {
+ const source = this._map.getSource(this._sourceId.EDGE);
+ if (!source) {
+ this._map.addSource(this._sourceId.EDGE, {
+ type: 'geojson',
+ data: this._shapeEdgeFeature,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.EDGE,
+ type: 'line',
+ source: this._sourceId.EDGE,
+ paint: {
+ 'line-color': this._style.edgeColor,
+ 'line-width': 2,
+ },
+ });
+ } else {
+ source.setData(this._shapeEdgeFeature);
+ }
+ }
+
+ _renderFill() {
+ const source = this._map.getSource(this._sourceId.FILL);
+ if (!source) {
+ this._map.addSource(this._sourceId.FILL, {
+ type: 'geojson',
+ data: this._shapeFillFeature,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.FILL,
+ type: 'fill',
+ source: this._sourceId.FILL,
+ paint: {
+ 'fill-color': this._style.fillColor,
+ },
+ });
+ } else {
+ source.setData(this._shapeFillFeature);
+ }
+ }
+
+ _clearMarkers() {
+ this._markers.forEach(m => m.remove());
+ this._markers = [];
+ }
+
+ clear(end = false) {
+ if (!this._map) return;
+ if (this._map.getSource(this._sourceId.FILL)) {
+ this._map.removeLayer(this._layerId.FILL);
+ this._map.removeSource(this._sourceId.FILL);
+ }
+ if (this._map.getSource(this._sourceId.EDGE)) {
+ this._map.removeLayer(this._layerId.EDGE);
+ this._map.removeSource(this._sourceId.EDGE);
+ }
+ this._clearMarkers();
+ this._coordinates = [];
+ if (end) this.end();
+ }
+
+ destroy() {
+ this.clear();
+ this.end();
+ this._map = null;
+ this.initStyle();
+ }
+}
+
+export default ShapeCreator;
diff --git a/src/core/ShapeEditor.js b/src/core/ShapeEditor.js
new file mode 100644
index 0000000..b834b01
--- /dev/null
+++ b/src/core/ShapeEditor.js
@@ -0,0 +1,562 @@
+/**
+ * 多边形编辑
+ */
+import { action, computed, makeObservable, observable, reaction, values } from 'mobx';
+import * as turf from '@turf/turf';
+import mapbox from 'mapbox-gl';
+import EventDispatcher from './EventDispatcher';
+
+class ShapeEditor extends EventDispatcher {
+ _topic = null;
+
+ // 地图实例
+ _map = null;
+
+ _dataSource = {};
+
+ // 构成形状的点坐标
+ _coordinates = [];
+
+ // 操作控制点
+ _markers = [];
+
+ // 形状是否扭结
+ _isKinked = false;
+
+ // 当前激活的marker索引
+ _activeMarkerIndex = -1;
+
+ // 操控点提示
+ _markerTooltip = (new mapbox.Popup({ closeButton: false, offset: 6, className: 'shape-editor-popup' })).setHTML('按住可拖拽控制点
点击可微调、删除控制点');
+
+ // 扩展点提示
+ _extPointTooltip = (new mapbox.Popup({ closeButton: false, offset: 4, className: 'shape-editor-popup' })).setText('点击可添加控制点');
+
+ // 坐标编辑器
+ _coordinatePopupEditor = new mapbox.Popup({ offset: 6, className: 'shape-editor-popup' });
+
+ // 在地图上点击以外区域时,是否自动提交本次编辑
+ _autoCommit = false;
+
+ // 视觉选项
+ _defaultStyle = {
+ edgeColor: 'rgba(0, 0, 255, 0.8)',
+ markerFillColor: 'rgba(0, 0, 255, 1)',
+ markerEdgeColor: 'rgba(255, 255, 255, 1)',
+ };
+
+ _style = {};
+
+ get _sourceId() {
+ return {
+ FILL: `${this._topic}-shape-editor-fill-source`,
+ EDGE: `${this._topic}-shape-editor-edge-source`,
+ VERTEX: `${this._topic}-shape-editor-vertex-source`,
+ EXT_POINT: `${this._topic}-shape-editor-ext-point-source`,
+ };
+ }
+
+ get _layerId() {
+ return {
+ FILL: `${this._topic}-shape-editor-fill-layer`,
+ EDGE: `${this._topic}-shape-editor-edge-layer`,
+ VERTEX: `${this._topic}-shape-editor-vertex-layer`,
+ EXT_POINT: `${this._topic}-shape-editor-ext-point-layer`,
+ };
+ }
+
+ // 形状填充
+ get _shapeFillFeature() {
+ return turf.lineToPolygon(turf.lineString(this._coordinates));
+ }
+
+ // 形状集合包围盒
+ get _shapeBoundingBox() {
+ return turf.bbox(this._shapeFillFeature);
+ }
+
+ // 形状边界线
+ get _shapeEdgeFeature() {
+ return turf.polygonToLine(this._shapeFillFeature);
+ }
+
+ // 形状顶点
+ get _shapeVertexFeature() {
+ return turf.multiPoint(this._coordinates);
+ }
+
+ // 形状扩展点
+ get _shapeExtPointFeatureCollection() {
+ const [first] = this._coordinates;
+ if (!first) return turf.featureCollection([]);
+
+ const points = this._coordinates.concat([first]).map(coordinate => turf.point(coordinate));
+ const midPoints = [];
+ for (let i = 1; i < points.length; i += 1) {
+ const midPoint = turf.midpoint(points[i - 1], points[i]);
+ midPoint.id = i;
+ midPoints.push(midPoint);
+ }
+ return turf.featureCollection(midPoints);
+ }
+
+ constructor(topic, autoCommit = false, mapInstance = null) {
+ super(['shape_kinked', 'kink_recovered', 'completed']);
+ this._topic = topic;
+ this._autoCommit = autoCommit;
+ if (mapInstance) this.setMap(mapInstance);
+
+ this.initStyle();
+ this._initCoordinateEditor();
+
+ makeObservable(this, {
+ _topic: observable,
+ _coordinates: observable,
+ _sourceId: computed,
+ _layerId: computed,
+ _shapeFillFeature: computed,
+ _shapeEdgeFeature: computed,
+ _shapeVertexFeature: computed,
+ _shapeExtPointFeatureCollection: computed,
+ loadDataSource: action,
+ _initCoordinates: action,
+ _updateCoordinates: action,
+ _insertIntoCoordinates: action,
+ _deleteCoordinates: action,
+ clear: action,
+ });
+
+ reaction(() => this._coordinates, () => {
+ this._render();
+ this._updateCoordinateEditor();
+ });
+ }
+
+ // 初始化视觉演示(需在loadTracks之前配置)
+ initStyle(style = {}) {
+ this._style = {
+ ...this._defaultStyle,
+ ...style,
+ };
+ }
+
+ setMap(mapInstance) {
+ if (this._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._map = mapInstance;
+ }
+
+ loadDataSource({ points, ...others } = {}) {
+ if (!this._map) {
+ throw new Error('请先设置地图实例');
+ }
+ let newPoints = points || [];
+ newPoints = Array.isArray(newPoints[0]) ? newPoints : newPoints.map(({ lng, lat }) => [lng, lat]);
+ this._dataSource = { points: newPoints, ...others };
+ this._initCoordinates();
+ }
+
+ // 提交编辑
+ commit() {
+ if (!this._coordinates.length) return;
+ const result = {
+ ...this._dataSource,
+ points: this._coordinates.map(coordinate => values(coordinate)),
+ };
+ this._trigger('completed', result);
+ this.clear();
+ }
+
+ // 初始化操控点坐标集
+ _initCoordinates() {
+ const { points } = this._dataSource;
+ this._coordinates = points;
+ this._showMarkers();
+ }
+
+ // 更新操控点坐标
+ _updateCoordinates(index, coordinate) {
+ const newCoordinates = [...this._coordinates];
+ newCoordinates[index] = coordinate;
+ this._coordinates = newCoordinates;
+ }
+
+ // 插入新的操控点坐标
+ _insertIntoCoordinates(index, coordinate) {
+ const newCoordinates = [...this._coordinates];
+ newCoordinates.splice(index, 0, coordinate);
+ this._coordinates = newCoordinates;
+ this._showMarkers();
+ }
+
+ // 删除操控点坐标
+ _deleteCoordinates(index) {
+ const newCoordinates = [...this._coordinates];
+ newCoordinates.splice(index - 0, 1);
+ this._coordinates = newCoordinates;
+ this._hidePopup();
+ this._showMarkers();
+ }
+
+ _initCoordinateEditor() {
+ const node = document.createElement('div');
+ node.className = 'form';
+ node.innerHTML = `
+
+
+
+
+
+
+ `;
+ this._coordinatePopupEditor.setDOMContent(node);
+ this._coordinatePopupEditor.on('close', () => {
+ this._activeMarkerIndex = -1;
+ });
+
+ node.querySelector('.confirm').addEventListener('click', this._onConfirmCoordinate);
+ node.querySelector('.delete').addEventListener('click', this._onDeleteMarker);
+ }
+
+ _updateCoordinateEditor(toggleButton = false) {
+ if (this._activeMarkerIndex < 0 || !this._coordinates.length) return;
+ const [lng, lat] = this._coordinates[this._activeMarkerIndex];
+ const node = this._coordinatePopupEditor.getElement();
+ if (!node) return;
+ node.querySelector('.lng').value = lng.toFixed(7);
+ node.querySelector('.lat').value = lat.toFixed(7);
+ if (toggleButton) {
+ const btn = node.querySelector('.delete');
+ btn.style.display = this._coordinates.length > 3 ? 'inline-block' : 'none';
+ }
+ }
+
+ _onConfirmCoordinate = () => {
+ const node = this._coordinatePopupEditor.getElement();
+ if (!node || this._activeMarkerIndex < 0) return;
+ const newLng = node.querySelector('.lng').value - 0;
+ const newLat = node.querySelector('.lat').value - 0;
+ const marker = this._markers[this._activeMarkerIndex];
+ marker.setLngLat([newLng, newLat]);
+ this._updateCoordinates(this._activeMarkerIndex, [newLng, newLat]);
+ this._hidePopup();
+ };
+
+ _onDeleteMarker = () => {
+ if (this._coordinates.length <= 3) return;
+ if (this._activeMarkerIndex < 0) return;
+ this._deleteCoordinates(this._activeMarkerIndex);
+ };
+
+ _hidePopup() {
+ this._markers.forEach(marker => marker.setPopup(null));
+ this._activeMarkerIndex = -1;
+ }
+
+ _render() {
+ if (!this._map) return;
+ if (this._coordinates.length < 3) return;
+
+ this._renderFill();
+ this._renderEdge();
+ this._renderVertex();
+ this._renderExtPoint();
+
+ this._map.off('click', this._onClickMap);
+ this._map.on('click', this._onClickMap);
+ }
+
+ // 渲染填充层,仅用于鼠标点击
+ _renderFill() {
+ const source = this._map.getSource(this._sourceId.FILL);
+ if (!source) {
+ this._map.addSource(this._sourceId.FILL, {
+ type: 'geojson',
+ data: this._shapeFillFeature,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.FILL,
+ type: 'fill',
+ source: this._sourceId.FILL,
+ paint: {
+ 'fill-color': 'rgba(0, 0, 0, 0.1)',
+ },
+ });
+ } else {
+ source.setData(this._shapeFillFeature);
+ }
+ }
+
+ _renderEdge() {
+ const source = this._map.getSource(this._sourceId.EDGE);
+ if (!source) {
+ this._map.addSource(this._sourceId.EDGE, {
+ type: 'geojson',
+ data: this._shapeEdgeFeature,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.EDGE,
+ type: 'line',
+ source: this._sourceId.EDGE,
+ paint: {
+ 'line-color': this._style.edgeColor,
+ 'line-width': 2,
+ 'line-dasharray': [1, 2],
+ },
+ layout: {
+ 'line-cap': 'round',
+ },
+ });
+ } else {
+ source.setData(this._shapeEdgeFeature);
+ }
+ }
+
+ // 渲染顶点,仅用于鼠标点击
+ _renderVertex() {
+ const source = this._map.getSource(this._sourceId.VERTEX);
+ if (!source) {
+ this._map.addSource(this._sourceId.VERTEX, {
+ type: 'geojson',
+ data: this._shapeVertexFeature,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.VERTEX,
+ type: 'circle',
+ source: this._sourceId.VERTEX,
+ paint: {
+ 'circle-color': this._style.edgeColor,
+ 'circle-radius': 4,
+ },
+ });
+ } else {
+ source.setData(this._shapeVertexFeature);
+ }
+ }
+
+ // 渲染扩展点
+ _renderExtPoint() {
+ const source = this._map.getSource(this._sourceId.EXT_POINT);
+ if (!source) {
+ this._map.addSource(this._sourceId.EXT_POINT, {
+ type: 'geojson',
+ data: this._shapeExtPointFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.EXT_POINT,
+ type: 'circle',
+ source: this._sourceId.EXT_POINT,
+ paint: {
+ 'circle-color': this._style.edgeColor,
+ 'circle-radius': 4,
+ },
+ });
+
+ this._map.on('click', this._layerId.EXT_POINT, this._onClickExtPoint);
+ this._map.on('mousemove', this._layerId.EXT_POINT, this._onMouserMoveExtPoint);
+ this._map.on('mouseleave', this._layerId.EXT_POINT, this._onMouserLeaveExtPoint);
+ } else {
+ source.setData(this._shapeExtPointFeatureCollection);
+ }
+ }
+
+ _onClickMap = e => {
+ if (!this._autoCommit) return;
+ const features = this._map.queryRenderedFeatures(e.point, {
+ layers: [
+ this._layerId.FILL,
+ this._layerId.EDGE,
+ this._layerId.VERTEX,
+ this._layerId.EXT_POINT,
+ ],
+ });
+ // length>=0表示点击了编辑区域,否则点击编辑区域以外(即:结束编辑)
+ if (features.length) return;
+
+ if (this._checkKinked()) {
+ this._trigger('shape_kinked', { action: 'close' });
+ return;
+ }
+
+ this.commit();
+ };
+
+ // 点击扩展点(即:插入新的操控点)
+ _onClickExtPoint = e => {
+ const [feature] = e.features;
+ this._insertIntoCoordinates(feature.id, turf.getCoord(feature));
+ };
+
+ _onMouserMoveExtPoint = e => {
+ this._map.getCanvas().style.cursor = 'pointer';
+ const [feature] = e.features;
+ this._extPointTooltip.setLngLat(turf.getCoord(feature)).addTo(this._map);
+ };
+
+ _onMouserLeaveExtPoint = () => {
+ this._map.getCanvas().style.cursor = '';
+ this._extPointTooltip.remove();
+ };
+
+ _onMouseMoveMarker = e => {
+ const { markerIndex } = e.target.dataset;
+ if (markerIndex === undefined) return;
+ const marker = this._markers[markerIndex];
+ if (this._activeMarkerIndex >= 0) return;
+ marker.setPopup(this._markerTooltip);
+ if (!marker.getPopup().isOpen()) {
+ marker.togglePopup();
+ }
+ };
+
+ _onMouseLeaveMarker = e => {
+ const { markerIndex } = e.target.dataset;
+ if (markerIndex === undefined) return;
+ const marker = this._markers[markerIndex];
+ const popup = marker.getPopup();
+ if (!popup || popup === this._coordinatePopupEditor) return;
+ if (popup.isOpen()) {
+ marker.togglePopup();
+ }
+ };
+
+ _onClickMarker = e => {
+ e.stopPropagation();
+ const { markerIndex } = e.target.dataset;
+ this._markers.forEach(marker => marker.setPopup(null));
+ if (markerIndex >= 0) {
+ this._activeMarkerIndex = markerIndex - 0;
+ const marker = this._markers[markerIndex];
+ marker.setPopup(this._coordinatePopupEditor);
+ if (!marker.getPopup().isOpen()) {
+ marker.togglePopup();
+ }
+ this._updateCoordinateEditor(true);
+ }
+ };
+
+ _onDragMarker = e => {
+ const marker = e.target;
+ const index = this._markers.indexOf(marker);
+ const { lng: newLng, lat: newLat } = marker.getLngLat();
+ this._updateCoordinates(index, [newLng, newLat]);
+ if (this._checkKinked()) {
+ this._isKinked = true;
+ this._trigger('shape_kinked', { action: 'drag' });
+ } else {
+ // 从扭结复原了
+ if (this._isKinked) {
+ this._trigger('kink_recovered');
+ }
+ this._isKinked = false;
+ }
+ };
+
+ // 检测形状是否扭结(判断新坐标加入后,或者对原有坐标进行判断)
+ _checkKinked(newCoordinate) {
+ if (this._coordinates.length < 3) return false;
+ if (newCoordinate) {
+ const lineFeature = turf.lineString([...this._coordinates, newCoordinate]);
+ const kinks = turf.kinks(lineFeature);
+ return !!kinks.features.length;
+ }
+ const kinks = turf.kinks(this._shapeFillFeature);
+ return !!kinks.features.length;
+ }
+
+ // 生成操作点
+ _genMarkerNode() {
+ const node = document.createElement('div');
+ node.style.cursor = 'pointer';
+ node.style.width = '12px';
+ node.style.height = '12px';
+ node.style.borderRadius = '50%';
+ node.style.background = this._style.markerFillColor;
+ node.style.boxSizing = 'border-box';
+ node.style.border = `2px solid ${this._style.markerEdgeColor}`;
+ node.addEventListener('mousemove', this._onMouseMoveMarker);
+ node.addEventListener('mouseleave', this._onMouseLeaveMarker);
+ node.addEventListener('click', this._onClickMarker, false);
+ return node;
+ }
+
+ // 显示操控点
+ _showMarkers() {
+ this._clearMarkers();
+ this._coordinates.forEach(coordinate => {
+ const element = this._genMarkerNode();
+ const marker = new mapbox.Marker({
+ element,
+ draggable: true,
+ }).setLngLat(coordinate).addTo(this._map);
+ this._markers.push(marker);
+ element.dataset.markerIndex = `${this._markers.length - 1}`;
+
+ marker.on('drag', this._onDragMarker);
+ });
+ }
+
+ // 缩放到包围盒
+ fitView({ top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
+ if (!this._coordinates.length) return;
+ this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 });
+ this._map.fitBounds(this._shapeBoundingBox, {
+ duration: 2000,
+ padding: {
+ top: top + cut,
+ bottom: bottom + cut,
+ left: left + cut,
+ right: right + cut,
+ },
+ });
+ }
+
+ _clearMarkers() {
+ this._markers.forEach(m => m.remove());
+ this._markers = [];
+ }
+
+ clear() {
+ if (!this._map) return;
+ this._map.off('click', this._onClickMap);
+ if (this._map.getSource(this._sourceId.FILL)) {
+ this._map.removeLayer(this._layerId.FILL);
+ this._map.removeSource(this._sourceId.FILL);
+ }
+ if (this._map.getSource(this._sourceId.EDGE)) {
+ this._map.removeLayer(this._layerId.EDGE);
+ this._map.removeSource(this._sourceId.EDGE);
+ }
+ if (this._map.getSource(this._sourceId.VERTEX)) {
+ this._map.removeLayer(this._layerId.VERTEX);
+ this._map.removeSource(this._sourceId.VERTEX);
+ }
+ if (this._map.getSource(this._sourceId.EXT_POINT)) {
+ this._map.off('click', this._layerId.EXT_POINT, this._onClickExtPoint);
+ this._map.off('mousemove', this._layerId.EXT_POINT, this._onMouserMoveExtPoint);
+ this._map.off('mouseleave', this._layerId.EXT_POINT, this._onMouserLeaveExtPoint);
+ this._map.removeLayer(this._layerId.EXT_POINT);
+ this._map.removeSource(this._sourceId.EXT_POINT);
+ }
+ this._clearMarkers();
+ this._coordinates = [];
+ this._activeMarkerIndex = -1;
+ this._isKinked = false;
+ }
+
+ // 销毁
+ destroy() {
+ this.clear();
+ this._map = null;
+ this._dataSource = {};
+ this.initStyle();
+ }
+}
+
+export default ShapeEditor;
diff --git a/src/core/ShapeRenderer.js b/src/core/ShapeRenderer.js
new file mode 100644
index 0000000..cd1f78d
--- /dev/null
+++ b/src/core/ShapeRenderer.js
@@ -0,0 +1,554 @@
+/**
+ * 任意多边形在地图上显示
+ */
+import { action, computed, makeObservable, observable, reaction } from 'mobx';
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+import EventDispatcher from './EventDispatcher';
+
+class ShapeRenderer extends EventDispatcher {
+ _topic = null;
+
+ // 地图实例
+ _map = null;
+
+ _dataSource = [];
+
+ // 是否完成初始渲染
+ isRendered = false;
+
+ // 鼠标经过的形状id
+ _hoveredShapeId = null;
+
+ // 显示配置项
+ _defaultOptions = {
+ // 显示填充
+ showFill: true,
+ // 显示描边
+ showStroke: true,
+ // 显示标签
+ showLabel: true,
+ // 显示标签(仅鼠标经过时)
+ showLabelOnOver: false,
+ };
+
+ _options = {};
+
+ // 视觉选项
+ _defaultStyle = {
+ fillColor: 'rgba(255, 255, 0, 0.1)',
+ fillHoverColor: 'rgba(255, 255, 0, 0.3)',
+ strokeColor: 'rgba(255, 255, 0, 0.5)',
+ strokeHoverColor: 'rgba(255, 255, 0, 1)',
+ labelColor: 'rgba(0, 0, 0, 1)',
+ labelStrokeColor: 'rgba(255, 255, 255, 1)',
+ labelMinZoom: 3,
+ };
+
+ _style = {};
+
+ get _sourceId() {
+ return {
+ FILL: `${this._topic}-shape-fill-source`,
+ STROKE: `${this._topic}-shape-stroke-source`,
+ LABEL: `${this._topic}-shape-label-source`,
+ };
+ }
+
+ get _layerId() {
+ return {
+ FILL: `${this._topic}-shape-fill-layer`,
+ STROKE: `${this._topic}-shape-stroke-layer`,
+ LABEL: `${this._topic}-shape-label-layer`,
+ };
+ }
+
+ // 形状填充色表达式
+ get _shapeFillColorExpression() {
+ return [
+ 'case',
+ [
+ 'any',
+ ['boolean', ['feature-state', 'hover'], false],
+ ['boolean', ['feature-state', 'highlight'], false],
+ ],
+ this._options.showFill ? this._style.fillHoverColor : 'rgba(0, 0, 0, 0)',
+ this._options.showFill ? this._style.fillColor : 'rgba(0, 0, 0, 0)',
+ ];
+ }
+
+ // 形状描边色表达式
+ get _shapeStrokeColorExpression() {
+ return [
+ 'case',
+ [
+ 'any',
+ ['boolean', ['feature-state', 'hover'], false],
+ ['boolean', ['feature-state', 'highlight'], false],
+ ],
+ this._options.showStroke ? this._style.strokeHoverColor : 'rgba(0, 0, 0, 0)',
+ this._options.showStroke ? this._style.strokeColor : 'rgba(0, 0, 0, 0)',
+ ];
+ }
+
+ // 形状标签透明度表达式
+ get _shapeLabelOpacityExpression() {
+ if (this._options.showLabelOnOver) {
+ return [
+ 'case',
+ [
+ 'any',
+ ['boolean', ['feature-state', 'hover'], false],
+ ['boolean', ['feature-state', 'highlight'], false],
+ ],
+ 1,
+ 0,
+ ];
+ }
+ return [
+ 'interpolate', ['linear'],
+ ['zoom'],
+ this._style.labelMinZoom - 0.01, 0,
+ this._style.labelMinZoom, 1,
+ ];
+ }
+
+ // 形状填充
+ get _shapeFillFeatures() {
+ return this._dataSource.map(({ id, points, ...others }) => {
+ const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
+ const lineFeature = turf.lineString(newPoints, { id, ...others });
+ const polygonFeature = turf.lineToPolygon(lineFeature);
+ polygonFeature.id = id;
+ return polygonFeature;
+ });
+ }
+
+ // 形状填充集合
+ get _shapeFillFeatureCollection() {
+ return turf.featureCollection(this._shapeFillFeatures);
+ }
+
+ // 形状描边
+ get _shapeStrokeFeatures() {
+ return this._shapeFillFeatures.map(polygonFeature => {
+ const lineFeature = turf.polygonToLine(polygonFeature);
+ const { id } = lineFeature.properties;
+ lineFeature.id = id;
+ return lineFeature;
+ });
+ }
+
+ // 形状描边集合
+ get _shapeStrokeFeatureCollection() {
+ return turf.featureCollection(this._shapeStrokeFeatures);
+ }
+
+ // 形状中心点
+ get _shapeCenterFeatures() {
+ return this._shapeFillFeatures.map(polygonFeature => {
+ const feature = turf.centerOfMass(polygonFeature);
+ feature.id = polygonFeature.id;
+ feature.properties = polygonFeature.properties;
+ return feature;
+ });
+ }
+
+ // 形状中心点集合
+ get _shapeCenterFeatureCollection() {
+ return turf.featureCollection(this._shapeCenterFeatures);
+ }
+
+ // 形状集合包围盒
+ get _shapeBoundingBox() {
+ return turf.bbox(this._shapeFillFeatureCollection);
+ }
+
+ constructor(topic, mapInstance = null) {
+ super(['rendered', 'mouse_move', 'mouse_leave']);
+ this._topic = topic;
+ if (mapInstance) this.setMap(mapInstance);
+
+ this.updateOptions();
+ this.updateStyle();
+
+ makeObservable(this, {
+ _topic: observable,
+ _dataSource: observable,
+ _options: observable,
+ _style: observable,
+ _sourceId: computed,
+ _layerId: computed,
+ _shapeFillColorExpression: computed,
+ _shapeStrokeColorExpression: computed,
+ _shapeLabelOpacityExpression: computed,
+ _shapeFillFeatures: computed,
+ _shapeFillFeatureCollection: computed,
+ _shapeStrokeFeatures: computed,
+ _shapeStrokeFeatureCollection: computed,
+ _shapeCenterFeatures: computed,
+ _shapeCenterFeatureCollection: computed,
+ _shapeBoundingBox: computed,
+ loadDataSource: action,
+ updateOptions: action,
+ updateStyle: action,
+ destroy: action,
+ });
+
+ reaction(() => this._dataSource, () => {
+ this._render();
+ });
+ }
+
+ // 更新配置项
+ updateOptions(options = {}) {
+ if (Object.keys(options).length) {
+ this._options = {
+ ...this._options,
+ ...options,
+ };
+ this._refreshVisibility();
+ } else {
+ this._options = {
+ ...this._defaultOptions,
+ };
+ }
+ }
+
+ // 更新视觉样式
+ updateStyle(style = {}) {
+ if (Object.keys(style).length) {
+ this._style = {
+ ...this._style,
+ ...style,
+ };
+ this._repaintStyle();
+ } else {
+ this._style = {
+ ...this._defaultStyle,
+ };
+ }
+ }
+
+ setMap(mapInstance) {
+ if (this._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._map = mapInstance;
+ }
+
+ // 载入数据(list->item必须包含id、name、points数组,points->item如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
+ loadDataSource(list) {
+ if (!this._map) {
+ throw new Error('请先设置地图实例');
+ }
+ this._dataSource = (list || []).map(item => {
+ const { id, points } = item;
+ return (id >= 0 && Array.isArray(points) && points.length >= 3) ? item : null;
+ }).filter(Boolean);
+ }
+
+ // 是否包含某个多边形
+ hasShape(shapeId) {
+ return this._dataSource.findIndex(({ id }) => shapeId === id) >= 0;
+ }
+
+ // 获取某个形状数据
+ getShape(shapeId) {
+ return this._dataSource.find(({ id }) => shapeId === id);
+ }
+
+ // 获取所有形状的中心点
+ getAllCenters() {
+ return this._shapeCenterFeatures.map(feature => turf.getCoord(feature));
+ }
+
+ _render() {
+ if (!this._map) return;
+ if (this._options.showFill) this._renderShapeFill();
+ if (this._options.showStroke) this._renderShapeStroke();
+ if (this._options.showLabel) this._renderShapeLabel();
+ this.isRendered = true;
+ this._trigger('rendered');
+ }
+
+ // 渲染形状填充
+ _renderShapeFill() {
+ const source = this._map.getSource(this._sourceId.FILL);
+ if (!source) {
+ this._map.addSource(this._sourceId.FILL, {
+ type: 'geojson',
+ data: this._shapeFillFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.FILL,
+ type: 'fill',
+ source: this._sourceId.FILL,
+ paint: {
+ 'fill-color': this._shapeFillColorExpression,
+ 'fill-opacity': [
+ 'case',
+ ['boolean', ['feature-state', 'visible'], true],
+ 1,
+ 0,
+ ],
+ },
+ });
+
+ this._map.on('click', this._layerId.FILL, this._onClick);
+ this._map.on('mousemove', this._layerId.FILL, this._onMouseMove);
+ this._map.on('mouseleave', this._layerId.FILL, this._onMouseLeave);
+ } else {
+ source.setData(this._shapeFillFeatureCollection);
+ }
+ }
+
+ // 渲染形状描边
+ _renderShapeStroke() {
+ const source = this._map.getSource(this._sourceId.STROKE);
+ if (!source) {
+ this._map.addSource(this._sourceId.STROKE, {
+ type: 'geojson',
+ data: this._shapeStrokeFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.STROKE,
+ type: 'line',
+ source: this._sourceId.STROKE,
+ paint: {
+ 'line-color': this._shapeStrokeColorExpression,
+ 'line-width': 2,
+ 'line-opacity': [
+ 'case',
+ ['boolean', ['feature-state', 'visible'], true],
+ 1,
+ 0,
+ ],
+ },
+ layout: {
+ 'line-cap': 'round',
+ },
+ });
+ } else {
+ source.setData(this._shapeStrokeFeatureCollection);
+ }
+ }
+
+ _renderShapeLabel() {
+ const source = this._map.getSource(this._sourceId.LABEL);
+ if (!source) {
+ this._map.addSource(this._sourceId.LABEL, {
+ type: 'geojson',
+ data: this._shapeCenterFeatureCollection,
+ });
+
+ this._map.addLayer({
+ id: this._layerId.LABEL,
+ type: 'symbol',
+ source: this._sourceId.LABEL,
+ layout: {
+ 'text-field': '{name}',
+ 'text-size': 12,
+ 'text-allow-overlap': true,
+ },
+ paint: {
+ 'text-color': this._style.labelColor,
+ 'text-halo-color': this._style.labelStrokeColor,
+ 'text-halo-width': 1,
+ 'text-opacity': this._shapeLabelOpacityExpression,
+ },
+ });
+ } else {
+ source.setData(this._shapeCenterFeatureCollection);
+ }
+ }
+
+ _onClick = e => {
+ if (!e.features.length) return;
+ const { lngLat, point } = e;
+ const [{ properties: detail }] = e.features;
+ this._trigger('click', { lngLat, point, detail });
+ };
+
+ _onMouseMove = e => {
+ this._map.getCanvas().style.cursor = 'pointer';
+
+ if (e.features.length > 0) {
+ if (this._hoveredShapeId !== null) {
+ const id = this._hoveredShapeId;
+ if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id }, { hover: false });
+ if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id }, { hover: false });
+ if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id }, { hover: false });
+ }
+ const [{ id }] = e.features;
+ if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id }, { hover: true });
+ if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id }, { hover: true });
+ if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id }, { hover: true });
+ this._hoveredShapeId = id;
+ }
+
+ const { lngLat, point } = e;
+ const [{ properties: detail }] = e.features;
+ this._trigger('mouse_move', { lngLat, point, detail });
+ };
+
+ _onMouseLeave = e => {
+ this._map.getCanvas().style.cursor = '';
+
+ if (this._hoveredShapeId !== null) {
+ if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: this._hoveredShapeId }, { hover: false });
+ if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: this._hoveredShapeId }, { hover: false });
+ if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id: this._hoveredShapeId }, { hover: false });
+ }
+
+ const { lngLat, point } = e;
+ let detail = {};
+ if (this._hoveredShapeId !== null) {
+ const { id, points, ...others } = this._dataSource.find(item => item.id === this._hoveredShapeId) || {};
+ detail = { id, ...others };
+ }
+ this._trigger('mouse_leave', { lngLat, point, detail });
+
+ this._hoveredShapeId = null;
+ };
+
+ // 隐藏某个形状
+ hideShape(shapeId) {
+ if (!this._map || !this.hasShape(shapeId)) return;
+ if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: shapeId }, { visible: false });
+ if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: shapeId }, { visible: false });
+ }
+
+ // 显示某个形状
+ showShape(shapeId) {
+ if (!this._map || !this.hasShape(shapeId)) return;
+ if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: shapeId }, { visible: true });
+ if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: shapeId }, { visible: true });
+ }
+
+ // 高亮某个形状(或取消高亮)
+ highlightShape(shapeId, stateValue = true) {
+ if (!this._map || !this.hasShape(shapeId)) return;
+ if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: shapeId }, { highlight: stateValue });
+ if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: shapeId }, { highlight: stateValue });
+ }
+
+ // 高亮一组形状(或取消高亮)
+ highlightShapes(keyName, keyValue, stateValue = true) {
+ if (!this._map) return;
+ turf.featureEach(this._shapeFillFeatureCollection, currentFeature => {
+ const { id } = currentFeature;
+ if (currentFeature.properties[keyName] === keyValue) this.highlightShape(id, stateValue);
+ });
+ }
+
+ // 刷新可见性
+ _refreshVisibility() {
+ if (!this._map) return;
+ if (this._map.getLayer(this._layerId.FILL)) {
+ this._renderShapeFill();
+ this._map.setLayoutProperty(this._layerId.FILL, 'visibility', this._options.showFill ? 'visible' : 'none');
+ }
+ if (this._map.getLayer(this._layerId.STROKE)) {
+ this._renderShapeStroke();
+ this._map.setLayoutProperty(this._layerId.STROKE, 'visibility', this._options.showStroke ? 'visible' : 'none');
+ }
+ if (this._map.getLayer(this._layerId.LABEL)) {
+ this._renderShapeLabel();
+ this._map.setLayoutProperty(this._layerId.LABEL, 'visibility', this._options.showLabel ? 'visible' : 'none');
+ }
+ }
+
+ // 重绘样式
+ _repaintStyle() {
+ if (!this._map) return;
+ if (this._map.getLayer(this._layerId.FILL)) {
+ this._map.setPaintProperty(this._layerId.FILL, 'fill-color', this._shapeFillColorExpression);
+ }
+ if (this._map.getLayer(this._layerId.STROKE)) {
+ this._map.setPaintProperty(this._layerId.STROKE, 'line-color', this._shapeStrokeColorExpression);
+ }
+ if (this._map.getLayer(this._layerId.LABEL)) {
+ this._map.setPaintProperty(this._layerId.LABEL, 'text-color', this._style.labelColor);
+ this._map.setPaintProperty(this._layerId.LABEL, 'text-halo-color', this._style.labelStrokeColor);
+ this._map.setPaintProperty(this._layerId.LABEL, 'text-opacity', this._shapeLabelOpacityExpression);
+ }
+ }
+
+ // 缩放到某个形状的包围盒
+ fitShape(shapeId, { top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
+ if (!this._map || !this.hasShape(shapeId)) return;
+ const { points } = this._dataSource.find(({ id }) => id === shapeId) || {};
+ if (!points) return;
+ const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
+ const pointFeature = turf.multiPoint(newPoints);
+ this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 });
+ this._map.fitBounds(turf.bbox(pointFeature), {
+ duration: 2000,
+ padding: {
+ top: top + cut,
+ bottom: bottom + cut,
+ left: left + cut,
+ right: right + cut,
+ },
+ });
+ }
+
+ // 缩放到所有形状的包围盒
+ fitView({ top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
+ if (!this._dataSource.length) return;
+ this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 });
+ this._map.fitBounds(this._shapeBoundingBox, {
+ duration: 2000,
+ padding: {
+ top: top + cut,
+ bottom: bottom + cut,
+ left: left + cut,
+ right: right + cut,
+ },
+ });
+ }
+
+ // 对其到所有形状的中心
+ alignCenter(centerOffset = 0) {
+ if (!this._dataSource.length) return;
+ const feature = turf.bboxPolygon(this._shapeBoundingBox);
+ const center = turf.center(feature);
+ this._map.panTo(turf.getCoord(center), {
+ offset: [centerOffset, 0],
+ });
+ }
+
+ clear() {
+ if (!this._map) return;
+ this.isRendered = false;
+ if (this._map.getSource(this._sourceId.FILL)) {
+ this._map.off('click', this._layerId.FILL, this._onClick);
+ this._map.off('mousemove', this._layerId.FILL, this._onMouseMove);
+ this._map.off('mouseleave', this._layerId.FILL, this._onMouseLeave);
+ this._map.removeLayer(this._layerId.FILL);
+ this._map.removeSource(this._sourceId.FILL);
+ }
+ if (this._map.getSource(this._sourceId.STROKE)) {
+ this._map.removeLayer(this._layerId.STROKE);
+ this._map.removeSource(this._sourceId.STROKE);
+ }
+ if (this._map.getSource(this._sourceId.LABEL)) {
+ this._map.removeLayer(this._layerId.LABEL);
+ this._map.removeSource(this._sourceId.LABEL);
+ }
+ }
+
+ destroy() {
+ this.clear();
+ this._map = null;
+ this._dataSource = [];
+ this.updateStyle();
+ this.updateOptions();
+ }
+}
+
+export default ShapeRenderer;
diff --git a/src/core/mapHelper.js b/src/core/mapHelper.js
new file mode 100644
index 0000000..4666df0
--- /dev/null
+++ b/src/core/mapHelper.js
@@ -0,0 +1,90 @@
+/**
+ * 地图助手
+ */
+import mapbox from 'mapbox-gl';
+import * as turf from '@turf/turf';
+
+class MapHelper {
+ _map = null;
+
+ setMap(mapInstance) {
+ if (this._map === mapInstance) return;
+ if (!(mapInstance instanceof mapbox.Map)) {
+ throw new Error('必须传入一个mapbox地图实例');
+ }
+ this._map = mapInstance;
+ window.map = mapInstance;
+ }
+
+ // 改变高清图层状态
+ changeHdLayer(toStatus, map = this._map) {
+ if (!map.getSource('hd-tiles')) {
+ map.addSource('hd-tiles', {
+ type: 'raster',
+ tiles: ['http://jiagu-map.oss-cn-shanghai.aliyuncs.com/tile/{z}/{x}/{y}.webp'],
+ tileSize: 256,
+ minzoom: 12,
+ maxzoom: 24,
+ });
+
+ map.addLayer({
+ id: 'hd-tiles-layer',
+ type: 'raster',
+ source: 'hd-tiles',
+ layout: {
+ visibility: 'none',
+ },
+ }, 'keyLayer');
+ }
+ map.setLayoutProperty('hd-tiles-layer', 'visibility', toStatus ? 'visible' : 'none');
+ }
+
+ // 获取一些点的包围盒(像素值)
+ getBoundingBoxProject = (points = []) => {
+ if (!this._map) return null;
+ const pointsFeature = turf.multiPoint(points);
+ const [westLng, southLat, eastLng, northLat] = turf.bbox(pointsFeature);
+ // project返回的是dom坐标系┏
+ const { x: x1, y: y1 } = this._map.project([westLng, southLat]);
+ const { x: x2, y: y2 } = this._map.project([eastLng, northLat]);
+ // 将dom坐标转换为webgl坐标┗
+ const { height } = this._map.getCanvas().getBoundingClientRect();
+ return [x1, height - y1, x2, height - y2];
+ };
+
+ // 获取包围盒区域图像数据
+ getImageData(bbox, padding = 0, size = 0) {
+ const [x1, y1, x2, y2] = bbox;
+ const glWidth = Math.abs(x2 - x1).toFixed(0) - 0 + padding * 2;
+ const glHeight = Math.abs(y2 - y1).toFixed(0) - 0 + padding * 2;
+ const diff = glWidth - glHeight; // 宽高差
+ const offset = Math.ceil(Math.abs(diff / 2)); // 起点偏移量
+ const originSize = diff >= 0 ? glWidth : glHeight;
+ const x = diff >= 0 ? (x1.toFixed(0) - padding) : (x1.toFixed(0) - offset - padding);
+ const y = diff < 0 ? (y1.toFixed(0) - padding) : (y1.toFixed(0) - offset - padding);
+
+ const gl = this._map.getCanvas().getContext('webgl');
+ const pixels = new Uint8Array(originSize * originSize * 4);
+ gl.readPixels(x, y, originSize, originSize, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+
+ const imageData = new ImageData(new Uint8ClampedArray(pixels), originSize, originSize);
+ const tempCanvas = document.createElement('canvas');
+ tempCanvas.width = originSize;
+ tempCanvas.height = originSize;
+ const tempCtx = tempCanvas.getContext('2d');
+ tempCtx.putImageData(imageData, 0, 0);
+
+ const canvas = document.createElement('canvas');
+ canvas.width = size || originSize;
+ canvas.height = size || originSize;
+ const ctx = canvas.getContext('2d');
+ ctx.setTransform(1, 0, 0, -1, 0, canvas.height);
+ ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
+
+ return new Promise((resolve) => {
+ canvas.toBlob((blob) => resolve(blob), 'image/png');
+ });
+ }
+}
+
+export default new MapHelper();
diff --git a/src/layout/MainContainer.vue b/src/layout/MainContainer.vue
index 7fd593e..e29dbc1 100644
--- a/src/layout/MainContainer.vue
+++ b/src/layout/MainContainer.vue
@@ -2,13 +2,14 @@
import { RouterView } from 'vue-router';
import MapLayer from '@/layout/MapLayer.vue';
import SideMenu from '@/layout/components/SideMenu.vue';
+ import TopBar from '@/layout/components/TopBar.vue';
- 云端无人机管理系统(管理员)
+
@@ -27,16 +28,21 @@
position: relative;
height: 100%;
pointer-events: none;
+ color: var(--td-text-color-primary);
:global {
.t-layout__header {
pointer-events: auto;
+ box-shadow: 0 0 2px 0 var(--td-component-border);
+ z-index: 2;
}
.t-layout__sider {
width: fit-content;
background-color: transparent;
pointer-events: auto;
+ box-shadow: 0 0 1px 0 var(--td-component-border);
+ z-index: 1;
}
.t-layout--with-sider {
@@ -44,6 +50,8 @@
}
.t-layout__content {
+ position: relative;
+ //color: var(--td-text-color-primary);
//pointer-events: none;
}
}
diff --git a/src/layout/MapLayer.vue b/src/layout/MapLayer.vue
index e99baa3..ab88467 100644
--- a/src/layout/MapLayer.vue
+++ b/src/layout/MapLayer.vue
@@ -2,6 +2,11 @@
import { onMounted } from 'vue';
import mapbox from 'mapbox-gl';
import config, { mapStyles } from '@/config/map';
+ import commonRefs from '@/utils/commonRefs';
+ import mapHelper from '@/core/mapHelper';
+ import 'mapbox-gl/dist/mapbox-gl.css';
+ import MultifunctionalBar from '@/layout/components/MultifunctionalBar.vue';
+ import ZoomBar from '@/layout/components/ZoomBar.vue';
let map;
@@ -105,6 +110,8 @@
});
map.on('style.load', () => {
+ commonRefs.setRef('map', map);
+ mapHelper.setMap(map);
});
map.on('load', () => {
@@ -121,17 +128,34 @@
-
+
diff --git a/src/layout/components/MultifunctionalBar.vue b/src/layout/components/MultifunctionalBar.vue
new file mode 100644
index 0000000..478b10e
--- /dev/null
+++ b/src/layout/components/MultifunctionalBar.vue
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 高清图层
+
+
+
+
+
+
+
+
+
+
+
+ 地块形状
+
+
+
+ 地块名称
+
+
+
+
+
+
+
diff --git a/src/layout/components/SideMenu.vue b/src/layout/components/SideMenu.vue
index 11c626f..68a27e6 100644
--- a/src/layout/components/SideMenu.vue
+++ b/src/layout/components/SideMenu.vue
@@ -1,17 +1,39 @@
-
+
+
+
+
+
+
+
+
+
+
+ 案例模板
+
@@ -24,19 +46,25 @@
设备管理
-
+
制造商管理
+
+
+
+
+ 监管者管理
+
架次管理
-
+
@@ -48,11 +76,6 @@
数据分析
-
-
-
-
-
diff --git a/src/layout/components/TopBar.vue b/src/layout/components/TopBar.vue
new file mode 100644
index 0000000..abbe05b
--- /dev/null
+++ b/src/layout/components/TopBar.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ 云端无人机管理系统(管理员)
+
+
+
+ 监察者模式
+
+
+
+
+
+
+
+
+ 退出登录
+
+
+
+
+
+
+
+
diff --git a/src/layout/components/ZoomBar.vue b/src/layout/components/ZoomBar.vue
new file mode 100644
index 0000000..806bf42
--- /dev/null
+++ b/src/layout/components/ZoomBar.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+ {{ level }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/router/index.js b/src/router/index.js
index 5cf5984..df8354f 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,18 +1,32 @@
import { createRouter, createWebHistory } from 'vue-router';
-// import auth from '@/utils/auth';
+import auth from '@/utils/auth';
const ExampleView = () => import('@/views/ExampleView/ExampleView.vue');
const LoginView = () => import('@/views/LoginView/LoginView.vue');
+const NoFlyZoneView = () => import('@/views/NoFlyZoneView/NoFlyZoneView.vue');
+const NoFlyZoneEditorView = () => import('@/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue');
+const ManufacturerView = () => import('@/views/ManufacturerView/ManufacturerView.vue');
+const RegulatorView = () => import('@/views/RegulatorView/RegulatorView.vue');
const routes = [
{
path: '/',
- redirect: () => '/login',
+ redirect: () => '/example',
},
{ path: '/example', name: 'ExampleView', component: ExampleView },
{ path: '/login', name: 'LoginView', component: LoginView },
+
+ { path: '/no-fly-zone', name: 'NoFlyZoneView', component: NoFlyZoneView },
+
+ { path: '/no-fly-zone/create', name: 'NoFlyZoneCreateView', component: NoFlyZoneEditorView, meta: { group: 'NoFlyZoneView' } },
+
+ { path: '/no-fly-zone/:id/edit', name: 'NoFlyZoneEditorView', component: NoFlyZoneEditorView, meta: { group: 'NoFlyZoneView' } },
+
+ { path: '/manufacturers', name: 'ManufacturerView', component: ManufacturerView },
+
+ { path: '/regulators', name: 'RegulatorView', component: RegulatorView },
];
const router = createRouter({
@@ -21,16 +35,16 @@ const router = createRouter({
});
// 前置路由钩子
-// router.beforeEach((to, from, next) => {
-// // 必须先登录
-// if (!['LoginView'].includes(to.name) && !auth.checkToken()) {
-// return next({
-// name: 'LoginView',
-// replace: true,
-// });
-// }
-//
-// return next();
-// });
+router.beforeEach((to, from, next) => {
+ // 必须先登录
+ if (!['LoginView'].includes(to.name) && !auth.checkToken()) {
+ return next({
+ name: 'LoginView',
+ replace: true,
+ });
+ }
+
+ return next();
+});
export default router;
diff --git a/src/stores/index.js b/src/stores/index.js
index 0b0a4ed..e10347e 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -7,3 +7,7 @@ const store = createPinia();
export default store;
export * from './modules/exampleStore';
+export * from './modules/noFlyZoneStore';
+export * from './modules/authStore';
+export * from './modules/manufacturerStore';
+export * from './modules/regulatorStore';
diff --git a/src/stores/modules/authStore.js b/src/stores/modules/authStore.js
new file mode 100644
index 0000000..4f31301
--- /dev/null
+++ b/src/stores/modules/authStore.js
@@ -0,0 +1,58 @@
+/**
+ * authStore
+ */
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+import http from '@/utils/http';
+import * as helpers from '@/utils/helpers';
+import * as urls from '@/config/urls';
+import auth from '@/utils/auth';
+import UserInfo from '@/utils/UserInfo';
+
+export const useAuthStore = defineStore('auth', () => {
+ // state
+ const userInfo = ref(UserInfo.get());
+
+ // const isPlatform = computed(() => {
+ // const { roles } = userInfo;
+ // return (roles || []).includes('platform');
+ // });
+
+ // const isFarmer = computed(() => {
+ // const { roles } = userInfo;
+ // return (roles || []).includes('farmer');
+ // });
+
+ // action
+ function loginWithPassword(formData = {}) {
+ const requestDate = helpers.pick(formData, [
+ 'phone',
+ 'password',
+ ]);
+ return http.getInstance(false).post(urls.LOGIN_WITH_PASSWORD, requestDate).then(({ data }) => {
+ // 获取信息
+ const { data: { accessToken } = {}, extra = {} } = data || {};
+ // 更新信息
+ const newUserInfo = {
+ ...userInfo.value,
+ ...extra,
+ };
+ userInfo.value = { ...newUserInfo };
+ // 保存信息
+ UserInfo.save(newUserInfo);
+ if (accessToken) {
+ auth.saveToken(accessToken);
+ }
+ return data;
+ });
+ }
+
+ return {
+ userInfo,
+ // isPlatform,
+ // isFarmer,
+ loginWithPassword,
+ };
+});
+
+export default {};
diff --git a/src/stores/modules/manufacturerStore.js b/src/stores/modules/manufacturerStore.js
new file mode 100644
index 0000000..1684994
--- /dev/null
+++ b/src/stores/modules/manufacturerStore.js
@@ -0,0 +1,121 @@
+/**
+ * 制造商
+ */
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+import http from '@/utils/http';
+import * as urls from '@/config/urls';
+import * as helpers from '@/utils/helpers';
+
+export const useManufacturerStore = defineStore('manufacturer', () => {
+ const manufacturerList = ref([]);
+ const manufacturerExtra = ref({ total: null });
+ const manufacturerQueries = ref({ page: 1, pageSize: 10, all: undefined, search: undefined });
+
+ function getManufacturerList(otherQueries = {}) {
+ return http.getInstance().get(urls.GET_MANUFACTURER_LIST, {
+ params: { ...manufacturerQueries.value, ...otherQueries },
+ }).then(({ data }) => {
+ const { data: list, extra } = data;
+ manufacturerList.value = list || [];
+ manufacturerExtra.value = { ...manufacturerExtra.value, ...(extra || {}) };
+ manufacturerQueries.value = { ...manufacturerQueries.value, ...otherQueries };
+ return data;
+ });
+ }
+
+ function createManufacturer(formData = {}, { refreshList = false } = {}) {
+ const reqData = helpers.pick(formData, [
+ 'manufacturerName',
+ 'manufacturerLogo',
+ 'contactPhone',
+ 'contactName',
+ // 'province',
+ // 'city',
+ // 'county',
+ // 'enterprise_name',
+ 'legal',
+ 'creditCode',
+ 'detailAddress',
+ 'email',
+ 'businessLicense',
+ 'companyLogo',
+ // 'idcardNum',
+ // 'idcardFrontPic',
+ // 'idcardReversePic',
+ // 'ruleProvince',
+ // 'ruleCity',
+ // 'ruleCounty',
+ ]);
+
+ return http.getInstance().post(urls.CREATE_MANUFACTURER, reqData).then(({ data }) => {
+ if (refreshList) getManufacturerList();
+ return data;
+ });
+ }
+
+ function updateManufacturer(formData = {}, { refreshList = false } = {}) {
+ const reqData = helpers.pick(formData, [
+ 'id',
+ 'manufacturerName',
+ 'manufacturerLogo',
+ 'contactPhone',
+ 'contactName',
+ // 'province',
+ // 'city',
+ // 'county',
+ // 'enterprise_name',
+ 'legal',
+ 'creditCode',
+ 'detailAddress',
+ 'email',
+ 'businessLicense',
+ 'companyLogo',
+ // 'idcardNum',
+ // 'idcardFrontPic',
+ // 'idcardReversePic',
+ // 'ruleProvince',
+ // 'ruleCity',
+ // 'ruleCounty',
+ ]);
+
+ const url = helpers.buildURL(urls.UPDATE_MANUFACTURER, reqData.id);
+ return http.getInstance().put(url, reqData).then(({ data }) => {
+ if (refreshList) getManufacturerList();
+ return data;
+ });
+ }
+
+ function deleteManufacturer(manufacturerId = '', { refreshList = false } = {}) {
+ const url = helpers.buildURL(urls.DELETE_MANUFACTURER, manufacturerId);
+ return http.getInstance().delete(url).then(({ data }) => {
+ if (refreshList) getManufacturerList();
+ return data;
+ });
+ }
+
+ function updateManufacturerDisable(formData = {}, { refreshList = false } = {}) {
+ const reqData = helpers.pick(formData, [
+ 'id',
+ ]);
+
+ const url = helpers.buildURL(urls.UPDATE_MANUFACTURER_DISABLE, reqData.id);
+ return http.getInstance().put(url).then(({ data }) => {
+ if (refreshList) getManufacturerList();
+ return data;
+ });
+ }
+
+ return {
+ manufacturerList,
+ manufacturerQueries,
+ manufacturerExtra,
+ getManufacturerList,
+ createManufacturer,
+ updateManufacturer,
+ deleteManufacturer,
+ updateManufacturerDisable,
+ };
+});
+
+export default null;
diff --git a/src/stores/modules/noFlyZoneStore.js b/src/stores/modules/noFlyZoneStore.js
new file mode 100644
index 0000000..fffa775
--- /dev/null
+++ b/src/stores/modules/noFlyZoneStore.js
@@ -0,0 +1,109 @@
+/**
+ * 禁飞区
+ */
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+import http from '@/utils/http';
+import * as urls from '@/config/urls';
+import * as helpers from '@/utils/helpers';
+
+export const useNoFlyZoneStore = defineStore('noFlyZone', () => {
+ const noFlyZoneList = ref([]);
+ const noFlyZoneExtra = ref({ total: null });
+ const noFlyZoneQueries = ref({ page: 1, pageSize: 10, all: undefined, type: undefined, search: undefined });
+
+ const noFlyZoneDetail = ref({});
+
+ function getNoFlyZoneList(otherQueries = {}) {
+ return http.getInstance().get(urls.GET_NO_FLY_ZONE_LIST, {
+ params: { ...noFlyZoneQueries.value, ...otherQueries },
+ }).then(({ data }) => {
+ const { data: list, extra } = data;
+ noFlyZoneList.value = list || [];
+ noFlyZoneExtra.value = { ...noFlyZoneExtra.value, ...(extra || {}) };
+ noFlyZoneQueries.value = { ...noFlyZoneQueries.value, ...otherQueries };
+ return data;
+ });
+ }
+
+ function getNoFlyZoneDetail(noFlyZoneId = '') {
+ const url = helpers.buildURL(urls.GET_NO_FLY_ZONE_DETAIL, noFlyZoneId);
+ return http.getInstance().get(url).then(({ data }) => {
+ const { data: detail } = data || {};
+ noFlyZoneDetail.value = detail;
+ return data;
+ });
+ }
+
+ function createNoFlyZone(formData = {}, { refreshList = false } = {}) {
+ const reqData = helpers.pick(formData, [
+ 'detailAddress',
+ 'effectTimeStart',
+ 'effectTimeEnd',
+ 'province',
+ // 'city',
+ // 'county',
+ 'orbit',
+ ]);
+
+ return http.getInstance().post(urls.CREATE_NO_FLY_ZONE, reqData).then(({ data }) => {
+ if (refreshList) getNoFlyZoneList();
+ return data;
+ });
+ }
+
+ function updateNoFlyZone(formData = {}, { refreshList = false } = {}) {
+ const reqData = helpers.pick(formData, [
+ 'id',
+ 'detailAddress',
+ 'effectTimeStart',
+ 'effectTimeEnd',
+ 'province',
+ // 'city',
+ // 'county',
+ 'orbit',
+ 'isEnable',
+ ]);
+
+ const url = helpers.buildURL(urls.UPDATE_NO_FLY_ZONE, reqData.id);
+ return http.getInstance().put(url, reqData).then(({ data }) => {
+ if (refreshList) getNoFlyZoneList();
+ return data;
+ });
+ }
+
+ function deleteNoFlyZone(noFlyZoneId = '', { refreshList = false } = {}) {
+ const url = helpers.buildURL(urls.DELETE_NO_FLY_ZONE, noFlyZoneId);
+ return http.getInstance().delete(url).then(({ data }) => {
+ if (refreshList) getNoFlyZoneList();
+ return data;
+ });
+ }
+
+ function updateNoFlyZoneState(formData = {}, { refreshList = false } = {}) {
+ const reqData = helpers.pick(formData, [
+ 'id',
+ ]);
+
+ const url = helpers.buildURL(urls.UPDATE_NO_FLY_ZONE_DISABLE, reqData.id);
+ return http.getInstance().put(url).then(({ data }) => {
+ if (refreshList) getNoFlyZoneList();
+ return data;
+ });
+ }
+
+ return {
+ noFlyZoneList,
+ noFlyZoneQueries,
+ noFlyZoneExtra,
+ noFlyZoneDetail,
+ getNoFlyZoneList,
+ createNoFlyZone,
+ updateNoFlyZone,
+ deleteNoFlyZone,
+ updateNoFlyZoneState,
+ getNoFlyZoneDetail,
+ };
+});
+
+export default null;
diff --git a/src/stores/modules/regulatorStore.js b/src/stores/modules/regulatorStore.js
new file mode 100644
index 0000000..f99a7c7
--- /dev/null
+++ b/src/stores/modules/regulatorStore.js
@@ -0,0 +1,51 @@
+/**
+ * 监管者
+ */
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+import http from '@/utils/http';
+import * as urls from '@/config/urls';
+import * as helpers from '@/utils/helpers';
+
+export const useRegulatorStore = defineStore('regulator', () => {
+ const regulatorList = ref([]);
+ const regulatorExtra = ref({ total: null });
+ const regulatorQueries = ref({ page: 1, pageSize: 10, all: undefined, search: undefined });
+
+ function getRegulatorList(otherQueries = {}) {
+ return http.getInstance().get(urls.GET_REGULATOR_LIST, {
+ params: { ...regulatorQueries.value, ...otherQueries },
+ }).then(({ data }) => {
+ const { data: list, extra } = data;
+ regulatorList.value = list || [];
+ regulatorExtra.value = { ...regulatorExtra.value, ...(extra || {}) };
+ regulatorQueries.value = { ...regulatorQueries.value, ...otherQueries };
+ return data;
+ });
+ }
+
+ function createRegulator(formData = {}, { refreshList = false } = {}) {
+ const reqData = helpers.pick(formData, [
+ 'phone',
+ 'password',
+ ]);
+
+ return http.getInstance().post(urls.CREATE_REGULATOR, reqData).then(({ data }) => {
+ if (refreshList) getRegulatorList();
+ return data;
+ });
+ }
+
+ return {
+ regulatorList,
+ regulatorQueries,
+ regulatorExtra,
+ getRegulatorList,
+ createRegulator,
+ // updateRegulator,
+ // deleteRegulator,
+ // updateRegulatorDisable,
+ };
+});
+
+export default null;
diff --git a/src/styles/common.less b/src/styles/common.less
index d8894d7..048010f 100644
--- a/src/styles/common.less
+++ b/src/styles/common.less
@@ -3,14 +3,125 @@
*/
@import "variables";
+:root {
+ color-scheme: dark;
+}
+
body,
html,
#app {
margin: 0;
padding: 0;
height: 100%;
+ color: white;
}
.t-layout {
background: transparent !important;
}
+
+.mapboxgl-popup.shape-editor-popup,
+.mapboxgl-popup.line-editor-popup {
+ .mapboxgl-popup-content {
+ background-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
+ border-bottom-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
+ border-top-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
+ border-right-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
+ border-left-color: fade(black, 60%);
+ }
+
+ .form {
+ padding: 10px 10px 0 6px;
+
+ & > div + div {
+ margin-top: 8px;
+ }
+
+ .buttons {
+ padding-left: 3em;
+ }
+
+ input {
+ border: 1px solid #2b6c5a;
+ background-color: fade(black, 30%);
+ padding: 2px 4px;
+
+ &:focus {
+ outline: 1px solid #00ffcc;
+ }
+ }
+
+ button {
+ background-color: #00ffcc;
+ border: none;
+ border-radius: 2px;
+ color: black;
+ cursor: pointer;
+
+ & + button {
+ margin-left: 5px;
+ }
+ }
+ }
+}
+
+.mapboxgl-popup.perception-popup {
+ .mapboxgl-popup-content {
+ padding: 0;
+ background-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
+ border-bottom-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
+ border-bottom-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
+ border-top-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
+ border-top-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
+ border-right-color: fade(black, 60%);
+ }
+
+ &.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
+ border-left-color: fade(black, 60%);
+ }
+}
+
+.mapboxgl-ctrl-bottom-left,
+.mapboxgl-ctrl-bottom-right {
+ opacity: 0.2;
+ transition: opacity 0.2s ease;
+
+ &:hover {
+ opacity: 1;
+ }
+}
+
+.success-bg-color {
+ background-color: #52c41a !important;
+}
+
+.danger-bg-color {
+ background-color: #f5222d !important;
+}
diff --git a/src/utils/UserInfo.js b/src/utils/UserInfo.js
new file mode 100644
index 0000000..873891c
--- /dev/null
+++ b/src/utils/UserInfo.js
@@ -0,0 +1,29 @@
+/**
+ * 用户信息
+ */
+
+class UserInfo {
+ static save(userInfo) {
+ localStorage.setItem('userInfo', JSON.stringify(userInfo));
+ }
+
+ static get() {
+ const userInfo = localStorage.getItem('userInfo');
+ if (!userInfo) {
+ return {};
+ }
+ try {
+ const result = JSON.parse(userInfo);
+ return typeof result === 'object' ? result : {};
+ } catch (e) {
+ //
+ }
+ return {};
+ }
+
+ static remove() {
+ localStorage.removeItem('userInfo');
+ }
+}
+
+export default UserInfo;
diff --git a/src/utils/commonRefs.js b/src/utils/commonRefs.js
new file mode 100644
index 0000000..152a368
--- /dev/null
+++ b/src/utils/commonRefs.js
@@ -0,0 +1,46 @@
+/*
+ * 公共引用
+ * 异步获取引用对象
+ */
+
+const refs = Symbol('refs');
+const pendingList = Symbol('pendingList');
+
+class CommonRefs {
+ [refs] = {};
+
+ [pendingList] = {};
+
+ setRef(key, ref) {
+ if (key in this[refs]) return;
+
+ this[refs][key] = ref;
+ const { [key]: resolves } = this[pendingList];
+ (resolves || []).forEach((resolve, index) => {
+ if (!resolve) return;
+ resolve(ref);
+ resolves[index] = undefined; // 只作废,不删除
+ });
+ }
+
+ getRef(key) {
+ return new Promise((resolve) => {
+ const { [key]: ref } = this[refs];
+ if (ref) {
+ resolve(ref);
+ } else if (key in this[pendingList]) {
+ this[pendingList][key].push(resolve);
+ } else {
+ this[pendingList][key] = [resolve];
+ }
+ });
+ }
+
+ removeRef(key) {
+ if (key in this[refs]) {
+ delete this[refs][key];
+ }
+ }
+}
+
+export default new CommonRefs();
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
new file mode 100644
index 0000000..e317843
--- /dev/null
+++ b/src/utils/helpers.js
@@ -0,0 +1,180 @@
+/**
+ * 辅助函数
+ */
+import moment from 'moment';
+import * as turf from '@turf/turf';
+
+/**
+ * 等待异步结果
+ * @param checker 检测函数,返回值为truthy时表示得到想要的结果,否则反之
+ * @param timeout 指定时间内没有得到想要的结果则超时
+ */
+export function until(checker = () => true, timeout = 2000) {
+ return new Promise((resolve) => {
+ let pollingTimer = null;
+
+ const timeoutTimer = setTimeout(() => {
+ clearInterval(pollingTimer);
+ resolve(false);
+ }, timeout);
+
+ pollingTimer = setInterval(() => {
+ const result = checker();
+ if (result) {
+ clearTimeout(timeoutTimer);
+ clearInterval(pollingTimer);
+ resolve(result);
+ }
+ }, 10);
+ });
+}
+
+/**
+ * 根据restful的uri构建实际url
+ * @param uri uri模版
+ * @param argv 插入到uri中的参数列表,按顺序插入
+ * @returns url
+ */
+export function buildURL(uri, ...argv) {
+ return uri.replace(/{\w+}|:[a-zA-Z]+/g, () => {
+ const res = argv.shift();
+ if (res === undefined) {
+ throw new Error('URI 参数不足');
+ }
+ return res;
+ });
+}
+
+/**
+ * 选取原object指定属性,构成新的object
+ * @param originObject
+ * @param pickKeys
+ */
+export function pick(originObject, pickKeys = []) {
+ const newObject = {};
+ pickKeys.forEach((key) => {
+ if (Object.prototype.hasOwnProperty.call(originObject, key)) {
+ newObject[key] = originObject[key];
+ }
+ });
+ return newObject;
+}
+
+/**
+ * 更新target对象的键值,使用source对象里的数据
+ * @param target
+ * @param source
+ */
+export function update(target, source) {
+ const result = { ...target };
+ Object.keys(target).forEach((key) => {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ result[key] = source[key];
+ }
+ });
+ return result;
+}
+
+/**
+ * 将对象的部分key重命名
+ * @param originObject 原始对象(也可以是相同结构的对象构成的数组)
+ * @param keysMapping 新keys映射(如{ name: 'username', sex: 'gender' },表示把原始对象中的name重命名为username、把sex重命名为gender,原始对象其他key不变)
+ * @returns {{}|*} 新对象
+ */
+export function renameKeys(originObject, keysMapping = {}) {
+ if (!Array.isArray(originObject)) {
+ const kvList = Object.keys(originObject).map((key) => {
+ const newKey = keysMapping[key] || key;
+ return { [newKey]: originObject[key] };
+ });
+ return Object.assign({}, ...kvList);
+ }
+ return originObject.map((obj) => renameKeys(obj, keysMapping));
+}
+
+/**
+ * 随机数
+ */
+export function rand(min, max, fraction = 0) {
+ const res = (Math.random() * ((max - min) + 1)) + min;
+ return res.toFixed(fraction) - 0;
+}
+
+/**
+ * 将input当着数字处理,若不是数字则返回代替字符
+ * @param input
+ * @param substitution
+ * @returns {string|number}
+ */
+export function numeric(input, substitution = '-') {
+ const type = typeof input;
+ if (type !== 'number' && type !== 'string') return substitution;
+
+ const output = Number(input);
+ return !Number.isNaN(output) ? output : substitution;
+}
+
+// 保留小数位(是整数则不保留)
+export const toFixed = (num, digits = 2) => (Number(num || 0).toFixed(digits)) - 0;
+
+/**
+ * 格式化时间戳
+ * @param timestamp
+ * @param format
+ * @returns {string}
+ */
+export function formatTime(timestamp, format = 'YYYY-MM-DD HH:mm:ss') {
+ return (timestamp || timestamp === 0) && moment(timestamp).isValid() ? moment(timestamp).format(format) : '-';
+}
+
+/**
+ * 将总秒数格式化为“x时y分z秒”形式
+ * @param sec
+ * @param precision
+ * @param isChinese
+ * @returns {string}
+ */
+export function popularTime(sec, precision = 0, isChinese = false) {
+ const { HOUR, MINUTE, SECOND } = isChinese ? { HOUR: '时', MINUTE: '分', SECOND: '秒' } : { HOUR: 'h', MINUTE: 'm', SECOND: 's' };
+ let remainingSec = sec;
+ const hours = Math.floor(remainingSec / 3600);
+
+ remainingSec -= hours * 3600;
+ const minutes = Math.floor(remainingSec / 60);
+
+ remainingSec -= minutes * 60;
+
+ return `${hours ? `${hours}${HOUR}` : ''}${minutes ? `${minutes}${MINUTE}` : ''}${remainingSec ? `${remainingSec.toFixed(precision)}${SECOND}` : ''}`;
+}
+
+/**
+ * 将秒转化为小时
+ * @param sec
+ * @returns {number}
+ */
+export function toHour(sec) {
+ return (sec / 3600).toFixed(2) - 0;
+}
+
+/**
+ * 对数组中的每个元素进行偏移计算,返回计算后的数组
+ * @param originArray 原数组(有数字构成)
+ * @param offsetArray 偏移量数组(与原数组元素一一对应)
+ * @returns {*[]} 计算后的新数组
+ */
+export function arrOffset(originArray = [], offsetArray = []) {
+ return originArray.map((item, index) => +item + (offsetArray[index] || 0));
+}
+
+/**
+ * 计算经纬度构成的区域面积
+ * @param coordinates 经纬度点列表
+ * @param unit 单位(默认:亩)
+ * @returns {number} 面积值(平方米)
+ */
+export function computeArea(coordinates, unit = 'mu') {
+ const lineFeature = turf.lineString(coordinates);
+ const polygonFeature = turf.lineToPolygon(lineFeature);
+ const { [unit]: rate } = { mu: 0.0015 };
+ return turf.area(polygonFeature) * rate;
+}
diff --git a/src/views/LoginView/LoginView.vue b/src/views/LoginView/LoginView.vue
index 166dc65..6fcc9da 100644
--- a/src/views/LoginView/LoginView.vue
+++ b/src/views/LoginView/LoginView.vue
@@ -1,13 +1,136 @@
- LoginView
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+
+
+
diff --git a/src/views/ManufacturerView/ManufacturerView.vue b/src/views/ManufacturerView/ManufacturerView.vue
new file mode 100644
index 0000000..8b0eb3c
--- /dev/null
+++ b/src/views/ManufacturerView/ManufacturerView.vue
@@ -0,0 +1,131 @@
+
+
+
+
+ 制造商管理
+
+
+ 新增制造商
+
+
+
+
+
+ 关键字查询
+
+ 查询
+
+
+
+
+
+ 正常
+ 禁用
+
+
+
+
+ {{ !row.disabled ? '禁用' : '启用' }}
+
+
+ 删除
+
+ 编辑
+ 详情
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/ManufacturerView/components/ManufacturerDetail.vue b/src/views/ManufacturerView/components/ManufacturerDetail.vue
new file mode 100644
index 0000000..1bd1c4c
--- /dev/null
+++ b/src/views/ManufacturerView/components/ManufacturerDetail.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+ 制造商详情
+
+
+
+
企业/组织名称:
+
{{ info.manufacturerName || '-' }}
+
法人代表:
+
{{ info.legal || '-' }}
+
统一社会信用代码:
+
{{ info.creditCode || '-' }}
+
联系人姓名:
+
{{ info.contactName || '-' }}
+
联系人电话:
+
{{ info.contactPhone || '-' }}
+
联系地址:
+
{{ info.detailAddress || '-' }}
+
E-mail:
+
{{ info.email || '-' }}
+
营业执照扫描件:
+
+
公司LOGO:
+
+
+
+
+
+
diff --git a/src/views/ManufacturerView/components/ManufacturerEditor.vue b/src/views/ManufacturerView/components/ManufacturerEditor.vue
new file mode 100644
index 0000000..7356fa0
--- /dev/null
+++ b/src/views/ManufacturerView/components/ManufacturerEditor.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+ {{ formData.id ? '更新' : '创建' }}制造商
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 取消
+
+
+
+
+
+
+
diff --git a/src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue b/src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue
new file mode 100644
index 0000000..764dde9
--- /dev/null
+++ b/src/views/NoFlyZoneEditorView/NoFlyZoneEditorView.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 返回列表
+
+
+
+
+
+
+
+
diff --git a/src/views/NoFlyZoneView/NoFlyZoneView.vue b/src/views/NoFlyZoneView/NoFlyZoneView.vue
new file mode 100644
index 0000000..6c8c894
--- /dev/null
+++ b/src/views/NoFlyZoneView/NoFlyZoneView.vue
@@ -0,0 +1,244 @@
+
+
+
+
+ 禁飞区管理
+
+
+ 新增禁飞区
+ 展开列表
+
+
+
+
+
+ 关键字查询
+
+ 查询
+
+
+
+
+
+ 有效
+ 无效
+
+
+
+ 启用
+ 禁用
+
+
+
+
+
+
+
+
+
+
+ {{ !row.disabled ? '禁用' : '启用' }}
+ 删除
+ 编辑
+ 查看
+
+
+
+ 查看
+
+
+
+
+
+
+
+
+
diff --git a/src/views/RegulatorView/RegulatorView.vue b/src/views/RegulatorView/RegulatorView.vue
new file mode 100644
index 0000000..f435cda
--- /dev/null
+++ b/src/views/RegulatorView/RegulatorView.vue
@@ -0,0 +1,86 @@
+
+
+
+
+ 监管者管理
+
+
+ 新增监管者
+
+
+
+
+
+ 关键字查询
+
+ 查询
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/RegulatorView/components/RegulatorEditor.vue b/src/views/RegulatorView/components/RegulatorEditor.vue
new file mode 100644
index 0000000..1af9c49
--- /dev/null
+++ b/src/views/RegulatorView/components/RegulatorEditor.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+ 创建监管者
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 取消
+
+
+
+
+
+
+
diff --git a/src/views/common/useGlobalSettings.js b/src/views/common/useGlobalSettings.js
new file mode 100644
index 0000000..3e31754
--- /dev/null
+++ b/src/views/common/useGlobalSettings.js
@@ -0,0 +1,23 @@
+/**
+ * 本地持久化配置项
+ */
+import { toValue } from 'vue';
+import { createGlobalState, useStorage } from '@vueuse/core';
+
+export const useGlobalSettings = createGlobalState(() => {
+ const settings = useStorage('settings', {
+ showMapHd: true,
+ showFieldFill: true,
+ showFieldName: true,
+ });
+
+ const valueOf = () => toValue(settings);
+
+ const set = (key, val) => {
+ if (key in settings.value) settings.value[key] = val;
+ };
+
+ return { set, valueOf };
+});
+
+export default null;