@ -1,4 +1,4 @@ |
|||
APP_TITLE=云端无人机管理系统 |
|||
APP_ICON_URL=//at.alicdn.com/t/c/font_4349903_2p0gyrtbo2.js |
|||
APP_ICON_URL=//at.alicdn.com/t/c/font_4349903_nbo0vtlaa9d.js |
|||
APP_DEVELOPMENT_BASE_URL=/api |
|||
APP_PRODUCTION_BASE_URL=/ |
|||
APP_PRODUCTION_BASE_URL=http://sgcloud-test.jiagutech.com/api |
|||
|
@ -0,0 +1,38 @@ |
|||
/** |
|||
* 飞机图标 |
|||
*/ |
|||
|
|||
class DroneIcon { |
|||
constructor(colorOption) { |
|||
const { |
|||
outlineColor = 'rgba(255, 255, 255, 1)', |
|||
bodyColor = 'rgba(255, 255, 255, 0.3)', |
|||
directionColor = 'rgba(255, 255, 255, 1)', |
|||
} = colorOption || {}; |
|||
|
|||
this.width = 128; |
|||
this.height = 128; |
|||
const canvas = document.createElement('canvas'); |
|||
canvas.width = this.width; |
|||
canvas.height = this.height; |
|||
const context = canvas.getContext('2d'); |
|||
|
|||
/* eslint-disable max-len */ |
|||
const outlinePath = new Path2D(); |
|||
outlinePath.addPath(new Path2D('M100.3,94.6c-0.6-0.9-1.1-1.7-1.8-2.5c-0.3-0.4-0.6-0.8-0.9-1.2s-0.6-0.8-1-1.1c-1.3-1.5-2.7-3-4.1-4.3 c-0.7-0.7-1.4-1.4-2.1-2l-2.2-2l-4.3-4.1L82.4,76V52l1.4-1.3l4.3-4.1l2.2-2c0.7-0.7,1.4-1.3,2.2-2c1.4-1.5,2.8-2.9,4.1-4.4 c0.3-0.4,0.7-0.8,1-1.2s0.6-0.8,0.9-1.2c0.6-0.8,1.2-1.6,1.8-2.5c0.5-0.8,0.3-1.9-0.5-2.4c-0.5-0.3-1.1-0.3-1.7-0.1 c-0.9,0.5-1.8,1-2.7,1.5c-0.4,0.3-0.8,0.5-1.3,0.8s-0.8,0.6-1.2,0.8c-1.6,1.1-3.2,2.3-4.7,3.6c-0.8,0.6-1.5,1.3-2.3,1.9l-2.2,2 L81.9,43c0-0.1-0.1-0.2-0.2-0.3l-4.1-7.1c-1.1-1.9-3.1-3-5.2-3H55.8c-2.2,0-4.1,1.1-5.2,3l-4.1,7.1c-0.1,0.1-0.2,0.3-0.2,0.4l-2-1.8 l-2.2-2c-0.7-0.7-1.5-1.3-2.2-1.9c-1.5-1.3-3.1-2.5-4.7-3.6L33.9,33c-0.4-0.3-0.8-0.5-1.3-0.8c-0.8-0.5-1.7-1-2.6-1.5 c-0.8-0.4-1.9-0.1-2.3,0.8c-0.3,0.5-0.2,1.2,0.1,1.6c0.5,0.9,1.1,1.7,1.7,2.5c0.3,0.4,0.6,0.8,0.9,1.2s0.6,0.8,1,1.2 c1.3,1.5,2.7,3,4.1,4.3c0.7,0.7,1.4,1.4,2.1,2l2.2,2l4.3,4.1l1.5,1.6v23.6L44,77.1l-4.3,4.1l-2.2,2c-0.7,0.7-1.4,1.3-2.1,2 c-1.4,1.4-2.8,2.8-4.1,4.3c-0.3,0.4-0.6,0.8-1,1.1s-0.6,0.8-0.9,1.2c-0.6,0.8-1.2,1.6-1.8,2.5c-0.5,0.8-0.3,1.9,0.5,2.4l0,0 c0.5,0.3,1.1,0.3,1.7,0.1c0.9-0.5,1.8-1,2.6-1.5l1.3-0.8l1.2-0.8c1.6-1.2,3.2-2.4,4.7-3.6c0.8-0.6,1.5-1.3,2.3-1.9l2.2-2l1.8-1.6 c0.1,0.3,0.3,0.6,0.4,0.9l4.1,7.1c1.1,1.9,3.1,3,5.2,3h16.5c2.2,0,4.2-1.1,5.2-3l4.1-7.1c0.1-0.3,0.3-0.5,0.4-0.8l1.7,1.4l2.2,2 c0.8,0.7,1.5,1.3,2.3,1.9c1.5,1.3,3.1,2.5,4.7,3.6l1.2,0.8l1.3,0.8c0.8,0.5,1.7,1,2.6,1.5c0.8,0.4,1.9,0.1,2.3-0.7 C100.5,95.7,100.5,95,100.3,94.6L100.3,94.6z M79.1,82.8c0,0.5-0.1,0.9-0.4,1.4l-4.1,7.1c-0.5,0.9-1.4,1.4-2.4,1.4H55.8 c-1,0-1.9-0.5-2.4-1.4l-4.1-7.1c-0.2-0.4-0.4-0.9-0.4-1.3v-37c0-0.1,0-0.2,0-0.2c0-0.4,0.2-0.8,0.3-1.1l4.1-7.1 c0.5-0.9,1.4-1.4,2.4-1.4h16.5c1,0,1.9,0.5,2.4,1.4l4.1,7.1c0.2,0.3,0.3,0.6,0.3,1c0,0.1,0,0.3,0,0.4L79.1,82.8z')); |
|||
outlinePath.addPath(new Path2D('M29.8,44.2c-0.3,0-0.7,0-1,0c-6.7,0-12.1-5.4-12.1-12.1C16.6,25.5,22,20,28.7,20c6.6,0,12,5.3,12.1,11.9 l3.1,2.8c0.2-0.9,0.2-1.7,0.2-2.6c0-8.5-6.9-15.3-15.4-15.3c-8.4,0-15.3,6.9-15.3,15.4s6.9,15.3,15.4,15.3c1.4,0,2.7-0.2,4-0.5 L29.8,44.2z M99.1,80.5c-1.4,0-2.8,0.2-4.1,0.6l3.1,2.8c0.3,0,0.7,0,1,0c6.7,0,12.1,5.4,12.1,12.1s-5.4,12.1-12.1,12.1 S87,102.7,87,96.1l-3.1-2.9c-0.1,0.9-0.2,1.7-0.2,2.6c0,8.5,6.9,15.4,15.4,15.4s15.4-6.9,15.4-15.4C114.4,87.4,107.6,80.5,99.1,80.5 L99.1,80.5L99.1,80.5z M99.1,16.8c-8.5,0-15.3,6.9-15.4,15.3c0,0.9,0.1,1.7,0.2,2.6l3.1-2.8c0.1-6.7,5.6-12,12.2-11.9 s12,5.6,11.9,12.2S105.6,44.1,99,44.1c-0.3,0-0.7,0-1,0L95,47c1.3,0.4,2.7,0.5,4.1,0.5c8.5,0,15.4-6.9,15.4-15.4 S107.6,16.8,99.1,16.8L99.1,16.8L99.1,16.8z M40.8,96c-0.1,6.7-5.6,12-12.2,11.9s-12-5.6-11.9-12.2c0.1-6.6,5.5-11.9,12.1-11.9 c0.4,0,0.7,0,1,0l3.1-2.8c-1.3-0.4-2.7-0.5-4.1-0.6c-8.5,0-15.4,6.9-15.4,15.4s6.9,15.4,15.4,15.4s15.3-6.9,15.4-15.3 c0-0.9-0.1-1.8-0.2-2.6L40.8,96z')); |
|||
context.fillStyle = outlineColor; |
|||
context.fill(outlinePath); |
|||
const bodyPath = new Path2D('M79,46c0-0.1,0-0.3,0-0.4c0-0.4-0.1-0.7-0.3-1l-4.1-7.1c-0.5-0.9-1.4-1.4-2.4-1.4H55.7c-1,0-1.9,0.5-2.4,1.4 l-4.1,7.1c-0.1,0.3-0.3,0.7-0.3,1.1c0,0,0,0.1,0,0.2v37c0,0.4,0.2,0.9,0.4,1.3l4.1,7.1c0.5,0.9,1.4,1.4,2.4,1.4h16.4 c1,0,1.9-0.5,2.4-1.4l4.1-7.1c0.3-0.5,0.4-0.9,0.4-1.4L79,46z M67.3,55.2c0,1.8-1.5,3.3-3.3,3.3s-3.3-1.5-3.3-3.3v-8.8 c0-1.8,1.5-3.3,3.3-3.3s3.3,1.5,3.3,3.3V55.2z'); |
|||
context.fillStyle = bodyColor; |
|||
context.fill(bodyPath); |
|||
const directionPath = new Path2D('M64,43.1c-1.8,0-3.3,1.5-3.3,3.3v8.8c0,1.8,1.5,3.3,3.3,3.3s3.3-1.5,3.3-3.3v-8.8C67.3,44.6,65.8,43.1,64,43.1z'); |
|||
context.fillStyle = directionColor; |
|||
context.fill(directionPath); |
|||
/* eslint-enable max-len */ |
|||
|
|||
this.data = context.getImageData(0, 0, this.width, this.height).data; |
|||
} |
|||
} |
|||
|
|||
export default DroneIcon; |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 224 B |
After Width: | Height: | Size: 727 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 863 B |
After Width: | Height: | Size: 893 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,42 @@ |
|||
<script setup> |
|||
import { computed, ref } from 'vue'; |
|||
import { useManufacturerStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
|
|||
const manufacturerStore = useManufacturerStore(); |
|||
const { getManufacturerList } = manufacturerStore; |
|||
|
|||
const list = ref([]); |
|||
const loading = ref(false); |
|||
function loadList() { |
|||
loading.value = true; |
|||
getManufacturerList({ page: 1, pageSize: 10, all: true, search: undefined }, { mergeArgs: false, mergeData: false }).then(({ data = [] } = {}) => { |
|||
// const { data: manufacturerList = [] } = data || {}; |
|||
// console.log(data); |
|||
list.value = data; |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}).finally(() => { |
|||
loading.value = false; |
|||
}); |
|||
} |
|||
|
|||
loadList(); |
|||
|
|||
const props = defineProps(['modelValue']); |
|||
const emit = defineEmits(['update:modelValue']); |
|||
|
|||
const value = computed({ |
|||
get() { |
|||
return props.modelValue; |
|||
}, |
|||
set(nv) { |
|||
emit('update:modelValue', nv); |
|||
}, |
|||
}); |
|||
const options = computed(() => list.value.map(({ manufacturerName, id }) => ({ label: manufacturerName, value: id }))); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-select v-model="value" :loading="loading" :options="options" placeholder="请选择制造商" filterable /> |
|||
</template> |
@ -0,0 +1,92 @@ |
|||
<script setup> |
|||
import { ref, watch } from 'vue'; |
|||
import { REQUEST_UPLOAD_FILE } from '@/config/urls'; |
|||
import auth from '@/utils/auth'; |
|||
|
|||
const props = defineProps({ |
|||
modelValue: { |
|||
type: Array, |
|||
default: () => [], |
|||
}, |
|||
}); |
|||
const emit = defineEmits(['update:modelValue']); |
|||
|
|||
const subject = ref('image'); |
|||
|
|||
const files = ref([]); |
|||
|
|||
watch(() => props.modelValue, (nv = []) => { |
|||
console.log('nv', nv); |
|||
if (!nv.length) files.value = []; |
|||
if (files.value.length) return; |
|||
files.value = [...props.modelValue]; |
|||
}, { deep: true }); |
|||
|
|||
const ABRIDGE_NAME = [10, 7]; |
|||
|
|||
const formatResponse = (res) => { |
|||
if (!res) { |
|||
return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' }; |
|||
} |
|||
return res; |
|||
}; |
|||
|
|||
function beforeUpload(UploadFile) { |
|||
const { type = 'image' } = UploadFile || {}; |
|||
if (type.includes('image')) { |
|||
subject.value = 'image'; |
|||
} |
|||
if (type.includes('video')) { |
|||
subject.value = 'video'; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
// const handleSuccess = ({ response }) => { |
|||
const handleSuccess = () => { |
|||
setTimeout(() => { |
|||
const temp = files.value.map(({ name, size, status, url, response, type }) => ({ |
|||
name, |
|||
size, |
|||
status, |
|||
url: url || response?.data?.url, |
|||
raw: {}, |
|||
type, |
|||
})); |
|||
emit('update:modelValue', temp); |
|||
}); |
|||
}; |
|||
|
|||
const handleRemove = ({ index }) => { |
|||
const temp = [...(props.modelValue || [])]; |
|||
temp.splice(index, 1); |
|||
emit('update:modelValue', temp); |
|||
}; |
|||
</script> |
|||
|
|||
<template> |
|||
<t-upload |
|||
v-model="files" |
|||
placeholder="支持批量上传文件,文件格式不限" |
|||
:action="REQUEST_UPLOAD_FILE(subject)" |
|||
:headers="{ |
|||
Authorization: `Bearer ${auth.getToken()}`, |
|||
}" |
|||
theme="file-flow" |
|||
multiple |
|||
:abridge-name="ABRIDGE_NAME" |
|||
auto-upload |
|||
show-thumbnail |
|||
allow-upload-duplicate-file |
|||
:format-response="formatResponse" |
|||
:before-upload="beforeUpload" |
|||
@success="handleSuccess" |
|||
@remove="handleRemove" |
|||
/> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
</style> |
@ -0,0 +1,31 @@ |
|||
<script setup> |
|||
defineProps({ |
|||
label: { |
|||
type: String, |
|||
default: '', |
|||
}, |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<dl :class="s.root"> |
|||
<dt v-if="label">{{ label }}</dt> |
|||
<dd><slot /></dd> |
|||
</dl> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
display: flex; |
|||
margin: 0; |
|||
|
|||
&:global > dt { |
|||
margin: 0; |
|||
} |
|||
|
|||
&:global > dd { |
|||
margin: 0; |
|||
flex: 1; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,143 @@ |
|||
<script setup> |
|||
import { computed } from 'vue'; |
|||
import deviceCruise from '@/core/deviceCruise'; |
|||
import { popularTime } from '@/utils/helpers'; |
|||
|
|||
const ready = computed(() => deviceCruise.ready); |
|||
const speedRate = computed(() => deviceCruise.speedRate); |
|||
const totalTime = computed(() => deviceCruise.totalTime); |
|||
const currentTime = computed({ |
|||
get() { |
|||
return deviceCruise.elapsedMs; |
|||
}, |
|||
set(val) { |
|||
deviceCruise.setCurrentTime(val); |
|||
}, |
|||
}); |
|||
// const currentTime = ref(0); |
|||
|
|||
// deviceCruise.change((elapsedMs = 0) => { |
|||
// currentTime.value = elapsedMs; |
|||
// }); |
|||
// const currentTime = ref(0); |
|||
// watch(() => device.value.elapsedMs, (nv) => { |
|||
// console.log(nv); |
|||
// }); |
|||
const showPlayButton = computed(() => deviceCruise.isPaused || deviceCruise.isStopped); |
|||
const sliderMarks = computed(() => ({ [totalTime.value]: popularTime(totalTime.value / 1000, 1) })); |
|||
|
|||
const handleTipFormat = computed(() => popularTime(deviceCruise.elapsedMs / 1000, 1) || 0); |
|||
|
|||
function onChangeSpeedRate() { |
|||
const speedRat = speedRate.value >= 8 ? 1 : speedRate.value * 2; |
|||
deviceCruise.setSpeedRate(speedRat); |
|||
} |
|||
|
|||
function onPlay() { |
|||
deviceCruise.handlePlay(); |
|||
} |
|||
|
|||
function onPause() { |
|||
deviceCruise.handlePause(); |
|||
} |
|||
|
|||
function onStop() { |
|||
deviceCruise.handleStop(); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="$style.root" v-if="ready"> |
|||
<t-slider |
|||
v-model="currentTime" |
|||
:tooltip-props="{ visible: false, content: `${handleTipFormat}`, placement: 'top' }" |
|||
:max="totalTime" :marks="sliderMarks" |
|||
/> |
|||
<div class="btn speedRate" @click="onChangeSpeedRate"> |
|||
<span>{{ speedRate }}</span> |
|||
<t-icon size="large" name="forward" /> |
|||
</div> |
|||
<t-icon name="play" size="large" class="btn" v-if="showPlayButton" @click="onPlay" /> |
|||
<t-icon name="pause" size="large" class="btn" v-else @click="onPause" /> |
|||
<t-icon name="stop" size="large" class="btn" @click="onStop" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module> |
|||
//@import "~@/styles/variables"; |
|||
|
|||
.root { |
|||
position: absolute; |
|||
left: 20px; |
|||
right: 20px; |
|||
bottom: 20px; |
|||
display: flex; |
|||
align-items: center; |
|||
background-color: fade(#eee, 90%); |
|||
padding: 20px; |
|||
border-radius: 5px; |
|||
|
|||
:global { |
|||
.t-slider { |
|||
//flex: 1; |
|||
//margin: 0 30px 0 0; |
|||
// |
|||
//&.ant-slider-with-marks { |
|||
// margin-bottom: 0; |
|||
// |
|||
// .ant-slider-mark-text { |
|||
// color: @primary-color; |
|||
// } |
|||
//} |
|||
|
|||
.t-slider__rail { |
|||
background-color: fade(white, 90%); |
|||
} |
|||
|
|||
&:hover .t-slider__rail { |
|||
background-color: fade(#4582e6, 40%); |
|||
} |
|||
|
|||
//.t-slider__track { |
|||
// background-color: #4582e6; |
|||
//} |
|||
// |
|||
.t-slider__button { |
|||
background-color: var(--td-brand-color); |
|||
border-color: white; |
|||
box-shadow: 0 0 0 5px fade(#4582e6, 40%); |
|||
} |
|||
// |
|||
//.ant-slider-handle { |
|||
// border-color: fade(white, 80%); |
|||
// background-color: @primary-color; |
|||
// box-shadow: 0 0 0 5px fade(@primary-color, 20%); |
|||
//} |
|||
} |
|||
|
|||
.speedRate { |
|||
display: flex; |
|||
//font-size: 14px; |
|||
line-height: 1; |
|||
user-select: none; |
|||
} |
|||
|
|||
//.t-icon { |
|||
// font-size: 16px; |
|||
//} |
|||
|
|||
.btn { |
|||
border: 1px solid var(--td-brand-color); |
|||
border-radius: 3px; |
|||
padding: 0 5px; |
|||
color: var(--td-brand-color); |
|||
margin-left: 10px; |
|||
height: 28px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,65 @@ |
|||
<script setup> |
|||
import deviceCruise from '@/core/deviceCruise'; |
|||
import TextRow from '@/components/TextRow.vue'; |
|||
import { computed } from 'vue'; |
|||
import { formatTime, toFixed } from '@/utils/helpers'; |
|||
|
|||
const info = computed(() => deviceCruise.timelyData); |
|||
|
|||
const isVisible = computed(() => !!Object.keys(info.value).length); |
|||
|
|||
const isBox = computed(() => { |
|||
const { yaw } = info.value; |
|||
return yaw === undefined; |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root" v-if="isVisible"> |
|||
<template v-if="isBox"> |
|||
<TextRow label="时间:">{{ formatTime(info.timestamp) }}</TextRow> |
|||
<TextRow label="经度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lng, 7) }}</ValueUnit></TextRow> |
|||
<TextRow label="纬度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lat, 7) }}</ValueUnit></TextRow> |
|||
<TextRow label="速度:"><ValueUnit unit="米/秒" tag="small">{{ toFixed(info.spd / 10, 2) }}</ValueUnit></TextRow> |
|||
</template> |
|||
<template v-else> |
|||
<TextRow label="时间:">{{ formatTime(info.timestamp) }}</TextRow> |
|||
<TextRow label="经度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lng, 7) }}</ValueUnit></TextRow> |
|||
<TextRow label="纬度:"><ValueUnit unit="°" tag="small">{{ toFixed(info.lat, 7) }}</ValueUnit></TextRow> |
|||
<TextRow label="对地高度:"><ValueUnit unit="米" tag="small">{{ toFixed(info.height, 2) }}</ValueUnit></TextRow> |
|||
<TextRow label="水平速度:"><ValueUnit unit="米/秒" tag="small">{{ toFixed(info.xspeed, 2) }}</ValueUnit></TextRow> |
|||
<TextRow label="垂直速度:"><ValueUnit unit="米/秒" tag="small">{{ toFixed(info.yspeed, 2) }}</ValueUnit></TextRow> |
|||
<TextRow label="航向角:"><ValueUnit unit="°" tag="small">{{ toFixed(info.yaw, 2) }}</ValueUnit></TextRow> |
|||
<TextRow label="横滚角:"><ValueUnit unit="°" tag="small">{{ toFixed(info.ra, 2) }}</ValueUnit></TextRow> |
|||
<TextRow label="俯仰角:"><ValueUnit unit="°" tag="small">{{ toFixed(info.pa, 2) }}</ValueUnit></TextRow> |
|||
<TextRow label="喷洒流速:"><ValueUnit unit="升/分钟" tag="small">{{ toFixed(info.flowSpeed, 2) }}</ValueUnit></TextRow> |
|||
<TextRow label="已施药量:"><ValueUnit unit="毫升" tag="small">{{ toFixed(info.dose, 2) }}</ValueUnit></TextRow> |
|||
</template> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
position: absolute; |
|||
right: 10px; |
|||
top: 10px; |
|||
//background-color: fade(black, 60%); |
|||
background-color: fade(#eee, 90%); |
|||
//color: whitesmoke; |
|||
color: black; |
|||
padding: 20px; |
|||
border-radius: 5px; |
|||
font-size: 14px; |
|||
|
|||
:global { |
|||
dl dt { |
|||
width: 70px; |
|||
text-align: right; |
|||
} |
|||
|
|||
dt { |
|||
//color: var(--td-brand-color); |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,295 @@ |
|||
<script setup> |
|||
import BasePanel from '@/components/BasePanel.vue'; |
|||
import Player from 'xgplayer'; |
|||
import 'xgplayer/dist/index.min.css'; |
|||
// import HlsPlugin from 'xgplayer-hls'; |
|||
import { onMounted, ref, onUnmounted, watch } from 'vue'; |
|||
// import MultipleFilesUploader from '@/components/MultipleFilesUploader.vue'; |
|||
// import mp4 from '@/assets/xgplayer-demo.mp4'; |
|||
// import liveCancel from '@/assets/live_cancel.png'; |
|||
|
|||
// let player = new Player({ |
|||
// id: 'mse', |
|||
// url: '//abc.com/**/*.mp4', |
|||
// height: '100%', |
|||
// width: '100%', |
|||
// }); |
|||
|
|||
const props = defineProps({ |
|||
list: { |
|||
type: Array, |
|||
default: () => [], |
|||
}, |
|||
}); |
|||
|
|||
const playerRef = ref(); |
|||
|
|||
const player = ref(); |
|||
|
|||
onMounted(() => { |
|||
const options = { |
|||
// id: 'player', |
|||
el: playerRef.value, |
|||
url: '', // 替换为你的 HLS 流地址 |
|||
// type: 'hls', |
|||
// fluid: true, |
|||
autoplayMuted: false, |
|||
autoplay: false, |
|||
lang: 'zh', |
|||
width: '100%', |
|||
height: '100%', |
|||
}; |
|||
player.value = new Player(options); |
|||
|
|||
// if (props.list.length) { |
|||
// setTimeout(() => { |
|||
// player.value.setConfig({ |
|||
// url: props.list[0]?.url, |
|||
// }); |
|||
// }); |
|||
// // onPlayNext(props.list[0]); |
|||
// } |
|||
}); |
|||
|
|||
const panelOpen = ref(false); |
|||
|
|||
function onPlayNext(media) { |
|||
player.value.playNext({ |
|||
url: media.url, |
|||
autoplay: true, |
|||
// poster: '新的海报图', |
|||
}); |
|||
} |
|||
|
|||
onUnmounted(() => { |
|||
if (player.value) player.value.destroy(); |
|||
}); |
|||
|
|||
watch(() => props.list, (nv) => { |
|||
if (!nv.length) { |
|||
player.value.playNext({ |
|||
url: [], |
|||
autoplay: false, |
|||
// poster: liveCancel, |
|||
}); |
|||
return; |
|||
} |
|||
// onPlayNext(nv[0]); |
|||
if (!player.value) return; |
|||
onPlayNext(nv[0]); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root"> |
|||
<div ref="playerRef" /> |
|||
|
|||
<BasePanel |
|||
:open="panelOpen" |
|||
class="list-container" |
|||
width="25vh" |
|||
style="z-index: 50" |
|||
expand-ability |
|||
:show-tip="false" |
|||
scrollbar |
|||
> |
|||
<div class="title">视频列表</div> |
|||
|
|||
<t-list> |
|||
<template v-if="list.length"> |
|||
<t-list-item v-for="item in list" :key="item.id"> |
|||
{{ item.name }} |
|||
<template #action> |
|||
<t-button variant="text" @click="onPlayNext(item)"> |
|||
<template #icon> |
|||
<t-icon name="play-circle" /> |
|||
</template> |
|||
</t-button> |
|||
</template> |
|||
</t-list-item> |
|||
</template> |
|||
|
|||
<template v-else> |
|||
<t-list-item> |
|||
<div style="text-align: center;">还没有上传视频哦 ~</div> |
|||
</t-list-item> |
|||
</template> |
|||
</t-list> |
|||
</BasePanel> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
position: relative; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
//pointer-events: none; |
|||
//background-color: fade(red, 80%); |
|||
|
|||
&:global:hover { |
|||
.list-container { |
|||
display: inline-block; |
|||
} |
|||
} |
|||
|
|||
:global { |
|||
|
|||
.list-container { |
|||
display: none; |
|||
padding: 0; |
|||
|
|||
.nav { |
|||
top: 50%; |
|||
transform: translateY(-60%); |
|||
} |
|||
|
|||
.nav .btn { |
|||
width: unset; |
|||
padding: 40px 2px; |
|||
} |
|||
|
|||
.title { |
|||
padding: var(--td-comp-paddingTB-s); |
|||
text-align: center; |
|||
border-bottom: 1px solid var(--td-component-stroke); |
|||
} |
|||
} |
|||
|
|||
//.urlList { |
|||
// pointer-events: auto; |
|||
// position: absolute; |
|||
// right: 0; |
|||
// top: 0; |
|||
// bottom: 0; |
|||
// z-index: 2; |
|||
// 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; |
|||
// |
|||
// &.expand{ |
|||
// transform: translateX(100%); |
|||
// } |
|||
// |
|||
// &.is-open { |
|||
// transform: translateX(0px); |
|||
// } |
|||
// |
|||
// .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); |
|||
// border: 1px solid var(--td-component-stroke); |
|||
// 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); |
|||
// border: 1px solid var(--td-component-stroke); |
|||
// 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,264 @@ |
|||
/** |
|||
* 圈地(算面积) |
|||
*/ |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
|
|||
const SOURCE_LINE = 'enclosure-source-line'; |
|||
const SOURCE_FILL = 'enclosure-source-fill'; |
|||
const SOURCE_SYMBOL = 'enclosure-source-symbol'; |
|||
const LAYER_LINE = 'enclosure-layer-line'; |
|||
const LAYER_FILL = 'enclosure-layer-fill'; |
|||
const LAYER_SYMBOL = 'enclosure-layer-symbol'; |
|||
|
|||
class Enclosure { |
|||
// 地图实例
|
|||
map = null; |
|||
|
|||
// 点坐标
|
|||
coordinates = []; |
|||
|
|||
// 操作点
|
|||
markers = []; |
|||
|
|||
// 形状是否已经闭合(即:是否已经圈完地了)
|
|||
isClosed = false; |
|||
|
|||
// 形状是否扭结了
|
|||
isKinked = false; |
|||
|
|||
// 视觉选项
|
|||
option = { |
|||
edgeColor: 'rgba(0, 0, 255, 0.8)', |
|||
fillColor: 'rgba(0, 0, 255, 0.3)', |
|||
markerColor: 'rgba(255, 255, 255, 0.8)', |
|||
labelColor: 'rgba(255, 255, 255, 0.8)', |
|||
labelStrokeColor: 'rgba(0, 0, 0, 1)', |
|||
labelFontSize: 14, |
|||
}; |
|||
|
|||
constructor(map, option = {}) { |
|||
this.map = map; |
|||
this.option = { |
|||
...this.option, |
|||
...option, |
|||
}; |
|||
} |
|||
|
|||
onDrawOn() { |
|||
this.map.getCanvas().style.cursor = 'crosshair'; |
|||
this.coordinates = []; |
|||
this.markers = []; |
|||
this.isClosed = false; |
|||
this.map.on('click', this.onClickMap); |
|||
this.map.on('style.load', this.draw); |
|||
this.map.fire('enclosure.on'); |
|||
} |
|||
|
|||
onDrawOff() { |
|||
this.map.getCanvas().style.cursor = ''; |
|||
if (this.map.getSource(SOURCE_LINE)) { |
|||
this.map.removeLayer(LAYER_LINE); |
|||
this.map.removeSource(SOURCE_LINE); |
|||
} |
|||
if (this.map.getSource(SOURCE_FILL)) { |
|||
this.map.removeLayer(LAYER_FILL); |
|||
this.map.removeSource(SOURCE_FILL); |
|||
} |
|||
if (this.map.getSource(SOURCE_SYMBOL)) { |
|||
this.map.removeLayer(LAYER_SYMBOL); |
|||
this.map.removeSource(SOURCE_SYMBOL); |
|||
} |
|||
this.markers.forEach(m => m.remove()); |
|||
this.map.off('click', this.onClickMap); |
|||
this.map.off('style.load', this.draw); |
|||
this.map.fire('enclosure.off'); |
|||
} |
|||
|
|||
// 生成线段geojson
|
|||
genLineFeature() { |
|||
const [first] = this.coordinates; |
|||
const coordinates = this.isClosed ? [...this.coordinates, first] : this.coordinates; |
|||
return turf.lineString(coordinates); |
|||
} |
|||
|
|||
// 生成多边形geojson
|
|||
// eslint-disable-next-line class-methods-use-this
|
|||
genPolygonFeature(lineFeature) { |
|||
return turf.lineToPolygon(lineFeature); |
|||
} |
|||
|
|||
// 检测形状是否扭结(判断新坐标加入后,或者对原有坐标进行判断)
|
|||
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 lineFeature = turf.lineString(this.coordinates); |
|||
const polygonFeature = turf.lineToPolygon(lineFeature); |
|||
const kinks = turf.kinks(polygonFeature); |
|||
return !!kinks.features.length; |
|||
} |
|||
|
|||
draw = () => { |
|||
const { length: count } = this.coordinates; |
|||
if (count < 2) return; |
|||
|
|||
// 显示边线
|
|||
const sourceLine = this.map.getSource(SOURCE_LINE); |
|||
const lineFeature = this.genLineFeature(); |
|||
if (!sourceLine) { |
|||
this.map.addSource(SOURCE_LINE, { |
|||
type: 'geojson', |
|||
data: lineFeature, |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_LINE, |
|||
type: 'line', |
|||
source: SOURCE_LINE, |
|||
paint: { |
|||
'line-color': this.option.edgeColor, |
|||
'line-width': 2, |
|||
}, |
|||
}); |
|||
} else { |
|||
sourceLine.setData(lineFeature); |
|||
} |
|||
|
|||
if (count < 3) return; |
|||
|
|||
// 显示多边形
|
|||
const sourceFill = this.map.getSource(SOURCE_FILL); |
|||
const polygonFeature = this.genPolygonFeature(lineFeature); |
|||
if (!sourceFill) { |
|||
this.map.addSource(SOURCE_FILL, { |
|||
type: 'geojson', |
|||
data: polygonFeature, |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_FILL, |
|||
type: 'fill', |
|||
source: SOURCE_FILL, |
|||
paint: { |
|||
'fill-color': this.option.fillColor, |
|||
}, |
|||
}); |
|||
} else { |
|||
sourceFill.setData(polygonFeature); |
|||
} |
|||
|
|||
if (!this.isClosed) return; |
|||
|
|||
const area = this.isKinked ? '0' : (turf.area(polygonFeature) * 0.0015).toFixed(2); // 亩
|
|||
const pointFeature = turf.centerOfMass(polygonFeature); |
|||
pointFeature.properties.area = this.isKinked ? '' : `${area}亩`; |
|||
|
|||
const sourceSymbol = this.map.getSource(SOURCE_SYMBOL); |
|||
if (!sourceSymbol) { |
|||
this.map.addSource(SOURCE_SYMBOL, { |
|||
type: 'geojson', |
|||
data: pointFeature, |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_SYMBOL, |
|||
type: 'symbol', |
|||
source: SOURCE_SYMBOL, |
|||
layout: { |
|||
'text-field': '{area}', |
|||
'text-anchor': 'top', |
|||
'text-size': this.option.labelFontSize, |
|||
'text-allow-overlap': true, |
|||
}, |
|||
paint: { |
|||
'text-color': this.option.labelColor, |
|||
'text-halo-color': this.option.labelStrokeColor, |
|||
'text-halo-width': 1, |
|||
}, |
|||
}); |
|||
} else { |
|||
sourceSymbol.setData(pointFeature); |
|||
} |
|||
}; |
|||
|
|||
// 闭合路径
|
|||
closePath() { |
|||
this.isClosed = true; |
|||
if (this.checkKinked()) { |
|||
this.isKinked = true; |
|||
this.map.fire('enclosure.kinked', { action: 'drag' }); |
|||
} else { |
|||
this.isKinked = false; |
|||
} |
|||
this.draw(); |
|||
} |
|||
|
|||
// 生成操作点
|
|||
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.option.markerColor; |
|||
node.style.boxSizing = 'border-box'; |
|||
node.style.border = `2px solid ${this.option.edgeColor}`; |
|||
node.addEventListener('click', e => { |
|||
e.stopPropagation(); |
|||
const [first] = this.markers; |
|||
// 当有3个以上的点出现,就可以点击第一个点,闭合多边形
|
|||
if (first && (first.getElement() === node) && this.coordinates.length >= 3) { |
|||
this.closePath(); |
|||
} |
|||
}, false); |
|||
return node; |
|||
} |
|||
|
|||
onClickMap = e => { |
|||
if (this.isClosed) return; |
|||
|
|||
const { lng, lat } = e.lngLat; |
|||
// 判断新坐标是否会造成扭结(即:新的边线是否会跟其他边线交叉)
|
|||
if (this.checkKinked([lng, lat])) { |
|||
this.map.fire('enclosure.kinked', { action: 'click' }); |
|||
return; |
|||
} |
|||
|
|||
const marker = new mapbox.Marker({ |
|||
element: this.genMarkerNode(), |
|||
draggable: true, |
|||
}).setLngLat(e.lngLat).addTo(this.map); |
|||
|
|||
this.coordinates.push([lng, lat]); |
|||
this.draw(); |
|||
this.markers.push(marker); |
|||
this.map.fire('enclosure.change', { coordinates: this.coordinates }); |
|||
|
|||
marker.on('drag', () => { |
|||
const index = this.markers.indexOf(marker); |
|||
const { lng: newLng, lat: newLat } = marker.getLngLat(); |
|||
this.coordinates[index] = [newLng, newLat]; |
|||
if (this.checkKinked()) { |
|||
this.isKinked = true; |
|||
this.map.fire('enclosure.kinked', { action: 'drag' }); |
|||
} else { |
|||
// 从扭结复原了
|
|||
if (this.isKinked) { |
|||
this.map.fire('enclosure.kink-recovered'); |
|||
} |
|||
this.isKinked = false; |
|||
} |
|||
this.draw(); |
|||
}); |
|||
|
|||
marker.on('dragend', () => { |
|||
this.map.fire('enclosure.change', { coordinates: this.coordinates }); |
|||
}); |
|||
}; |
|||
} |
|||
|
|||
export default Enclosure; |
@ -0,0 +1,34 @@ |
|||
/** |
|||
* 图标 |
|||
*/ |
|||
|
|||
const check = Symbol('Icon.check'); |
|||
|
|||
class Icon { |
|||
constructor(name, image, zoom, offset) { |
|||
Icon[check](image); |
|||
|
|||
this.name = name; |
|||
this.zoom = zoom || 1; |
|||
this.offset = offset || [0, 0]; |
|||
this.data = image; |
|||
} |
|||
|
|||
update(image) { |
|||
Icon[check](image); |
|||
this.data = image; |
|||
} |
|||
|
|||
static [check](image) { |
|||
if ( |
|||
!(image instanceof HTMLImageElement) |
|||
&& !(image instanceof ImageBitmap) |
|||
&& !(image instanceof ImageData) |
|||
&& !('data' in image && 'width' in image && 'height' in image) |
|||
) { |
|||
throw new Error('image类型错误'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export default Icon; |
@ -0,0 +1,234 @@ |
|||
/** |
|||
* 带图标的点 |
|||
*/ |
|||
import { action, computed, makeObservable, observable, reaction } from 'mobx'; |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
import EventDispatcher from '../EventDispatcher'; |
|||
import Icon from './Icon'; |
|||
|
|||
class IconPointRenderer extends EventDispatcher { |
|||
_topic = null; |
|||
|
|||
_map = null; |
|||
|
|||
_icons = {}; |
|||
|
|||
_dataSource = []; |
|||
|
|||
get _sourceId() { |
|||
return `${this._topic}-icon-point-source`; |
|||
} |
|||
|
|||
get _layerId() { |
|||
return `${this._topic}-icon-point-layer`; |
|||
} |
|||
|
|||
get _pointFeatures() { |
|||
return this._dataSource.map(({ id, iconName, point, ...others }) => { |
|||
const { [iconName]: icon } = this._icons; |
|||
const { zoom: iconZoom = 1, offset: iconOffset = [0, 0] } = icon || {}; |
|||
const feature = turf.point(point, { id, iconName, iconZoom, iconOffset, ...others }); |
|||
feature.id = id; |
|||
return feature; |
|||
}); |
|||
} |
|||
|
|||
get _pointFeatureCollection() { |
|||
return turf.featureCollection(this._pointFeatures); |
|||
} |
|||
|
|||
constructor(topic, icons = []) { |
|||
super(['mousemove', 'mouseleave', 'mouseenter']); |
|||
this._topic = topic; |
|||
if (!icons.length) { |
|||
throw new Error('入参icons至少包含一个图标元素'); |
|||
} |
|||
icons.forEach(this.updateIcon); |
|||
|
|||
makeObservable(this, { |
|||
_topic: observable, |
|||
_icons: observable, |
|||
_dataSource: observable, |
|||
_sourceId: computed, |
|||
_layerId: computed, |
|||
_pointFeatures: computed, |
|||
_pointFeatureCollection: computed, |
|||
loadDataSource: action, |
|||
updateIcon: action, |
|||
destroy: action, |
|||
}); |
|||
|
|||
reaction(() => this._dataSource, () => { |
|||
this._render(); |
|||
}); |
|||
reaction(() => this._icons, () => { |
|||
this._render(); |
|||
}); |
|||
} |
|||
|
|||
setMap(mapInstance) { |
|||
if (this._map === mapInstance) return; |
|||
if (!(mapInstance instanceof mapbox.Map)) { |
|||
throw new Error('必须传入一个mapbox地图实例'); |
|||
} |
|||
this._map = mapInstance; |
|||
} |
|||
|
|||
// 载入数据
|
|||
// list->item必须包含id、point、iconName(其中point如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
|
|||
// list->item可以包含rotate,表示旋转角度
|
|||
loadDataSource(list) { |
|||
if (!this._map) { |
|||
throw new Error('请先设置地图实例'); |
|||
} |
|||
this._dataSource = (list || []).map(item => { |
|||
const { id, iconName, point } = item; |
|||
const newItem = { ...item }; |
|||
if (!Array.isArray(point)) { |
|||
const { lng, lat } = point; |
|||
newItem.point = [lng, lat]; |
|||
} |
|||
return (id >= 0 && iconName && Array.isArray(newItem.point)) ? newItem : null; |
|||
}).filter(Boolean); |
|||
if (!this._dataSource.length) { |
|||
console.warn('数据列表为空'); |
|||
} |
|||
} |
|||
|
|||
updateIcon = icon => { |
|||
if (!(icon instanceof Icon)) { |
|||
throw new Error(`入参icon必须是${Icon}的实例`); |
|||
} |
|||
this._icons = { |
|||
...this._icons, |
|||
[icon.name]: icon, |
|||
}; |
|||
}; |
|||
|
|||
_loadIcons() { |
|||
if (!this._map) return; |
|||
Object.values(this._icons).forEach(icon => { |
|||
if (this._map.hasImage(icon.name)) { |
|||
this._map.updateImage(icon.name, icon.data); |
|||
} else { |
|||
this._map.addImage(icon.name, icon.data); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
_render() { |
|||
if (!this._map) return; |
|||
this._loadIcons(); |
|||
this._renderPoints(); |
|||
} |
|||
|
|||
_renderPoints() { |
|||
const source = this._map.getSource(this._sourceId); |
|||
if (!source) { |
|||
this._map.addSource(this._sourceId, { |
|||
type: 'geojson', |
|||
data: this._pointFeatureCollection, |
|||
}); |
|||
|
|||
this._map.addLayer({ |
|||
id: this._layerId, |
|||
type: 'symbol', |
|||
source: this._sourceId, |
|||
paint: { |
|||
'icon-opacity': [ |
|||
'case', |
|||
['boolean', ['feature-state', 'visible'], true], |
|||
1, |
|||
0, |
|||
], |
|||
// 'icon-translate': ['get', 'iconOffset'], // 缩放之后平移的像素值(mark:icon-translate不支持表达式)
|
|||
}, |
|||
layout: { |
|||
'icon-image': ['get', 'iconName'], |
|||
'icon-size': ['get', 'iconZoom'], |
|||
'icon-offset': ['get', 'iconOffset'], // 乘以缩放值后,才是最终偏移量
|
|||
'icon-rotate': ['coalesce', ['get', 'rotate'], ['number', 0]], |
|||
'icon-allow-overlap': true, |
|||
}, |
|||
}); |
|||
|
|||
this._map.on('click', this._layerId, this._onClick); |
|||
this._map.on('mouseenter', this._layerId, this._onMouseEnter); |
|||
this._map.on('mousemove', this._layerId, this._onMouseMove); |
|||
this._map.on('mouseleave', this._layerId, this._onMouseLeave); |
|||
} else { |
|||
source.setData(this._pointFeatureCollection); |
|||
} |
|||
} |
|||
|
|||
_onClick = e => { |
|||
if (!e.features.length) return; |
|||
const { lngLat, point } = e; |
|||
const [{ properties: detail }] = e.features; |
|||
this._trigger('click', { lngLat, point, detail }); |
|||
}; |
|||
|
|||
_onMouseMove = () => { |
|||
this._map.getCanvas().style.cursor = 'pointer'; |
|||
}; |
|||
|
|||
_onMouseEnter = e => { |
|||
if (!e.features.length) return; |
|||
const { lngLat, point } = e; |
|||
const [{ properties: detail }] = e.features; |
|||
this._trigger('mouseenter', { lngLat, point, detail }); |
|||
}; |
|||
|
|||
_onMouseLeave = e => { |
|||
this._map.getCanvas().style.cursor = ''; |
|||
|
|||
const { lngLat, point } = e; |
|||
this._trigger('mouseleave', { lngLat, point }); |
|||
}; |
|||
|
|||
// 是否包含某个点
|
|||
hasPoint(pointId) { |
|||
return this._dataSource.findIndex(({ id }) => pointId === id) >= 0; |
|||
} |
|||
|
|||
// 隐藏某个点
|
|||
hidePoint(pointId) { |
|||
if (!this._map || !this.hasPoint(pointId)) return; |
|||
if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: false }); |
|||
} |
|||
|
|||
// 显示某个点
|
|||
showPoint(pointId) { |
|||
if (!this._map || !this.hasPoint(pointId)) return; |
|||
if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: true }); |
|||
} |
|||
|
|||
changeVisibility(stateValue) { |
|||
if (!this._map) return; |
|||
if (this._map.getLayer(this._layerId)) { |
|||
this._map.setLayoutProperty(this._layerId, 'visibility', stateValue ? 'visible' : 'none'); |
|||
} |
|||
} |
|||
|
|||
clear() { |
|||
if (!this._map) return; |
|||
if (this._map.getSource(this._sourceId)) { |
|||
this._map.off('click', this._layerId, this._onClick); |
|||
this._map.off('mousemove', this._layerId, this._onMouseMove); |
|||
this._map.off('mouseleave', this._layerId, this._onMouseLeave); |
|||
this._map.off('mouseenter', this._layerId, this._onMouseEnter); |
|||
this._map.removeLayer(this._layerId); |
|||
this._map.removeSource(this._sourceId); |
|||
} |
|||
} |
|||
|
|||
destroy() { |
|||
this.icons = {}; |
|||
this._dataSource = []; |
|||
this.clear(); |
|||
this._map = null; |
|||
} |
|||
} |
|||
|
|||
export default IconPointRenderer; |
@ -0,0 +1,138 @@ |
|||
/** |
|||
* 位置点 |
|||
*/ |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
|
|||
const SOURCE_SYMBOL = 'pinpoint-source-symbol'; |
|||
const LAYER_SYMBOL = 'pinpoint-layer-symbol'; |
|||
|
|||
class Pinpoint { |
|||
// 地图实例
|
|||
map = null; |
|||
|
|||
// 点坐标
|
|||
coordinates = []; |
|||
|
|||
// 操作点
|
|||
pins = []; |
|||
|
|||
// 视觉选项
|
|||
option = { |
|||
mainColor: 'rgba(255,0,0,0.7)', |
|||
secondaryColor: '#fff', |
|||
shadowColor: 'rgba(255,0,0,0.2)', |
|||
fontSize: 12, |
|||
}; |
|||
|
|||
constructor(map, option = {}) { |
|||
this.map = map; |
|||
this.option = { |
|||
...this.option, |
|||
...option, |
|||
}; |
|||
} |
|||
|
|||
// 开启打点
|
|||
onPinOn() { |
|||
this.map.getCanvas().style.cursor = 'crosshair'; |
|||
this.coordinates = []; |
|||
this.pins = []; |
|||
this.map.on('click', this.onClickMap); |
|||
this.map.on('style.load', this.draw); |
|||
this.map.fire('pinpoint.on'); |
|||
} |
|||
|
|||
// 关闭打点
|
|||
onPinOff() { |
|||
this.map.getCanvas().style.cursor = ''; |
|||
if (this.map.getSource(SOURCE_SYMBOL)) { |
|||
this.map.removeLayer(LAYER_SYMBOL); |
|||
this.map.removeSource(SOURCE_SYMBOL); |
|||
} |
|||
this.pins.forEach(m => m.remove()); |
|||
this.map.off('click', this.onClickMap); |
|||
this.map.off('style.load', this.draw); |
|||
this.map.fire('pinpoint.off'); |
|||
} |
|||
|
|||
// 生成点geojson
|
|||
genPointFeature() { |
|||
const pointFeatures = this.coordinates.map(([lng, lat]) => turf.point([lng, lat], { |
|||
text: `${Number(lng).toFixed(7)}\n${Number(lat).toFixed(7)}`, |
|||
})); |
|||
return turf.featureCollection(pointFeatures); |
|||
} |
|||
|
|||
draw = () => { |
|||
const sourceSymbol = this.map.getSource(SOURCE_SYMBOL); |
|||
if (!sourceSymbol) { |
|||
this.map.addSource(SOURCE_SYMBOL, { |
|||
type: 'geojson', |
|||
data: this.genPointFeature(), |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_SYMBOL, |
|||
type: 'symbol', |
|||
source: SOURCE_SYMBOL, |
|||
layout: { |
|||
'text-field': '{text}', |
|||
'text-anchor': 'top', |
|||
'text-justify': 'right', |
|||
'text-size': this.option.fontSize, |
|||
'text-offset': [0, 0.8], |
|||
'text-allow-overlap': true, |
|||
}, |
|||
paint: { |
|||
'text-color': this.option.mainColor, |
|||
'text-halo-color': this.option.secondaryColor, |
|||
'text-halo-width': 1, |
|||
}, |
|||
}); |
|||
} else { |
|||
sourceSymbol.setData(this.genPointFeature()); |
|||
} |
|||
}; |
|||
|
|||
// 生成操作点
|
|||
genPinNode() { |
|||
const node = document.createElement('div'); |
|||
node.style.width = '17px'; |
|||
node.style.height = '30px'; |
|||
/* eslint-disable max-len */ |
|||
node.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16.48 29.5">
|
|||
<path d="M4.72,27.12c0,1.3,1.59,2.38,3.54,2.38s3.54-1,3.54-2.38-1.59-2.39-3.54-2.39S4.72,25.82,4.72,27.12Z" fill="${this.option.shadowColor}" /> |
|||
<path d="M9.7.12A8.26,8.26,0,1,0,7.07,16.43V26a1.18,1.18,0,0,0,1.17,1.17h0A1.17,1.17,0,0,0,9.41,26V16.43a8.25,8.25,0,0,0,7.07-8.12h0A8.25,8.25,0,0,0,9.7.12ZM8.26,10.62A2.36,2.36,0,0,1,8.19,5.9h.07a2.36,2.36,0,0,1,0,4.72Z" fill="${this.option.mainColor}" /> |
|||
</svg>`; |
|||
/* eslint-enable max-len */ |
|||
return node; |
|||
} |
|||
|
|||
onClickMap = e => { |
|||
const marker = new mapbox.Marker({ |
|||
element: this.genPinNode(), |
|||
offset: [0, -12], |
|||
draggable: true, |
|||
}).setLngLat(e.lngLat).addTo(this.map); |
|||
|
|||
const { lng, lat } = e.lngLat; |
|||
this.coordinates.push([lng, lat]); |
|||
this.draw(); |
|||
this.pins.push(marker); |
|||
this.map.fire('pinpoint.change', { coordinates: this.coordinates }); |
|||
|
|||
marker.on('drag', () => { |
|||
const index = this.pins.indexOf(marker); |
|||
const { lng: newLng, lat: newLat } = marker.getLngLat(); |
|||
this.coordinates[index] = [newLng, newLat]; |
|||
this.draw(); |
|||
}); |
|||
|
|||
marker.on('dragend', () => { |
|||
this.map.fire('pinpoint.change', { coordinates: this.coordinates }); |
|||
}); |
|||
}; |
|||
} |
|||
|
|||
export default Pinpoint; |
@ -0,0 +1,185 @@ |
|||
/** |
|||
* 测距尺 |
|||
*/ |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
|
|||
const SOURCE_LINE = 'ruler-source-line'; |
|||
const SOURCE_SYMBOL = 'ruler-source-symbol'; |
|||
const LAYER_LINE = 'ruler-layer-line'; |
|||
const LAYER_SYMBOL = 'ruler-layer-symbol'; |
|||
const labelFormat = n => (n < 1000 ? `${Number(n).toFixed(2)}米` : `${(n / 1000).toFixed(2)}千米`); |
|||
|
|||
class Ruler { |
|||
// 地图实例
|
|||
map = null; |
|||
|
|||
// 点坐标
|
|||
coordinates = []; |
|||
|
|||
// 距离值(含单位)
|
|||
labels = []; |
|||
|
|||
// 操作点
|
|||
markers = []; |
|||
|
|||
// 视觉选项
|
|||
option = { |
|||
mainColor: 'rgba(255,0,0,0.8)', |
|||
secondaryColor: '#fff', |
|||
fontSize: 12, |
|||
}; |
|||
|
|||
constructor(map, option = {}) { |
|||
this.map = map; |
|||
this.option = { |
|||
...this.option, |
|||
...option, |
|||
}; |
|||
} |
|||
|
|||
// 开启测量(外部调用)
|
|||
onMeasuringOn() { |
|||
this.map.getCanvas().style.cursor = 'crosshair'; |
|||
this.coordinates = []; |
|||
this.markers = []; |
|||
this.labels = []; |
|||
this.map.on('click', this.onClickMap); |
|||
this.map.on('style.load', this.draw); |
|||
this.map.fire('ruler.on'); |
|||
} |
|||
|
|||
// 关闭测量(外部调用)
|
|||
onMeasuringOff() { |
|||
this.map.getCanvas().style.cursor = ''; |
|||
if (this.map.getSource(SOURCE_LINE)) { |
|||
this.map.removeLayer(LAYER_LINE); |
|||
this.map.removeSource(SOURCE_LINE); |
|||
} |
|||
if (this.map.getSource(SOURCE_SYMBOL)) { |
|||
this.map.removeLayer(LAYER_SYMBOL); |
|||
this.map.removeSource(SOURCE_SYMBOL); |
|||
} |
|||
this.markers.forEach(m => m.remove()); |
|||
this.map.off('click', this.onClickMap); |
|||
this.map.off('style.load', this.draw); |
|||
this.map.fire('ruler.off'); |
|||
} |
|||
|
|||
// 生成线段geojson
|
|||
genLineFeature() { |
|||
return turf.lineString(this.coordinates); |
|||
} |
|||
|
|||
// 生成点geojson
|
|||
genPointFeature() { |
|||
const pointFeatures = this.coordinates.map((coordinate, index) => turf.point(coordinate, { |
|||
text: this.labels[index], |
|||
})); |
|||
return turf.featureCollection(pointFeatures); |
|||
} |
|||
|
|||
// 绘制
|
|||
draw = () => { |
|||
if (this.coordinates.length >= 2) { |
|||
const sourceLine = this.map.getSource(SOURCE_LINE); |
|||
if (!sourceLine) { |
|||
this.map.addSource(SOURCE_LINE, { |
|||
type: 'geojson', |
|||
data: this.genLineFeature(), |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_LINE, |
|||
type: 'line', |
|||
source: SOURCE_LINE, |
|||
paint: { |
|||
'line-color': this.option.mainColor, |
|||
'line-width': 2, |
|||
}, |
|||
}); |
|||
} else { |
|||
sourceLine.setData(this.genLineFeature()); |
|||
} |
|||
} |
|||
|
|||
const sourceSymbol = this.map.getSource(SOURCE_SYMBOL); |
|||
if (!sourceSymbol) { |
|||
this.map.addSource(SOURCE_SYMBOL, { |
|||
type: 'geojson', |
|||
data: this.genPointFeature(), |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_SYMBOL, |
|||
type: 'symbol', |
|||
source: SOURCE_SYMBOL, |
|||
layout: { |
|||
'text-field': '{text}', |
|||
'text-anchor': 'top', |
|||
'text-size': this.option.fontSize, |
|||
'text-offset': [0, 0.8], |
|||
'text-allow-overlap': true, |
|||
}, |
|||
paint: { |
|||
'text-color': this.option.mainColor, |
|||
'text-halo-color': this.option.secondaryColor, |
|||
'text-halo-width': 1, |
|||
}, |
|||
}); |
|||
} else { |
|||
sourceSymbol.setData(this.genPointFeature()); |
|||
} |
|||
}; |
|||
|
|||
// 生成操作点
|
|||
genMarkerNode() { |
|||
const node = document.createElement('div'); |
|||
node.style.width = '12px'; |
|||
node.style.height = '12px'; |
|||
node.style.borderRadius = '50%'; |
|||
node.style.background = this.option.secondaryColor; |
|||
node.style.boxSizing = 'border-box'; |
|||
node.style.border = `2px solid ${this.option.mainColor}`; |
|||
return node; |
|||
} |
|||
|
|||
onClickMap = e => { |
|||
const marker = new mapbox.Marker({ |
|||
element: this.genMarkerNode(), |
|||
draggable: true, |
|||
}).setLngLat(e.lngLat).addTo(this.map); |
|||
|
|||
const { lng, lat } = e.lngLat; |
|||
this.coordinates.push([lng, lat]); |
|||
this.updateLabels(); |
|||
this.draw(); |
|||
this.markers.push(marker); |
|||
this.map.fire('ruler.change', { coordinates: this.coordinates }); |
|||
|
|||
marker.on('drag', () => { |
|||
const index = this.markers.indexOf(marker); |
|||
const { lng: newLng, lat: newLat } = marker.getLngLat(); |
|||
this.coordinates[index] = [newLng, newLat]; |
|||
this.updateLabels(); |
|||
this.draw(); |
|||
}); |
|||
|
|||
marker.on('dragend', () => { |
|||
this.map.fire('ruler.change', { coordinates: this.coordinates }); |
|||
}); |
|||
}; |
|||
|
|||
// 更新测量结果值
|
|||
updateLabels() { |
|||
const { coordinates } = this; |
|||
let sum = 0; |
|||
this.labels = coordinates.map((coordinate, index) => { |
|||
if (index === 0) return labelFormat(0); |
|||
sum += turf.distance(coordinates[index - 1], coordinates[index], { units: 'meters' }); |
|||
return labelFormat(sum); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export default Ruler; |
@ -0,0 +1,455 @@ |
|||
/** |
|||
* 多边形组显示 |
|||
*/ |
|||
import { action, computed, makeObservable, observable, reaction } from 'mobx'; |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
|
|||
class ShapeGroupRenderer { |
|||
_topic = null; |
|||
|
|||
// 地图实例
|
|||
_map = null; |
|||
|
|||
// 分组标识
|
|||
_groupIdKey = null; |
|||
|
|||
// 分组名称标识
|
|||
_groupNameKey = null; |
|||
|
|||
// 分组父级标识
|
|||
_groupParentIdKey = null; |
|||
|
|||
_dataSource = []; |
|||
|
|||
_defaultOptions = { |
|||
// 显示描边
|
|||
showStroke: true, |
|||
// 显示标签
|
|||
showLabel: true, |
|||
// 显示填充
|
|||
showFill: true, |
|||
}; |
|||
|
|||
_options = {}; |
|||
|
|||
_defaultStyle = { |
|||
color: 'rgba(255, 95, 0, 0.7)', |
|||
hoverColor: 'rgba(255, 95, 0, 1)', |
|||
offset: -2, |
|||
isDashed: false, |
|||
labelColor: 'rgba(255, 95, 0, 1)', |
|||
labelStrokeColor: 'rgba(0, 0, 0, 0.4)', |
|||
labelMinZoom: 3, |
|||
}; |
|||
|
|||
// 视觉选项
|
|||
_style = {}; |
|||
|
|||
get _sourceId() { |
|||
return { |
|||
FILL: `${this._topic}-shape-fill-source`, |
|||
STROKE: `${this._topic}-shape-hull-stroke-source`, |
|||
LABEL: `${this._topic}-shape-hull-label-source`, |
|||
}; |
|||
} |
|||
|
|||
get _layerId() { |
|||
return { |
|||
FILL: `${this._topic}-shape-fill-source`, |
|||
STROKE: `${this._topic}-shape-hull-stroke-layer`, |
|||
LABEL: `${this._topic}-shape-hull-label-layer`, |
|||
}; |
|||
} |
|||
|
|||
// 形状填充
|
|||
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 _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 _strokeColorExpression() { |
|||
return [ |
|||
'case', |
|||
[ |
|||
'any', |
|||
['boolean', ['feature-state', 'hover'], false], |
|||
['boolean', ['feature-state', 'highlight'], false], |
|||
], |
|||
this._style.hoverColor, |
|||
this._style.color, |
|||
]; |
|||
} |
|||
|
|||
get _strokeDashedExpression() { |
|||
return this._style.isDashed ? [2, 2] : [1]; |
|||
} |
|||
|
|||
// 标签透明度表达式
|
|||
get _labelOpacityExpression() { |
|||
return [ |
|||
'interpolate', ['linear'], |
|||
['zoom'], |
|||
this._style.labelMinZoom - 0.01, 0, |
|||
this._style.labelMinZoom, 1, |
|||
]; |
|||
} |
|||
|
|||
// 分组数据
|
|||
get _group() { |
|||
const group = {}; |
|||
Object.values(this._dataSource).forEach(item => { |
|||
const { [this._groupIdKey]: groupId } = item; |
|||
if (groupId) { |
|||
group[groupId] = [ |
|||
...(group[groupId] || []), |
|||
item, |
|||
]; |
|||
} |
|||
}); |
|||
return group; |
|||
} |
|||
|
|||
// 每个组中所有形状边界点集合
|
|||
get _pointFeatureCollections() { |
|||
const group = {}; |
|||
Object.keys(this._group).forEach(groupId => { |
|||
const groupPoints = this._group[groupId].map(({ points }) => points).flat(); |
|||
const pointFeatures = groupPoints.map(point => turf.point(point)); |
|||
group[groupId] = turf.featureCollection(pointFeatures); |
|||
}); |
|||
return group; |
|||
} |
|||
|
|||
// 每个组的凸包
|
|||
get _convexHulls() { |
|||
return Object.keys(this._pointFeatureCollections).map(groupId => { |
|||
const convexHull = turf.convex(this._pointFeatureCollections[groupId]); |
|||
const [{ [this._groupNameKey]: groupName, [this._groupParentIdKey]: groupParentId }] = this._group[groupId]; |
|||
convexHull.id = groupId - 0; |
|||
convexHull.properties = { |
|||
[this._groupIdKey]: groupId - 0, |
|||
[this._groupNameKey]: groupName || '', |
|||
[this._groupParentIdKey]: (groupParentId - 0) || 0, |
|||
}; |
|||
return convexHull; |
|||
}); |
|||
} |
|||
|
|||
// 组凸包集合
|
|||
get _convexHullFeatureCollection() { |
|||
return turf.featureCollection(this._convexHulls); |
|||
} |
|||
|
|||
// 各分组中心点
|
|||
get _groupCenterFeatures() { |
|||
return this._convexHulls.map(feature => { |
|||
const pointFeature = turf.centerOfMass(feature); |
|||
pointFeature.properties = feature.properties; |
|||
return pointFeature; |
|||
}); |
|||
} |
|||
|
|||
// 分组中心点集合
|
|||
get _groupCenterFeatureCollection() { |
|||
return turf.featureCollection(this._groupCenterFeatures); |
|||
} |
|||
|
|||
constructor(topic, groupIdKey, groupNameKey = null, groupParentIdKey = null) { |
|||
this._topic = topic; |
|||
this._groupIdKey = groupIdKey; |
|||
this._groupNameKey = groupNameKey; |
|||
this._groupParentIdKey = groupParentIdKey; |
|||
|
|||
this.updateOptions(); |
|||
this.updateStyle(); |
|||
|
|||
makeObservable(this, { |
|||
_topic: observable, |
|||
_dataSource: observable, |
|||
_options: observable, |
|||
_style: observable, |
|||
_sourceId: computed, |
|||
_layerId: computed, |
|||
_strokeColorExpression: computed, |
|||
_strokeDashedExpression: computed, |
|||
_labelOpacityExpression: computed, |
|||
_group: computed, |
|||
_pointFeatureCollections: computed, |
|||
_convexHulls: computed, |
|||
_convexHullFeatureCollection: computed, |
|||
_groupCenterFeatures: computed, |
|||
_groupCenterFeatureCollection: 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、%_groupIdKey%、points数组,points->item如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
|
|||
loadDataSource(list) { |
|||
if (!this._map) { |
|||
throw new Error('请先设置地图实例'); |
|||
} |
|||
this._dataSource = (list || []).map(item => { |
|||
const { id, points } = item; |
|||
const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]); |
|||
return (id >= 0 && Array.isArray(points) && points.length >= 3) ? { ...item, points: newPoints } : null; |
|||
}).filter(Boolean); |
|||
} |
|||
|
|||
_render() { |
|||
if (!this._map) return; |
|||
if (this._options.showStroke) this._renderStroke(); |
|||
if (this._options.showLabel) this._renderLabel(); |
|||
if (this._options.showFill) this._renderShapeFill(); |
|||
} |
|||
|
|||
// 渲染形状填充
|
|||
_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); |
|||
} |
|||
} |
|||
|
|||
_renderStroke() { |
|||
const source = this._map.getSource(this._sourceId.STROKE); |
|||
if (!source) { |
|||
this._map.addSource(this._sourceId.STROKE, { |
|||
type: 'geojson', |
|||
data: this._convexHullFeatureCollection, |
|||
}); |
|||
|
|||
this._map.addLayer({ |
|||
id: this._layerId.STROKE, |
|||
type: 'line', |
|||
source: this._sourceId.STROKE, |
|||
paint: { |
|||
'line-color': this._strokeColorExpression, |
|||
'line-width': [ |
|||
'case', |
|||
[ |
|||
'any', |
|||
['boolean', ['feature-state', 'hover'], false], |
|||
['boolean', ['feature-state', 'highlight'], false], |
|||
], |
|||
2, |
|||
1, |
|||
], |
|||
'line-dasharray': this._strokeDashedExpression, |
|||
'line-offset': this._defaultStyle.offset, |
|||
}, |
|||
}); |
|||
} else { |
|||
source.setData(this._convexHullFeatureCollection); |
|||
} |
|||
} |
|||
|
|||
_renderLabel() { |
|||
const source = this._map.getSource(this._sourceId.LABEL); |
|||
if (!source) { |
|||
this._map.addSource(this._sourceId.LABEL, { |
|||
type: 'geojson', |
|||
data: this._groupCenterFeatureCollection, |
|||
}); |
|||
|
|||
this._map.addLayer({ |
|||
id: this._layerId.LABEL, |
|||
type: 'symbol', |
|||
source: this._sourceId.LABEL, |
|||
layout: { |
|||
'text-field': `{${this._groupNameKey}}`, |
|||
'text-size': 12, |
|||
'text-allow-overlap': true, |
|||
}, |
|||
paint: { |
|||
'text-color': this._style.labelColor, |
|||
'text-halo-color': this._style.labelStrokeColor, |
|||
'text-halo-width': 1, |
|||
'text-translate': [0, -20], |
|||
'text-opacity': this._labelOpacityExpression, |
|||
}, |
|||
}); |
|||
} else { |
|||
source.setData(this._groupCenterFeatureCollection); |
|||
} |
|||
} |
|||
|
|||
// 高亮某个分组框(或取消高亮)
|
|||
highlightGroup(groupId, stateValue = true) { |
|||
if (!this._map) return; |
|||
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: groupId }, { highlight: stateValue }); |
|||
if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id: groupId }, { highlight: stateValue }); |
|||
} |
|||
|
|||
// 高亮一组分组框(或取消高亮)
|
|||
highlightGroups(keyName, keyValue, stateValue = true) { |
|||
if (!this._map) return; |
|||
turf.featureEach(this._convexHullFeatureCollection, currentFeature => { |
|||
const { id } = currentFeature; |
|||
if (currentFeature.properties[keyName] === keyValue) this.highlightGroup(id, stateValue); |
|||
}); |
|||
} |
|||
|
|||
_refreshVisibility() { |
|||
if (!this._map) return; |
|||
if (this._map.getLayer(this._layerId.STROKE)) this._map.setLayoutProperty(this._layerId.STROKE, 'visibility', this._options.showStroke ? 'visible' : 'none'); |
|||
if (this._map.getLayer(this._layerId.LABEL)) 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._strokeColorExpression); |
|||
this._map.setPaintProperty(this._layerId.STROKE, 'line-dasharray', this._strokeDashedExpression); |
|||
this._map.setPaintProperty(this._layerId.STROKE, 'line-offset', this._defaultStyle.offset); |
|||
} |
|||
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._labelOpacityExpression); |
|||
} |
|||
} |
|||
|
|||
// 缩放到包围盒
|
|||
fit(groupId, { top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) { |
|||
if (!this._map || !(groupId in this._group)) return; |
|||
const feature = this._convexHullFeatureCollection.features.find(({ id }) => id === groupId); |
|||
if (!feature) return; |
|||
this._map.fitBounds(turf.bbox(feature), { |
|||
duration: 2000, |
|||
padding: { |
|||
top: top + cut, |
|||
bottom: bottom + cut, |
|||
left: left + cut, |
|||
right: right + cut, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
clear() { |
|||
if (!this._map) return; |
|||
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); |
|||
} |
|||
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); |
|||
} |
|||
} |
|||
|
|||
destroy() { |
|||
this.clear(); |
|||
this._map = null; |
|||
this._dataSource = []; |
|||
this.updateStyle(); |
|||
this.updateOptions(); |
|||
} |
|||
} |
|||
|
|||
export default ShapeGroupRenderer; |
@ -0,0 +1,63 @@ |
|||
/** |
|||
* 地块助手 |
|||
*/ |
|||
import blockRenderer from './blockRenderer'; |
|||
import groupRenderer from './groupRenderer'; |
|||
import subFarmRenderer from './subFarmRenderer'; |
|||
|
|||
class BlockHelper { |
|||
static blockRenderer = blockRenderer; |
|||
|
|||
static groupRenderer = groupRenderer; |
|||
|
|||
static subFarmRenderer = subFarmRenderer; |
|||
|
|||
static setMap(map) { |
|||
blockRenderer.setMap(map); |
|||
groupRenderer.setMap(map); |
|||
subFarmRenderer.setMap(map); |
|||
} |
|||
|
|||
static loadDataSource(list = []) { |
|||
blockRenderer.loadDataSource(list); |
|||
groupRenderer.loadDataSource(list); |
|||
subFarmRenderer.loadDataSource(list); |
|||
} |
|||
|
|||
static getBlock(blockId) { |
|||
return blockRenderer.getShape(blockId - 0); |
|||
} |
|||
|
|||
static highlightBlock(blockId, stateValue) { |
|||
blockRenderer.highlightShape(blockId - 0, stateValue); |
|||
} |
|||
|
|||
static highlightGroup(groupId, stateValue) { |
|||
groupRenderer.highlightGroup(groupId - 0, stateValue); |
|||
blockRenderer.highlightShapes('groupId', groupId - 0, stateValue); |
|||
} |
|||
|
|||
static highlightSubFarm(subFarmId, stateValue) { |
|||
subFarmRenderer.highlightGroup(subFarmId - 0, stateValue); |
|||
groupRenderer.highlightGroups('subFarmId', subFarmId - 0, stateValue); |
|||
blockRenderer.highlightShapes('subFarmId', subFarmId - 0, stateValue); |
|||
} |
|||
|
|||
static fitBlock(blockId, padding = {}, cut = 120) { |
|||
blockRenderer.fitShape(blockId - 0, padding, cut); |
|||
} |
|||
|
|||
static fitGroup(groupId, padding = {}, cut = 120) { |
|||
groupRenderer.fit(groupId - 0, padding, cut); |
|||
} |
|||
|
|||
static fitSubFarm(subFarmId, padding = {}, cut = 120) { |
|||
subFarmRenderer.fit(subFarmId - 0, padding, cut); |
|||
} |
|||
|
|||
static fitView(padding = {}, cut = 120) { |
|||
blockRenderer.fitView(padding, cut); |
|||
} |
|||
} |
|||
|
|||
export default BlockHelper; |
@ -0,0 +1,23 @@ |
|||
/** |
|||
* 地块显示 |
|||
*/ |
|||
import ShapeRenderer from '../ShapeRenderer'; |
|||
|
|||
class BlockRenderer extends ShapeRenderer { |
|||
_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: 16, |
|||
}; |
|||
|
|||
constructor() { |
|||
super('block'); |
|||
this.updateStyle(); |
|||
} |
|||
} |
|||
|
|||
export default new BlockRenderer(); |
@ -0,0 +1,23 @@ |
|||
/** |
|||
* 地块分组显示 |
|||
*/ |
|||
import ShapeGroupRenderer from '../ShapeGroupRenderer'; |
|||
|
|||
class GroupRenderer extends ShapeGroupRenderer { |
|||
_defaultStyle = { |
|||
color: 'rgba(255, 95, 0, 0.7)', |
|||
hoverColor: 'rgba(255, 95, 0, 1)', |
|||
offset: -2, |
|||
isDashed: true, |
|||
labelColor: 'rgba(255, 95, 0, 1)', |
|||
labelStrokeColor: 'rgba(0, 0, 0, 0.4)', |
|||
labelMinZoom: 14, |
|||
}; |
|||
|
|||
constructor() { |
|||
super('blockGroup', 'groupId', 'groupName', 'subFarmId'); |
|||
this.updateStyle(); |
|||
} |
|||
} |
|||
|
|||
export default new GroupRenderer(); |
@ -0,0 +1,26 @@ |
|||
/** |
|||
* 地块分组显示 |
|||
*/ |
|||
import ShapeGroupRenderer from '../ShapeGroupRenderer'; |
|||
|
|||
class SubFarmRenderer extends ShapeGroupRenderer { |
|||
_defaultOptions = { |
|||
showStroke: true, |
|||
showLabel: false, |
|||
}; |
|||
|
|||
_defaultStyle = { |
|||
color: 'rgba(170, 0, 255, 0.7)', |
|||
hoverColor: 'rgba(170, 0, 255, 1)', |
|||
offset: -5, |
|||
isDashed: false, |
|||
}; |
|||
|
|||
constructor() { |
|||
super('blockSubFarm', 'subFarmId', 'subFarmName', 'farmId'); |
|||
this.updateOptions(); |
|||
this.updateStyle(); |
|||
} |
|||
} |
|||
|
|||
export default new SubFarmRenderer(); |
@ -0,0 +1,356 @@ |
|||
/** |
|||
* 集群控制助手 |
|||
* 分段显示地块边+选择边+显示航线轨迹 |
|||
*/ |
|||
import { action, computed, makeObservable, observable, reaction } from 'mobx'; |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
import chroma from 'chroma-js'; |
|||
|
|||
const SOURCE_CLUSTER_CONTROL_EDGE_LINE = 'cluster-control-edge-line-source'; |
|||
const LAYER_CLUSTER_CONTROL_EDGE_LINE = 'cluster-control-edge-line-layer'; |
|||
const SOURCE_CLUSTER_CONTROL_EDGE_LABEL = 'cluster-control-edge-label-source'; |
|||
const LAYER_CLUSTER_CONTROL_EDGE_LABEL = 'cluster-control-edge-label-layer'; |
|||
const SOURCE_CLUSTER_CONTROL_TRACK_LINE = 'cluster-control-track-line-source'; |
|||
const LAYER_CLUSTER_CONTROL_TRACK_LINE = 'cluster-control-track-line-layer'; |
|||
const SOURCE_CLUSTER_CONTROL_TRACK_LABEL = 'cluster-control-track-label-source'; |
|||
const LAYER_CLUSTER_CONTROL_TRACK_LABEL = 'cluster-control-track-label-layer'; |
|||
|
|||
class ClusterControlHelper { |
|||
// 地图实例
|
|||
map = null; |
|||
|
|||
// 单个地块数据
|
|||
dataSource = {}; |
|||
|
|||
// 多条轨迹
|
|||
tracks = []; |
|||
|
|||
// 鼠标经过的边索引
|
|||
hoveredEdgeIndex = null; |
|||
|
|||
// 选边回调
|
|||
pickEdgeCallback = () => {}; |
|||
|
|||
// 地块各边集合
|
|||
get edgeLineFeatureCollection() { |
|||
const { points } = this.dataSource; |
|||
if (!Array.isArray(points) || !points.length) { |
|||
return turf.featureCollection([]); |
|||
} |
|||
const boundaryFeature = turf.lineString(points); |
|||
return turf.lineSegment(turf.lineToPolygon(boundaryFeature)); |
|||
} |
|||
|
|||
// 每条边中点
|
|||
get edgeLineMiddlePointFeatureCollection() { |
|||
const points = []; |
|||
turf.featureEach(this.edgeLineFeatureCollection, (feature, index) => { |
|||
const [point1, point2] = turf.getCoords(feature).map(position => turf.point(position)); |
|||
const point = turf.midpoint(point1, point2); |
|||
point.id = index; |
|||
point.properties = { n: index + 1 }; |
|||
points.push(point); |
|||
}); |
|||
return turf.featureCollection(points); |
|||
} |
|||
|
|||
// 航线颜色
|
|||
get trackColors() { |
|||
return chroma.scale([ |
|||
chroma.hsl(0, 1, 0.4), |
|||
chroma.hsl(30, 1, 0.4), |
|||
chroma.hsl(60, 1, 0.4), |
|||
chroma.hsl(90, 1, 0.4), |
|||
chroma.hsl(120, 1, 0.4), |
|||
chroma.hsl(150, 1, 0.4), |
|||
chroma.hsl(180, 1, 0.4), |
|||
chroma.hsl(210, 1, 0.4), |
|||
chroma.hsl(240, 1, 0.4), |
|||
chroma.hsl(270, 1, 0.4), |
|||
chroma.hsl(300, 1, 0.4), |
|||
chroma.hsl(330, 1, 0.4), |
|||
chroma.hsl(360, 1, 0.4), |
|||
]).colors(this.tracks.length + 1); |
|||
} |
|||
|
|||
// 航线
|
|||
get trackLineFeatureCollection() { |
|||
const features = this.tracks.map((track, index) => { |
|||
const color = this.trackColors[index]; |
|||
return turf.lineString(track, { color }, { id: index }); |
|||
}); |
|||
return turf.featureCollection(features); |
|||
} |
|||
|
|||
// 航线编号
|
|||
get trackLabelFeatureCollection() { |
|||
const showNumber = this.tracks.length > 1; |
|||
const features = this.tracks.map((track, index) => { |
|||
const color = this.trackColors[index]; |
|||
const number = showNumber ? index + 1 : ''; |
|||
const [point] = track; |
|||
return turf.point(point, { color, number }, { id: index }); |
|||
}); |
|||
return turf.featureCollection(features); |
|||
} |
|||
|
|||
constructor() { |
|||
makeObservable(this, { |
|||
dataSource: observable, |
|||
tracks: observable, |
|||
edgeLineFeatureCollection: computed, |
|||
edgeLineMiddlePointFeatureCollection: computed, |
|||
trackColors: computed, |
|||
trackLineFeatureCollection: computed, |
|||
trackLabelFeatureCollection: computed, |
|||
loadBlock: action, |
|||
loadTracks: action, |
|||
destroy: action, |
|||
}); |
|||
|
|||
reaction(() => this.dataSource, () => { |
|||
this.render(); |
|||
}); |
|||
reaction(() => this.tracks, () => { |
|||
this.renderTracks(); |
|||
}); |
|||
} |
|||
|
|||
// 设置地图实例
|
|||
setMap(map) { |
|||
if (this.map === map) return; |
|||
if (!(map instanceof mapbox.Map)) { |
|||
throw new Error('必须传入一个mapbox地图实例'); |
|||
} |
|||
this.map = map; |
|||
} |
|||
|
|||
// 载入地块详情
|
|||
loadBlock(detail) { |
|||
if (!this.map) { |
|||
throw new Error('请先设置地图实例'); |
|||
} |
|||
const { points, ...otherDetail } = detail; |
|||
const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]); |
|||
this.dataSource = { |
|||
...otherDetail, |
|||
points: newPoints, |
|||
}; |
|||
} |
|||
|
|||
// 载入多条轨迹
|
|||
loadTracks(tracks) { |
|||
this.tracks = (tracks || []).map(track => (track || []).map(({ longitude, latitude }) => [longitude, latitude])); |
|||
} |
|||
|
|||
render() { |
|||
if (!this.map) return; |
|||
this.renderEdgeLine(); |
|||
this.renderEdgeLabel(); |
|||
} |
|||
|
|||
// 边界线
|
|||
renderEdgeLine() { |
|||
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE); |
|||
if (!source) { |
|||
this.map.addSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE, { |
|||
type: 'geojson', |
|||
data: this.edgeLineFeatureCollection, |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_CLUSTER_CONTROL_EDGE_LINE, |
|||
type: 'line', |
|||
source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, |
|||
layout: { |
|||
'line-cap': 'round', |
|||
}, |
|||
paint: { |
|||
'line-color': [ |
|||
'case', |
|||
['boolean', ['feature-state', 'hover'], false], |
|||
'rgba(0,0,0,1)', |
|||
'rgba(0,0,0,0.5)', |
|||
], |
|||
'line-width': 6, |
|||
}, |
|||
}); |
|||
|
|||
this.map.on('mouseenter', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onEnterEdge); |
|||
this.map.on('mouseleave', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onLeaveEdge); |
|||
this.map.on('click', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onPickEdge); |
|||
} else { |
|||
source.setData(this.edgeLineFeatureCollection); |
|||
} |
|||
} |
|||
|
|||
renderEdgeLabel() { |
|||
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL); |
|||
if (!source) { |
|||
this.map.addSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL, { |
|||
type: 'geojson', |
|||
data: this.edgeLineMiddlePointFeatureCollection, |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_CLUSTER_CONTROL_EDGE_LABEL, |
|||
type: 'symbol', |
|||
source: SOURCE_CLUSTER_CONTROL_EDGE_LABEL, |
|||
layout: { |
|||
'text-field': '{n}', |
|||
'text-size': 16, |
|||
'text-allow-overlap': true, |
|||
}, |
|||
paint: { |
|||
'text-color': 'white', |
|||
'text-halo-color': 'black', |
|||
'text-halo-width': 2, |
|||
}, |
|||
}); |
|||
} else { |
|||
source.setData(this.edgeLineMiddlePointFeatureCollection); |
|||
} |
|||
} |
|||
|
|||
renderTracks() { |
|||
if (!this.map) return; |
|||
// 轨迹线
|
|||
{ |
|||
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE); |
|||
if (!source) { |
|||
this.map.addSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE, { |
|||
type: 'geojson', |
|||
data: this.trackLineFeatureCollection, |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_CLUSTER_CONTROL_TRACK_LINE, |
|||
type: 'line', |
|||
source: SOURCE_CLUSTER_CONTROL_TRACK_LINE, |
|||
layout: { |
|||
'line-cap': 'round', |
|||
}, |
|||
paint: { |
|||
'line-color': ['get', 'color'], |
|||
'line-width': 2, |
|||
}, |
|||
}); |
|||
} else { |
|||
this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE).setData(this.trackLineFeatureCollection); |
|||
this.fadeTracks(1); |
|||
} |
|||
} |
|||
// 轨迹编号
|
|||
{ |
|||
const source = this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL); |
|||
if (!source) { |
|||
this.map.addSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL, { |
|||
type: 'geojson', |
|||
data: this.trackLabelFeatureCollection, |
|||
}); |
|||
|
|||
this.map.addLayer({ |
|||
id: LAYER_CLUSTER_CONTROL_TRACK_LABEL, |
|||
type: 'symbol', |
|||
source: SOURCE_CLUSTER_CONTROL_TRACK_LABEL, |
|||
layout: { |
|||
'text-field': ['get', 'number'], |
|||
'text-allow-overlap': true, |
|||
}, |
|||
paint: { |
|||
'text-color': ['get', 'color'], |
|||
'text-halo-width': 1, |
|||
'text-halo-color': 'black', |
|||
}, |
|||
}); |
|||
} else { |
|||
this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL).setData(this.trackLabelFeatureCollection); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 淡化
|
|||
fadeTracks(opacity) { |
|||
if (!this.map) return; |
|||
if (this.map.getLayer(LAYER_CLUSTER_CONTROL_TRACK_LINE)) { |
|||
this.map.setPaintProperty(LAYER_CLUSTER_CONTROL_TRACK_LINE, 'line-opacity', opacity); |
|||
} |
|||
} |
|||
|
|||
onEnterEdge = e => { |
|||
if (!e.features.length) return; |
|||
this.map.getCanvas().style.cursor = 'pointer'; |
|||
if (this.hoveredEdgeIndex !== null) { |
|||
this.map.setFeatureState( |
|||
{ source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex }, |
|||
{ hover: false }, |
|||
); |
|||
} |
|||
this.hoveredEdgeIndex = e.features[0].id; |
|||
this.map.setFeatureState( |
|||
{ source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex }, |
|||
{ hover: true }, |
|||
); |
|||
}; |
|||
|
|||
onLeaveEdge = () => { |
|||
this.map.getCanvas().style.cursor = ''; |
|||
if (this.hoveredEdgeIndex !== null) { |
|||
this.map.setFeatureState( |
|||
{ source: SOURCE_CLUSTER_CONTROL_EDGE_LINE, id: this.hoveredEdgeIndex }, |
|||
{ hover: false }, |
|||
); |
|||
} |
|||
this.hoveredEdgeIndex = null; |
|||
}; |
|||
|
|||
onPickEdge = e => { |
|||
const [{ id } = {}] = e.features; |
|||
this.pickEdgeCallback(id); |
|||
}; |
|||
|
|||
// 清除边
|
|||
clearEdge() { |
|||
if (!this.map) return; |
|||
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE)) { |
|||
this.map.off('mouseenter', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onEnterEdge); |
|||
this.map.off('mouseleave', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onLeaveEdge); |
|||
this.map.off('click', LAYER_CLUSTER_CONTROL_EDGE_LINE, this.onPickEdge); |
|||
this.map.removeLayer(LAYER_CLUSTER_CONTROL_EDGE_LINE); |
|||
this.map.removeSource(SOURCE_CLUSTER_CONTROL_EDGE_LINE); |
|||
} |
|||
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL)) { |
|||
this.map.removeLayer(LAYER_CLUSTER_CONTROL_EDGE_LABEL); |
|||
this.map.removeSource(SOURCE_CLUSTER_CONTROL_EDGE_LABEL); |
|||
} |
|||
} |
|||
|
|||
// 清除航线
|
|||
clearTrack() { |
|||
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE)) { |
|||
this.map.removeLayer(LAYER_CLUSTER_CONTROL_TRACK_LINE); |
|||
this.map.removeSource(SOURCE_CLUSTER_CONTROL_TRACK_LINE); |
|||
} |
|||
if (this.map.getSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL)) { |
|||
this.map.removeLayer(LAYER_CLUSTER_CONTROL_TRACK_LABEL); |
|||
this.map.removeSource(SOURCE_CLUSTER_CONTROL_TRACK_LABEL); |
|||
} |
|||
} |
|||
|
|||
clearAll() { |
|||
this.clearEdge(); |
|||
this.clearTrack(); |
|||
} |
|||
|
|||
destroy() { |
|||
this.clearAll(); |
|||
this.pickEdgeCallback = () => {}; |
|||
this.hoveredEdgeIndex = null; |
|||
this.tracks = []; |
|||
this.dataSource = {}; |
|||
this.map = null; |
|||
} |
|||
} |
|||
|
|||
export default new ClusterControlHelper(); |
@ -0,0 +1,308 @@ |
|||
/** |
|||
* 设备按轨迹巡航 |
|||
*/ |
|||
// import Vue from 'vue';
|
|||
import { reactive } from 'vue'; |
|||
import { interpolate } from 'popmotion'; |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
import chroma from 'chroma-js'; |
|||
import Icon from './IconPoint/Icon'; |
|||
import DroneIcon from '../assets/DroneIcon'; |
|||
|
|||
const SOURCE_DEVICE_CRUISE_POINT = 'device-cruise-point-source'; |
|||
const LAYER_DEVICE_CRUISE_POINT = 'device-cruise-point-layer'; |
|||
const DEVICE_CRUISE_POINT_ICON = 'device-cruise-point-icon'; |
|||
|
|||
class DeviceCruise { |
|||
_that = null; |
|||
|
|||
// 地图实例
|
|||
_map = null; |
|||
|
|||
_icon = null; |
|||
|
|||
// 单条轨迹数据
|
|||
_dataSource = {}; |
|||
|
|||
// 巡航速率
|
|||
speedRate = 1; |
|||
|
|||
// 动画计时器
|
|||
_timer = null; |
|||
|
|||
// 上一帧时间点
|
|||
_lastFrameAt = 0; |
|||
|
|||
// 累计播放时长(毫秒数)
|
|||
elapsedMs = 0; |
|||
|
|||
// 每帧期望间隔(毫秒数,实际间隔取决于浏览器fps)
|
|||
_fpsInterval = 1000 / 30; |
|||
|
|||
// 上一帧时间戳
|
|||
_lastFrameTimestamp = 0; |
|||
|
|||
// 巡航到的时间点数据
|
|||
timelyData = {}; |
|||
|
|||
isPlaying = false; |
|||
|
|||
isPaused = false; |
|||
|
|||
isStopped = true; |
|||
|
|||
// 视觉样式
|
|||
_defaultStyle = { |
|||
deviceMainColor: '#1890ff', |
|||
deviceDirectionColor: 'rgba(255,255,0,0.75)', |
|||
}; |
|||
|
|||
_style = {}; |
|||
|
|||
// 是否准备完毕
|
|||
get ready() { |
|||
return Object.keys(this._that._dataSource).length > 0; |
|||
} |
|||
|
|||
get _points() { |
|||
const { points } = this._that._dataSource; |
|||
return points || []; |
|||
} |
|||
|
|||
// 基准时间点(从0开始的毫秒数)
|
|||
get _datumTime() { |
|||
const [{ timestamp: startTs } = {}] = this._that._points || []; |
|||
return this._that._points.map(({ timestamp }) => timestamp - startTs); |
|||
} |
|||
|
|||
// 轨迹总时间(毫秒数)
|
|||
get totalTime() { |
|||
return this._that._datumTime[this._that._datumTime.length - 1] || 0; |
|||
} |
|||
|
|||
// 是否已经开始播放了
|
|||
get isStarted() { |
|||
return this._that.isPlaying || this._that.isPaused; |
|||
} |
|||
|
|||
constructor() { |
|||
this._that = reactive(this); |
|||
// Vue.observable(this);
|
|||
this._that.initStyle(); |
|||
// eslint-disable-next-line no-constructor-return
|
|||
return this._that; |
|||
} |
|||
|
|||
// 初始化视觉演示(需在loadTracks之前配置)
|
|||
initStyle(style = {}) { |
|||
this._that._style = { |
|||
...this._that._defaultStyle, |
|||
...style, |
|||
}; |
|||
} |
|||
|
|||
// 设置地图实例
|
|||
setMap(mapInstance) { |
|||
if (this._that._map === mapInstance) return; |
|||
if (!(mapInstance instanceof mapbox.Map)) { |
|||
throw new Error('必须传入一个mapbox地图实例'); |
|||
} |
|||
this._that._map = mapInstance; |
|||
} |
|||
|
|||
setIcon(icon) { |
|||
if (!(icon instanceof Icon)) { |
|||
throw new Error(`入参icon必须是${Icon}的实例`); |
|||
} |
|||
this._that._icon = icon; |
|||
if (this._that._map.hasImage(this._that._icon.name)) { |
|||
this._that._map.updateImage(this._that._icon.name, this._that._icon.data); |
|||
} else { |
|||
this._that._map.addImage(this._that._icon.name, this._that._icon.data); |
|||
} |
|||
} |
|||
|
|||
// 载入轨迹数据(point中必须包含lng, lat, timestamp, yaw)
|
|||
loadTrack({ id, points, ...others }) { |
|||
if (!this._that._map) { |
|||
throw new Error('请先设置地图实例'); |
|||
} |
|||
this._that._dataSource = { |
|||
id, |
|||
points: (points || []).map(item => ({ ...item })), |
|||
...others, |
|||
}; |
|||
this._that._reset(); |
|||
this._that._initDevice(); |
|||
} |
|||
|
|||
// 设置速率
|
|||
setSpeedRate(val) { |
|||
this._that.speedRate = val; |
|||
} |
|||
|
|||
// 设置当前巡航时间点
|
|||
setCurrentTime(ms) { |
|||
if (ms < 0 || ms > this._that.totalTime) return; |
|||
this._that.elapsedMs = ms; |
|||
// if (!this._that.isStarted) {
|
|||
this._that._renderDevice(); |
|||
// }
|
|||
} |
|||
|
|||
// 获取指定毫秒处的数据值
|
|||
_getTimelyData(ms = 0) { |
|||
const genTimelyData = interpolate(this._that._datumTime, this._that._points); |
|||
return genTimelyData(ms); |
|||
} |
|||
|
|||
_reset() { |
|||
this._that.isPlaying = false; |
|||
this._that.isPaused = false; |
|||
this._that.isStopped = true; |
|||
this._that.speedRate = 1; |
|||
this._that._lastFrameAt = 0; |
|||
this._that.elapsedMs = 0; |
|||
this._that._lastFrameTimestamp = 0; |
|||
} |
|||
|
|||
_initDevice() { |
|||
if (!this._that._icon) { |
|||
const icon = new Icon(DEVICE_CRUISE_POINT_ICON, new DroneIcon({ |
|||
outlineColor: this._that._style.deviceMainColor, |
|||
bodyColor: chroma(this._that._style.deviceMainColor).alpha(0.5).css(), |
|||
directionColor: this._that._style.deviceDirectionColor, |
|||
}), 0.25); |
|||
this._that.setIcon(icon); |
|||
} |
|||
|
|||
const [point] = this._that._points; |
|||
const { lng, lat, yaw } = point || {}; |
|||
const pointFeature = point ? turf.point([lng, lat], { |
|||
...point, |
|||
yaw: (+Number(yaw) || 0).toFixed(2), |
|||
}) : turf.multiPoint([]); |
|||
|
|||
const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT); |
|||
if (!source) { |
|||
this._that._map.addSource(SOURCE_DEVICE_CRUISE_POINT, { |
|||
type: 'geojson', |
|||
data: pointFeature, |
|||
}); |
|||
|
|||
this._that._map.addLayer({ |
|||
id: LAYER_DEVICE_CRUISE_POINT, |
|||
type: 'symbol', |
|||
source: SOURCE_DEVICE_CRUISE_POINT, |
|||
layout: { |
|||
'icon-image': this._that._icon.name, |
|||
'icon-size': this._that._icon.zoom, |
|||
'icon-offset': this._that._icon.offset, |
|||
'icon-rotate': ['get', 'yaw'], |
|||
'icon-allow-overlap': true, |
|||
}, |
|||
}); |
|||
} else { |
|||
source.setData(pointFeature); |
|||
this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-image', this._that._icon.name); |
|||
this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-size', this._that._icon.zoom); |
|||
this._that._map.setLayoutProperty(LAYER_DEVICE_CRUISE_POINT, 'icon-offset', this._that._icon.offset); |
|||
} |
|||
} |
|||
|
|||
_clearDevice() { |
|||
if (!this._that._map) return; |
|||
if (this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT)) { |
|||
this._that._map.removeLayer(LAYER_DEVICE_CRUISE_POINT); |
|||
this._that._map.removeSource(SOURCE_DEVICE_CRUISE_POINT); |
|||
} |
|||
} |
|||
|
|||
_renderDevice() { |
|||
if (!this._that._points.length) { |
|||
const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT); |
|||
if (source) source.setData(turf.featureCollection([])); |
|||
return; |
|||
} |
|||
|
|||
const point = this._that._getTimelyData(this._that.elapsedMs); |
|||
this._that.timelyData = point; |
|||
if (!point) return; |
|||
|
|||
const { lng, lat, yaw } = point; |
|||
const pointFeature = turf.point([lng, lat], { |
|||
...point, |
|||
yaw: (+Number(yaw) || 0).toFixed(2), |
|||
}); |
|||
|
|||
const source = this._that._map.getSource(SOURCE_DEVICE_CRUISE_POINT); |
|||
source.setData(pointFeature); |
|||
} |
|||
|
|||
_ticker = timestamp => { |
|||
this._that.elapsedMs += Math.round((timestamp - this._that._lastFrameAt) * this._that.speedRate); |
|||
this._that._lastFrameAt = timestamp; |
|||
if (this._that.elapsedMs > this._that.totalTime) { |
|||
return; |
|||
} |
|||
|
|||
// 在期望的间隔内_renderDevice,而不是每个tick都_renderDevice(目的:降低render频率,提高显示性能)
|
|||
const now = Date.now(); |
|||
const timeDiff = now - this._that._lastFrameTimestamp; |
|||
if (timeDiff > this._that._fpsInterval) { |
|||
this._that._lastFrameTimestamp = now - (timeDiff % this._that._fpsInterval); // 矫正时间戳
|
|||
this._that._renderDevice(); |
|||
} |
|||
|
|||
this._that._timer = requestAnimationFrame(this._that._ticker); |
|||
}; |
|||
|
|||
// 开始播放巡航动画、恢复播放巡航动画
|
|||
handlePlay() { |
|||
this._that.isPlaying = true; |
|||
this._that.isPaused = false; |
|||
this._that.isStopped = false; |
|||
this._that._lastFrameAt = performance.now(); |
|||
requestAnimationFrame(this._that._ticker); |
|||
} |
|||
|
|||
// 暂停播放巡航动画
|
|||
handlePause() { |
|||
this._that.isPlaying = false; |
|||
this._that.isPaused = true; |
|||
this._that.isStopped = false; |
|||
cancelAnimationFrame(this._that._timer); |
|||
} |
|||
|
|||
// 停止播放巡航动画
|
|||
handleStop() { |
|||
this._that.isPlaying = false; |
|||
this._that.isPaused = false; |
|||
this._that.isStopped = true; |
|||
this._that._lastFrameAt = 0; |
|||
this._that.elapsedMs = 0; |
|||
cancelAnimationFrame(this._that._timer); |
|||
this._that._renderDevice(); |
|||
this._that.timelyData = {}; |
|||
} |
|||
|
|||
clear() { |
|||
if (!this._that._map) return; |
|||
|
|||
this._that.handleStop(); |
|||
this._that._clearDevice(); |
|||
this._that._reset(); |
|||
this._that._dataSource = {}; |
|||
this._that.timelyData = {}; |
|||
} |
|||
|
|||
destroy() { |
|||
this._that.clear(); |
|||
this._that._timer = null; |
|||
this._that._map = null; |
|||
this._that.initStyle(); |
|||
} |
|||
} |
|||
|
|||
export default new DeviceCruise(); |
@ -0,0 +1,17 @@ |
|||
/** |
|||
* 障碍物助手 |
|||
*/ |
|||
import obstacleRenderer from './obstacleRenderer'; |
|||
import blockObstacleRenderer from './blockObstacleRenderer'; |
|||
|
|||
class ObstacleHelper { |
|||
static obstacleRenderer = obstacleRenderer; |
|||
|
|||
static blockObstacleRenderer = blockObstacleRenderer; |
|||
|
|||
static setMap(map) { |
|||
obstacleRenderer.setMap(map); |
|||
} |
|||
} |
|||
|
|||
export default ObstacleHelper; |
@ -0,0 +1,31 @@ |
|||
/** |
|||
* 地块障碍物 |
|||
*/ |
|||
import ShapeRenderer from '../ShapeRenderer'; |
|||
|
|||
class BlockObstacleRenderer extends ShapeRenderer { |
|||
_defaultOptions = { |
|||
showFill: true, |
|||
showStroke: false, |
|||
showLabel: true, |
|||
showLabelOnOver: true, |
|||
}; |
|||
|
|||
_defaultStyle = { |
|||
fillColor: 'rgba(255, 0, 0, 0.2)', |
|||
fillHoverColor: 'rgba(255, 0, 0, 0.8)', |
|||
strokeColor: 'rgba(255, 0, 0, 0.5)', |
|||
strokeHoverColor: 'rgba(255, 0, 0, 1)', |
|||
labelColor: 'rgba(0, 0, 0, 1)', |
|||
labelStrokeColor: 'rgba(255, 255, 255, 1)', |
|||
labelMinZoom: 3, |
|||
}; |
|||
|
|||
constructor() { |
|||
super('block-obstacle'); |
|||
this.updateOptions(); |
|||
this.updateStyle(); |
|||
} |
|||
} |
|||
|
|||
export default new BlockObstacleRenderer(); |
@ -0,0 +1,31 @@ |
|||
/** |
|||
* 所有障碍物 |
|||
*/ |
|||
import ShapeRenderer from '../ShapeRenderer'; |
|||
|
|||
class ObstacleRenderer extends ShapeRenderer { |
|||
_defaultOptions = { |
|||
showFill: true, |
|||
showStroke: false, |
|||
showLabel: true, |
|||
showLabelOnOver: true, |
|||
}; |
|||
|
|||
_defaultStyle = { |
|||
fillColor: 'rgba(255, 0, 0, 0.2)', |
|||
fillHoverColor: 'rgba(255, 0, 0, 0.8)', |
|||
strokeColor: 'rgba(255, 0, 0, 0.5)', |
|||
strokeHoverColor: 'rgba(255, 0, 0, 1)', |
|||
labelColor: 'rgba(0, 0, 0, 1)', |
|||
labelStrokeColor: 'rgba(255, 255, 255, 1)', |
|||
labelMinZoom: 3, |
|||
}; |
|||
|
|||
constructor() { |
|||
super('obstacle'); |
|||
this.updateOptions(); |
|||
this.updateStyle(); |
|||
} |
|||
} |
|||
|
|||
export default new ObstacleRenderer(); |
@ -0,0 +1,443 @@ |
|||
/** |
|||
* 多轨迹在地图上显示 |
|||
*/ |
|||
import { action, computed, makeObservable, observable, reaction } from 'mobx'; |
|||
import mapbox from 'mapbox-gl'; |
|||
import * as turf from '@turf/turf'; |
|||
import EventDispatcher from './EventDispatcher'; |
|||
|
|||
const SOURCE_RUNNING_TRACK_LINE = 'running-track-line-source'; |
|||
const LAYER_RUNNING_TRACK_LINE = 'running-track-line-layer'; |
|||
const SOURCE_WORKING_TRACK_LINE = 'running-working-line-source'; |
|||
const LAYER_WORKING_TRACK_LINE = 'running-working-line-layer'; |
|||
const SOURCE_START_TRACK_POINT = 'start-track-point-source'; |
|||
const LAYER_START_TRACK_POINT = 'start-track-point-layer'; |
|||
const SOURCE_END_TRACK_POINT = 'end-track-point-source'; |
|||
const LAYER_END_TRACK_POINT = 'end-track-point-layer'; |
|||
|
|||
class TrackRenderer extends EventDispatcher { |
|||
// 地图实例
|
|||
_map = null; |
|||
|
|||
// 轨迹容器
|
|||
_tracksStore = []; |
|||
|
|||
// 每条轨迹唯一键
|
|||
_trackKey = 'id'; |
|||
|
|||
// 鼠标经过的轨迹id
|
|||
_hoveredId = null; |
|||
|
|||
// 鼠标经过显示的气泡框
|
|||
_popup = new mapbox.Popup({ closeButton: false }); |
|||
|
|||
// 以回调形式获取popup显示内容
|
|||
_getPopupHtml = () => 'Nothing'; |
|||
|
|||
// 显示配置项
|
|||
_defaultOptions = { |
|||
// 最小显示级别
|
|||
minZoom: 3, |
|||
// 显示端点
|
|||
showEndpoint: true, |
|||
// 显示作业轨迹
|
|||
showWorkingTrack: true, |
|||
// 显示行驶轨迹
|
|||
showRunningTrack: true, |
|||
// 显示气泡框
|
|||
showPopup: false, |
|||
}; |
|||
|
|||
_options = {}; |
|||
|
|||
// 视觉样式
|
|||
_defaultStyle = { |
|||
startPointColor: 'rgba(0,0,255,0.8)', |
|||
endPointColor: 'rgba(0,255,0,0.8)', |
|||
pointStrokeColor: '#ffffff', |
|||
runningTrackColor: 'rgba(255,255,255,0.3)', |
|||
runningTrackHoverColor: 'rgba(255,255,255,0.8)', |
|||
workingTrackColor: 'rgba(255,0,0,0.3)', |
|||
workingTrackHoverColor: 'rgba(255,0,0,0.6)', |
|||
}; |
|||
|
|||
_style = {}; |
|||
|
|||
// 所有轨迹起始点
|
|||
get _startPointsFeature() { |
|||
if (!this._options.showEndpoint) return turf.featureCollection([]); |
|||
|
|||
const positions = this._tracksStore.map(({ points }) => { |
|||
const [{ lng, lat }] = points; |
|||
return [lng, lat]; |
|||
}); |
|||
return turf.multiPoint(positions); |
|||
} |
|||
|
|||
// 所有轨迹结束点
|
|||
get _endPointsFeature() { |
|||
if (!this._options.showEndpoint) return turf.featureCollection([]); |
|||
|
|||
const positions = this._tracksStore.map(({ points }) => { |
|||
const { lng, lat } = points[points.length - 1]; |
|||
return [lng, lat]; |
|||
}); |
|||
return turf.multiPoint(positions); |
|||
} |
|||
|
|||
// 行驶轨迹集合
|
|||
get _runningTrackFeatureCollection() { |
|||
if (!this._options.showRunningTrack) return turf.featureCollection([]); |
|||
|
|||
const features = this._tracksStore.map(detail => { |
|||
const { points, [this._trackKey]: id, ...others } = detail; |
|||
const positions = points.map(({ lng, lat }) => ([lng, lat])); |
|||
return turf.lineString(positions, { id, ...others }, { id }); |
|||
}); |
|||
return turf.featureCollection(features); |
|||
} |
|||
|
|||
// 作业轨迹集合
|
|||
get _workingTrackFeatureCollection() { |
|||
if (!this._options.showWorkingTrack) return turf.featureCollection([]); |
|||
|
|||
const features = this._tracksStore.map(({ points, [this._trackKey]: id }) => { |
|||
const segments = TrackRenderer.pickWorkingTrackSegments(points); |
|||
return turf.multiLineString(segments, {}, { id }); |
|||
}); |
|||
return turf.featureCollection(features); |
|||
} |
|||
|
|||
// 限位框
|
|||
get _boundingBox() { |
|||
return turf.bbox(this._runningTrackFeatureCollection); |
|||
} |
|||
|
|||
// 拾取某个轨迹里的各个作业片段
|
|||
static pickWorkingTrackSegments(trackPoints) { |
|||
const result = []; |
|||
let segment = []; |
|||
trackPoints.forEach(({ lng, lat, flowSpeed }) => { |
|||
if (flowSpeed > 0) { |
|||
segment.push([lng, lat]); |
|||
} else { |
|||
if (segment.length >= 2) { |
|||
result.push(segment); |
|||
} |
|||
segment = []; |
|||
} |
|||
}); |
|||
return result; |
|||
} |
|||
|
|||
constructor() { |
|||
super(['rendered']); |
|||
|
|||
this.initOptions(); |
|||
this.initStyle(); |
|||
|
|||
makeObservable(this, { |
|||
_tracksStore: observable, |
|||
_startPointsFeature: computed, |
|||
_endPointsFeature: computed, |
|||
_runningTrackFeatureCollection: computed, |
|||
_workingTrackFeatureCollection: computed, |
|||
loadTracks: action, |
|||
destroy: action, |
|||
}); |
|||
|
|||
reaction(() => this._tracksStore, () => { |
|||
if (this._tracksStore.length) { |
|||
this._render(); |
|||
} else { |
|||
this._clear(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// 初始化配置(需在loadTracks之前配置)
|
|||
initOptions(options = {}) { |
|||
this._options = { |
|||
...this._defaultOptions, |
|||
...options, |
|||
}; |
|||
} |
|||
|
|||
// 初始化视觉演示(需在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; |
|||
} |
|||
|
|||
// 载入轨迹数据
|
|||
loadTracks(list, trackKey = 'id') { |
|||
if (!this._map) { |
|||
throw new Error('请先设置地图实例'); |
|||
} |
|||
// list = [{ id: 123, points: [{ lng, lat, yaw, flowSpeed }] }];
|
|||
this._tracksStore = (list || []).slice(); |
|||
this._trackKey = trackKey; |
|||
} |
|||
|
|||
// 设置气泡框显示内容回调
|
|||
setPopupContentCallback(callback) { |
|||
if (typeof callback !== 'function') { |
|||
throw new Error('入参必须是一个函数'); |
|||
} |
|||
this._getPopupHtml = callback; |
|||
} |
|||
|
|||
_render() { |
|||
if (this._options.showRunningTrack) this._renderRunningTrack(); |
|||
if (this._options.showWorkingTrack) this._renderWorkingTrack(); |
|||
if (this._options.showRunningTrack) { |
|||
this._renderStartPoint(); |
|||
this._renderEndPoint(); |
|||
} |
|||
this._trigger('rendered'); |
|||
} |
|||
|
|||
_renderStartPoint() { |
|||
const source = this._map.getSource(SOURCE_START_TRACK_POINT); |
|||
if (!source) { |
|||
this._map.addSource(SOURCE_START_TRACK_POINT, { |
|||
type: 'geojson', |
|||
data: this._startPointsFeature, |
|||
}); |
|||
|
|||
this._map.addLayer({ |
|||
id: LAYER_START_TRACK_POINT, |
|||
type: 'circle', |
|||
source: SOURCE_START_TRACK_POINT, |
|||
paint: { |
|||
'circle-radius': 4, |
|||
'circle-color': this._style.startPointColor, |
|||
'circle-stroke-width': 1, |
|||
'circle-stroke-color': this._style.pointStrokeColor, |
|||
'circle-stroke-opacity': 0.6, |
|||
'circle-opacity': [ |
|||
'interpolate', ['linear'], |
|||
['zoom'], |
|||
this._options.minZoom - 0.01, 0, |
|||
this._options.minZoom, 1, |
|||
], |
|||
}, |
|||
}); |
|||
} else { |
|||
source.setData(this._startPointsFeature); |
|||
} |
|||
} |
|||
|
|||
_renderEndPoint() { |
|||
const source = this._map.getSource(SOURCE_END_TRACK_POINT); |
|||
if (!source) { |
|||
this._map.addSource(SOURCE_END_TRACK_POINT, { |
|||
type: 'geojson', |
|||
data: this._endPointsFeature, |
|||
}); |
|||
|
|||
this._map.addLayer({ |
|||
id: LAYER_END_TRACK_POINT, |
|||
type: 'circle', |
|||
source: SOURCE_END_TRACK_POINT, |
|||
paint: { |
|||
'circle-radius': 4, |
|||
'circle-color': this._style.endPointColor, |
|||
'circle-stroke-width': 1, |
|||
'circle-stroke-color': this._style.pointStrokeColor, |
|||
'circle-stroke-opacity': 0.6, |
|||
'circle-opacity': [ |
|||
'interpolate', ['linear'], |
|||
['zoom'], |
|||
this._options.minZoom - 0.01, 0, |
|||
this._options.minZoom, 1, |
|||
], |
|||
}, |
|||
}); |
|||
} else { |
|||
source.setData(this._endPointsFeature); |
|||
} |
|||
} |
|||
|
|||
_renderRunningTrack() { |
|||
const source = this._map.getSource(SOURCE_RUNNING_TRACK_LINE); |
|||
if (!source) { |
|||
this._map.addSource(SOURCE_RUNNING_TRACK_LINE, { |
|||
type: 'geojson', |
|||
data: this._runningTrackFeatureCollection, |
|||
}); |
|||
|
|||
this._map.addLayer({ |
|||
id: LAYER_RUNNING_TRACK_LINE, |
|||
type: 'line', |
|||
source: SOURCE_RUNNING_TRACK_LINE, |
|||
paint: { |
|||
'line-color': [ |
|||
'case', |
|||
['boolean', ['feature-state', 'hover'], false], |
|||
this._style.runningTrackHoverColor, |
|||
this._style.runningTrackColor, |
|||
], |
|||
'line-width': 3, |
|||
'line-opacity': [ |
|||
'interpolate', ['linear'], |
|||
['zoom'], |
|||
this._options.minZoom - 0.01, 0, |
|||
this._options.minZoom, 1, |
|||
], |
|||
}, |
|||
layout: { |
|||
'line-cap': 'round', |
|||
'line-join': 'round', |
|||
}, |
|||
}); |
|||
|
|||
this._map.on('mousemove', LAYER_RUNNING_TRACK_LINE, this._onMouseMove); |
|||
this._map.on('mouseleave', LAYER_RUNNING_TRACK_LINE, this._onMouseLeave); |
|||
this._map.on('click', LAYER_RUNNING_TRACK_LINE, this._onClick); |
|||
} else { |
|||
source.setData(this._runningTrackFeatureCollection); |
|||
} |
|||
} |
|||
|
|||
_renderWorkingTrack() { |
|||
const source = this._map.getSource(SOURCE_WORKING_TRACK_LINE); |
|||
if (!source) { |
|||
this._map.addSource(SOURCE_WORKING_TRACK_LINE, { |
|||
type: 'geojson', |
|||
data: this._workingTrackFeatureCollection, |
|||
}); |
|||
|
|||
this._map.addLayer({ |
|||
id: LAYER_WORKING_TRACK_LINE, |
|||
type: 'line', |
|||
source: SOURCE_WORKING_TRACK_LINE, |
|||
paint: { |
|||
'line-color': [ |
|||
'case', |
|||
['boolean', ['feature-state', 'hover'], false], |
|||
this._style.workingTrackHoverColor, |
|||
this._style.workingTrackColor, |
|||
], |
|||
'line-width': 6, |
|||
'line-opacity': [ |
|||
'interpolate', ['linear'], |
|||
['zoom'], |
|||
this._options.minZoom - 0.01, 0, |
|||
this._options.minZoom, 1, |
|||
], |
|||
}, |
|||
layout: { |
|||
'line-cap': 'round', |
|||
'line-join': 'round', |
|||
}, |
|||
}); |
|||
} else { |
|||
source.setData(this._workingTrackFeatureCollection); |
|||
} |
|||
} |
|||
|
|||
_onMouseMove = e => { |
|||
if (this._map.getZoom() < this._options.minZoom) return; |
|||
this._map.getCanvas().style.cursor = 'pointer'; |
|||
if (e.features.length > 0) { |
|||
if (this._hoveredId !== null) { |
|||
const id = this._hoveredId; |
|||
if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id }, { hover: false }); |
|||
if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id }, { hover: false }); |
|||
} |
|||
const [{ id }] = e.features; |
|||
if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id }, { hover: true }); |
|||
if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id }, { hover: true }); |
|||
this._hoveredId = id; |
|||
} |
|||
if (this._options.showPopup) { |
|||
const [{ properties: detail }] = e.features; |
|||
this._popup.setLngLat(e.lngLat).setHTML(this._getPopupHtml(detail)).addTo(this._map); |
|||
} |
|||
}; |
|||
|
|||
_onMouseLeave = () => { |
|||
if (this._map.getZoom() < this._options.minZoom) return; |
|||
this._map.getCanvas().style.cursor = ''; |
|||
if (this._hoveredId !== null) { |
|||
if (this._options.showRunningTrack) this._map.setFeatureState({ source: SOURCE_RUNNING_TRACK_LINE, id: this._hoveredId }, { hover: false }); |
|||
if (this._options.showWorkingTrack) this._map.setFeatureState({ source: SOURCE_WORKING_TRACK_LINE, id: this._hoveredId }, { hover: false }); |
|||
} |
|||
this._hoveredId = null; |
|||
if (this._options.showPopup) { |
|||
this._popup.remove(); |
|||
} |
|||
}; |
|||
|
|||
_onClick = e => { |
|||
if (this._map.getZoom() < this._options.minZoom) return; |
|||
if (!e.features.length) return; |
|||
const [{ properties: detail }] = e.features; |
|||
const { points } = this._tracksStore.find(({ [this._trackKey]: key }) => key === detail.id) || {}; |
|||
this._trigger('click', { ...detail, points: (points || []).map(item => ({ ...item })) }); |
|||
}; |
|||
|
|||
// 缩放到所有轨迹总边界
|
|||
fitView({ top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) { |
|||
if (!this._tracksStore.length) return; |
|||
this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 }); |
|||
this._map.fitBounds(this._boundingBox, { |
|||
duration: 2000, |
|||
padding: { |
|||
top: top + cut, |
|||
bottom: bottom + cut, |
|||
left: left + cut, |
|||
right: right + cut, |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
_clear() { |
|||
if (!this._map) return; |
|||
if (this._map.getSource(SOURCE_RUNNING_TRACK_LINE)) { |
|||
this._map.off('mousemove', LAYER_RUNNING_TRACK_LINE, this._onMouseMove); |
|||
this._map.off('mouseleave', LAYER_RUNNING_TRACK_LINE, this._onMouseLeave); |
|||
this._map.off('click', LAYER_RUNNING_TRACK_LINE, this._onClick); |
|||
this._map.removeLayer(LAYER_RUNNING_TRACK_LINE); |
|||
this._map.removeSource(SOURCE_RUNNING_TRACK_LINE); |
|||
} |
|||
if (this._map.getSource(SOURCE_WORKING_TRACK_LINE)) { |
|||
this._map.removeLayer(LAYER_WORKING_TRACK_LINE); |
|||
this._map.removeSource(SOURCE_WORKING_TRACK_LINE); |
|||
} |
|||
if (this._map.getSource(SOURCE_START_TRACK_POINT)) { |
|||
this._map.removeLayer(LAYER_START_TRACK_POINT); |
|||
this._map.removeSource(SOURCE_START_TRACK_POINT); |
|||
} |
|||
if (this._map.getSource(SOURCE_END_TRACK_POINT)) { |
|||
this._map.removeLayer(LAYER_END_TRACK_POINT); |
|||
this._map.removeSource(SOURCE_END_TRACK_POINT); |
|||
} |
|||
if (this._options.showPopup) { |
|||
this._popup.remove(); |
|||
} |
|||
} |
|||
|
|||
destroy() { |
|||
this._clear(); |
|||
this._map = null; |
|||
this._tracksStore = []; |
|||
this._getPopupHtml = () => 'Nothing'; |
|||
this.initOptions(); |
|||
this.initStyle(); |
|||
} |
|||
} |
|||
|
|||
export default new TrackRenderer(); |
@ -0,0 +1,108 @@ |
|||
<script setup> |
|||
// import BasePanel from '@/components/BasePanel.vue'; |
|||
import Player from 'xgplayer'; |
|||
import 'xgplayer/dist/index.min.css'; |
|||
// import LivePreset from 'xgplayer/es/presets/live'; |
|||
import HlsPlugin from 'xgplayer-hls'; |
|||
import { onMounted, ref, onUnmounted } from 'vue'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useLiveStore } from '@/stores'; |
|||
|
|||
const liveStore = useLiveStore(); |
|||
const { liveUrl } = storeToRefs(liveStore); |
|||
|
|||
console.log(liveUrl); |
|||
|
|||
// console.log(liveUrl); |
|||
|
|||
const visible = ref(false); |
|||
|
|||
const playerRef = ref(); |
|||
|
|||
const player = ref(); |
|||
|
|||
onMounted(() => { |
|||
const options = { |
|||
// id: 'player', |
|||
el: playerRef.value, |
|||
url: '', // 替换为你的 HLS 流地址 |
|||
// url: 'http://pull.jiagutech.com/sgcloud/1736050936087842816.m3u8', // 替换为你的 HLS 流地址 |
|||
// url: 'http://pull.jiagutech.com/live/test.m3u8', // 替换为你的 HLS 流地址 |
|||
type: 'hls', |
|||
ignores: ['progress', 'time', 'playbackrate', 'pip'], |
|||
// fluid: true, |
|||
autoplayMuted: false, |
|||
autoplay: false, |
|||
plugins: [HlsPlugin], |
|||
lang: 'zh', |
|||
width: '100%', |
|||
height: '100%', |
|||
}; |
|||
player.value = new Player(options); |
|||
|
|||
eventBus.on('show-live-dialog', () => { |
|||
if (player.value) { |
|||
// player.value.resetState(); |
|||
player.value.playNext({ |
|||
url: liveUrl.value?.m3u8Url, |
|||
autoplayMuted: true, |
|||
autoplay: true, |
|||
}); |
|||
} |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
if (player.value) player.value.destroy(); |
|||
eventBus.off('show-live-dialog'); |
|||
}); |
|||
|
|||
function onCancel() { |
|||
// |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
mode="full-screen" |
|||
:footer="null" |
|||
@closed="onCancel" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">直播</div> |
|||
</template> |
|||
|
|||
<div class="container"> |
|||
<div ref="playerRef" /> |
|||
</div> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
:global { |
|||
.container { |
|||
height: 100%; |
|||
border-radius: var(--td-radius-medium); |
|||
overflow: hidden; |
|||
display: flex; |
|||
|
|||
.video-player { |
|||
flex: 1; |
|||
margin-left: var(--td-comp-margin-s); |
|||
margin-top: var(--td-comp-margin-s); |
|||
border-radius: var(--td-radius-medium); |
|||
} |
|||
} |
|||
|
|||
.t-dialog__position_fullscreen { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,105 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref } from 'vue'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import { useAuthStore } from '@/stores'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
import { storeToRefs } from 'pinia'; |
|||
// import ImageUploader from '@/components/ImageUploader.vue'; |
|||
// import { update } from '@/utils/helpers'; |
|||
|
|||
const visible = ref(false); |
|||
|
|||
const authStore = useAuthStore(); |
|||
const { userInfo } = storeToRefs(authStore); |
|||
const { updatePassword } = authStore; |
|||
|
|||
const form = ref(); |
|||
const formData = ref({ |
|||
id: undefined, |
|||
phone: undefined, |
|||
oldPassword: undefined, |
|||
newPassword: undefined, |
|||
}); |
|||
|
|||
const FORM_RULES = { |
|||
phone: [{ required: true, message: '请输入' }], |
|||
oldPassword: [{ required: true, message: '请输入原密码' }], |
|||
newPassword: [{ required: true, message: '请输入新密码' }], |
|||
}; |
|||
|
|||
function onCancel() { |
|||
visible.value = false; |
|||
form.value.reset(); |
|||
} |
|||
|
|||
function onSubmit({ validateResult }) { |
|||
if (validateResult === true) { |
|||
updatePassword(formData.value).then(() => { |
|||
MessagePlugin.success('更改成功'); |
|||
onCancel(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-change-password', () => { |
|||
formData.value.id = userInfo.value.id; |
|||
formData.value.phone = userInfo.value.phone; |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-change-password'); |
|||
}); |
|||
</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" |
|||
colon |
|||
@submit="onSubmit" |
|||
> |
|||
<t-form-item name="id" v-show="false" /> |
|||
|
|||
<t-form-item label="手机号" name="phone"> |
|||
<t-input v-model="formData.phone" disabled /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="原密码" name="oldPassword"> |
|||
<t-input v-model="formData.oldPassword" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="新密码" name="newPassword"> |
|||
<t-input v-model="formData.newPassword" /> |
|||
</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,111 @@ |
|||
/** |
|||
* 成果管理 |
|||
*/ |
|||
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 useAchievementStore = defineStore('achievement', () => { |
|||
const achievementList = ref([]); |
|||
const achievementExtra = ref({ total: null }); |
|||
const achievementQueries = ref({ page: 1, pageSize: 10 }); |
|||
|
|||
// const achievementDetail = ref({});
|
|||
|
|||
function getAchievementList(otherQueries = {}) { |
|||
return http.getInstance().get(urls.GET_ACHIEVEMENT_LIST, { |
|||
params: { ...achievementQueries.value, ...otherQueries }, |
|||
}).then(({ data }) => { |
|||
const { data: list, extra } = data; |
|||
achievementList.value = list || []; |
|||
achievementExtra.value = { ...achievementExtra.value, ...(extra || {}) }; |
|||
achievementQueries.value = { ...achievementQueries.value, ...otherQueries }; |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
// function getAchievementDetail(achievementId = '') {
|
|||
// const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, achievementId);
|
|||
// return http.getInstance().get(url).then(({ data }) => {
|
|||
// const { data: detail } = data || {};
|
|||
// achievementDetail.value = detail;
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
function createAchievementShareCode(formData = {}, { refreshList = false } = {}) { |
|||
const reqData = helpers.pick(formData, [ |
|||
'ids', |
|||
]); |
|||
|
|||
return http.getInstance().post(urls.CREATE_ACHIEVEMENT_SHARE_CODE, reqData).then(({ data }) => { |
|||
if (refreshList) getAchievementList(); |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function bindingAchievement(formData = {}, { refreshList = false } = {}) { |
|||
const reqData = helpers.pick(formData, [ |
|||
'id', |
|||
]); |
|||
const url = helpers.buildURL(urls.BINDING_ACHIEVEMENT, reqData.id); |
|||
return http.getInstance().post(url).then(({ data }) => { |
|||
if (refreshList) getAchievementList(); |
|||
return data; |
|||
}); |
|||
} |
|||
//
|
|||
// function updateAchievement(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// 'manufacturerId',
|
|||
// 'sn',
|
|||
// 'type',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
|
|||
// return http.getInstance().put(url, reqData).then(({ data }) => {
|
|||
// if (refreshList) getAchievementList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function deleteAchievement(achievementId = '', { refreshList = false } = {}) {
|
|||
// const url = helpers.buildURL(urls.DELETE_SORTIE, achievementId);
|
|||
// return http.getInstance().delete(url).then(({ data }) => {
|
|||
// if (refreshList) getAchievementList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function updateAchievementState(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
|
|||
// return http.getInstance().put(url).then(({ data }) => {
|
|||
// if (refreshList) getAchievementList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
return { |
|||
achievementList, |
|||
achievementQueries, |
|||
achievementExtra, |
|||
// achievementDetail,
|
|||
getAchievementList, |
|||
// createAchievement,
|
|||
// updateAchievement,
|
|||
// deleteAchievement,
|
|||
// updateAchievementState,
|
|||
// getAchievementDetail,
|
|||
createAchievementShareCode, |
|||
bindingAchievement, |
|||
}; |
|||
}); |
|||
|
|||
export default null; |
@ -0,0 +1,100 @@ |
|||
/** |
|||
* 设备管理 |
|||
*/ |
|||
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 useDeviceStore = defineStore('device', () => { |
|||
const deviceList = ref([]); |
|||
const deviceExtra = ref({ total: null }); |
|||
const deviceQueries = ref({ page: 1, pageSize: 10, all: undefined, type: undefined, search: undefined }); |
|||
|
|||
const deviceDetail = ref({}); |
|||
|
|||
function getDeviceList(otherQueries = {}) { |
|||
return http.getInstance().get(urls.GET_DEVICE_LIST, { |
|||
params: { ...deviceQueries.value, ...otherQueries }, |
|||
}).then(({ data }) => { |
|||
const { data: list, extra } = data; |
|||
deviceList.value = list || []; |
|||
deviceExtra.value = { ...deviceExtra.value, ...(extra || {}) }; |
|||
deviceQueries.value = { ...deviceQueries.value, ...otherQueries }; |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function getDeviceDetail(deviceId = '') { |
|||
const url = helpers.buildURL(urls.GET_DEVICE_DETAIL, deviceId); |
|||
return http.getInstance().get(url).then(({ data }) => { |
|||
const { data: detail } = data || {}; |
|||
deviceDetail.value = detail; |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function createDevice(formData = {}, { refreshList = false } = {}) { |
|||
const reqData = helpers.pick(formData, [ |
|||
'manufacturerId', |
|||
'sn', |
|||
'type', |
|||
]); |
|||
|
|||
return http.getInstance().post(urls.CREATE_DEVICE, reqData).then(({ data }) => { |
|||
if (refreshList) getDeviceList(); |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function updateDevice(formData = {}, { refreshList = false } = {}) { |
|||
const reqData = helpers.pick(formData, [ |
|||
'id', |
|||
'manufacturerId', |
|||
'sn', |
|||
'type', |
|||
]); |
|||
|
|||
const url = helpers.buildURL(urls.UPDATE_DEVICE, reqData.id); |
|||
return http.getInstance().put(url, reqData).then(({ data }) => { |
|||
if (refreshList) getDeviceList(); |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function deleteDevice(deviceId = '', { refreshList = false } = {}) { |
|||
const url = helpers.buildURL(urls.DELETE_DEVICE, deviceId); |
|||
return http.getInstance().delete(url).then(({ data }) => { |
|||
if (refreshList) getDeviceList(); |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function updateDeviceState(formData = {}, { refreshList = false } = {}) { |
|||
const reqData = helpers.pick(formData, [ |
|||
'id', |
|||
]); |
|||
|
|||
const url = helpers.buildURL(urls.UPDATE_DEVICE_LOCKED, reqData.id); |
|||
return http.getInstance().put(url).then(({ data }) => { |
|||
if (refreshList) getDeviceList(); |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
deviceList, |
|||
deviceQueries, |
|||
deviceExtra, |
|||
deviceDetail, |
|||
getDeviceList, |
|||
createDevice, |
|||
updateDevice, |
|||
deleteDevice, |
|||
updateDeviceState, |
|||
getDeviceDetail, |
|||
}; |
|||
}); |
|||
|
|||
export default null; |
@ -0,0 +1,110 @@ |
|||
/** |
|||
* 直播 |
|||
*/ |
|||
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 useLiveStore = defineStore('live', () => { |
|||
const liveUrl = ref({}); |
|||
const liveQueries = ref({ id: undefined }); |
|||
|
|||
function getLiveUrl(otherQueries = {}) { |
|||
const url = helpers.buildURL(urls.GET_PULL_STREAM_URL, otherQueries.id); |
|||
return http.getInstance().get(url, { |
|||
params: { ...liveQueries.value, ...otherQueries }, |
|||
}).then(({ data }) => { |
|||
const { data: Urls } = data; |
|||
liveUrl.value = Urls || {}; |
|||
// liveExtra.value = { ...liveExtra.value, ...(extra || {}) };
|
|||
liveQueries.value = { ...liveQueries.value, ...otherQueries }; |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
// function getLiveDetail(liveId = '') {
|
|||
// const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, liveId);
|
|||
// return http.getInstance().get(url).then(({ data }) => {
|
|||
// const { data: detail } = data || {};
|
|||
// liveDetail.value = detail;
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
// function createLiveShareCode(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'ids',
|
|||
// ]);
|
|||
//
|
|||
// return http.getInstance().post(urls.CREATE_ACHIEVEMENT_SHARE_CODE, reqData).then(({ data }) => {
|
|||
// if (refreshList) getLiveList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
// function bindingLive(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// ]);
|
|||
// const url = helpers.buildURL(urls.BINDING_ACHIEVEMENT, reqData.id);
|
|||
// return http.getInstance().post(url).then(({ data }) => {
|
|||
// if (refreshList) getLiveList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function updateLive(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// 'manufacturerId',
|
|||
// 'sn',
|
|||
// 'type',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
|
|||
// return http.getInstance().put(url, reqData).then(({ data }) => {
|
|||
// if (refreshList) getLiveList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function deleteLive(liveId = '', { refreshList = false } = {}) {
|
|||
// const url = helpers.buildURL(urls.DELETE_SORTIE, liveId);
|
|||
// return http.getInstance().delete(url).then(({ data }) => {
|
|||
// if (refreshList) getLiveList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function updateLiveState(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
|
|||
// return http.getInstance().put(url).then(({ data }) => {
|
|||
// if (refreshList) getLiveList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
return { |
|||
liveUrl, |
|||
liveQueries, |
|||
getLiveUrl, |
|||
// liveExtra,
|
|||
// liveDetail,
|
|||
// getLiveList,
|
|||
// createLive,
|
|||
// updateLive,
|
|||
// deleteLive,
|
|||
// updateLiveState,
|
|||
// getLiveDetail,
|
|||
// createLiveShareCode,
|
|||
// bindingLive,
|
|||
}; |
|||
}); |
|||
|
|||
export default null; |
@ -0,0 +1,55 @@ |
|||
/** |
|||
* 媒体管理 |
|||
*/ |
|||
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 useMediaStore = defineStore('media', () => { |
|||
const mediaList = ref([]); |
|||
const mediaQueries = ref({ sortieId: undefined }); |
|||
|
|||
function getMediaList(sortieId = '') { |
|||
const id = sortieId || mediaQueries.value.sortieId; |
|||
const url = helpers.buildURL(urls.GET_MEDIA_LIST, id); |
|||
return http.getInstance().get(url).then(({ data }) => { |
|||
const { data: list } = data; |
|||
mediaList.value = list || []; |
|||
mediaQueries.value = { mediaId: sortieId || mediaQueries.value.sortieId }; |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function createMedia(formData = {}, { refreshList = false } = {}) { |
|||
const reqData = helpers.pick(formData, [ |
|||
'id', |
|||
'medias', |
|||
]); |
|||
|
|||
const url = helpers.buildURL(urls.CREATE_MEDIA, reqData.id); |
|||
return http.getInstance().post(url, reqData).then(({ data }) => { |
|||
if (refreshList) getMediaList(); |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function deleteMedia(mediaId = '', { refreshList = false } = {}) { |
|||
const url = helpers.buildURL(urls.DELETE_MEDIA, mediaId); |
|||
return http.getInstance().delete(url).then(({ data }) => { |
|||
if (refreshList) getMediaList(); |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
mediaList, |
|||
mediaQueries, |
|||
getMediaList, |
|||
createMedia, |
|||
deleteMedia, |
|||
}; |
|||
}); |
|||
|
|||
export default null; |
@ -0,0 +1,124 @@ |
|||
/** |
|||
* 实时监控 |
|||
*/ |
|||
import { ref } from 'vue'; |
|||
import { defineStore } from 'pinia'; |
|||
import mapConfig from '@/config/map'; |
|||
import http from '@/utils/http'; |
|||
import * as urls from '@/config/urls'; |
|||
// import * as helpers from '@/utils/helpers';
|
|||
|
|||
export const useMonitorStore = defineStore('monitor', () => { |
|||
const mapOptions = ref({ |
|||
currentZoom: mapConfig.zoom, |
|||
currentCenter: mapConfig.center, |
|||
currentRange: { |
|||
maxLat: 67.332486, |
|||
maxLng: 135.227994, |
|||
minLat: 18.186979, |
|||
minLng: 49.880052, |
|||
}, |
|||
}); |
|||
const monitorList = ref([]); |
|||
const monitorExtra = ref({ total: null }); |
|||
const monitorQueries = ref({ }); |
|||
|
|||
// const monitorDetail = ref({});
|
|||
|
|||
function getMonitorList(otherQueries = {}) { |
|||
return http.getInstance().get(urls.GET_ONLINE_DEVICE, { |
|||
params: { ...monitorQueries.value, ...otherQueries }, |
|||
}).then(({ data }) => { |
|||
const { data: list, extra } = data; |
|||
monitorList.value = list || []; |
|||
monitorExtra.value = { ...monitorExtra.value, ...(extra || {}) }; |
|||
// monitorQueries.value = { ...monitorQueries.value, ...otherQueries };
|
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
// function getMonitorDetail(monitorId = '') {
|
|||
// const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, monitorId);
|
|||
// return http.getInstance().get(url).then(({ data }) => {
|
|||
// const { data: detail } = data || {};
|
|||
// monitorDetail.value = detail;
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
// function createMonitorShareCode(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'ids',
|
|||
// ]);
|
|||
//
|
|||
// return http.getInstance().post(urls.CREATE_ACHIEVEMENT_SHARE_CODE, reqData).then(({ data }) => {
|
|||
// if (refreshList) getMonitorList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function bindingMonitor(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// ]);
|
|||
// const url = helpers.buildURL(urls.BINDING_ACHIEVEMENT, reqData.id);
|
|||
// return http.getInstance().post(url).then(({ data }) => {
|
|||
// if (refreshList) getMonitorList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function updateMonitor(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// 'manufacturerId',
|
|||
// 'sn',
|
|||
// 'type',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
|
|||
// return http.getInstance().put(url, reqData).then(({ data }) => {
|
|||
// if (refreshList) getMonitorList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function deleteMonitor(monitorId = '', { refreshList = false } = {}) {
|
|||
// const url = helpers.buildURL(urls.DELETE_SORTIE, monitorId);
|
|||
// return http.getInstance().delete(url).then(({ data }) => {
|
|||
// if (refreshList) getMonitorList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function updateMonitorState(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
|
|||
// return http.getInstance().put(url).then(({ data }) => {
|
|||
// if (refreshList) getMonitorList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
return { |
|||
// monitorList,
|
|||
// monitorQueries,
|
|||
// monitorExtra,
|
|||
// monitorDetail,
|
|||
// getMonitorList,
|
|||
// createMonitor,
|
|||
// updateMonitor,
|
|||
// deleteMonitor,
|
|||
// updateMonitorState,
|
|||
// getMonitorDetail,
|
|||
// createMonitorShareCode,
|
|||
// bindingMonitor,
|
|||
getMonitorList, |
|||
mapOptions, |
|||
}; |
|||
}); |
|||
|
|||
export default null; |
@ -0,0 +1,87 @@ |
|||
/** |
|||
* 架次管理 |
|||
*/ |
|||
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 useSortieStore = defineStore('sortie', () => { |
|||
const sortieList = ref([]); |
|||
const sortieExtra = ref({ total: null }); |
|||
const sortieQueries = ref({ page: 1, pageSize: 10, search: undefined }); |
|||
|
|||
const sortieDetail = ref({}); |
|||
|
|||
function getSortieList(otherQueries = {}) { |
|||
return http.getInstance().get(urls.GET_SORTIE_LIST, { |
|||
params: { ...sortieQueries.value, ...otherQueries }, |
|||
}).then(({ data }) => { |
|||
const { data: list, extra } = data; |
|||
sortieList.value = list || []; |
|||
sortieExtra.value = { ...sortieExtra.value, ...(extra || {}) }; |
|||
sortieQueries.value = { ...sortieQueries.value, ...otherQueries }; |
|||
return data; |
|||
}); |
|||
} |
|||
|
|||
function getSortieDetail(sortieId = '') { |
|||
const url = helpers.buildURL(urls.GET_SORTIE_DETAIL, sortieId); |
|||
return http.getInstance().get(url).then(({ data }) => { |
|||
const { data: detail } = data || {}; |
|||
sortieDetail.value = detail; |
|||
return data; |
|||
}); |
|||
} |
|||
//
|
|||
// function updateSortie(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// 'manufacturerId',
|
|||
// 'sn',
|
|||
// 'type',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE, reqData.id);
|
|||
// return http.getInstance().put(url, reqData).then(({ data }) => {
|
|||
// if (refreshList) getSortieList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function deleteSortie(sortieId = '', { refreshList = false } = {}) {
|
|||
// const url = helpers.buildURL(urls.DELETE_SORTIE, sortieId);
|
|||
// return http.getInstance().delete(url).then(({ data }) => {
|
|||
// if (refreshList) getSortieList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
//
|
|||
// function updateSortieState(formData = {}, { refreshList = false } = {}) {
|
|||
// const reqData = helpers.pick(formData, [
|
|||
// 'id',
|
|||
// ]);
|
|||
//
|
|||
// const url = helpers.buildURL(urls.UPDATE_SORTIE_LOCKED, reqData.id);
|
|||
// return http.getInstance().put(url).then(({ data }) => {
|
|||
// if (refreshList) getSortieList();
|
|||
// return data;
|
|||
// });
|
|||
// }
|
|||
|
|||
return { |
|||
sortieList, |
|||
sortieQueries, |
|||
sortieExtra, |
|||
sortieDetail, |
|||
getSortieList, |
|||
// createSortie,
|
|||
// updateSortie,
|
|||
// deleteSortie,
|
|||
// updateSortieState,
|
|||
getSortieDetail, |
|||
}; |
|||
}); |
|||
|
|||
export default null; |
@ -0,0 +1,466 @@ |
|||
:root,:root[theme-mode="light"] { |
|||
--brand-main: var(--td-brand-color-5); |
|||
--td-brand-color-light: var(--td-brand-color-1); |
|||
--td-brand-color-focus: var(--td-brand-color-2); |
|||
--td-brand-color-disabled: var(--td-brand-color-3); |
|||
--td-brand-color-hover: var(--td-brand-color-4); |
|||
--td-brand-color: var(--td-brand-color-5); |
|||
--td-brand-color-active: var(--td-brand-color-6); |
|||
--td-brand-color-1: #eef4ff; |
|||
--td-brand-color-2: #d1e4ff; |
|||
--td-brand-color-3: #a3ccff; |
|||
--td-brand-color-4: #6bb2ff; |
|||
--td-brand-color-5: #0894fa; |
|||
--td-brand-color-6: #007ad3; |
|||
--td-brand-color-7: #0060a8; |
|||
--td-brand-color-8: #004881; |
|||
--td-brand-color-9: #00325c; |
|||
--td-brand-color-10: #00203f; |
|||
--td-warning-color-1: #fef3e6; |
|||
--td-warning-color-2: #f9e0c7; |
|||
--td-warning-color-3: #f7c797; |
|||
--td-warning-color-4: #f2995f; |
|||
--td-warning-color-5: #ed7b2f; |
|||
--td-warning-color-6: #d35a21; |
|||
--td-warning-color-7: #ba431b; |
|||
--td-warning-color-8: #9e3610; |
|||
--td-warning-color-9: #842b0b; |
|||
--td-warning-color-10: #5a1907; |
|||
--td-warning-color: var(--td-warning-color-5); |
|||
--td-warning-color-hover: var(--td-warning-color-4); |
|||
--td-warning-color-focus: var(--td-warning-color-2); |
|||
--td-warning-color-active: var(--td-warning-color-6); |
|||
--td-warning-color-disabled: var(--td-warning-color-3); |
|||
--td-warning-color-light: var(--td-warning-color-1); |
|||
--td-error-color-1: #fdecee; |
|||
--td-error-color-2: #f9d7d9; |
|||
--td-error-color-3: #f8b9be; |
|||
--td-error-color-4: #f78d94; |
|||
--td-error-color-5: #f36d78; |
|||
--td-error-color-6: #e34d59; |
|||
--td-error-color-7: #c9353f; |
|||
--td-error-color-8: #b11f26; |
|||
--td-error-color-9: #951114; |
|||
--td-error-color-10: #680506; |
|||
--td-error-color: var(--td-error-color-6); |
|||
--td-error-color-hover: var(--td-error-color-5); |
|||
--td-error-color-focus: var(--td-error-color-2); |
|||
--td-error-color-active: var(--td-error-color-7); |
|||
--td-error-color-disabled: var(--td-error-color-3); |
|||
--td-error-color-light: var(--td-error-color-1); |
|||
--td-success-color-1: #e8f8f2; |
|||
--td-success-color-2: #bcebdc; |
|||
--td-success-color-3: #85dbbe; |
|||
--td-success-color-4: #48c79c; |
|||
--td-success-color-5: #00a870; |
|||
--td-success-color-6: #078d5c; |
|||
--td-success-color-7: #067945; |
|||
--td-success-color-8: #056334; |
|||
--td-success-color-9: #044f2a; |
|||
--td-success-color-10: #033017; |
|||
--td-success-color: var(--td-success-color-5); |
|||
--td-success-color-hover: var(--td-success-color-4); |
|||
--td-success-color-focus: var(--td-success-color-2); |
|||
--td-success-color-active: var(--td-success-color-6); |
|||
--td-success-color-disabled: var(--td-success-color-3); |
|||
--td-success-color-light: var(--td-success-color-1); |
|||
--td-gray-color-1: #f3f3f3; |
|||
--td-gray-color-2: #eee; |
|||
--td-gray-color-3: #e7e7e7; |
|||
--td-gray-color-4: #dcdcdc; |
|||
--td-gray-color-5: #c5c5c5; |
|||
--td-gray-color-6: #a6a6a6; |
|||
--td-gray-color-7: #8b8b8b; |
|||
--td-gray-color-8: #777; |
|||
--td-gray-color-9: #5e5e5e; |
|||
--td-gray-color-10: #4b4b4b; |
|||
--td-gray-color-11: #383838; |
|||
--td-gray-color-12: #2c2c2c; |
|||
--td-gray-color-13: #242424; |
|||
--td-gray-color-14: #181818; |
|||
--td-bg-color-container: #fff; |
|||
--td-bg-color-container-select: #fff; |
|||
--td-bg-color-page: var(--td-gray-color-2); |
|||
--td-bg-color-container-hover: var(--td-gray-color-1); |
|||
--td-bg-color-container-active: var(--td-gray-color-3); |
|||
--td-bg-color-secondarycontainer: var(--td-gray-color-1); |
|||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-2); |
|||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-4); |
|||
--td-bg-color-component: var(--td-gray-color-3); |
|||
--td-bg-color-component-hover: var(--td-gray-color-4); |
|||
--td-bg-color-component-active: var(--td-gray-color-6); |
|||
--td-bg-color-component-disabled: var(--td-gray-color-2); |
|||
--td-component-stroke: var(--td-gray-color-3); |
|||
--td-component-border: var(--td-gray-color-4); |
|||
--td-font-white-1: #ffffff; |
|||
--td-font-white-2: rgba(255, 255, 255, 0.55); |
|||
--td-font-white-3: rgba(255, 255, 255, 0.35); |
|||
--td-font-white-4: rgba(255, 255, 255, 0.22); |
|||
--td-font-gray-1: rgba(0, 0, 0, 0.9); |
|||
--td-font-gray-2: rgba(0, 0, 0, 0.6); |
|||
--td-font-gray-3: rgba(0, 0, 0, 0.4); |
|||
--td-font-gray-4: rgba(0, 0, 0, 0.26); |
|||
--td-brand-color-light-hover: var(--td-brand-color-2); |
|||
--td-warning-color-light-hover: var(--td-warning-color-2); |
|||
--td-error-color-light-hover: var(--td-error-color-2); |
|||
--td-success-color-light-hover: var(--td-success-color-2); |
|||
--td-bg-color-secondarycomponent: var(--td-gray-color-4); |
|||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-5); |
|||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-6); |
|||
--td-table-shadow-color: rgba(0, 0, 0, 8%); |
|||
--td-scrollbar-color: rgba(0, 0, 0, 10%); |
|||
--td-scrollbar-hover-color: rgba(0, 0, 0, 30%); |
|||
--td-scroll-track-color: #fff; |
|||
--td-bg-color-specialcomponent: #fff; |
|||
--td-border-level-1-color: var(--td-gray-color-3); |
|||
--td-border-level-2-color: var(--td-gray-color-4); |
|||
--td-shadow-inset-top: inset 0 0.5px 0 #dcdcdc; |
|||
--td-shadow-inset-right: inset 0.5px 0 0 #dcdcdc; |
|||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #dcdcdc; |
|||
--td-shadow-inset-left: inset -0.5px 0 0 #dcdcdc; |
|||
--td-mask-active: rgba(0, 0, 0, 0.6); |
|||
--td-mask-disabled: rgba(255, 255, 255, 0.6); |
|||
/* 字体配置 */ |
|||
--td-font-family: PingFang SC, Microsoft YaHei, Arial Regular; |
|||
--td-font-family-medium: PingFang SC, Microsoft YaHei, Arial Medium; |
|||
--td-font-size-link-small: 12px; |
|||
--td-font-size-link-medium: 14px; |
|||
--td-font-size-link-large: 16px; |
|||
--td-font-size-mark-small: 12px; |
|||
--td-font-size-mark-medium: 14px; |
|||
--td-font-size-body-small: 12px; |
|||
--td-font-size-body-medium: 14px; |
|||
--td-font-size-body-large: 16px; |
|||
--td-font-size-title-small: 14px; |
|||
--td-font-size-title-medium: 16px; |
|||
--td-font-size-title-large: 20px; |
|||
--td-font-size-headline-small: 24px; |
|||
--td-font-size-headline-medium: 28px; |
|||
--td-font-size-headline-large: 36px; |
|||
--td-font-size-display-medium: 48px; |
|||
--td-font-size-display-large: 64px; |
|||
--td-line-height-common: 8px; |
|||
--td-line-height-link-small: calc( var(--td-font-size-link-small) + var(--td-line-height-common) ); |
|||
--td-line-height-link-medium: calc( var(--td-font-size-link-medium) + var(--td-line-height-common) ); |
|||
--td-line-height-link-large: calc( var(--td-font-size-link-large) + var(--td-line-height-common) ); |
|||
--td-line-height-mark-small: calc( var(--td-font-size-mark-small) + var(--td-line-height-common) ); |
|||
--td-line-height-mark-medium: calc( var(--td-font-size-mark-medium) + var(--td-line-height-common) ); |
|||
--td-line-height-body-small: calc( var(--td-font-size-body-small) + var(--td-line-height-common) ); |
|||
--td-line-height-body-medium: calc( var(--td-font-size-body-medium) + var(--td-line-height-common) ); |
|||
--td-line-height-body-large: calc( var(--td-font-size-body-large) + var(--td-line-height-common) ); |
|||
--td-line-height-title-small: calc( var(--td-font-size-title-small) + var(--td-line-height-common) ); |
|||
--td-line-height-title-medium: calc( var(--td-font-size-title-medium) + var(--td-line-height-common) ); |
|||
--td-line-height-title-large: calc( var(--td-font-size-title-medium) + var(--td-line-height-common) ); |
|||
--td-line-height-headline-small: calc( var(--td-font-size-headline-small) + var(--td-line-height-common) ); |
|||
--td-line-height-headline-medium: calc( var(--td-font-size-headline-medium) + var(--td-line-height-common) ); |
|||
--td-line-height-headline-large: calc( var(--td-font-size-headline-large) + var(--td-line-height-common) ); |
|||
--td-line-height-display-medium: calc( var(--td-font-size-display-medium) + var(--td-line-height-common) ); |
|||
--td-line-height-display-large: calc( var(--td-font-size-display-large) + var(--td-line-height-common) ); |
|||
--td-font-link-small: var(--td-font-size-link-small) / var(--td-line-height-link-small) var(--td-font-family); |
|||
--td-font-link-medium: var(--td-font-size-link-medium) / var(--td-line-height-link-medium) var(--td-font-family); |
|||
--td-font-link-large: var(--td-font-size-link-large) / var(--td-line-height-link-large) var(--td-font-family); |
|||
--td-font-mark-small: 600 var(--td-font-size-mark-small) / var(--td-line-height-mark-small) var(--td-font-family); |
|||
--td-font-mark-medium: 600 var(--td-font-size-mark-medium) / var(--td-line-height-mark-medium) var(--td-font-family); |
|||
--td-font-body-small: var(--td-font-size-body-small) / var(--td-line-height-body-small) var(--td-font-family); |
|||
--td-font-body-medium: var(--td-font-size-body-medium) / var(--td-line-height-body-medium) var(--td-font-family); |
|||
--td-font-body-large: var(--td-font-size-body-large) / var(--td-line-height-body-large) var(--td-font-family); |
|||
--td-font-title-small: var(--td-font-size-title-small) / var(--td-line-height-title-small) var(--td-font-family); |
|||
--td-font-title-medium: var(--td-font-size-title-medium) / var(--td-line-height-title-medium) var(--td-font-family); |
|||
--td-font-title-large: var(--td-font-size-title-large) / var(--td-line-height-title-large) var(--td-font-family); |
|||
--td-font-headline-small: var(--td-font-size-headline-small) / var(--td-line-height-headline-small) var(--td-font-family); |
|||
--td-font-headline-medium: var(--td-font-size-headline-medium) / var(--td-line-height-headline-medium) var(--td-font-family); |
|||
--td-font-headline-large: var(--td-font-size-headline-large) / var(--td-line-height-headline-large) var(--td-font-family); |
|||
--td-font-display-medium: var(--td-font-size-display-medium) / var(--td-line-height-display-medium) var(--td-font-family); |
|||
--td-font-display-large: var(--td-font-size-display-large) / var(--td-line-height-display-large) var(--td-font-family); |
|||
/* 字体颜色 */ |
|||
--td-text-color-primary: var(--td-font-gray-1); |
|||
--td-text-color-secondary: var(--td-font-gray-2); |
|||
--td-text-color-placeholder: var(--td-font-gray-3); |
|||
--td-text-color-disabled: var(--td-font-gray-4); |
|||
--td-text-color-anti: #fff; |
|||
--td-text-color-brand: var(--td-brand-color); |
|||
--td-text-color-link: var(--td-brand-color); |
|||
/* end 字体配置 */ /* 圆角配置 */ |
|||
--td-radius-small: 2px; |
|||
--td-radius-default: 3px; |
|||
--td-radius-medium: 6px; |
|||
--td-radius-large: 9px; |
|||
--td-radius-extraLarge: 12px; |
|||
--td-radius-round: 999px; |
|||
--td-radius-circle: 50%; |
|||
/* end 圆角配置 *//* 阴影配置 */ |
|||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12); |
|||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1); |
|||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08); |
|||
/* end 阴影配置 *//* 尺寸配置 */ |
|||
--td-size-1: 2px; |
|||
--td-size-2: 4px; |
|||
--td-size-3: 6px; |
|||
--td-size-4: 8px; |
|||
--td-size-5: 12px; |
|||
--td-size-6: 16px; |
|||
--td-size-7: 20px; |
|||
--td-size-8: 24px; |
|||
--td-size-9: 28px; |
|||
--td-size-10: 32px; |
|||
--td-size-11: 36px; |
|||
--td-size-12: 40px; |
|||
--td-size-13: 48px; |
|||
--td-size-14: 56px; |
|||
--td-size-15: 64px; |
|||
--td-size-16: 72px; |
|||
--td-comp-size-xxxs: var(--td-size-6); |
|||
--td-comp-size-xxs: var(--td-size-7); |
|||
--td-comp-size-xs: var(--td-size-8); |
|||
--td-comp-size-s: var(--td-size-9); |
|||
--td-comp-size-m: var(--td-size-10); |
|||
--td-comp-size-l: var(--td-size-11); |
|||
--td-comp-size-xl: var(--td-size-12); |
|||
--td-comp-size-xxl: var(--td-size-13); |
|||
--td-comp-size-xxxl: var(--td-size-14); |
|||
--td-comp-size-xxxxl: var(--td-size-15); |
|||
--td-comp-size-xxxxxl: var(--td-size-16); |
|||
--td-pop-padding-s: var(--td-size-2); |
|||
--td-pop-padding-m: var(--td-size-3); |
|||
--td-pop-padding-l: var(--td-size-4); |
|||
--td-pop-padding-xl: var(--td-size-5); |
|||
--td-pop-padding-xxl: var(--td-size-6); |
|||
--td-comp-paddingLR-xxs: var(--td-size-1); |
|||
--td-comp-paddingLR-xs: var(--td-size-2); |
|||
--td-comp-paddingLR-s: var(--td-size-4); |
|||
--td-comp-paddingLR-m: var(--td-size-5); |
|||
--td-comp-paddingLR-l: var(--td-size-6); |
|||
--td-comp-paddingLR-xl: var(--td-size-8); |
|||
--td-comp-paddingLR-xxl: var(--td-size-10); |
|||
--td-comp-paddingTB-xxs: var(--td-size-1); |
|||
--td-comp-paddingTB-xs: var(--td-size-2); |
|||
--td-comp-paddingTB-s: var(--td-size-4); |
|||
--td-comp-paddingTB-m: var(--td-size-5); |
|||
--td-comp-paddingTB-l: var(--td-size-6); |
|||
--td-comp-paddingTB-xl: var(--td-size-8); |
|||
--td-comp-paddingTB-xxl: var(--td-size-10); |
|||
--td-comp-margin-xxs: var(--td-size-1); |
|||
--td-comp-margin-xs: var(--td-size-2); |
|||
--td-comp-margin-s: var(--td-size-4); |
|||
--td-comp-margin-m: var(--td-size-5); |
|||
--td-comp-margin-l: var(--td-size-6); |
|||
--td-comp-margin-xl: var(--td-size-7); |
|||
--td-comp-margin-xxl: var(--td-size-8); |
|||
--td-comp-margin-xxxl: var(--td-size-10); |
|||
--td-comp-margin-xxxxl: var(--td-size-12); |
|||
/* end 尺寸配置 */ |
|||
} |
|||
|
|||
:root[theme-mode="dark"] { |
|||
--brand-main: var(--td-brand-color-6); |
|||
--td-brand-color-light: var(--td-brand-color-1); |
|||
--td-brand-color-focus: var(--td-brand-color-2); |
|||
--td-brand-color-disabled: var(--td-brand-color-3); |
|||
--td-brand-color-hover: var(--td-brand-color-5); |
|||
--td-brand-color: var(--td-brand-color-6); |
|||
--td-brand-color-active: var(--td-brand-color-7); |
|||
--td-brand-color-1: #0894fa20; |
|||
--td-brand-color-2: #00325c; |
|||
--td-brand-color-3: #004881; |
|||
--td-brand-color-4: #0060a8; |
|||
--td-brand-color-5: #007ad3; |
|||
--td-brand-color-6: #0894fa; |
|||
--td-brand-color-7: #6bb2ff; |
|||
--td-brand-color-8: #a3ccff; |
|||
--td-brand-color-9: #d1e4ff; |
|||
--td-brand-color-10: #eef4ff; |
|||
--td-warning-color-1: #4f2a1d; |
|||
--td-warning-color-2: #582f21; |
|||
--td-warning-color-3: #733c23; |
|||
--td-warning-color-4: #a75d2b; |
|||
--td-warning-color-5: #cf6e2d; |
|||
--td-warning-color-6: #dc7633; |
|||
--td-warning-color-7: #e8935c; |
|||
--td-warning-color-8: #ecbf91; |
|||
--td-warning-color-9: #eed7bf; |
|||
--td-warning-color-10: #f3e9dc; |
|||
--td-error-color-1: #472324; |
|||
--td-error-color-2: #5e2a2d; |
|||
--td-error-color-3: #703439; |
|||
--td-error-color-4: #83383e; |
|||
--td-error-color-5: #a03f46; |
|||
--td-error-color-6: #c64751; |
|||
--td-error-color-7: #de6670; |
|||
--td-error-color-8: #ec888e; |
|||
--td-error-color-9: #edb1b6; |
|||
--td-error-color-10: #eeced0; |
|||
--td-success-color-1: #193a2a; |
|||
--td-success-color-2: #1a4230; |
|||
--td-success-color-3: #17533d; |
|||
--td-success-color-4: #0d7a55; |
|||
--td-success-color-5: #059465; |
|||
--td-success-color-6: #43af8a; |
|||
--td-success-color-7: #46bf96; |
|||
--td-success-color-8: #80d2b6; |
|||
--td-success-color-9: #b4e1d3; |
|||
--td-success-color-10: #deede8; |
|||
--td-gray-color-1: #f3f3f3; |
|||
--td-gray-color-2: #eee; |
|||
--td-gray-color-3: #e7e7e7; |
|||
--td-gray-color-4: #dcdcdc; |
|||
--td-gray-color-5: #c5c5c5; |
|||
--td-gray-color-6: #a6a6a6; |
|||
--td-gray-color-7: #8b8b8b; |
|||
--td-gray-color-8: #777; |
|||
--td-gray-color-9: #5e5e5e; |
|||
--td-gray-color-10: #4b4b4b; |
|||
--td-gray-color-11: #383838; |
|||
--td-gray-color-12: #2c2c2c; |
|||
--td-gray-color-13: #242424; |
|||
--td-gray-color-14: #181818; |
|||
--td-bg-color-page: var(--td-gray-color-14); |
|||
--td-bg-color-container: var(--td-gray-color-13); |
|||
--td-bg-color-container-hover: var(--td-gray-color-12); |
|||
--td-bg-color-container-active: var(--td-gray-color-10); |
|||
--td-bg-color-container-select: var(--td-gray-color-9); |
|||
--td-bg-color-secondarycontainer: var(--td-gray-color-12); |
|||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11); |
|||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9); |
|||
--td-bg-color-component: var(--td-gray-color-11); |
|||
--td-bg-color-component-hover: var(--td-gray-color-10); |
|||
--td-bg-color-component-active: var(--td-gray-color-9); |
|||
--td-bg-color-component-disabled: var(--td-gray-color-12); |
|||
--td-component-stroke: var(--td-gray-color-11); |
|||
--td-component-border: var(--td-gray-color-9); |
|||
--td-font-white-1: rgba(255, 255, 255, 0.9); |
|||
--td-font-white-2: rgba(255, 255, 255, 0.55); |
|||
--td-font-white-3: rgba(255, 255, 255, 0.35); |
|||
--td-font-white-4: rgba(255, 255, 255, 0.22); |
|||
--td-font-gray-1: rgba(0, 0, 0, 0.9); |
|||
--td-font-gray-2: rgba(0, 0, 0, 0.6); |
|||
--td-font-gray-3: rgba(0, 0, 0, 0.4); |
|||
--td-font-gray-4: rgba(0, 0, 0, 0.26); |
|||
--td-gray-color-1: #f3f3f3; |
|||
--td-gray-color-2: #eee; |
|||
--td-gray-color-3: #e7e7e7; |
|||
--td-gray-color-4: #dcdcdc; |
|||
--td-gray-color-5: #c5c5c5; |
|||
--td-gray-color-6: #a6a6a6; |
|||
--td-gray-color-7: #8b8b8b; |
|||
--td-gray-color-8: #777; |
|||
--td-gray-color-9: #5e5e5e; |
|||
--td-gray-color-10: #4b4b4b; |
|||
--td-gray-color-11: #383838; |
|||
--td-gray-color-12: #2c2c2c; |
|||
--td-gray-color-13: #242424; |
|||
--td-gray-color-14: #181818; |
|||
--td-bg-color-page: var(--td-gray-color-14); |
|||
--td-bg-color-container: var(--td-gray-color-13); |
|||
--td-bg-color-container-hover: var(--td-gray-color-12); |
|||
--td-bg-color-container-active: var(--td-gray-color-10); |
|||
--td-bg-color-container-select: var(--td-gray-color-9); |
|||
--td-bg-color-secondarycontainer: var(--td-gray-color-12); |
|||
--td-bg-color-secondarycontainer-hover: var(--td-gray-color-11); |
|||
--td-bg-color-secondarycontainer-active: var(--td-gray-color-9); |
|||
--td-bg-color-component: var(--td-gray-color-11); |
|||
--td-bg-color-component-hover: var(--td-gray-color-10); |
|||
--td-bg-color-component-active: var(--td-gray-color-9); |
|||
--td-bg-color-secondarycomponent: var(--td-gray-color-10); |
|||
--td-bg-color-secondarycomponent-hover: var(--td-gray-color-9); |
|||
--td-bg-color-secondarycomponent-active: var(--td-gray-color-8); |
|||
--td-bg-color-component-disabled: var(--td-gray-color-12); |
|||
--td-component-stroke: var(--td-gray-color-11); |
|||
--td-component-border: var(--td-gray-color-9); |
|||
--td-font-white-1: rgba(255, 255, 255, 0.9); |
|||
--td-font-white-2: rgba(255, 255, 255, 0.55); |
|||
--td-font-white-3: rgba(255, 255, 255, 0.35); |
|||
--td-font-white-4: rgba(255, 255, 255, 0.22); |
|||
--td-font-gray-1: rgba(0, 0, 0, 0.9); |
|||
--td-font-gray-2: rgba(0, 0, 0, 0.6); |
|||
--td-font-gray-3: rgba(0, 0, 0, 0.4); |
|||
--td-font-gray-4: rgba(0, 0, 0, 0.26); |
|||
--td-text-color-primary: var(--td-font-white-1); |
|||
--td-text-color-secondary: var(--td-font-white-2); |
|||
--td-text-color-placeholder: var(--td-font-white-3); |
|||
--td-text-color-disabled: var(--td-font-white-4); |
|||
--td-text-color-anti: #fff; |
|||
--td-text-color-brand: var(--td-brand-color); |
|||
--td-text-color-link: var(--td-brand-color); |
|||
--td-table-shadow-color: rgba(0, 0, 0, 55%); |
|||
--td-scrollbar-color: rgba(255, 255, 255, 10%); |
|||
--td-scrollbar-hover-color: rgba(255, 255, 255, 30%); |
|||
--td-scroll-track-color: #333; |
|||
--td-bg-color-specialcomponent: transparent; |
|||
--td-border-level-1-color: var(--td-gray-color-11); |
|||
--td-border-level-2-color: var(--td-gray-color-9); |
|||
--td-mask-active: rgba(0, 0, 0, 0.4); |
|||
--td-mask-disabled: rgba(0, 0, 0, 0.6); |
|||
--td-shadow-inset-top: inset 0 0.5px 0 #5e5e5e; |
|||
--td-shadow-inset-right: inset 0.5px 0 0 #5e5e5e; |
|||
--td-shadow-inset-bottom: inset 0 -0.5px 0 #5e5e5e; |
|||
--td-shadow-inset-left: inset -0.5px 0 0 #5e5e5e; |
|||
/* 圆角配置 */ |
|||
--td-radius-small: 2px; |
|||
--td-radius-default: 3px; |
|||
--td-radius-medium: 6px; |
|||
--td-radius-large: 9px; |
|||
--td-radius-extraLarge: 12px; |
|||
--td-radius-round: 999px; |
|||
--td-radius-circle: 50%; |
|||
/* end 圆角配置 *//* 阴影配置 */ |
|||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12); |
|||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1); |
|||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.05), 0 16px 24px 2px rgba(0, 0, 0, 0.04), 0 8px 10px -5px rgba(0, 0, 0, 0.08); |
|||
/* end 阴影配置 *//* 尺寸配置 */ |
|||
--td-size-1: 2px; |
|||
--td-size-2: 4px; |
|||
--td-size-3: 6px; |
|||
--td-size-4: 8px; |
|||
--td-size-5: 12px; |
|||
--td-size-6: 16px; |
|||
--td-size-7: 20px; |
|||
--td-size-8: 24px; |
|||
--td-size-9: 28px; |
|||
--td-size-10: 32px; |
|||
--td-size-11: 36px; |
|||
--td-size-12: 40px; |
|||
--td-size-13: 48px; |
|||
--td-size-14: 56px; |
|||
--td-size-15: 64px; |
|||
--td-size-16: 72px; |
|||
--td-comp-size-xxxs: var(--td-size-6); |
|||
--td-comp-size-xxs: var(--td-size-7); |
|||
--td-comp-size-xs: var(--td-size-8); |
|||
--td-comp-size-s: var(--td-size-9); |
|||
--td-comp-size-m: var(--td-size-10); |
|||
--td-comp-size-l: var(--td-size-11); |
|||
--td-comp-size-xl: var(--td-size-12); |
|||
--td-comp-size-xxl: var(--td-size-13); |
|||
--td-comp-size-xxxl: var(--td-size-14); |
|||
--td-comp-size-xxxxl: var(--td-size-15); |
|||
--td-comp-size-xxxxxl: var(--td-size-16); |
|||
--td-pop-padding-s: var(--td-size-2); |
|||
--td-pop-padding-m: var(--td-size-3); |
|||
--td-pop-padding-l: var(--td-size-4); |
|||
--td-pop-padding-xl: var(--td-size-5); |
|||
--td-pop-padding-xxl: var(--td-size-6); |
|||
--td-comp-paddingLR-xxs: var(--td-size-1); |
|||
--td-comp-paddingLR-xs: var(--td-size-2); |
|||
--td-comp-paddingLR-s: var(--td-size-4); |
|||
--td-comp-paddingLR-m: var(--td-size-5); |
|||
--td-comp-paddingLR-l: var(--td-size-6); |
|||
--td-comp-paddingLR-xl: var(--td-size-8); |
|||
--td-comp-paddingLR-xxl: var(--td-size-10); |
|||
--td-comp-paddingTB-xxs: var(--td-size-1); |
|||
--td-comp-paddingTB-xs: var(--td-size-2); |
|||
--td-comp-paddingTB-s: var(--td-size-4); |
|||
--td-comp-paddingTB-m: var(--td-size-5); |
|||
--td-comp-paddingTB-l: var(--td-size-6); |
|||
--td-comp-paddingTB-xl: var(--td-size-8); |
|||
--td-comp-paddingTB-xxl: var(--td-size-10); |
|||
--td-comp-margin-xxs: var(--td-size-1); |
|||
--td-comp-margin-xs: var(--td-size-2); |
|||
--td-comp-margin-s: var(--td-size-4); |
|||
--td-comp-margin-m: var(--td-size-5); |
|||
--td-comp-margin-l: var(--td-size-6); |
|||
--td-comp-margin-xl: var(--td-size-7); |
|||
--td-comp-margin-xxl: var(--td-size-8); |
|||
--td-comp-margin-xxxl: var(--td-size-10); |
|||
--td-comp-margin-xxxxl: var(--td-size-12); |
|||
/* end 尺寸配置 */ |
|||
} |
@ -0,0 +1,226 @@ |
|||
<script setup> |
|||
import { OverlayScrollbars } from 'overlayscrollbars'; |
|||
import { computed } from 'vue'; |
|||
|
|||
const props = defineProps({ |
|||
list: { |
|||
type: Array, |
|||
default: () => [], |
|||
}, |
|||
}); |
|||
|
|||
let scrollBar; |
|||
const vScroll = { |
|||
mounted: (el) => { |
|||
scrollBar = OverlayScrollbars(el, { |
|||
paddingAbsolute: true, |
|||
overflow: { |
|||
x: 'hidden', |
|||
y: 'scroll', |
|||
}, |
|||
scrollbars: { |
|||
theme: 'os-theme-light', |
|||
autoHide: 'leave', |
|||
autoHideDelay: 300, |
|||
}, |
|||
}); |
|||
}, |
|||
}; |
|||
function onScrollTo(direction = 'down') { |
|||
const { scrollOffsetElement } = scrollBar.elements(); |
|||
let top; |
|||
if (direction === 'down') { |
|||
top = scrollOffsetElement.scrollTop + 300; |
|||
} |
|||
if (direction === 'up') { |
|||
top = scrollOffsetElement.scrollTop - 300; |
|||
} |
|||
scrollOffsetElement.scrollTo({ |
|||
behavior: 'smooth', |
|||
top, |
|||
}); |
|||
} |
|||
|
|||
const urlList = computed(() => props.list.map(({ url }) => url)); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root"> |
|||
<div class="page-turning-left" @click="onScrollTo('up')"> |
|||
<t-icon name="chevron-up" /> |
|||
</div> |
|||
<div class="container" v-scroll> |
|||
<div class="custom-space"> |
|||
<template v-for="(item, index) in list" :key="item.id"> |
|||
<t-image-viewer |
|||
:images="urlList" |
|||
:default-index="index" |
|||
> |
|||
<template #trigger="{ open: onPreview }"> |
|||
<div class="image-box"> |
|||
<t-image |
|||
shape="round" |
|||
fit="cover" |
|||
:style="{ 'width': '20vh', height: '15vh' }" |
|||
:src="item.url" |
|||
error="" |
|||
> |
|||
<template #overlay-content> |
|||
<div class="overlay-container"> |
|||
<div class="img-type"> |
|||
{{ item.name }} |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image> |
|||
<div class="image-hover" @click="onPreview"> |
|||
<span><t-icon name="browse" size="1.4em" /> 预览</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image-viewer> |
|||
</template> |
|||
|
|||
<template v-if="!list.length"> |
|||
<t-image |
|||
shape="round" |
|||
fit="cover" |
|||
:style="{ 'width': '20vh', height: '15vh' }" |
|||
src="" |
|||
error="" |
|||
> |
|||
<template #overlay-content> |
|||
<div class="overlay-container"> |
|||
<div class="img-type"> |
|||
没有上传图片哦 ~ |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
<div class="page-turning-right" @click="onScrollTo('down')"> |
|||
<t-icon name="chevron-down" /> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
|
|||
:global { |
|||
.page-turning-left { |
|||
background-color: fade(black, 35%); |
|||
padding: 0px 24px; |
|||
border-top-left-radius: 10px; |
|||
border-top-right-radius: 10px; |
|||
margin-bottom: 2px; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
background-color: fade(black, 55%); |
|||
} |
|||
} |
|||
|
|||
.page-turning-right { |
|||
background-color: fade(black, 35%); |
|||
padding: 0px 24px; |
|||
border-bottom-left-radius: 10px; |
|||
border-bottom-right-radius: 10px; |
|||
margin-top: 2px; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
background-color: fade(black, 55%); |
|||
} |
|||
} |
|||
|
|||
.container { |
|||
flex: 1; |
|||
background-color: fade(black, 35%); |
|||
padding: var(--td-comp-paddingTB-s); |
|||
border-radius: var(--td-radius-large); |
|||
overflow: hidden; |
|||
|
|||
.custom-space { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: var(--td-comp-margin-l); |
|||
|
|||
.image-box { |
|||
flex-shrink: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.overlay-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
|
|||
.img-quantity { |
|||
position: absolute; |
|||
top: 5px; |
|||
right: 5px; |
|||
background-color: fade(black, 60%); |
|||
padding: 2px 10px; |
|||
border-radius: 40px; |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 12px; |
|||
|
|||
.text { |
|||
margin-left: 5px; |
|||
} |
|||
} |
|||
|
|||
.img-type { |
|||
position: absolute; |
|||
bottom: 5px; |
|||
left: 5px; |
|||
background-color: fade(black, 60%); |
|||
padding: 2px 15px; |
|||
border-radius: 40px; |
|||
font-size: 12px; |
|||
} |
|||
} |
|||
|
|||
.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,178 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref } from 'vue'; |
|||
import { REQUEST_UPLOAD_FILE } from '@/config/urls'; |
|||
import auth from '@/utils/auth'; |
|||
// import { storeToRefs } from 'pinia'; |
|||
import { useMediaStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
|
|||
const mediaStore = useMediaStore(); |
|||
const { getMediaList, createMedia, deleteMedia } = mediaStore; |
|||
|
|||
const sortieId = ref(); |
|||
const visible = ref(false); |
|||
|
|||
const files = ref([]); |
|||
function init() { |
|||
if (!sortieId.value) return; |
|||
getMediaList(sortieId.value).then(({ data }) => { |
|||
(data || []).forEach(item => { |
|||
const temp = item; |
|||
temp.status = 'success'; |
|||
temp.raw = {}; |
|||
}); |
|||
files.value = [...(data || []), ...files.value]; |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
const mediaList = ref([]); |
|||
function loadList() { |
|||
if (!sortieId.value) return; |
|||
getMediaList(sortieId.value).then(({ data }) => { |
|||
mediaList.value = [...(data || [])]; |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
function onCancel() { |
|||
files.value = []; |
|||
mediaList.value = []; |
|||
} |
|||
|
|||
const subject = ref('image'); |
|||
|
|||
const ABRIDGE_NAME = [10, 7]; |
|||
|
|||
const formatResponse = (res) => { |
|||
if (!res) { |
|||
return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' }; |
|||
} |
|||
return res; |
|||
}; |
|||
|
|||
function beforeUpload(UploadFile) { |
|||
const { type = 'image' } = UploadFile || {}; |
|||
if (type.includes('image')) { |
|||
subject.value = 'image'; |
|||
} |
|||
if (type.includes('video')) { |
|||
subject.value = 'video'; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
const handleSuccess = ({ currentFiles = [], response = [] } = {}) => { |
|||
console.log('aaa', currentFiles, response); |
|||
const medias = currentFiles.map(({ name, size, type, status, response: { code, data } }) => { |
|||
if (status !== 'success') return null; |
|||
if (code !== 200) return null; |
|||
let typeInt; |
|||
if (type.includes('image')) { |
|||
typeInt = 1; |
|||
} |
|||
if (type.includes('video')) { |
|||
typeInt = 2; |
|||
} |
|||
return { |
|||
name, |
|||
size, |
|||
type: typeInt, |
|||
url: data, |
|||
}; |
|||
}).filter(Boolean); |
|||
// const { name, size, type } = currentFiles[0] || {}; |
|||
// const { code, data } = response[0] || {}; |
|||
// if (code !== 200) return; |
|||
// let typeInt; |
|||
// if (type.includes('image')) { |
|||
// typeInt = 1; |
|||
// } |
|||
// if (type.includes('video')) { |
|||
// typeInt = 2; |
|||
// } |
|||
const formData = { |
|||
id: sortieId.value, |
|||
medias, |
|||
}; |
|||
createMedia(formData).then(() => { |
|||
if (!visible.value) return; |
|||
loadList(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
}; |
|||
|
|||
const handleRemove = ({ file } = {}) => { |
|||
if (file.status !== 'success') return; |
|||
let mediaId; |
|||
|
|||
if (file.id) { |
|||
mediaId = file.id; |
|||
} else { |
|||
const { response: { data } } = file; |
|||
const media = mediaList.value.find(({ url }) => url === data); |
|||
mediaId = media?.id; |
|||
} |
|||
|
|||
if (!mediaId) return; |
|||
deleteMedia(mediaId).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
}; |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-media-manage', (row) => { |
|||
if (!row.id) return; |
|||
sortieId.value = row.id; |
|||
init(); |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-media-manage'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
width="fit-content" |
|||
:footer="null" |
|||
@closed="onCancel" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">媒体管理</div> |
|||
</template> |
|||
|
|||
<t-upload |
|||
v-model="files" |
|||
placeholder="支持上传图片、视频文件" |
|||
:action="REQUEST_UPLOAD_FILE(subject)" |
|||
:headers="{ |
|||
Authorization: `Bearer ${auth.getToken()}`, |
|||
}" |
|||
theme="file-flow" |
|||
multiple |
|||
:abridge-name="ABRIDGE_NAME" |
|||
auto-upload |
|||
show-thumbnail |
|||
allow-upload-duplicate-file |
|||
:format-response="formatResponse" |
|||
:before-upload="beforeUpload" |
|||
@success="handleSuccess" |
|||
@remove="handleRemove" |
|||
/> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
</style> |
@ -0,0 +1,173 @@ |
|||
<script setup> |
|||
import { computed, onMounted, onUnmounted, ref } from 'vue'; |
|||
// import { REQUEST_UPLOAD_FILE } from '@/config/urls'; |
|||
// import auth from '@/utils/auth'; |
|||
// import { storeToRefs } from 'pinia'; |
|||
import { useMediaStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
import ImageBar from '@/views/SortieView/components/ImageBar.vue'; |
|||
import VideoPlayer from '@/components/VideoPlayer.vue'; |
|||
|
|||
const mediaStore = useMediaStore(); |
|||
const { getMediaList } = mediaStore; |
|||
|
|||
const sortieId = ref(); |
|||
const visible = ref(false); |
|||
|
|||
// const files = ref([]); |
|||
// function init() { |
|||
// if (!sortieId.value) return; |
|||
// getMediaList(sortieId.value).then(({ data }) => { |
|||
// // data.forEach(item => { |
|||
// // const temp = item; |
|||
// // temp.status = 'success'; |
|||
// // temp.raw = {}; |
|||
// // }); |
|||
// // files.value = [...data, ...files.value]; |
|||
// }).catch(({ message }) => { |
|||
// if (message) MessagePlugin.error(message); |
|||
// }); |
|||
// } |
|||
|
|||
const mediaList = ref([]); |
|||
|
|||
const imageList = computed(() => mediaList.value.filter(({ type }) => type === 1)); |
|||
const videoList = computed(() => mediaList.value.filter(({ type }) => type === 2)); |
|||
|
|||
function loadList() { |
|||
if (!sortieId.value) return; |
|||
getMediaList(sortieId.value).then(({ data = [] } = {}) => { |
|||
mediaList.value = [...(data || [])]; |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
function onCancel() { |
|||
mediaList.value = []; |
|||
} |
|||
|
|||
// const subject = ref('image'); |
|||
|
|||
// const ABRIDGE_NAME = [10, 7]; |
|||
// |
|||
// const formatResponse = (res) => { |
|||
// if (!res) { |
|||
// return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' }; |
|||
// } |
|||
// return res; |
|||
// }; |
|||
// |
|||
// function beforeUpload(UploadFile) { |
|||
// const { type = 'image' } = UploadFile || {}; |
|||
// if (type.includes('image')) { |
|||
// subject.value = 'image'; |
|||
// } |
|||
// if (type.includes('video')) { |
|||
// subject.value = 'video'; |
|||
// } |
|||
// return true; |
|||
// } |
|||
|
|||
// const handleSuccess = ({ currentFiles = [], response = [] } = {}) => { |
|||
// const { name, size, type } = currentFiles[0] || {}; |
|||
// const { code, data } = response[0] || {}; |
|||
// if (code !== 200) return; |
|||
// let typeInt; |
|||
// if (type.includes('image')) { |
|||
// typeInt = 1; |
|||
// } |
|||
// if (type.includes('video')) { |
|||
// typeInt = 2; |
|||
// } |
|||
// const formData = { |
|||
// id: sortieId.value, |
|||
// medias: [{ |
|||
// name, |
|||
// size, |
|||
// type: typeInt, |
|||
// url: data, |
|||
// }], |
|||
// }; |
|||
// createMedia(formData).then(() => { |
|||
// loadList(); |
|||
// }).catch(({ message }) => { |
|||
// if (message) MessagePlugin.error(message); |
|||
// }); |
|||
// }; |
|||
|
|||
// const handleRemove = ({ file } = {}) => { |
|||
// if (file.status !== 'success') return; |
|||
// let mediaId; |
|||
// |
|||
// if (file.id) { |
|||
// mediaId = file.id; |
|||
// } else { |
|||
// const { response: { data } } = file; |
|||
// const media = mediaList.value.find(({ url }) => url === data); |
|||
// mediaId = media?.id; |
|||
// } |
|||
// |
|||
// if (!mediaId) return; |
|||
// deleteMedia(mediaId).catch(({ message }) => { |
|||
// if (message) MessagePlugin.error(message); |
|||
// }); |
|||
// }; |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-sortie-detail', (row) => { |
|||
if (!row.id) return; |
|||
sortieId.value = row.id; |
|||
loadList(); |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-sortie-detail'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
mode="full-screen" |
|||
:footer="null" |
|||
@closed="onCancel" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">查看媒体</div> |
|||
</template> |
|||
|
|||
<div class="container"> |
|||
<ImageBar :list="imageList" class="image-bar" /> |
|||
|
|||
<VideoPlayer :list="videoList" class="video-player" /> |
|||
</div> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
:global { |
|||
.container { |
|||
height: 100%; |
|||
display: flex; |
|||
|
|||
.video-player { |
|||
flex: 1; |
|||
margin-left: var(--td-comp-margin-s); |
|||
margin-top: var(--td-comp-margin-s); |
|||
border-radius: var(--td-radius-medium); |
|||
} |
|||
} |
|||
|
|||
.t-dialog__position_fullscreen { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,147 @@ |
|||
<script setup> |
|||
import BasePanel from '@/components/BasePanel.vue'; |
|||
import { computed, ref } from 'vue'; |
|||
import { storeToRefs } from 'pinia'; |
|||
import { useDeviceStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import DeviceEditor from '@/views/DeviceView/components/DeviceEditor.vue'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
import { formatTime, popularTime } from '@/utils/helpers'; |
|||
|
|||
const deviceStore = useDeviceStore(); |
|||
const { deviceList, deviceQueries, deviceExtra } = storeToRefs(deviceStore); |
|||
const { getDeviceList, updateDeviceState, deleteDevice } = deviceStore; |
|||
|
|||
function loadList(queries = {}) { |
|||
getDeviceList(queries).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
loadList({ page: 1, pageSize: 10, all: undefined, type: undefined, search: undefined }); |
|||
|
|||
function onPageChange({ current, pageSize }) { |
|||
loadList({ page: current, pageSize }); |
|||
} |
|||
|
|||
const search = ref(); |
|||
function onSearchList() { |
|||
loadList({ search: search.value }); |
|||
} |
|||
|
|||
function onResetList() { |
|||
search.value = undefined; |
|||
onSearchList(); |
|||
} |
|||
|
|||
const typeLabel = { 1: '抛投无人机', 2: '巡检无人机' }; |
|||
const columns = [ |
|||
{ colKey: 'serial-number', title: '序列号', ellipsis: true, width: 100 }, |
|||
{ colKey: 'sn', title: '飞控ID', ellipsis: true }, |
|||
{ colKey: 'manufacturerName', title: '制造商', ellipsis: true }, |
|||
{ colKey: 'type', title: '机型', ellipsis: true, cell: (_, { row }) => (typeLabel[row.type] || '-') }, |
|||
{ colKey: 'onLine', title: '是否在线', ellipsis: true }, |
|||
{ colKey: 'locked', title: '锁定状态', ellipsis: true }, |
|||
{ colKey: 'workTime', title: '工作时长', ellipsis: true, cell: (_, { row }) => popularTime(row.workTime) || '0s' }, |
|||
{ colKey: 'createdAt', title: '添加时间', ellipsis: true, cell: (_, { row }) => formatTime(row.createdAt * 1000), width: 200 }, |
|||
{ colKey: 'operation', title: '操作', width: '300px' }, |
|||
]; |
|||
|
|||
function onShowEditor(row = {}) { |
|||
eventBus.emit('show-device-editor', row); |
|||
} |
|||
|
|||
const pagination = computed(() => ({ |
|||
current: deviceQueries.value.page, |
|||
pageSize: deviceQueries.value.pageSize, |
|||
total: deviceExtra.value.total, |
|||
})); |
|||
|
|||
function onChangeState(row = {}) { |
|||
updateDeviceState(row, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('更改成功'); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
function onDeleteManufacturer(row = {}) { |
|||
deleteDevice(row.id, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('成功删除'); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<BasePanel> |
|||
<template #header> |
|||
<t-space> |
|||
<div class="vertical-line" /> |
|||
<div class="header-title">设备管理</div> |
|||
</t-space> |
|||
</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-button theme="default" @click="onResetList">重置</t-button> |
|||
</t-space> |
|||
</template> |
|||
|
|||
<t-table |
|||
class="table-custom" |
|||
row-key="id" |
|||
:data="deviceList" |
|||
:columns="columns" |
|||
cell-empty-content="-" |
|||
:pagination="pagination" |
|||
@page-change="onPageChange" |
|||
> |
|||
<template #onLine="{ row }"> |
|||
<t-tag theme="success" v-if="row.onLine">在线</t-tag> |
|||
<t-tag theme="danger" v-else>离线</t-tag> |
|||
</template> |
|||
|
|||
<template #locked="{ row }"> |
|||
<t-tag theme="success" v-if="!row.locked">未锁定</t-tag> |
|||
<t-tag theme="danger" v-else>已锁定</t-tag> |
|||
</template> |
|||
<template #operation="{ row }"> |
|||
<t-space align="center"> |
|||
<t-popconfirm :content="`确认${!row.locked ? '锁定' : '解锁'}吗?`" @confirm="onChangeState(row)"> |
|||
<t-button :theme="!row.locked ? 'danger' : 'success'">{{ !row.locked ? '锁定' : '解锁' }}</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-space> |
|||
</template> |
|||
</t-table> |
|||
</BasePanel> |
|||
|
|||
<DeviceEditor /> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
|
|||
.dropdown { |
|||
:global { |
|||
.t-dropdown__submenu ul { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,117 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref, computed } from 'vue'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import { useDeviceStore } from '@/stores'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
// import ImageUploader from '@/components/ImageUploader.vue'; |
|||
import ManufacturerSelector from '@/components/ManufacturerSelector.vue'; |
|||
import { update } from '@/utils/helpers'; |
|||
|
|||
const visible = ref(false); |
|||
|
|||
const deviceStore = useDeviceStore(); |
|||
const { createDevice, updateDevice } = deviceStore; |
|||
|
|||
const form = ref(); |
|||
const formData = ref({ |
|||
id: undefined, |
|||
manufacturerId: undefined, |
|||
sn: undefined, |
|||
type: 1, |
|||
}); |
|||
const FORM_RULES = { |
|||
sn: [{ required: true, message: '请输入飞控ID' }], |
|||
manufacturerId: [{ required: true, message: '请选择制造商' }], |
|||
type: [{ required: true, message: '请选择' }], |
|||
}; |
|||
|
|||
function onCancel() { |
|||
visible.value = false; |
|||
form.value.reset(); |
|||
} |
|||
|
|||
function onSubmit({ validateResult }) { |
|||
if (validateResult === true) { |
|||
if (formData.value.id) { |
|||
updateDevice(formData.value, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('更新成功'); |
|||
onCancel(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
return; |
|||
} |
|||
createDevice(formData.value, { refreshList: true }).then(() => { |
|||
MessagePlugin.success('创建成功'); |
|||
onCancel(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-device-editor', (row = {}) => { |
|||
if (row.id) { |
|||
formData.value = update(formData.value, row); |
|||
} |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-device-editor'); |
|||
}); |
|||
|
|||
const options = [{ value: 1, label: '抛投无人机' }, { value: 2, label: '巡检无人机' }]; |
|||
|
|||
const isEdit = computed(() => !!formData.value?.id); |
|||
</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;">{{ formData.id ? '更新' : '录入' }}设备</div> |
|||
</template> |
|||
<t-form |
|||
ref="form" |
|||
:data="formData" |
|||
:rules="FORM_RULES" |
|||
colon |
|||
@submit="onSubmit" |
|||
> |
|||
<t-form-item name="id" v-show="false" /> |
|||
|
|||
<t-form-item label="飞控ID" name="sn"> |
|||
<t-input v-model="formData.sn" :disabled="isEdit" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="制造商" name="manufacturerId"> |
|||
<ManufacturerSelector v-model="formData.manufacturerId" /> |
|||
</t-form-item> |
|||
|
|||
<t-form-item label="机型" name="type"> |
|||
<t-select v-model="formData.type" :options="options" placeholder="请选择机型" /> |
|||
</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,105 @@ |
|||
<script setup> |
|||
// import { mapState, mapGetters, mapActions } from 'vuex'; |
|||
// import _throttle from 'lodash.throttle'; |
|||
// import { getAddressLabel } from '../../../utils/cascadeDistrict'; |
|||
// import deviceTypes from '../../../config/deviceTypes'; |
|||
// import eventBus from '@/utils/eventBus'; |
|||
import { onMounted, onUnmounted } from 'vue'; |
|||
|
|||
// const detail = ref({}); |
|||
|
|||
// const dynamicInfo = computed(() => { |
|||
// // eslint-disable-next-line no-shadow |
|||
// const { dynamicInfo } = detail.value; |
|||
// return dynamicInfo || {}; |
|||
// }); |
|||
|
|||
// const baseInfo = computed(() => { |
|||
// // eslint-disable-next-line no-shadow |
|||
// const { baseInfo } = detail.value; |
|||
// return baseInfo || {}; |
|||
// }); |
|||
|
|||
// const isVisible = ref(false); |
|||
|
|||
onMounted(() => { |
|||
// |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
// |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root"> |
|||
<t-space align="center"> |
|||
<t-divider layout="vertical" /> |
|||
<t-statistic title="无人机总数" :value="123456" prefix="$" unit="个" color="blue"> |
|||
<template #prefix> |
|||
<t-icon name="data-base" /> |
|||
</template> |
|||
</t-statistic> |
|||
<t-divider layout="vertical" /> |
|||
<t-statistic title="在线数量" :value="123456" prefix="$" unit="个" color="orange"> |
|||
<template #prefix> |
|||
<t-icon name="gps" /> |
|||
</template> |
|||
</t-statistic> |
|||
<t-divider layout="vertical" /> |
|||
<t-statistic title="总架次时长" :value="123456" prefix="$" unit="h" color="green"> |
|||
<template #prefix> |
|||
<t-icon name="time" /> |
|||
</template> |
|||
</t-statistic> |
|||
<t-divider layout="vertical" /> |
|||
<t-statistic title="总架次数" :value="123456" prefix="$" unit="次" color="red"> |
|||
<template #prefix> |
|||
<t-icon name="analytics" /> |
|||
</template> |
|||
</t-statistic> |
|||
<t-divider layout="vertical" /> |
|||
</t-space> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
//display: flex; |
|||
//justify-content: flex-end; |
|||
position: absolute; |
|||
top: 10px; |
|||
right: 10px; |
|||
z-index: 1; |
|||
//width: 35vh; |
|||
//height: 200px; |
|||
background-color: fade(#000, 60%); |
|||
//border-radius: 4px; |
|||
//overflow: hidden; |
|||
//transition: right .2s ease; |
|||
//pointer-events: none; |
|||
box-sizing: border-box; |
|||
border: 10px solid transparent; |
|||
border-image: url("../../../assets/bg.svg"); |
|||
border-image-repeat: stretch; |
|||
border-image-slice: 10; |
|||
|
|||
box-shadow: 0 0 10px 0 var(--td-brand-color); |
|||
//&:global::after { |
|||
// content: ''; |
|||
// position: absolute; |
|||
// top: 0; |
|||
// bottom: 0; |
|||
// right: 0; |
|||
// left: 0; |
|||
// //background: url("../../../assets/bg.svg") no-repeat scroll center bottom transparent; |
|||
//} |
|||
|
|||
:global { |
|||
// |
|||
.t-statistic-title { |
|||
color: fade(#eee, 60%); |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,303 @@ |
|||
<script setup> |
|||
// import { mapState, mapGetters, mapActions } from 'vuex'; |
|||
// import _throttle from 'lodash.throttle'; |
|||
// import { getAddressLabel } from '../../../utils/cascadeDistrict'; |
|||
// import deviceTypes from '../../../config/deviceTypes'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
import { computed, onMounted, onUnmounted, ref } from 'vue'; |
|||
|
|||
const detail = ref({}); |
|||
|
|||
const dynamicInfo = computed(() => { |
|||
// eslint-disable-next-line no-shadow |
|||
const { dynamicInfo } = detail.value; |
|||
return dynamicInfo || {}; |
|||
}); |
|||
|
|||
const baseInfo = computed(() => { |
|||
// eslint-disable-next-line no-shadow |
|||
const { baseInfo } = detail.value; |
|||
return baseInfo || {}; |
|||
}); |
|||
|
|||
const isVisible = ref(false); |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-real-time-plane', (data = {}) => { |
|||
detail.value = data; |
|||
isVisible.value = true; |
|||
console.log(isVisible.value); |
|||
}); |
|||
eventBus.on('hide-all-panels', () => { |
|||
isVisible.value = false; |
|||
console.log(isVisible.value); |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-real-time-plane'); |
|||
eventBus.off('hide-all-panels'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root" v-if="isVisible"> |
|||
<div class="columns"> |
|||
<div class="column custom1" v-if="dynamicInfo.online"> |
|||
<div class="columns is-mobile is-gapless is-marginless ui-own-title"> |
|||
<div class="column">实时动态信息</div> |
|||
<div class="column is-narrow" /> |
|||
</div> |
|||
<table class="table" v-if="baseInfo.recorderType !== 2"> |
|||
<colgroup> |
|||
<col width="150"> |
|||
<col width="200"> |
|||
</colgroup> |
|||
<tr> |
|||
<th>农机状态</th> |
|||
<td>{{ dynamicInfo.landStatus ? '-' : '作业中' }}</td> |
|||
</tr> |
|||
<tr> |
|||
<th>当前位置</th> |
|||
<td /> |
|||
</tr> |
|||
<!-- <tr>--> |
|||
<!-- <th>卫星数</th>--> |
|||
<!-- <td>{{ dynamicInfo.satelliteCount }}</td>--> |
|||
<!-- </tr>--> |
|||
<tr> |
|||
<th>定位精度</th> |
|||
<td>{{ }}</td> |
|||
</tr> |
|||
<tr> |
|||
<th>经纬度</th> |
|||
<!-- <td>{{ dynamicInfo.lng }}, {{ dynamicInfo.lat }}</td>--> |
|||
</tr> |
|||
<!-- <tr>--> |
|||
<!-- <th>海拔高度</th>--> |
|||
<!-- <td>{{ dynamicInfo.altitude }}m</td>--> |
|||
<!-- </tr>--> |
|||
<tr> |
|||
<th>对地高度</th> |
|||
<td>{{ dynamicInfo.height }}m</td> |
|||
</tr> |
|||
<tr> |
|||
<th>水平速度</th> |
|||
<td>{{ dynamicInfo.xspeed }}m/s</td> |
|||
</tr> |
|||
<!-- <tr>--> |
|||
<!-- <th>垂直速度</th>--> |
|||
<!-- <td>{{ dynamicInfo.yspeed }}m/s</td>--> |
|||
<!-- </tr>--> |
|||
<!-- <tr>--> |
|||
<!-- <th>航向角</th>--> |
|||
<!-- <td>{{ dynamicInfo.mha }}°</td>--> |
|||
<!-- </tr>--> |
|||
<!-- <tr>--> |
|||
<!-- <th>俯仰角</th>--> |
|||
<!-- <td>{{ dynamicInfo.pa }}°</td>--> |
|||
<!-- </tr>--> |
|||
<!-- <tr>--> |
|||
<!-- <th>横滚角</th>--> |
|||
<!-- <td>{{ dynamicInfo.ra }}°</td>--> |
|||
<!-- </tr>--> |
|||
<tr> |
|||
<th>喷洒流速</th> |
|||
<td>{{ dynamicInfo.flowSpeed }}L/min</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
<div class="column custom2"> |
|||
<div class="columns ui-own-title"> |
|||
<div class="column">基本信息</div> |
|||
<div class="column" v-if="false"> |
|||
<b>违章记录</b> |
|||
</div> |
|||
</div> |
|||
<table class="table"> |
|||
<colgroup> |
|||
<col width="120"> |
|||
<col width="200"> |
|||
</colgroup> |
|||
<tr> |
|||
<th>农机ID</th> |
|||
<td>{{ baseInfo.droneId }}</td> |
|||
</tr> |
|||
<tr> |
|||
<th>农机类型</th> |
|||
<td>{{ baseInfo.deviceType }}</td> |
|||
</tr> |
|||
<tr> |
|||
<th>制造商</th> |
|||
<td>{{ baseInfo.zzAccountName }}</td> |
|||
</tr> |
|||
<tr> |
|||
<th>制造商</th> |
|||
<td>{{ baseInfo.zzAccountName }}</td> |
|||
</tr> |
|||
<tr> |
|||
<th>制造商</th> |
|||
<td>{{ baseInfo.zzAccountName }}</td> |
|||
</tr> |
|||
<tr v-if="false"> |
|||
<th>机型</th> |
|||
<td>{{ baseInfo.modelName }}</td> |
|||
</tr> |
|||
<tr v-if="false"> |
|||
<th>运营人</th> |
|||
<td>{{ baseInfo.owner }}</td> |
|||
</tr> |
|||
<tr v-if="false"> |
|||
<th>实名登记标识</th> |
|||
<td>{{ baseInfo.regMark }}</td> |
|||
</tr> |
|||
</table> |
|||
<div class="columns ui-own-title" v-if="false"> |
|||
<div class="column">动态统计信息</div> |
|||
<div class="column"> |
|||
<b>历史作业</b> |
|||
</div> |
|||
</div> |
|||
<table class="table" v-if="false"> |
|||
<colgroup> |
|||
<col width="120"> |
|||
<col width="150"> |
|||
</colgroup> |
|||
<tr> |
|||
<th>总作业时长</th> |
|||
<td>{{ dynamicInfo.flyTotalDuration }}h</td> |
|||
</tr> |
|||
<tr> |
|||
<th>总作业里程</th> |
|||
<td>{{ dynamicInfo.flyTotalMileage }}km</td> |
|||
</tr> |
|||
<tr> |
|||
<th>总作业亩数</th> |
|||
<td>10000亩</td> |
|||
</tr> |
|||
<tr> |
|||
<th>飞控总亩数</th> |
|||
<td>10000亩</td> |
|||
</tr> |
|||
<tr> |
|||
<th>总喷洒量</th> |
|||
<td>{{ dynamicInfo.sparyTotalAmount }}L</td> |
|||
</tr> |
|||
<tr> |
|||
<th>总作业架次</th> |
|||
<td>{{ dynamicInfo.flySeqCount }}</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
//display: flex; |
|||
//justify-content: flex-end; |
|||
position: absolute; |
|||
top: 106px; |
|||
right: 10px; |
|||
z-index: 1; |
|||
width: 35vh; |
|||
//height: 200px; |
|||
//background-color: fade(#000, 20%); |
|||
//border-radius: 4px; |
|||
//overflow: hidden; |
|||
//transition: right .2s ease; |
|||
//pointer-events: none; |
|||
|
|||
//background-image: url("../../../assets/map-tan-1.png"); |
|||
//background-repeat: no-repeat; |
|||
//background-size: contain; |
|||
//background-size: 100% 100%; |
|||
|
|||
//box-sizing: border-box; |
|||
//border: 10px solid transparent; |
|||
//border-image: url("../../../assets/bg.svg"); |
|||
//border-image-repeat: stretch; |
|||
//border-image-slice: 10; |
|||
// |
|||
//box-shadow: 0 0 10px 0 var(--td-brand-color); |
|||
|
|||
//&:global::after { |
|||
// content: ''; |
|||
// position: absolute; |
|||
// top: 0; |
|||
// bottom: 0; |
|||
// right: 0; |
|||
// left: 0; |
|||
// //background: url("../../../assets/bg.svg") no-repeat scroll center bottom transparent; |
|||
//} |
|||
|
|||
:global { |
|||
//.demo-item { |
|||
// width: 100px; |
|||
// height: 100px; |
|||
// background-color: red; |
|||
//} |
|||
|
|||
.columns { |
|||
.custom1 { |
|||
min-height: 232px; |
|||
background-image: url("../../../assets/map-tan-1-1.png"); |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
background-size: 100% 100%; |
|||
} |
|||
|
|||
.custom2 { |
|||
min-height: 246px; |
|||
margin-top: -1px; |
|||
background-image: url("../../../assets/map-tan-1-2.png"); |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
background-size: 100% 100%; |
|||
} |
|||
} |
|||
|
|||
th { |
|||
color: #27e7dc; |
|||
} |
|||
|
|||
td { |
|||
color: white; |
|||
} |
|||
|
|||
.column { |
|||
pointer-events: auto; |
|||
} |
|||
|
|||
.ui-own-title { |
|||
//background-color: var(--td-brand-color); |
|||
color: var(--td-text-color-anti); |
|||
padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-xl) 0 var(--td-comp-paddingLR-xl); |
|||
line-height: 1; |
|||
font-weight: bold; |
|||
font-size: var(--td-font-size-title-medium); |
|||
//.column:first-child:before { |
|||
// content: ''; |
|||
// display: inline-block; |
|||
// width: 3px; |
|||
// height: 100%; |
|||
// background-color: #fff; |
|||
// margin-right: 5px; |
|||
// vertical-align: -3px; |
|||
//} |
|||
//b { |
|||
// font-weight: normal; |
|||
// border-bottom: 1px solid; |
|||
// cursor: pointer; |
|||
//} |
|||
} |
|||
.table { |
|||
margin-bottom: 0; |
|||
font-size: 14px; |
|||
|
|||
padding: 10px; |
|||
} |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,229 @@ |
|||
<script setup> |
|||
import { OverlayScrollbars } from 'overlayscrollbars'; |
|||
import { computed } from 'vue'; |
|||
|
|||
const props = defineProps({ |
|||
list: { |
|||
type: Array, |
|||
default: () => [], |
|||
}, |
|||
}); |
|||
|
|||
// let scrollBar; |
|||
const vScroll = { |
|||
mounted: (el) => { |
|||
// scrollBar = OverlayScrollbars(el, { |
|||
OverlayScrollbars(el, { |
|||
paddingAbsolute: true, |
|||
overflow: { |
|||
x: 'hidden', |
|||
y: 'scroll', |
|||
}, |
|||
scrollbars: { |
|||
theme: 'os-theme-light', |
|||
autoHide: 'leave', |
|||
autoHideDelay: 300, |
|||
}, |
|||
}); |
|||
}, |
|||
}; |
|||
// function onScrollTo(direction = 'down') { |
|||
// const { scrollOffsetElement } = scrollBar.elements(); |
|||
// let top; |
|||
// if (direction === 'down') { |
|||
// top = scrollOffsetElement.scrollTop + 300; |
|||
// } |
|||
// if (direction === 'up') { |
|||
// top = scrollOffsetElement.scrollTop - 300; |
|||
// } |
|||
// scrollOffsetElement.scrollTo({ |
|||
// behavior: 'smooth', |
|||
// top, |
|||
// }); |
|||
// } |
|||
|
|||
const urlList = computed(() => props.list.map(({ url }) => url)); |
|||
</script> |
|||
|
|||
<template> |
|||
<div :class="s.root"> |
|||
<!-- <div class="page-turning-left" @click="onScrollTo('up')">--> |
|||
<!-- <t-icon name="chevron-up" />--> |
|||
<!-- </div>--> |
|||
<div class="container" v-scroll> |
|||
<div class="custom-space"> |
|||
<template v-for="(item, index) in list" :key="item.id"> |
|||
<t-image-viewer |
|||
:images="urlList" |
|||
:default-index="index" |
|||
> |
|||
<template #trigger="{ open: onPreview }"> |
|||
<div class="image-box"> |
|||
<t-image |
|||
shape="round" |
|||
fit="cover" |
|||
:style="{ 'width': '20vh', height: '15vh' }" |
|||
:src="item.url" |
|||
error="" |
|||
> |
|||
<template #overlay-content> |
|||
<div class="overlay-container"> |
|||
<div class="img-type"> |
|||
{{ item.name }} |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image> |
|||
<div class="image-hover" @click="onPreview"> |
|||
<span><t-icon name="browse" size="1.4em" /> 预览</span> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image-viewer> |
|||
</template> |
|||
|
|||
<template v-if="!list.length"> |
|||
<t-image |
|||
shape="round" |
|||
fit="cover" |
|||
:style="{ 'width': '20vh', height: '15vh' }" |
|||
src="" |
|||
error="" |
|||
> |
|||
<template #overlay-content> |
|||
<div class="overlay-container"> |
|||
<div class="img-type"> |
|||
没有上传图片哦 ~ |
|||
</div> |
|||
</div> |
|||
</template> |
|||
</t-image> |
|||
</template> |
|||
</div> |
|||
</div> |
|||
<!-- <div class="page-turning-right" @click="onScrollTo('down')">--> |
|||
<!-- <t-icon name="chevron-down" />--> |
|||
<!-- </div>--> |
|||
</div> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
|
|||
:global { |
|||
.page-turning-left { |
|||
background-color: fade(black, 35%); |
|||
padding: 0px 24px; |
|||
border-top-left-radius: 10px; |
|||
border-top-right-radius: 10px; |
|||
margin-bottom: 2px; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
background-color: fade(black, 55%); |
|||
} |
|||
} |
|||
|
|||
.page-turning-right { |
|||
background-color: fade(black, 35%); |
|||
padding: 0px 24px; |
|||
border-bottom-left-radius: 10px; |
|||
border-bottom-right-radius: 10px; |
|||
margin-top: 2px; |
|||
cursor: pointer; |
|||
|
|||
&:hover { |
|||
background-color: fade(black, 55%); |
|||
} |
|||
} |
|||
|
|||
.container { |
|||
flex: 1; |
|||
//background-color: fade(black, 35%); |
|||
background-color: var(--td-component-border); |
|||
padding: var(--td-comp-paddingTB-s); |
|||
border-radius: var(--td-radius-large); |
|||
overflow: hidden; |
|||
|
|||
.custom-space { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: var(--td-comp-margin-l); |
|||
|
|||
.image-box { |
|||
flex-shrink: 0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.overlay-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
|
|||
.img-quantity { |
|||
position: absolute; |
|||
top: 5px; |
|||
right: 5px; |
|||
background-color: fade(black, 60%); |
|||
padding: 2px 10px; |
|||
border-radius: 40px; |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 12px; |
|||
|
|||
.text { |
|||
margin-left: 5px; |
|||
} |
|||
} |
|||
|
|||
.img-type { |
|||
position: absolute; |
|||
bottom: 5px; |
|||
left: 5px; |
|||
background-color: fade(black, 60%); |
|||
padding: 2px 15px; |
|||
border-radius: 40px; |
|||
font-size: 12px; |
|||
color: white; |
|||
} |
|||
} |
|||
|
|||
.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,178 @@ |
|||
<script setup> |
|||
import { onMounted, onUnmounted, ref } from 'vue'; |
|||
import { REQUEST_UPLOAD_FILE } from '@/config/urls'; |
|||
import auth from '@/utils/auth'; |
|||
// import { storeToRefs } from 'pinia'; |
|||
import { useMediaStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
|
|||
const mediaStore = useMediaStore(); |
|||
const { getMediaList, createMedia, deleteMedia } = mediaStore; |
|||
|
|||
const sortieId = ref(); |
|||
const visible = ref(false); |
|||
|
|||
const files = ref([]); |
|||
function init() { |
|||
if (!sortieId.value) return; |
|||
getMediaList(sortieId.value).then(({ data }) => { |
|||
(data || []).forEach(item => { |
|||
const temp = item; |
|||
temp.status = 'success'; |
|||
temp.raw = {}; |
|||
}); |
|||
files.value = [...(data || []), ...files.value]; |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
const mediaList = ref([]); |
|||
function loadList() { |
|||
if (!sortieId.value) return; |
|||
getMediaList(sortieId.value).then(({ data }) => { |
|||
mediaList.value = [...(data || [])]; |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
function onCancel() { |
|||
files.value = []; |
|||
mediaList.value = []; |
|||
} |
|||
|
|||
const subject = ref('image'); |
|||
|
|||
const ABRIDGE_NAME = [10, 7]; |
|||
|
|||
const formatResponse = (res) => { |
|||
if (!res) { |
|||
return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' }; |
|||
} |
|||
return res; |
|||
}; |
|||
|
|||
function beforeUpload(UploadFile) { |
|||
const { type = 'image' } = UploadFile || {}; |
|||
if (type.includes('image')) { |
|||
subject.value = 'image'; |
|||
} |
|||
if (type.includes('video')) { |
|||
subject.value = 'video'; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
const handleSuccess = ({ currentFiles = [], response = [] } = {}) => { |
|||
console.log('aaa', currentFiles, response); |
|||
const medias = currentFiles.map(({ name, size, type, status, response: { code, data } }) => { |
|||
if (status !== 'success') return null; |
|||
if (code !== 200) return null; |
|||
let typeInt; |
|||
if (type.includes('image')) { |
|||
typeInt = 1; |
|||
} |
|||
if (type.includes('video')) { |
|||
typeInt = 2; |
|||
} |
|||
return { |
|||
name, |
|||
size, |
|||
type: typeInt, |
|||
url: data, |
|||
}; |
|||
}).filter(Boolean); |
|||
// const { name, size, type } = currentFiles[0] || {}; |
|||
// const { code, data } = response[0] || {}; |
|||
// if (code !== 200) return; |
|||
// let typeInt; |
|||
// if (type.includes('image')) { |
|||
// typeInt = 1; |
|||
// } |
|||
// if (type.includes('video')) { |
|||
// typeInt = 2; |
|||
// } |
|||
const formData = { |
|||
id: sortieId.value, |
|||
medias, |
|||
}; |
|||
createMedia(formData).then(() => { |
|||
if (!visible.value) return; |
|||
loadList(); |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
}; |
|||
|
|||
const handleRemove = ({ file } = {}) => { |
|||
if (file.status !== 'success') return; |
|||
let mediaId; |
|||
|
|||
if (file.id) { |
|||
mediaId = file.id; |
|||
} else { |
|||
const { response: { data } } = file; |
|||
const media = mediaList.value.find(({ url }) => url === data); |
|||
mediaId = media?.id; |
|||
} |
|||
|
|||
if (!mediaId) return; |
|||
deleteMedia(mediaId).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
}; |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-media-manage', (row) => { |
|||
if (!row.id) return; |
|||
sortieId.value = row.id; |
|||
init(); |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-media-manage'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
width="fit-content" |
|||
:footer="null" |
|||
@closed="onCancel" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">媒体管理</div> |
|||
</template> |
|||
|
|||
<t-upload |
|||
v-model="files" |
|||
placeholder="支持上传图片、视频文件" |
|||
:action="REQUEST_UPLOAD_FILE(subject)" |
|||
:headers="{ |
|||
Authorization: `Bearer ${auth.getToken()}`, |
|||
}" |
|||
theme="file-flow" |
|||
multiple |
|||
:abridge-name="ABRIDGE_NAME" |
|||
auto-upload |
|||
show-thumbnail |
|||
allow-upload-duplicate-file |
|||
:format-response="formatResponse" |
|||
:before-upload="beforeUpload" |
|||
@success="handleSuccess" |
|||
@remove="handleRemove" |
|||
/> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
} |
|||
</style> |
@ -0,0 +1,173 @@ |
|||
<script setup> |
|||
import { computed, onMounted, onUnmounted, ref } from 'vue'; |
|||
// import { REQUEST_UPLOAD_FILE } from '@/config/urls'; |
|||
// import auth from '@/utils/auth'; |
|||
// import { storeToRefs } from 'pinia'; |
|||
import { useMediaStore } from '@/stores'; |
|||
import { MessagePlugin } from 'tdesign-vue-next'; |
|||
import eventBus from '@/utils/eventBus'; |
|||
import ImageBar from '@/views/SortieView/components/ImageBar.vue'; |
|||
import VideoPlayer from '@/components/VideoPlayer.vue'; |
|||
|
|||
const mediaStore = useMediaStore(); |
|||
const { getMediaList } = mediaStore; |
|||
|
|||
const sortieId = ref(); |
|||
const visible = ref(false); |
|||
|
|||
// const files = ref([]); |
|||
// function init() { |
|||
// if (!sortieId.value) return; |
|||
// getMediaList(sortieId.value).then(({ data }) => { |
|||
// // data.forEach(item => { |
|||
// // const temp = item; |
|||
// // temp.status = 'success'; |
|||
// // temp.raw = {}; |
|||
// // }); |
|||
// // files.value = [...data, ...files.value]; |
|||
// }).catch(({ message }) => { |
|||
// if (message) MessagePlugin.error(message); |
|||
// }); |
|||
// } |
|||
|
|||
const mediaList = ref([]); |
|||
|
|||
const imageList = computed(() => mediaList.value.filter(({ type }) => type === 1)); |
|||
const videoList = computed(() => mediaList.value.filter(({ type }) => type === 2)); |
|||
|
|||
function loadList() { |
|||
if (!sortieId.value) return; |
|||
getMediaList(sortieId.value).then(({ data = [] } = {}) => { |
|||
mediaList.value = [...(data || [])]; |
|||
}).catch(({ message }) => { |
|||
if (message) MessagePlugin.error(message); |
|||
}); |
|||
} |
|||
|
|||
function onCancel() { |
|||
mediaList.value = []; |
|||
} |
|||
|
|||
// const subject = ref('image'); |
|||
|
|||
// const ABRIDGE_NAME = [10, 7]; |
|||
// |
|||
// const formatResponse = (res) => { |
|||
// if (!res) { |
|||
// return { status: 'fail', error: '上传失败,原因:文件过大或网络不通' }; |
|||
// } |
|||
// return res; |
|||
// }; |
|||
// |
|||
// function beforeUpload(UploadFile) { |
|||
// const { type = 'image' } = UploadFile || {}; |
|||
// if (type.includes('image')) { |
|||
// subject.value = 'image'; |
|||
// } |
|||
// if (type.includes('video')) { |
|||
// subject.value = 'video'; |
|||
// } |
|||
// return true; |
|||
// } |
|||
|
|||
// const handleSuccess = ({ currentFiles = [], response = [] } = {}) => { |
|||
// const { name, size, type } = currentFiles[0] || {}; |
|||
// const { code, data } = response[0] || {}; |
|||
// if (code !== 200) return; |
|||
// let typeInt; |
|||
// if (type.includes('image')) { |
|||
// typeInt = 1; |
|||
// } |
|||
// if (type.includes('video')) { |
|||
// typeInt = 2; |
|||
// } |
|||
// const formData = { |
|||
// id: sortieId.value, |
|||
// medias: [{ |
|||
// name, |
|||
// size, |
|||
// type: typeInt, |
|||
// url: data, |
|||
// }], |
|||
// }; |
|||
// createMedia(formData).then(() => { |
|||
// loadList(); |
|||
// }).catch(({ message }) => { |
|||
// if (message) MessagePlugin.error(message); |
|||
// }); |
|||
// }; |
|||
|
|||
// const handleRemove = ({ file } = {}) => { |
|||
// if (file.status !== 'success') return; |
|||
// let mediaId; |
|||
// |
|||
// if (file.id) { |
|||
// mediaId = file.id; |
|||
// } else { |
|||
// const { response: { data } } = file; |
|||
// const media = mediaList.value.find(({ url }) => url === data); |
|||
// mediaId = media?.id; |
|||
// } |
|||
// |
|||
// if (!mediaId) return; |
|||
// deleteMedia(mediaId).catch(({ message }) => { |
|||
// if (message) MessagePlugin.error(message); |
|||
// }); |
|||
// }; |
|||
|
|||
onMounted(() => { |
|||
eventBus.on('show-sortie-detail', (row) => { |
|||
if (!row.id) return; |
|||
sortieId.value = row.id; |
|||
loadList(); |
|||
visible.value = true; |
|||
}); |
|||
}); |
|||
|
|||
onUnmounted(() => { |
|||
eventBus.off('show-sortie-detail'); |
|||
}); |
|||
</script> |
|||
|
|||
<template> |
|||
<t-dialog |
|||
:class="s.root" |
|||
v-model:visible="visible" |
|||
mode="full-screen" |
|||
:footer="null" |
|||
@closed="onCancel" |
|||
> |
|||
<template #header> |
|||
<div style="flex: 1; text-align: center;">查看媒体</div> |
|||
</template> |
|||
|
|||
<div class="container"> |
|||
<ImageBar :list="imageList" class="image-bar" /> |
|||
|
|||
<VideoPlayer :list="videoList" class="video-player" /> |
|||
</div> |
|||
</t-dialog> |
|||
</template> |
|||
|
|||
<style lang="less" module="s"> |
|||
.root { |
|||
// |
|||
:global { |
|||
.container { |
|||
height: 100%; |
|||
display: flex; |
|||
|
|||
.video-player { |
|||
flex: 1; |
|||
margin-left: var(--td-comp-margin-s); |
|||
//margin-top: var(--td-comp-margin-s); |
|||
border-radius: var(--td-radius-medium); |
|||
} |
|||
} |
|||
|
|||
.t-dialog__position_fullscreen { |
|||
height: 100%; |
|||
} |
|||
} |
|||
} |
|||
</style> |