@ -1,4 +1,4 @@ |
|||||
APP_TITLE=云端无人机管理系统 |
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_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> |