|
@@ -0,0 +1,367 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="dialogVisible"
|
|
|
|
|
+ width="719"
|
|
|
|
|
+ :title="`预览-${overviewCameraData.name}`"
|
|
|
|
|
+ center
|
|
|
|
|
+ align-center
|
|
|
|
|
+ class="camera-overview-popover--custom"
|
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
|
+ @close="emits('update:dialogVisible', false)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="camera-overview-popover--custom__content">
|
|
|
|
|
+ <main class="main">
|
|
|
|
|
+ <div class="cameraVideo">
|
|
|
|
|
+ <CameraLiveVideo ref="cameraLiveVideoRef" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="presetAddWrapper" v-if="!!overviewCameraData.isPtz">
|
|
|
|
|
+ <CameraViewScale @update:ControlPerspective="activePresetToken = ''" />
|
|
|
|
|
+ <CameraDirectionControl @update:ControlPerspective="activePresetToken = ''" />
|
|
|
|
|
+ <ElButton type="primary" style="margin-top: 20px; width: 100px" @click="handleAddPreset" v-show="displayPresetList.length < 10">添加预置位</ElButton>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </main>
|
|
|
|
|
+ <footer class="footer" v-if="!!overviewCameraData.isPtz && presetList.length > 0">
|
|
|
|
|
+ <div class="footer-header">
|
|
|
|
|
+ <div class="edit-preset-position-icon-wrapper" @click="toggleEditMode">
|
|
|
|
|
+ <img :src="isEditMode ? EditPresetPositionFocusIcon : EditPresetPositionIcon" alt="编辑预置位" />
|
|
|
|
|
+ <span v-show="isEditMode">完成编辑</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="pagination-control" v-if="displayPresetList.length > 0">
|
|
|
|
|
+ <el-button type="text" :disabled="currentPage === 1" @click="prevPage" :icon="ArrowLeft" />
|
|
|
|
|
+ <span>{{ currentPage }}/{{ totalPages }}</span>
|
|
|
|
|
+ <el-button type="text" :disabled="currentPage === totalPages" @click="nextPage" :icon="ArrowRight" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="preset-position-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="preset-position-item"
|
|
|
|
|
+ v-for="item in currentPageItems"
|
|
|
|
|
+ :key="item.presetToken"
|
|
|
|
|
+ :class="{ 'active-preset': activePresetToken === item.presetToken }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img
|
|
|
|
|
+ :src="item.imageUrl || PresetPositionItem"
|
|
|
|
|
+ alt="预置位"
|
|
|
|
|
+ style="cursor: pointer"
|
|
|
|
|
+ @click="handleGoToPreset(item)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="isEditMode"
|
|
|
|
|
+ :src="DeletePresetPositionIcon"
|
|
|
|
|
+ alt="删除预置位"
|
|
|
|
|
+ class="delete-preset-position-icon"
|
|
|
|
|
+ @click="handleDeletePreset(item)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="preset-position-name">{{ item.presetName }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </footer>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script lang="ts" setup>
|
|
|
|
|
+ import { ref, watch, computed, onMounted } from 'vue';
|
|
|
|
|
+ import { CameraDetailServer } from '@/types/camera/type';
|
|
|
|
|
+ import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
|
|
|
|
|
+ import EditPresetPositionIcon from '@/assets/icons/edit-preset-position.svg';
|
|
|
|
|
+ import DeletePresetPositionIcon from '@/assets/icons/delete-preset-position.svg';
|
|
|
|
|
+ import PresetPositionItem from '@/assets/icons/preset-placeholder-img.svg';
|
|
|
|
|
+ import EditPresetPositionFocusIcon from '@/assets/icons/edit-preset-position-focus.svg';
|
|
|
|
|
+ import { dataURLtoBlob } from '@/utils/file/base64Conver';
|
|
|
|
|
+ import {
|
|
|
|
|
+ getPresetListApi,
|
|
|
|
|
+ PresetListResp,
|
|
|
|
|
+ deletePresetApi,
|
|
|
|
|
+ createPresetApi,
|
|
|
|
|
+ goToPresetApi,
|
|
|
|
|
+ uploadPresetImageApi,
|
|
|
|
|
+ } from '@/api/camera/camera-preview';
|
|
|
|
|
+ import CameraLiveVideo from '@/modules/algo-params-setting-base/components/CameraLiveVideo/CameraLiveVideo.vue';
|
|
|
|
|
+ import CameraViewScale from '@/modules/algo-params-setting-base/components/CameraViewSetting/CameraViewScale.vue';
|
|
|
|
|
+ import CameraDirectionControl from '@/modules/algo-params-setting-base/components/CameraDirectionControl/CameraDirectionControl.vue';
|
|
|
|
|
+ import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
|
+
|
|
|
|
|
+ const emits = defineEmits(['update:dialogVisible']);
|
|
|
|
|
+ const dialogVisible = ref(false);
|
|
|
|
|
+ const presetList = ref<PresetListResp[]>([]);
|
|
|
|
|
+ const displayPresetList = ref<PresetListResp[]>([]);
|
|
|
|
|
+ const isEditMode = ref(false);
|
|
|
|
|
+ const activePresetToken = ref<string>(''); // 当前激活的预置位token
|
|
|
|
|
+ const cameraLiveVideoRef = ref<InstanceType<typeof CameraLiveVideo> | null>(null); // 添加对CameraLiveVideo的引用并指定正确类型
|
|
|
|
|
+
|
|
|
|
|
+ const props = defineProps<{
|
|
|
|
|
+ dialogVisible: boolean;
|
|
|
|
|
+ overviewCameraData: CameraDetailServer;
|
|
|
|
|
+ }>();
|
|
|
|
|
+ const currentPage = ref(1);
|
|
|
|
|
+ const pageSize = 5;
|
|
|
|
|
+ const presetPositionCount = computed(() => displayPresetList.value.length); // 总预置位数量
|
|
|
|
|
+
|
|
|
|
|
+ const totalPages = computed(() => Math.ceil(presetPositionCount.value / pageSize));
|
|
|
|
|
+
|
|
|
|
|
+ const currentPageItems = computed(() => {
|
|
|
|
|
+ const startIdx = (currentPage.value - 1) * pageSize;
|
|
|
|
|
+ const endIdx = Math.min(startIdx + pageSize, presetPositionCount.value);
|
|
|
|
|
+ return displayPresetList.value.slice(startIdx, endIdx);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const prevPage = () => {
|
|
|
|
|
+ if (currentPage.value > 1) {
|
|
|
|
|
+ currentPage.value--;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const nextPage = () => {
|
|
|
|
|
+ if (currentPage.value < totalPages.value) {
|
|
|
|
|
+ currentPage.value++;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const toggleEditMode = () => {
|
|
|
|
|
+ isEditMode.value = !isEditMode.value;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleGoToPreset = (item: PresetListResp) => {
|
|
|
|
|
+ const cameraId = props.overviewCameraData.id;
|
|
|
|
|
+ if (!cameraId) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置当前激活的预置位
|
|
|
|
|
+ activePresetToken.value = item.presetToken;
|
|
|
|
|
+
|
|
|
|
|
+ // 调用前往预置位的API
|
|
|
|
|
+ goToPresetApi({ presetToken: item.presetToken, cameraId });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleDeletePreset = (item: PresetListResp) => {
|
|
|
|
|
+ const cameraId = props.overviewCameraData.id;
|
|
|
|
|
+ if (!cameraId) return;
|
|
|
|
|
+ const index = displayPresetList.value.findIndex((preset) => preset.presetToken === item.presetToken);
|
|
|
|
|
+ if (index !== -1) {
|
|
|
|
|
+ ElMessageBox.confirm(
|
|
|
|
|
+ '该预置位可能存在关联的电子围栏。删除该预置位将会删除对应的电子围栏信息,请确认是否删除?',
|
|
|
|
|
+ '删除确认',
|
|
|
|
|
+ {
|
|
|
|
|
+ confirmButtonText: '确定',
|
|
|
|
|
+ cancelButtonText: '取消',
|
|
|
|
|
+ },
|
|
|
|
|
+ ).then(async () => {
|
|
|
|
|
+ await deletePresetApi(item.presetToken, String(cameraId));
|
|
|
|
|
+ ElMessage.success('删除成功');
|
|
|
|
|
+ await getPresetList();
|
|
|
|
|
+ const currentPageStartIdx = (currentPage.value - 1) * pageSize;
|
|
|
|
|
+ if (currentPageStartIdx >= displayPresetList.value.length && currentPage.value > 1) {
|
|
|
|
|
+ currentPage.value--;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleAddPreset = () => {
|
|
|
|
|
+ ElMessageBox.prompt('', '添加预置位', {
|
|
|
|
|
+ confirmButtonText: '确定',
|
|
|
|
|
+ cancelButtonText: '取消',
|
|
|
|
|
+ inputPlaceholder: '请输入预置位名称',
|
|
|
|
|
+ inputValidator: (value) => {
|
|
|
|
|
+ if (!value) {
|
|
|
|
|
+ return '预置位名称不能为空';
|
|
|
|
|
+ }
|
|
|
|
|
+ const isExist = presetList.value.find((item) => item.presetName === value);
|
|
|
|
|
+ if (isExist) {
|
|
|
|
|
+ return '预置位名称已存在';
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+ }).then(async ({ value }) => {
|
|
|
|
|
+ const cameraId = props.overviewCameraData.id;
|
|
|
|
|
+ if (!cameraId) return;
|
|
|
|
|
+
|
|
|
|
|
+ let imageBase64 = '';
|
|
|
|
|
+ if (cameraLiveVideoRef.value) {
|
|
|
|
|
+ imageBase64 = cameraLiveVideoRef.value.captureImage();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 未来可以在这里把imageBase64传给后端
|
|
|
|
|
+ const blob = dataURLtoBlob(imageBase64);
|
|
|
|
|
+ const url = await uploadPresetImageApi(blob, 'CAMERA_IMAGE');
|
|
|
|
|
+ if (!url) {
|
|
|
|
|
+ ElMessage.error('上传预置位图片失败');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = await createPresetApi({ presetName: value, cameraId, imageUrl: url.url });
|
|
|
|
|
+ if (res) {
|
|
|
|
|
+ ElMessage.success('添加预置位成功');
|
|
|
|
|
+ await getPresetList();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const getPresetList = async () => {
|
|
|
|
|
+ const cameraId = props.overviewCameraData.id;
|
|
|
|
|
+ if (!cameraId) return;
|
|
|
|
|
+ presetList.value = await getPresetListApi(cameraId);
|
|
|
|
|
+ displayPresetList.value = [...presetList.value];
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ onMounted(async () => {
|
|
|
|
|
+ await getPresetList();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => props.dialogVisible,
|
|
|
|
|
+ (newVal) => {
|
|
|
|
|
+ dialogVisible.value = newVal;
|
|
|
|
|
+ if (newVal) {
|
|
|
|
|
+ // 每次对话框打开时,重置编辑状态
|
|
|
|
|
+ isEditMode.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true },
|
|
|
|
|
+ );
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss">
|
|
|
|
|
+ .camera-overview-popover--custom {
|
|
|
|
|
+ padding: 20px 24px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05), 0px 6px 16px 0px rgba(0, 0, 0, 0.08),
|
|
|
|
|
+ 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
|
|
|
|
|
+ .el-dialog__header {
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ color: rgba(0, 0, 0, 0.88);
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ }
|
|
|
|
|
+ &__content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+
|
|
|
|
|
+ .main {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ flex-grow: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 377px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 9px;
|
|
|
|
|
+
|
|
|
|
|
+ .footer-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ .edit-preset-position-icon-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ span {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #1777ff;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .pagination-control {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: rgba(0, 0, 0, 0.88);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .preset-position-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .preset-position-item {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &.active-preset {
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ box-shadow: 0 0 8px 2px rgba(24, 144, 255, 0.6);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ &::after {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border: 2px solid #1890ff;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .preset-position-name {
|
|
|
|
|
+ background: rgba(24, 144, 255, 0.8);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .delete-preset-position-icon {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -4px;
|
|
|
|
|
+ right: -4px;
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ scale: 1.2;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ height: 68px;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .preset-position-name {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
+ padding: 2px 4px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .cameraVideo {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ z-index: 8;
|
|
|
|
|
+ background: #ccc;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+ .presetAddWrapper {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: -50px;
|
|
|
|
|
+ right: -30px;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ transform: scale(0.6);
|
|
|
|
|
+ .el-button {
|
|
|
|
|
+ transform: scale(1.5);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+</style>
|