xiaosi
1 year ago
35 changed files with 4429 additions and 32 deletions
@ -0,0 +1,14 @@ |
|||
<script setup> |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root"> |
|||
#[[$END$]]# |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
</style> |
After Width: | Height: | Size: 934 KiB |
@ -0,0 +1,231 @@ |
|||
<script setup> |
|||
import { ref, useSlots } from 'vue'; |
|||
// import { useRoute, useRouter } from 'vue-router'; |
|||
|
|||
const emit = defineEmits(['toggle']); |
|||
|
|||
const props = defineProps({ |
|||
width: { |
|||
type: String, |
|||
default: '100%', |
|||
}, |
|||
minWidth: { |
|||
type: String, |
|||
default: 'auto', |
|||
}, |
|||
// fullWidth: { |
|||
// type: Boolean, |
|||
// default: true, |
|||
// }, |
|||
open: { |
|||
type: Boolean, |
|||
default: true, |
|||
}, |
|||
scrollbar: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
expandAbility: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}); |
|||
|
|||
const { 'top-bar': topBar, footer, header, 'header-extra': headerExtra } = useSlots(); |
|||
const isOpen = ref(false); |
|||
// const finalWidth = computed(() => (props.fullWidth ? 'calc(100% - 70px)' : props.width)); |
|||
isOpen.value = props.open; |
|||
|
|||
// onMounted(() => { |
|||
// setTimeout(() => { |
|||
// isOpen.value = props.open; |
|||
// }, 200); |
|||
// }); |
|||
|
|||
// const route = useRoute(); |
|||
// const router = useRouter(); |
|||
// const isActive = (routeName) => { |
|||
// const { group } = route.meta; |
|||
// return route.name === routeName || group === routeName; |
|||
// }; |
|||
|
|||
function onToggle() { |
|||
isOpen.value = !isOpen.value; |
|||
emit('toggle', isOpen.value); |
|||
} |
|||
|
|||
// function onNavTo(routeName) { |
|||
// isOpen.value = false; |
|||
// setTimeout(() => { |
|||
// router.push({ name: routeName }); |
|||
// }, 200); |
|||
// } |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="[s.root, isOpen ? 'is-open' : '', expandAbility ? 'expand' : '']" :style="{ '--width': width, 'min-width': minWidth }"> |
|||
<div class="wrapper"> |
|||
<div class="header" v-if="!!header || !!headerExtra"> |
|||
<t-space size="small"> |
|||
<slot name="header" /> |
|||
</t-space> |
|||
<div class="interspace" /> |
|||
<slot name="header-extra" /> |
|||
</div> |
|||
<div class="top-bar" v-if="!!topBar"> |
|||
<slot name="top-bar" /> |
|||
</div> |
|||
<div class="body" v-scrollbar:[props.scrollbar].padding> |
|||
<slot /> |
|||
</div> |
|||
<div class="footer" v-if="!!footer"> |
|||
<slot name="footer" /> |
|||
</div> |
|||
</div> |
|||
<div class="nav" v-if="expandAbility"> |
|||
<t-tooltip placement="left" :delay="0" :content="isOpen ? '收起' : '展开'"> |
|||
<div class="btn" @click="onToggle"> |
|||
<t-icon name="chevron-right-double" v-if="isOpen" /> |
|||
<t-icon name="chevron-left-double" v-else /> |
|||
</div> |
|||
</t-tooltip> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
pointer-events: auto; |
|||
position: absolute; |
|||
right: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
//z-index: 1; |
|||
color: var(--td-text-color-primary); |
|||
width: var(--width); |
|||
background-color: var(--td-bg-color-page); |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
box-sizing: border-box; |
|||
transition: all 0.2s ease; |
|||
|
|||
&:global.expand{ |
|||
transform: translateX(100%); |
|||
} |
|||
|
|||
&:global.is-open { |
|||
transform: translateX(0px); |
|||
} |
|||
|
|||
:global { |
|||
.wrapper { |
|||
height: 100%; |
|||
//background-color: var(--td-bg-color-container); |
|||
background-color: transparent; |
|||
//border-radius: var(--td-radius-medium); |
|||
//border-radius: var(--td-radius-medium); |
|||
//border-left-color: var(--td-gray-color-10); |
|||
//border-left: 1px solid var(--td-component-stroke); |
|||
//backdrop-filter: blur(20px); |
|||
display: flex; |
|||
flex-direction: column; |
|||
//overflow: hidden; |
|||
|
|||
.header { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
min-height: var(--td-comp-size-xxl); |
|||
font-size: var(--td-font-size-title-large); |
|||
background-color: var(--td-bg-color-container); |
|||
//background-color: fade(black, 30%); |
|||
border-radius: var(--td-radius-medium); |
|||
box-sizing: border-box; |
|||
//box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%); |
|||
box-shadow: 0 0 1px 0 var(--td-component-border); |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
|
|||
.interspace { |
|||
flex: 1; |
|||
} |
|||
|
|||
.t-space .t-icon { |
|||
font-size: var(--td-font-size-headline-medium); |
|||
} |
|||
} |
|||
|
|||
.top-bar { |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
} |
|||
|
|||
.body { |
|||
flex: 1; |
|||
//padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
overflow: auto; |
|||
//box-shadow: 0 0 5px 0 rgba(174, 174, 174, 60%); |
|||
box-shadow: 0 0 1px 0 var(--td-component-border); |
|||
background-color: var(--td-bg-color-container); |
|||
border-radius: var(--td-radius-medium); |
|||
} |
|||
|
|||
.footer { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
min-height: var(--td-comp-size-xxl); |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
//background-color: fade(black, 30%); |
|||
box-sizing: border-box; |
|||
} |
|||
} |
|||
|
|||
.nav { |
|||
position: absolute; |
|||
right: 100%; |
|||
//left: 100%; |
|||
top: var(--td-comp-paddingTB-m); |
|||
|
|||
.btn { |
|||
width: var(--td-comp-size-l); |
|||
min-height: var(--td-comp-size-m); |
|||
padding: var(--td-comp-paddingTB-m) 0; |
|||
background-color: var(--td-bg-color-container); |
|||
border-right: 1px solid var(--td-bg-color-container); |
|||
border-radius: var(--td-radius-medium) 0 0 var(--td-radius-medium); |
|||
backdrop-filter: blur(10px); |
|||
transition: background-color ease 0.2s; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
box-sizing: border-box; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
background-color: var(--td-brand-color-hover); |
|||
} |
|||
|
|||
& + .btn { |
|||
border-top: 1px solid fade(black, 50%); |
|||
} |
|||
|
|||
span { |
|||
display: none; |
|||
width: 1em; |
|||
font-size: var(--td-font-size-link-medium); |
|||
line-height: 1.2; |
|||
margin-top: 5px; |
|||
} |
|||
|
|||
&.is-active { |
|||
background-color: var(--td-brand-color-active); |
|||
border-top-color: transparent; |
|||
|
|||
span { |
|||
display: block; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,92 @@ |
|||
<script setup> |
|||
import { ref, watchEffect } from 'vue'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import { REQUEST_UPLOAD_FILE } from '@/config/urls'; |
|||
import auth from '@/utils/auth'; |
|||
|
|||
const props = defineProps({ |
|||
modelValue: { |
|||
type: [String, Array], |
|||
default: '', |
|||
}, |
|||
subject: { |
|||
type: String, |
|||
required: true, |
|||
}, |
|||
multiple: { |
|||
type: Boolean, |
|||
default: false, |
|||
}, |
|||
}); |
|||
const emit = defineEmits(['update:modelValue']); |
|||
|
|||
const originFile = ref([]); |
|||
|
|||
watchEffect(() => { |
|||
if (props.multiple && props.modelValue) { |
|||
originFile.value = [...(props.modelValue || []).map((url) => ({ name: 'image', url }))]; |
|||
} else if (props.modelValue) { |
|||
originFile.value = [{ name: 'image', url: props.modelValue }]; |
|||
} else { |
|||
originFile.value = []; |
|||
} |
|||
}); |
|||
|
|||
const uploadRef = ref(); |
|||
|
|||
const handleFail = () => { |
|||
MessagePlugin.error('文件上传失败'); |
|||
}; |
|||
const handleSuccess = ({ response }) => { |
|||
if (props.multiple) { |
|||
const [{ data }] = response; |
|||
const temp = [...(props.modelValue || [])]; |
|||
temp.push(data); |
|||
emit('update:modelValue', temp); |
|||
return; |
|||
} |
|||
const { data } = response; |
|||
emit('update:modelValue', data); |
|||
}; |
|||
const handleRemove = ({ index }) => { |
|||
if (props.multiple) { |
|||
const temp = [...(props.modelValue || [])]; |
|||
temp.splice(index, 1); |
|||
emit('update:modelValue', temp); |
|||
return; |
|||
} |
|||
emit('update:modelValue', ''); |
|||
}; |
|||
</script> |
|||
<template> |
|||
<t-upload |
|||
:class="s.root" |
|||
ref="uploadRef" |
|||
v-model="originFile" |
|||
:action="REQUEST_UPLOAD_FILE(subject)" |
|||
theme="image" |
|||
accept="image/*" |
|||
:headers="{ |
|||
Authorization: `Bearer ${auth.getToken()}`, |
|||
}" |
|||
:locale="{ |
|||
triggerUploadText: { |
|||
image: '请选择图片', |
|||
}, |
|||
}" |
|||
:multiple="multiple" |
|||
@fail="handleFail" |
|||
@success="handleSuccess" |
|||
@remove="handleRemove" |
|||
/> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
:global { |
|||
.t-upload__card-name { |
|||
display: none |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -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; |
@ -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('按住可拖拽控制点<br>点击可微调控制点<br>点击第一个控制点可闭合形状'); |
|||
|
|||
// 闭合形状提示
|
|||
_completeTooltip = (new mapbox.Popup({ closeButton: false, offset: 6, className: 'shape-editor-popup' })).setHTML('按住可拖拽控制点<br>点击可微调控制点,或闭合形状'); |
|||
|
|||
// 坐标编辑器
|
|||
_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 = ` |
|||
<div><label>经度:<input class="lng" type="text" value=""></label></div> |
|||
<div><label>纬度:<input class="lat" type="text" value=""></label></div> |
|||
<div class="buttons"> |
|||
<button class="confirm">确定</button> |
|||
<button class="close-shape success-bg-color">闭合形状</button> |
|||
</div> |
|||
`;
|
|||
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; |
@ -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('按住可拖拽控制点<br>点击可微调、删除控制点'); |
|||
|
|||
// 扩展点提示
|
|||
_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 = ` |
|||
<div><label>经度:<input class="lng" type="text" value=""></label></div> |
|||
<div><label>纬度:<input class="lat" type="text" value=""></label></div> |
|||
<div class="buttons"> |
|||
<button class="confirm">确定</button> |
|||
<button class="delete danger-bg-color">删除此点</button> |
|||
</div> |
|||
`;
|
|||
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; |
@ -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; |
@ -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(); |
@ -0,0 +1,167 @@ |
|||
<script setup> |
|||
import { onMounted, ref, watchEffect } from 'vue'; |
|||
import commonRefs from '@/utils/commonRefs'; |
|||
import { useGlobalSettings } from '@/views/common/useGlobalSettings'; |
|||
// import { useGlobalFarm } from '@/views/common/useGlobalFarm'; |
|||
// import { useGlobalFields } from '@/views/common/useGlobalFields'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
|
|||
const isReady = ref(false); |
|||
const showMapHd = ref(true); |
|||
const showFieldFill = ref(true); |
|||
const showFieldName = ref(true); |
|||
|
|||
watchEffect(() => { |
|||
const settings = useGlobalSettings().valueOf(); |
|||
showMapHd.value = settings.showMapHd; |
|||
showFieldFill.value = settings.showFieldFill; |
|||
showFieldName.value = settings.showFieldName; |
|||
}); |
|||
|
|||
function init() { |
|||
eventBus.emit('show-map-hd-layer', showMapHd.value); |
|||
} |
|||
|
|||
function onToggleMapHd() { |
|||
const result = !showMapHd.value; |
|||
eventBus.emit('show-map-hd-layer', result); |
|||
useGlobalSettings().set('showMapHd', result); |
|||
} |
|||
|
|||
function onToggleFill(val) { |
|||
// useGlobalFields().toggleFill(val); |
|||
useGlobalSettings().set('showFieldFill', val); |
|||
} |
|||
|
|||
function onToggleName(val) { |
|||
// useGlobalFields().toggleName(val); |
|||
useGlobalSettings().set('showFieldName', val); |
|||
} |
|||
|
|||
onMounted(async () => { |
|||
await commonRefs.getRef('map'); |
|||
isReady.value = true; |
|||
init(); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root" v-if="isReady"> |
|||
<t-tooltip content="视角回归" placement="left"> |
|||
<div class="cell"> |
|||
<font-icon name="icon-positioning" /> |
|||
</div> |
|||
</t-tooltip> |
|||
<t-popup placement="left-bottom" show-arrow :overlay-class-name="s.popup_free"> |
|||
<div class="cell"> |
|||
<font-icon name="icon-layer" /> |
|||
</div> |
|||
<template #content> |
|||
<div class="top"> |
|||
<t-space size="4px" direction="vertical" align="center" :class="[showMapHd ? 'active' : '']"> |
|||
<div class="icon" @click="onToggleMapHd"> |
|||
<img src="@/assets/map-icon-hd.png" alt=""> |
|||
</div> |
|||
<span>高清图层</span> |
|||
</t-space> |
|||
</div> |
|||
</template> |
|||
</t-popup> |
|||
<t-popup placement="left-bottom" show-arrow :overlay-class-name="s.popup_single_col"> |
|||
<div class="cell"> |
|||
<font-icon name="icon-config" /> |
|||
</div> |
|||
<template #content> |
|||
<t-space size="small" align="center"> |
|||
<t-switch :label="['显示', '隐藏']" :value="showFieldFill" @change="onToggleFill" /> |
|||
<span>地块形状</span> |
|||
</t-space> |
|||
<t-space size="small" align="center"> |
|||
<t-switch :label="['显示', '隐藏']" :value="showFieldName" @change="onToggleName" /> |
|||
<span>地块名称</span> |
|||
</t-space> |
|||
</template> |
|||
</t-popup> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.popup_single_col { |
|||
:global { |
|||
.t-popup__content { |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
display: grid; |
|||
grid-template-columns: 1fr; |
|||
gap: var(--td-comp-margin-m); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.popup_free { |
|||
:global { |
|||
.t-popup__content { |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
} |
|||
|
|||
.top { |
|||
display: grid; |
|||
grid-template-columns: repeat(5fr); |
|||
gap: var(--td-comp-margin-m); |
|||
|
|||
.t-space { |
|||
opacity: 0.6; |
|||
transition: opacity .2s ease; |
|||
|
|||
.icon { |
|||
border: 2px solid transparent; |
|||
border-radius: 100%; |
|||
padding: 1px; |
|||
transition: border .2s ease; |
|||
cursor: pointer; |
|||
|
|||
img { |
|||
vertical-align: top; |
|||
transition: opacity .2s ease; |
|||
} |
|||
} |
|||
|
|||
&.active { |
|||
opacity: 1; |
|||
|
|||
.icon { |
|||
border-color: var(--td-brand-color); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.root { |
|||
background-color: fade(black, 35%); |
|||
backdrop-filter: blur(6px); |
|||
border-radius: 100px; |
|||
border: 1px solid white; |
|||
padding: 10px; |
|||
color: white; |
|||
pointer-events: auto; |
|||
|
|||
:global { |
|||
.cell { |
|||
padding: 10px 0; |
|||
font-size: 12px; |
|||
text-align: center; |
|||
border-top: 1px solid fade(white, 50%); |
|||
cursor: pointer; |
|||
|
|||
&:first-child { |
|||
border-top: none; |
|||
} |
|||
|
|||
.t-icon { |
|||
font-size: 18px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,66 @@ |
|||
<script setup> |
|||
import { useRouter } from 'vue-router'; |
|||
import { DialogPlugin } from 'tdesign-vue-next'; |
|||
|
|||
const router = useRouter(); |
|||
|
|||
function LogOut() { |
|||
const dialog = DialogPlugin({ |
|||
header: '操作确认', |
|||
body: '确认退出吗?', |
|||
onConfirm: () => { |
|||
localStorage.clear(); |
|||
router.push({ |
|||
name: 'LoginView', |
|||
}); |
|||
dialog.hide(); |
|||
}, |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root"> |
|||
<t-space align="center"> |
|||
<t-image |
|||
src="https://tdesign.gtimg.com/demo/demo-image-1.png" |
|||
:style="{ width: '38px', height: '38px' }" |
|||
shape="circle" |
|||
fit="cover" |
|||
/> |
|||
<div class="title">云端无人机管理系统(管理员)</div> |
|||
</t-space> |
|||
|
|||
<t-space align="center"> |
|||
<t-button theme="primary" shape="round" variant="base">监察者模式</t-button> |
|||
<t-dropdown> |
|||
<t-avatar> |
|||
<template #icon> |
|||
<t-icon name="user" /> |
|||
</template> |
|||
</t-avatar> |
|||
<t-dropdown-menu> |
|||
<t-dropdown-item @click="LogOut"> |
|||
退出登录 |
|||
</t-dropdown-item> |
|||
</t-dropdown-menu> |
|||
</t-dropdown> |
|||
</t-space> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-xl); |
|||
|
|||
:global { |
|||
.title { |
|||
white-space: nowrap; |
|||
font-size: var(--td-font-size-headline-medium); |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,84 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref } from 'vue'; |
|||
import commonRefs from '@/utils/commonRefs'; |
|||
|
|||
const level = ref(0); |
|||
|
|||
async function updateLevel() { |
|||
const map = await commonRefs.getRef('map'); |
|||
level.value = map.level; |
|||
} |
|||
|
|||
onMounted(async () => { |
|||
const map = await commonRefs.getRef('map'); |
|||
level.value = map.level; |
|||
map.on('postUpdate', updateLevel); |
|||
}); |
|||
|
|||
onUnmounted(async () => { |
|||
const map = await commonRefs.getRef('map'); |
|||
map.off('postUpdate', updateLevel); |
|||
}); |
|||
|
|||
async function onZoomIn() { |
|||
const map = await commonRefs.getRef('map'); |
|||
map.zoomIn(); |
|||
} |
|||
|
|||
async function onZoomOut() { |
|||
const map = await commonRefs.getRef('map'); |
|||
map.zoomOut(); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root" v-if="level"> |
|||
<t-tooltip content="放大地图" placement="left"> |
|||
<div class="cell" @click="onZoomIn"> |
|||
<font-icon name="icon-zoom-in" /> |
|||
</div> |
|||
</t-tooltip> |
|||
<t-tooltip content="当前级别" placement="left"> |
|||
<div class="cell text">{{ level }}</div> |
|||
</t-tooltip> |
|||
<t-tooltip content="缩小地图" placement="left"> |
|||
<div class="cell" @click="onZoomOut"> |
|||
<font-icon name="icon-zoom-out" /> |
|||
</div> |
|||
</t-tooltip> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
background-color: fade(black, 35%); |
|||
backdrop-filter: blur(6px); |
|||
border-radius: 100px; |
|||
border: 1px solid white; |
|||
padding: 10px; |
|||
color: white; |
|||
pointer-events: auto; |
|||
|
|||
:global { |
|||
.cell { |
|||
padding: 10px 0; |
|||
font-size: 12px; |
|||
text-align: center; |
|||
border-top: 1px solid fade(white, 50%); |
|||
cursor: pointer; |
|||
|
|||
&:first-child { |
|||
border-top: none; |
|||
} |
|||
|
|||
&.text { |
|||
cursor: default; |
|||
} |
|||
|
|||
.t-icon { |
|||
font-size: 18px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -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 {}; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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(); |
@ -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; |
|||
} |
@ -1,13 +1,136 @@ |
|||
<script setup> |
|||
import { ref } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { useAuthStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
|
|||
const router = useRouter(); // 类似 this.$router |
|||
|
|||
const { loginWithPassword } = useAuthStore(); |
|||
|
|||
const { APP_TITLE } = import.meta.env; // 获取环境变量 标题 |
|||
const title = ref(APP_TITLE); // 标题 |
|||
|
|||
const formRules = { // form规则 |
|||
phone: [{ required: true, message: '请输入账户' }], |
|||
password: [{ required: true, message: '请输入密码' }], |
|||
}; |
|||
const form = ref(); // 类似 this.$ref |
|||
const formData = ref({ // 表单数据 |
|||
phone: '18188888888', |
|||
password: 'asdf1234', |
|||
}); |
|||
|
|||
const onSubmit = ({ validateResult }) => { // 提交函数 |
|||
if (validateResult !== true) return; // 表单校验 |
|||
// 发送请求 |
|||
loginWithPassword(formData.value).then(() => { |
|||
router.replace('/'); |
|||
MessagePlugin.success('登录成功'); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
}; |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root">LoginView</div> |
|||
<div :class="s.root"> |
|||
<!-- 登录卡片 --> |
|||
<div class="login-card"> |
|||
<!-- 标题 --> |
|||
<span class="title">{{ title }}</span> |
|||
<!-- 表单 --> |
|||
<t-form ref="form" :data="formData" :rules="formRules" :label-width="0" @submit="onSubmit"> |
|||
<t-form-item name="phone"> |
|||
<t-input v-model="formData.phone" clearable placeholder="请输入账户名" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item name="password"> |
|||
<t-input v-model="formData.password" type="password" clearable placeholder="请输入密码" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item> |
|||
<t-button theme="primary" type="submit" block> |
|||
登录 |
|||
</t-button> |
|||
</t-form-item> |
|||
</t-form> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
height: inherit; |
|||
background: url("../../assets/login_bg.png") no-repeat scroll center bottom transparent; |
|||
background-size: cover; |
|||
// |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
|
|||
:global { |
|||
.login-card { |
|||
position: relative; |
|||
width: 70vh; |
|||
min-width: 380px; |
|||
padding: 8vh 10vh; |
|||
border-radius: var(--td-radius-extraLarge); |
|||
background-color: fade(black, 40%); |
|||
backdrop-filter: blur(5px); |
|||
box-sizing: border-box; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
|
|||
.title { |
|||
position: absolute; |
|||
top: 0; |
|||
transform: translateY(calc(-150% - var(--td-comp-margin-m))); |
|||
color: white; |
|||
//font-size: var(--td-font-size-display-large); |
|||
font-size: 8vh; |
|||
white-space: nowrap; |
|||
letter-spacing: 1vw; |
|||
text-shadow: 0 0 8px black; |
|||
} |
|||
|
|||
.t-form { |
|||
min-width: 280px; |
|||
width: 20vw; |
|||
|
|||
.t-input { |
|||
color: white; |
|||
background-color: transparent; |
|||
|
|||
&:focus { |
|||
box-shadow: unset; |
|||
} |
|||
|
|||
.t-input__inner { |
|||
color: white; |
|||
|
|||
&::placeholder { |
|||
color: fade(white, 70%); |
|||
} |
|||
} |
|||
|
|||
.t-input__suffix > .t-icon { |
|||
color: fade(white, 40%); |
|||
|
|||
&:hover { |
|||
color: fade(white, 90%); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.t-input--focused { |
|||
box-shadow: unset; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
|
@ -0,0 +1,131 @@ |
|||
<script setup> |
|||
import BasePanel from '@/components/BasePanel.vue'; |
|||
import { computed, ref } from 'vue'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useManufacturerStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import ManufacturerEditor from '@/views/ManufacturerView/components/ManufacturerEditor.vue'; |
|||
import ManufacturerDetail from '@/views/ManufacturerView/components/ManufacturerDetail.vue'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
|
|||
const manufacturerStore = useManufacturerStore(); |
|||
const { manufacturerList, manufacturerQueries, manufacturerExtra } = storeToRefs(manufacturerStore); |
|||
const { getManufacturerList, updateManufacturerDisable, deleteManufacturer } = manufacturerStore; |
|||
|
|||
function loadList(queries = {}) { |
|||
getManufacturerList(queries).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
loadList(); |
|||
|
|||
function onPageChange({ current, pageSize }) { |
|||
loadList({ page: current, pageSize }); |
|||
} |
|||
|
|||
const search = ref(); |
|||
function onSearchList() { |
|||
loadList({ search: search.value }); |
|||
} |
|||
|
|||
const columns = [ |
|||
{ colKey: 'manufacturerName', title: '企业/组织名称', ellipsis: true }, |
|||
{ colKey: 'contactName', title: '联系人名称', ellipsis: true }, |
|||
{ colKey: 'contactPhone', title: '联系人电话(账号)', ellipsis: true }, |
|||
{ colKey: 'disabled', title: '状态', ellipsis: true }, |
|||
{ colKey: 'operation', title: '操作', width: '350px' }, |
|||
]; |
|||
|
|||
function onShowEditor(row = {}) { |
|||
eventBus.emit('show-manufacturer-editor', row); |
|||
} |
|||
|
|||
function onShowDetail(row = {}) { |
|||
eventBus.emit('show-manufacturer-detail', row); |
|||
} |
|||
|
|||
const pagination = computed(() => ({ |
|||
current: manufacturerQueries.value.page, |
|||
pageSize: manufacturerQueries.value.pageSize, |
|||
total: manufacturerExtra.value.total, |
|||
})); |
|||
|
|||
function onChangeState(row = {}) { |
|||
updateManufacturerDisable(row, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('更改成功'); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
function onDeleteManufacturer(row = {}) { |
|||
deleteManufacturer(row.id, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('成功删除'); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<BasePanel> |
|||
<template #header>制造商管理</template> |
|||
<template #header-extra> |
|||
<t-space> |
|||
<t-button @click="onShowEditor">新增制造商</t-button> |
|||
</t-space> |
|||
</template> |
|||
|
|||
<template #top-bar> |
|||
<t-space align="center"> |
|||
<div>关键字查询</div> |
|||
<t-input v-model="search" clearable /> |
|||
<t-button @click="onSearchList">查询</t-button> |
|||
</t-space> |
|||
</template> |
|||
|
|||
<t-table |
|||
row-key="id" |
|||
:data="manufacturerList" |
|||
:columns="columns" |
|||
cell-empty-content="-" |
|||
:pagination="pagination" |
|||
@page-change="onPageChange" |
|||
> |
|||
<template #disabled="{ row }"> |
|||
<t-tag theme="success" v-if="!row.disabled">正常</t-tag> |
|||
<t-tag theme="danger" v-else>禁用</t-tag> |
|||
</template> |
|||
<template #operation="{ row }"> |
|||
<t-space align="center"> |
|||
<t-popconfirm :content="`确认${!row.disabled ? '禁用' : '启用'}吗?`" @confirm="onChangeState(row)"> |
|||
<t-button :theme="!row.disabled ? 'danger' : 'success'">{{ !row.disabled ? '禁用' : '启用' }}</t-button> |
|||
</t-popconfirm> |
|||
<t-popconfirm :content="`确认删除吗?`" @confirm="onDeleteManufacturer(row)"> |
|||
<t-button theme="danger">删除</t-button> |
|||
</t-popconfirm> |
|||
<t-button @click="onShowEditor(row)">编辑</t-button> |
|||
<t-button @click="onShowDetail(row)">详情</t-button> |
|||
</t-space> |
|||
</template> |
|||
</t-table> |
|||
</BasePanel> |
|||
|
|||
<ManufacturerEditor /> |
|||
<ManufacturerDetail /> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
|
|||
.dropdown { |
|||
:global { |
|||
.t-dropdown__submenu ul { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,152 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref } from 'vue'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
|
|||
const visible = ref(false); |
|||
|
|||
/* function onCancel() { |
|||
visible.value = false; |
|||
info.value = {}; |
|||
// form.value.reset(); |
|||
} */ |
|||
|
|||
const info = ref({}); |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-manufacturer-detail', (row = {}) => { |
|||
info.value = { ...row }; |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-manufacturer-detail'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
:footer="null" |
|||
top="70px" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">制造商详情</div> |
|||
</template> |
|||
|
|||
<div class="grid-container"> |
|||
<div class="grid-item label">企业/组织名称:</div> |
|||
<div class="grid-item">{{ info.manufacturerName || '-' }}</div> |
|||
<div class="grid-item label">法人代表:</div> |
|||
<div class="grid-item">{{ info.legal || '-' }}</div> |
|||
<div class="grid-item label">统一社会信用代码:</div> |
|||
<div class="grid-item">{{ info.creditCode || '-' }}</div> |
|||
<div class="grid-item label">联系人姓名:</div> |
|||
<div class="grid-item">{{ info.contactName || '-' }}</div> |
|||
<div class="grid-item label">联系人电话:</div> |
|||
<div class="grid-item">{{ info.contactPhone || '-' }}</div> |
|||
<div class="grid-item label">联系地址:</div> |
|||
<div class="grid-item">{{ info.detailAddress || '-' }}</div> |
|||
<div class="grid-item label">E-mail:</div> |
|||
<div class="grid-item">{{ info.email || '-' }}</div> |
|||
<div class="grid-item label">营业执照扫描件:</div> |
|||
<div class="grid-item"> |
|||
<t-image-viewer |
|||
:images="[info.businessLicense]" |
|||
> |
|||
<template #trigger="{ open: onPreview }"> |
|||
<div class="image-box"> |
|||
<t-image |
|||
shape="round" |
|||
fit="cover" |
|||
:style="{ 'min-width': '160px', height: '120px' }" |
|||
:src="info.businessLicense" |
|||
error="" |
|||
/> |
|||
<div class="image-hover" @click="onPreview"> |
|||
<span><t-icon name="browse" size="1.4em" /> 预览</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image-viewer> |
|||
</div> |
|||
<div class="grid-item label">公司LOGO:</div> |
|||
<div class="grid-item"> |
|||
<t-image-viewer |
|||
:images="[info.companyLogo]" |
|||
> |
|||
<template #trigger="{ open: onPreview }"> |
|||
<div class="image-box"> |
|||
<t-image |
|||
shape="round" |
|||
fit="cover" |
|||
:style="{ 'min-width': '160px', height: '120px' }" |
|||
:src="info.companyLogo" |
|||
error="" |
|||
/> |
|||
<div class="image-hover" @click="onPreview"> |
|||
<span><t-icon name="browse" size="1.4em" /> 预览</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image-viewer> |
|||
</div> |
|||
</div> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
:global { |
|||
.grid-container { |
|||
display: grid; |
|||
grid-template-columns: 160px auto; |
|||
grid-gap: 10px; |
|||
|
|||
.label { |
|||
text-align: right; |
|||
} |
|||
} |
|||
|
|||
.grid-item { |
|||
color: var(--td-text-color-primary); |
|||
padding: 10px; |
|||
|
|||
.image-box { |
|||
//width: 40px; |
|||
//height: 60px; |
|||
display: inline-flex; |
|||
position: relative; |
|||
justify-content: center; |
|||
align-items: center; |
|||
border-radius: var(--td-radius-small); |
|||
overflow: hidden; |
|||
|
|||
.image-hover { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
opacity: 0; |
|||
background-color: rgba(0, 0, 0, 0.6); |
|||
color: var(--td-text-color-anti); |
|||
line-height: 22px; |
|||
transition: 0.2s; |
|||
z-index: 1; |
|||
} |
|||
|
|||
&:hover .image-hover { |
|||
opacity: 1; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,155 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref } from 'vue'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import { useManufacturerStore } from '@/stores'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
import ImageUploader from '@/components/ImageUploader.vue'; |
|||
import { update } from '@/utils/helpers'; |
|||
|
|||
const visible = ref(false); |
|||
|
|||
const manufacturerStore = useManufacturerStore(); |
|||
const { createManufacturer, updateManufacturer } = manufacturerStore; |
|||
|
|||
const form = ref(); |
|||
const formData = ref({ |
|||
id: undefined, |
|||
manufacturerName: undefined, |
|||
manufacturerLogo: undefined, |
|||
contactPhone: undefined, |
|||
contactName: undefined, |
|||
// province: undefined, |
|||
// city: undefined, |
|||
// county: undefined, |
|||
// enterprise_name: undefined, |
|||
legal: undefined, |
|||
creditCode: undefined, |
|||
detailAddress: undefined, |
|||
email: undefined, |
|||
businessLicense: undefined, |
|||
companyLogo: undefined, |
|||
// idcardNum: undefined, |
|||
// idcardFrontPic: undefined, |
|||
// idcardReversePic: undefined, |
|||
// ruleProvince: undefined, |
|||
// ruleCity: undefined, |
|||
// ruleCounty: undefined, |
|||
}); |
|||
const FORM_RULES = { |
|||
manufacturerName: [{ required: true, message: '请输入企业/组织名称' }], |
|||
contactPhone: [{ required: true, message: '请输入联系人电话' }], |
|||
contactName: [{ required: true, message: '请输入联系人名称' }], |
|||
}; |
|||
|
|||
function onCancel() { |
|||
visible.value = false; |
|||
form.value.reset(); |
|||
} |
|||
|
|||
function onSubmit({ validateResult }) { |
|||
if (validateResult === true) { |
|||
if (formData.value.id) { |
|||
updateManufacturer(formData.value, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('更新成功'); |
|||
onCancel(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
return; |
|||
} |
|||
createManufacturer(formData.value, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('创建成功'); |
|||
onCancel(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-manufacturer-editor', (row = {}) => { |
|||
if (row.id) { |
|||
formData.value = update(formData.value, row); |
|||
} |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-manufacturer-editor'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
:close-btn="false" |
|||
:footer="null" |
|||
@closed="onCancel" |
|||
top="70px" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">{{ formData.id ? '更新' : '创建' }}制造商</div> |
|||
</template> |
|||
<t-form |
|||
ref="form" |
|||
:data="formData" |
|||
:rules="FORM_RULES" |
|||
label-width="150px" |
|||
colon |
|||
@submit="onSubmit" |
|||
> |
|||
<t-form-item name="id" v-show="false" /> |
|||
|
|||
<t-form-item label="企业/组织名称" name="manufacturerName"> |
|||
<t-input v-model="formData.manufacturerName" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="法人代表" name="legal"> |
|||
<t-input v-model="formData.legal" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="统一社会信用代码" name="creditCode"> |
|||
<t-input v-model="formData.creditCode" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="联系人姓名" name="contactName"> |
|||
<t-input v-model="formData.contactName" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="联系人电话" name="contactPhone" help="创建后的登录账号"> |
|||
<t-input v-model="formData.contactPhone" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="联系地址" name="detailAddress"> |
|||
<t-input v-model="formData.detailAddress" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="E-mail" name="email"> |
|||
<t-input v-model="formData.email" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="营业执照扫描件" name="businessLicense"> |
|||
<ImageUploader subject="device_pic" v-model="formData.businessLicense" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="公司LOGO" name="companyLogo"> |
|||
<ImageUploader subject="device_pic" v-model="formData.companyLogo" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item> |
|||
<t-space> |
|||
<t-button theme="primary" type="submit">提交</t-button> |
|||
<t-button theme="default" @click="onCancel">取消</t-button> |
|||
</t-space> |
|||
</t-form-item> |
|||
</t-form> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
</style> |
@ -0,0 +1,261 @@ |
|||
<script setup> |
|||
import { computed, onMounted, onUnmounted, ref } from 'vue'; |
|||
import ShapeCreator from '@/core/ShapeCreator'; |
|||
import ShapeEditor from '@/core/ShapeEditor'; |
|||
import ShapeRenderer from '@/core/ShapeRenderer'; |
|||
import commonRefs from '@/utils/commonRefs'; |
|||
import { MessagePlugin, NotifyPlugin } from 'tdesign-vue-next'; |
|||
import { useRoute, useRouter } from 'vue-router'; |
|||
import { useNoFlyZoneStore } from '@/stores'; |
|||
import { update } from '@/utils/helpers'; |
|||
|
|||
let Tips; |
|||
const noFlyZoneCreator = new ShapeCreator('noFlyZone-form'); |
|||
const noFlyZoneEditor = new ShapeEditor('noFlyZone-form', true); |
|||
const noFlyZoneRenderer = new ShapeRenderer('noFlyZone-form'); |
|||
|
|||
const route = useRoute(); |
|||
const router = useRouter(); |
|||
|
|||
const { createNoFlyZone, getNoFlyZoneDetail, updateNoFlyZone } = useNoFlyZoneStore(); |
|||
|
|||
const formData = ref({ |
|||
id: undefined, |
|||
detailAddress: undefined, |
|||
effectTimeStart: undefined, |
|||
effectTimeEnd: undefined, |
|||
province: undefined, |
|||
// city: undefined, |
|||
// county: undefined, |
|||
orbit: undefined, |
|||
}); |
|||
const FORM_RULES = { |
|||
detailAddress: [{ required: true, message: '请输入禁飞区名称' }], |
|||
effectTimeStart: [{ required: true, message: '请选择有效期范围' }], |
|||
province: [{ required: true, message: '请输入禁飞区位置' }], |
|||
orbit: [{ required: true, type: Array, message: '请绘制禁飞区' }], |
|||
}; |
|||
|
|||
const effectiveTime = computed({ |
|||
get() { |
|||
if (formData.value.effectTimeStart && formData.value.effectTimeEnd) { |
|||
return [formData.value.effectTimeStart * 1000, formData.value.effectTimeEnd * 1000]; |
|||
} |
|||
return []; |
|||
}, |
|||
|
|||
set(nv) { |
|||
const [startDay, endDay] = nv || []; |
|||
formData.value.effectTimeStart = (startDay / 1000) || undefined; |
|||
formData.value.effectTimeEnd = (endDay / 1000) || undefined; |
|||
}, |
|||
}); |
|||
|
|||
const onSubmit = ({ validateResult, firstError }) => { |
|||
if (validateResult === true) { |
|||
(formData.value.orbit || []).push({ ...formData.value.orbit[0] }); |
|||
const data = { ...formData.value, orbit: JSON.stringify(formData.value.orbit) }; |
|||
if (formData.value.id) { |
|||
updateNoFlyZone(data).then(() => { |
|||
MessagePlugin.success('更新成功'); |
|||
router.back(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
return; |
|||
} |
|||
createNoFlyZone(data).then(() => { |
|||
MessagePlugin.success('添加成功'); |
|||
router.back(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} else { |
|||
MessagePlugin.warning(firstError); |
|||
} |
|||
}; |
|||
|
|||
function showOperationTips(action) { |
|||
let content = ''; |
|||
switch (action) { |
|||
case 1: |
|||
content = '在地图上,沿着禁飞区边缘逐个打点,最后闭合形状,即可围成一个禁飞区'; |
|||
break; |
|||
case 2: |
|||
content = h => [ |
|||
h('div', '请根据操作点上的提示进行编辑禁飞区'), |
|||
h('div', '点击禁飞区以外区域,即可完成禁飞区编辑'), |
|||
]; |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
if (!content) return; |
|||
Tips = NotifyPlugin.info({ |
|||
title: '操作提示', |
|||
content, |
|||
offset: [0, 54], |
|||
duration: 0, |
|||
}); |
|||
} |
|||
|
|||
function closeOperationTips() { |
|||
if (Tips) NotifyPlugin.close(Tips); |
|||
} |
|||
|
|||
const isEdit = computed(() => !!route?.params?.id); |
|||
|
|||
async function init() { |
|||
// 等待地图实例 |
|||
const map = await commonRefs.getRef('map'); |
|||
noFlyZoneCreator.setMap(map); |
|||
noFlyZoneEditor.setMap(map); |
|||
noFlyZoneRenderer.setMap(map); |
|||
|
|||
noFlyZoneRenderer.updateStyle({ |
|||
fillColor: 'rgba(255, 0, 0, 0.1)', |
|||
fillHoverColor: 'rgba(255, 0, 0, 0.3)', |
|||
strokeColor: 'rgba(255, 0, 0, 0.5)', |
|||
strokeHoverColor: 'rgba(255, 0, 0, 1)', |
|||
}); |
|||
|
|||
noFlyZoneCreator.on('shape_kinked', ({ action }) => { |
|||
if (action === 'drag') { |
|||
MessagePlugin.error('禁飞区形状扭结了'); |
|||
return; |
|||
} |
|||
if (action === 'click') { |
|||
MessagePlugin.error('在这个位置打点会造成禁飞区形状扭结'); |
|||
return; |
|||
} |
|||
if (action === 'close') { |
|||
MessagePlugin.error('禁飞区形状扭结了,无法闭合'); |
|||
} |
|||
}); |
|||
noFlyZoneCreator.on('completed', (coordinates) => { |
|||
noFlyZoneCreator.end(); |
|||
noFlyZoneEditor.loadDataSource({ id: 0, points: coordinates }); |
|||
closeOperationTips(); |
|||
showOperationTips(2); |
|||
}); |
|||
|
|||
noFlyZoneEditor.on('shape_kinked', ({ action }) => { |
|||
if (action === 'drag') { |
|||
MessagePlugin.error('禁飞区形状扭结了'); |
|||
return; |
|||
} |
|||
if (action === 'close') { |
|||
MessagePlugin.error('禁飞区形状扭结了,无法结束编辑'); |
|||
} |
|||
}); |
|||
|
|||
noFlyZoneEditor.on('completed', ({ points }) => { |
|||
formData.value.orbit = (points || []).map(([lng, lat]) => ({ lng, lat })); |
|||
const detail = { |
|||
id: formData.value.id || 0, |
|||
points, |
|||
}; |
|||
noFlyZoneRenderer.loadDataSource([detail]); |
|||
closeOperationTips(); |
|||
}); |
|||
|
|||
noFlyZoneRenderer.on('click', () => { |
|||
noFlyZoneRenderer.clear(); |
|||
noFlyZoneEditor.loadDataSource({ |
|||
id: formData.value.id || 0, |
|||
points: formData.value.orbit, |
|||
}); |
|||
showOperationTips(2); |
|||
}); |
|||
|
|||
if (isEdit.value) { |
|||
noFlyZoneRenderer.loadDataSource([{ id: formData.value.id, points: formData.value.orbit }]); |
|||
noFlyZoneRenderer.fitShape(formData.value.id); |
|||
noFlyZoneRenderer.clear(); |
|||
noFlyZoneEditor.loadDataSource({ id: formData.value.id, points: formData.value.orbit }); |
|||
} else { |
|||
noFlyZoneCreator.start(); |
|||
showOperationTips(1); |
|||
} |
|||
} |
|||
|
|||
onMounted(() => { |
|||
if (isEdit.value) { |
|||
getNoFlyZoneDetail(route.params.id).then(({ data }) => { |
|||
formData.value = update(formData.value, data); |
|||
formData.value.orbit = JSON.parse(formData.value.orbit); |
|||
if (formData.value.orbit.length) { |
|||
(formData.value.orbit || []).splice(formData.value.orbit.length - 1, 1); |
|||
} |
|||
init(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
return; |
|||
} |
|||
init(); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
noFlyZoneCreator.clear(true); |
|||
noFlyZoneEditor.clear(); |
|||
noFlyZoneRenderer.clear(); |
|||
closeOperationTips(); |
|||
}); |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root"> |
|||
<div class="container"> |
|||
<t-form |
|||
ref="form" |
|||
:data="formData" |
|||
:rules="FORM_RULES" |
|||
label-width="calc(2em + 60px)" |
|||
layout="inline" |
|||
scroll-to-first-error="smooth" |
|||
@submit="onSubmit" |
|||
> |
|||
<t-form-item name="id" v-show="false" /> |
|||
<t-form-item name="orbit" v-show="false" /> |
|||
|
|||
<t-form-item label="禁飞区名称" name="detailAddress"> |
|||
<t-input v-model="formData.detailAddress" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="禁飞区位置" name="province"> |
|||
<t-input v-model="formData.province" placeholder="XX省 XX市 XX(区/县)" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="生效日期" name="effectTimeStart"> |
|||
<t-date-range-picker value-type="time-stamp" v-model="effectiveTime" clearable /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label-width="0" style="min-width: unset;"> |
|||
<t-space size="small"> |
|||
<t-button theme="primary" type="submit">提交</t-button> |
|||
<t-button theme="default" @click="router.back()">返回列表</t-button> |
|||
</t-space> |
|||
</t-form-item> |
|||
</t-form> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
position: absolute; |
|||
top: 0; |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-l); |
|||
|
|||
:global { |
|||
.container { |
|||
pointer-events: auto; |
|||
background-color: var(--td-bg-color-container); |
|||
border-radius: var(--td-radius-medium); |
|||
padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-l); |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,244 @@ |
|||
<script setup> |
|||
import BasePanel from '@/components/BasePanel.vue'; |
|||
import { computed, ref, onMounted, onUnmounted } from 'vue'; |
|||
import { useRouter } from 'vue-router'; |
|||
import { formatTime } from '@/utils/helpers'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useNoFlyZoneStore } from '@/stores'; |
|||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'; |
|||
import ShapeRenderer from '@/core/ShapeRenderer'; |
|||
import commonRefs from '@/utils/commonRefs'; |
|||
import gcoord from 'gcoord'; |
|||
|
|||
const noFlyZoneRenderer = new ShapeRenderer('noFlyZone-show'); |
|||
|
|||
async function init() { |
|||
const map = await commonRefs.getRef('map'); |
|||
noFlyZoneRenderer.setMap(map); |
|||
|
|||
noFlyZoneRenderer.updateStyle({ |
|||
fillColor: 'rgba(255, 0, 0, 0.1)', |
|||
fillHoverColor: 'rgba(255, 0, 0, 0.3)', |
|||
strokeColor: 'rgba(255, 0, 0, 0.5)', |
|||
strokeHoverColor: 'rgba(255, 0, 0, 1)', |
|||
}); |
|||
} |
|||
|
|||
onMounted(() => { |
|||
init(); |
|||
}); |
|||
|
|||
const noFlyZoneStore = useNoFlyZoneStore(); |
|||
const { noFlyZoneList, noFlyZoneQueries, noFlyZoneExtra } = storeToRefs(noFlyZoneStore); |
|||
const { getNoFlyZoneList, deleteNoFlyZone, updateNoFlyZoneState } = noFlyZoneStore; |
|||
|
|||
function onDeleteNoFlyZone(row = {}) { |
|||
const dialog = DialogPlugin({ |
|||
header: '操作确认', |
|||
body: '确认删除此禁飞区吗?', |
|||
onConfirm: () => { |
|||
deleteNoFlyZone(row.id, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('删除成功'); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}).finally(() => { |
|||
dialog.hide(); |
|||
}); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function onUpdateNoFlyZoneState(row = {}) { |
|||
const dialog = DialogPlugin({ |
|||
header: '操作确认', |
|||
body: `确认${!row.disabled ? '禁用' : '启用'}此禁飞区吗?`, |
|||
onConfirm: () => { |
|||
updateNoFlyZoneState(row, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('更改成功'); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}).finally(() => { |
|||
dialog.hide(); |
|||
}); |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function loadList(queries = {}) { |
|||
getNoFlyZoneList(queries).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
loadList(); |
|||
|
|||
function onPageChange({ current, pageSize }) { |
|||
loadList({ page: current, pageSize }); |
|||
} |
|||
|
|||
function onFilterChange({ type = undefined } = {}) { |
|||
loadList({ type }); |
|||
} |
|||
|
|||
const search = ref(); |
|||
function onSearchList() { |
|||
loadList({ search: search.value }); |
|||
} |
|||
|
|||
const columns = [ |
|||
{ colKey: 'detailAddress', title: '禁飞区名称', ellipsis: true, width: '300px' }, |
|||
{ |
|||
colKey: 'province', |
|||
title: '禁飞区位置', |
|||
ellipsis: true, |
|||
cell: (_, { row }) => { |
|||
const { province, city, county } = row; |
|||
if (county) { |
|||
return `${province || ''}${city || ''}${county || ''}`; |
|||
} |
|||
if (province) { |
|||
return province; |
|||
} |
|||
return '-'; |
|||
}, |
|||
|
|||
}, |
|||
{ |
|||
colKey: 'type', |
|||
title: '禁飞区类型', |
|||
ellipsis: true, |
|||
width: '150px', |
|||
cell: (_, { row }) => (row.type === 1 ? '标准禁飞区' : '民航禁飞区'), |
|||
filter: { |
|||
type: 'single', |
|||
list: [ |
|||
{ label: '标准禁飞区', value: 1 }, |
|||
{ label: '民航禁飞区', value: 2 }, |
|||
], |
|||
showConfirmAndReset: true, |
|||
}, |
|||
}, |
|||
{ |
|||
colKey: 'time', |
|||
title: '有效时间', |
|||
ellipsis: true, |
|||
cell: (_, { row }) => `${formatTime(row.effectTimeStart * 1000, 'YYYY-MM-DD')}/${formatTime(row.effectTimeEnd * 1000, 'YYYY-MM-DD')}`, |
|||
}, |
|||
{ colKey: 'effectStatus', title: '是否有效', ellipsis: true }, |
|||
{ colKey: 'disabled', title: '当前状态', ellipsis: true }, |
|||
{ colKey: 'operation', title: '操作', fixed: 'right', width: '100px' }, |
|||
]; |
|||
|
|||
const expandAbility = ref(false); |
|||
const width = computed(() => (expandAbility.value ? '50vh' : '100%')); |
|||
|
|||
const router = useRouter(); |
|||
function onNavTo(routeName, param = {}) { |
|||
router.push({ |
|||
name: routeName, |
|||
...(param.id ? { params: { id: param.id } } : {}), |
|||
}); |
|||
} |
|||
|
|||
const pagination = computed(() => ({ |
|||
current: noFlyZoneQueries.value.page, |
|||
pageSize: noFlyZoneQueries.value.pageSize, |
|||
total: noFlyZoneExtra.value.total, |
|||
showPageSize: !expandAbility.value, |
|||
size: expandAbility.value ? 'small' : 'medium', |
|||
})); |
|||
|
|||
function onShowNoFlyZone(row = {}) { |
|||
if (!expandAbility.value) { |
|||
expandAbility.value = true; |
|||
} |
|||
const { orbit: pointStr, id } = row; |
|||
const orbit = JSON.parse(pointStr); |
|||
const points = orbit.map(item => gcoord.transform([item.lng, item.lat], gcoord.WGS84, gcoord.GCJ02)); |
|||
const detail = { |
|||
id, |
|||
points, |
|||
}; |
|||
noFlyZoneRenderer.loadDataSource([detail]); |
|||
noFlyZoneRenderer.fitShape(id, { right: 80 }, 150); |
|||
} |
|||
|
|||
onUnmounted(() => { |
|||
noFlyZoneRenderer.clear(); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<BasePanel :width="width" :expand-ability="expandAbility"> |
|||
<template #header>禁飞区管理</template> |
|||
<template #header-extra> |
|||
<t-space> |
|||
<t-button @click="onNavTo('NoFlyZoneCreateView')">新增禁飞区</t-button> |
|||
<t-button v-if="expandAbility" @click="expandAbility = false">展开列表</t-button> |
|||
</t-space> |
|||
</template> |
|||
|
|||
<template #top-bar> |
|||
<t-space align="center"> |
|||
<div>关键字查询</div> |
|||
<t-input v-model="search" clearable /> |
|||
<t-button @click="onSearchList">查询</t-button> |
|||
</t-space> |
|||
</template> |
|||
|
|||
<t-table |
|||
row-key="id" |
|||
:data="noFlyZoneList" |
|||
:columns="columns" |
|||
cell-empty-content="-" |
|||
:pagination="pagination" |
|||
@filter-change="onFilterChange" |
|||
@page-change="onPageChange" |
|||
> |
|||
<template #effectStatus="{ row }"> |
|||
<t-tag theme="primary" v-if="row.effectStatus === 1">有效</t-tag> |
|||
<t-tag theme="warning" v-else>无效</t-tag> |
|||
</template> |
|||
|
|||
<template #disabled="{ row }"> |
|||
<t-tag theme="success" v-if="!row.disabled">启用</t-tag> |
|||
<t-tag theme="danger" v-else>禁用</t-tag> |
|||
</template> |
|||
<template #operation="{ row }"> |
|||
<t-dropdown> |
|||
<t-button shape="square" variant="text"> |
|||
<template #icon> |
|||
<t-icon name="view-list" /> |
|||
</template> |
|||
</t-button> |
|||
<t-dropdown-menu> |
|||
<template v-if="row.type === 1"> |
|||
<t-dropdown-item @click="onUpdateNoFlyZoneState(row)">{{ !row.disabled ? '禁用' : '启用' }}</t-dropdown-item> |
|||
<t-dropdown-item @click="onDeleteNoFlyZone(row)">删除</t-dropdown-item> |
|||
<t-dropdown-item @click="onNavTo('NoFlyZoneEditorView', row)">编辑</t-dropdown-item> |
|||
<t-dropdown-item @click="onShowNoFlyZone(row)">查看</t-dropdown-item> |
|||
</template> |
|||
|
|||
<template v-else> |
|||
<t-dropdown-item @click="onShowNoFlyZone(row)">查看</t-dropdown-item> |
|||
</template> |
|||
</t-dropdown-menu> |
|||
</t-dropdown> |
|||
</template> |
|||
</t-table> |
|||
</BasePanel> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
|
|||
.dropdown { |
|||
:global { |
|||
.t-dropdown__submenu ul { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,86 @@ |
|||
<script setup> |
|||
import BasePanel from '@/components/BasePanel.vue'; |
|||
import { computed } from 'vue'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useRegulatorStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import RegulatorEditor from '@/views/RegulatorView/components/RegulatorEditor.vue'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
|
|||
const regulatorStore = useRegulatorStore(); |
|||
const { regulatorList, regulatorQueries, regulatorExtra } = storeToRefs(regulatorStore); |
|||
const { getRegulatorList } = regulatorStore; |
|||
|
|||
function loadList(queries = {}) { |
|||
getRegulatorList(queries).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
loadList(); |
|||
|
|||
function onPageChange({ current, pageSize }) { |
|||
loadList({ page: current, pageSize }); |
|||
} |
|||
|
|||
const columns = [ |
|||
{ colKey: 'serial-number', title: '序列号', ellipsis: true }, |
|||
// { colKey: 'contactName', title: '联系人名称', ellipsis: true }, |
|||
{ colKey: 'phone', title: '手机号(账号)', ellipsis: true }, |
|||
]; |
|||
|
|||
function onShowEditor(row = {}) { |
|||
eventBus.emit('show-regulator-editor', row); |
|||
} |
|||
|
|||
const pagination = computed(() => ({ |
|||
current: regulatorQueries.value.page, |
|||
pageSize: regulatorQueries.value.pageSize, |
|||
total: regulatorExtra.value.total, |
|||
})); |
|||
|
|||
</script> |
|||
|
|||
<template> |
|||
<BasePanel> |
|||
<template #header>监管者管理</template> |
|||
<template #header-extra> |
|||
<t-space> |
|||
<t-button @click="onShowEditor">新增监管者</t-button> |
|||
</t-space> |
|||
</template> |
|||
|
|||
<template #top-bar> |
|||
<t-space align="center" v-if="false"> |
|||
<div>关键字查询</div> |
|||
<t-input clearable /> |
|||
<t-button>查询</t-button> |
|||
</t-space> |
|||
</template> |
|||
|
|||
<t-table |
|||
row-key="id" |
|||
:data="regulatorList" |
|||
:columns="columns" |
|||
cell-empty-content="-" |
|||
:pagination="pagination" |
|||
@page-change="onPageChange" |
|||
/> |
|||
</BasePanel> |
|||
|
|||
<RegulatorEditor /> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
|
|||
.dropdown { |
|||
:global { |
|||
.t-dropdown__submenu ul { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,90 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref } from 'vue'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import { useRegulatorStore } from '@/stores'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
|
|||
const visible = ref(false); |
|||
|
|||
const regulatorStore = useRegulatorStore(); |
|||
const { createRegulator } = regulatorStore; |
|||
|
|||
const form = ref(); |
|||
const formData = ref({ |
|||
phone: undefined, |
|||
password: undefined, |
|||
}); |
|||
const FORM_RULES = { |
|||
phone: [{ required: true, message: '请输入手机号' }], |
|||
password: [{ required: true, message: '请输入密码' }], |
|||
}; |
|||
|
|||
function onCancel() { |
|||
visible.value = false; |
|||
form.value.reset(); |
|||
} |
|||
|
|||
function onSubmit({ validateResult }) { |
|||
if (validateResult === true) { |
|||
createRegulator(formData.value, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('创建成功'); |
|||
onCancel(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-regulator-editor', () => { |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-regulator-editor'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
:close-btn="false" |
|||
:footer="null" |
|||
@closed="onCancel" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">创建监管者</div> |
|||
</template> |
|||
<t-form |
|||
ref="form" |
|||
:data="formData" |
|||
:rules="FORM_RULES" |
|||
label-width="150px" |
|||
colon |
|||
@submit="onSubmit" |
|||
> |
|||
<t-form-item label="手机号" name="phone" help="创建后的登录账号"> |
|||
<t-input v-model="formData.phone" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="密码" name="password"> |
|||
<t-input type="password" v-model="formData.password" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item> |
|||
<t-space> |
|||
<t-button theme="primary" type="submit">提交</t-button> |
|||
<t-button theme="default" @click="onCancel">取消</t-button> |
|||
</t-space> |
|||
</t-form-item> |
|||
</t-form> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
</style> |
@ -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; |
Loading…
Reference in new issue