媒体库-第一版

main
滕嵩 3 months ago
parent 4583fe0c4e
commit e1c29bd6f7

@ -29,6 +29,15 @@
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.6.0/libs/flv.min.1.6.3.js"></script>
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.6.0/libs/dash.all.min.4.4.1.js"></script>
<script src="https://web.sdk.qcloud.com/player/tcplayer/release/v4.6.0/tcplayer.v4.6.0.min.js"></script>
<!-- 引入海康插件内容 -->
<script type="text/javascript" src="./public/monitor/monitorHK/jquery-1.12.4.min.js"></script>
<script type="text/javascript" src="./public/monitor/monitorHK/jsencrypt.min.js"></script>
<script type="text/javascript" src="./public/monitor/monitorHK/jsWebControl-1.0.0.min.js"></script>
<!-- 引入乐橙插件内容 -->
<script type="text/javascript" src="./public/monitor/monitorLC/imouPlayer.js"></script>
<!-- 引入青犀插件内容 -->
<script type="text/javascript" src="./public/monitor/monitorQX/EasyPlayer-element.min.js"></script>
</head>
<body>
<div id="app">

@ -100,6 +100,7 @@
"echarts": "^5.4.3",
"element-plus": "^2.6.0",
"exceljs": "^4.4.0",
"fabric": "^4.6.0",
"highlight.js": "^11.9.0",
"js-md5": "^0.8.3",
"kml-geojson": "^1.2.2",
@ -115,6 +116,7 @@
"min-dash": "^4.2.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"panolens": "^0.12.1",
"path-to-regexp": "^6.2.1",
"pinia": "2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
@ -126,10 +128,12 @@
"showdown": "^2.1.0",
"sortablejs": "^1.15.1",
"terraformer-wkt-parser": "^1.2.1",
"three": "^0.145.0",
"tinymce": "^5.10.9",
"unocss": "0.58.3",
"uuid": "^9.0.1",
"vditor": "^3.9.8",
"video.js": "^8.6.1",
"vue": "3.3.4",
"vue-color-kit": "^1.0.6",
"vue-i18n": "^9.8.0",
@ -169,6 +173,8 @@
"@vben/ts-config": "workspace:*",
"@vben/types": "workspace:*",
"@vben/vite-config": "workspace:*",
"@vicons/carbon": "^0.12.0",
"@vicons/ionicons5": "~0.11.0",
"@vue/compiler-sfc": "^3.4.5",
"@vue/test-utils": "^2.4.3",
"cross-env": "^7.0.3",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,94 @@
<template>
<div class="m-4 mr-0 overflow-hidden bg-white">
<BasicTree
ref="asyncExpandTreeRef"
treeWrapperClassName="h-[calc(100%-35px)] overflow-auto"
:clickRowToExpand="false"
:treeData="treeData"
:fieldNames="{ key: 'id', title: 'name' }"
@select="handleSelect"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, unref } from 'vue';
import { BasicTree, TreeItem, TreeActionType } from '@/components/Tree';
import { getChildrenTree } from '@/api/demo/system';
import { isArray } from '@/utils/is';
const emit = defineEmits(['select']);
const treeData = ref([]);
const asyncExpandTreeRef = ref<Nullable<TreeActionType>>(null);
async function fetch() {
// treeData.value = (await getChildrenTree({ parentId: 0 })) as unknown as TreeItem[];
// console.log(treeData.value);
treeData.value = [
{
id: 'meitiku',
name: '媒体库',
children: [
{
id: 'allResource',
name: '全部资源',
},
{
id: 'wideAngle',
name: '广角照片',
},
{
id: 'zoom',
name: '变焦照片',
},
{
id: 'infrared',
name: '红外照片',
},
{
id: 'video',
name: '视频资源',
},
],
},
{
id: 'achievementModel',
name: '成果模型',
children: [
{
id: '2d',
name: '二维正摄',
},
{
id: '3d',
name: '三维模型',
},
],
},
{
id: 'gas',
name: '气体探测',
},
{
id: 'vr',
name: 'VR全景',
},
];
//
nextTick(() => {
unref(asyncExpandTreeRef)?.filterByLevel(1);
});
}
function handleSelect(keys) {
emit('select', keys[0]);
}
onMounted(() => {
fetch();
});
defineExpose({
fetch,
});
</script>

@ -0,0 +1,483 @@
<template>
<PageWrapper dense contentFullHeight fixedHeight contentClass="flex">
<LeftTree ref="childRef" class="w-1/5 xl:w-1/6" @select="handleSelect" />
<BasicTable class="w-4/5 xl:w-5/6" @register="registerTable" :searchInfo="searchInfo">
<template #toolbar>
<!-- <PermissionBtn @btnEvent="onBtnClicked"></PermissionBtn> -->
<a-button :icon="h(PlusOutlined)" type="primary" @click="addFolder"></a-button>
<a-button :icon="h(ColumnHeightOutlined)" @click="moveFolderOrFile"></a-button>
<a-button :icon="h(DeleteOutlined)" @click="deleteFolderOrFile"> </a-button>
<a-button :icon="h(DownloadOutlined)" @click="compressFolderOrFile"></a-button>
<a-radio-group v-model:value="tableType">
<a-radio-button value="table"><BarsOutlined /></a-radio-button>
<a-radio-button value="store"><AppstoreOutlined /></a-radio-button>
</a-radio-group>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<FolderOpenOutlined v-if="record.type == 'folder'" style="font-size: 20px" />
<img v-if="record.type == 'img'" :src="record.url" :width="30" :height="20" />
{{ record.name }}
</template>
<template v-if="column.key === 'size'">
{{ record.size ? record.size : '-' }}
</template>
<template v-if="column.key === 'label'">
<div v-if="record.label && record.label.length > 0">
<a-tag color="success" v-for="la in record.label" :key="la">{{ la }}</a-tag>
</div>
</template>
<template v-if="column.key === 'action'">
<a-button type="text">
<EditOutlined @click="renameRecord(record)" />
</a-button>
<a-button type="text" v-if="record.type != 'folder'">
<EyeOutlined @click="lookRecord(record)" />
</a-button>
</template>
</template>
</BasicTable>
<!-- 新建文件夹 -->
<AddFolderModal @register="addFolderModal" @success="handleSuccess" />
<!-- 移动 -->
<MoveFileModal @register="moveFileModal" @success="handleSuccess" />
<!-- 压缩 -->
<CompressFileModal @register="compressFileModal" @success="handleSuccess" />
<!-- 重命名 -->
<RenameModal @register="renameModal" @success="handleSuccess" />
<!-- 预览弹窗 -->
<a-modal
v-model:open="open"
width="100%"
wrap-class-name="full-modal"
:centered="true"
:closable="false"
:footer="null"
:destroyOnClose="true"
:mask="false"
:maskClosable="false"
@ok="handleOk"
>
<Preview
:nowPreviewRecord="nowPreviewRecord"
:previewRecordList="previewRecordList"
@chooseNowPreviewRecord="chooseNowPreviewRecord"
@closeModal="closeModal"
@reloadTable="reload"
/>
</a-modal>
</PageWrapper>
</template>
<script lang="ts" setup>
import { reactive, ref, h } from 'vue';
import { BasicTable, useTable, TableAction } from '@/components/Table';
import { getOrgList, deleteDept } from '@/api/demo/system';
import { PageWrapper } from '@/components/Page';
import { useModal } from '@/components/Modal';
import { useMessage } from '@/hooks/web/useMessage';
import {
EditOutlined,
DeleteOutlined,
PlusOutlined,
ColumnHeightOutlined,
DownloadOutlined,
AppstoreOutlined,
BarsOutlined,
FolderOutlined,
FolderOpenOutlined,
EyeOutlined,
} from '@ant-design/icons-vue';
import LeftTree from './LeftTree.vue';
import { AddFolderModal } from './modal/modal';
import { MoveFileModal } from './modal/modal';
import { CompressFileModal } from './modal/modal';
import { RenameModal } from './modal/modal';
import Preview from './preview/preview.vue';
import { PermissionBtn } from '@/components/PermissionBtn/index';
import { columns, searchFormSchema } from './modal.data';
import dayjs from 'dayjs';
const { createConfirm, createMessage } = useMessage();
const data = ref([
{
id: '1',
name: '普通飞行1',
createtime: '2025-03-24 18:13:17',
type: 'folder',
children: [
{
id: '21',
name: '普通飞行2-1',
createtime: '2025-03-24 18:13:17',
type: 'folder',
children: [
{
id: '31',
name: '普通飞行3-1',
createtime: '2025-03-24 18:13:17',
type: 'img',
},
],
},
{
id: '22',
name: '普通飞行2-2',
createtime: '2025-03-24 18:13:17',
type: 'folder',
children: [
{
id: '32',
name: '普通飞行3-2',
createtime: '2025-03-24 18:13:17',
type: 'img',
},
{
id: '33',
name: '普通飞行3-3',
createtime: '2025-03-24 18:13:17',
type: 'img',
},
],
},
],
},
{
id: '1-2',
name: '南山风景照.jpg',
createTime: '2020-10-22 17:33:22',
type: 'img',
url: 'https://cdn.colorhub.me/QgpUMkZxNhU/rs:auto:0:500:0/g:ce/fn:colorhub/bG9jYWw6Ly8vZmIvNmYvMjlkMTE1NjRkNmI5ZmRhOTczYmU3ZmUyNmMyMDkwM2MwZjU5ZmI2Zi5qcGVn.webp',
imgtype: '原片',
taskname: '佛山大火救援项目',
airlineName: '火灾救援勘查航线',
width: 889,
height: 500,
size: '6.2M',
photographFeiji: '救援机1007',
photographNumber: 'GD610',
photographMan: 'zachzhou',
photographTime: '2020-10-22 00:00:00',
label: ['人', '车'],
lat: 35.362625,
lng: 118.033886,
canvasJson:
'{"version":"4.6.0","objects":[{"type":"path","version":"4.6.0","originX":"left","originY":"top","left":520.5,"top":634,"width":0,"height":0,"fill":null,"stroke":"#ffffff","strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"round","strokeDashOffset":0,"strokeLineJoin":"round","strokeUniform":false,"strokeMiterLimit":10,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"path":[["M",520.999,634.5],["L",521.001,634.5]]},{"type":"rect","version":"4.6.0","originX":"left","originY":"top","left":523,"top":352.5,"width":100,"height":100,"fill":"#ffffff00","stroke":"#ffffff","strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":0.73,"scaleY":0.73,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"rx":0,"ry":0},{"type":"textbox","version":"4.6.0","originX":"left","originY":"top","left":523,"top":426.5,"width":200,"height":22.6,"fill":"#ffffff","stroke":null,"strokeWidth":1,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"fontFamily":"Times New Roman","fontWeight":"normal","fontSize":20,"text":"太阳","underline":false,"overline":false,"linethrough":false,"textAlign":"left","fontStyle":"normal","lineHeight":1.16,"textBackgroundColor":"","charSpacing":0,"styles":{},"direction":"ltr","path":null,"pathStartOffset":0,"pathSide":"left","minWidth":20,"splitByGrapheme":false},{"type":"path","version":"4.6.0","originX":"left","originY":"top","left":784,"top":436.5,"width":190.01,"height":85.01,"fill":null,"stroke":"#ffffff","strokeWidth":4,"strokeDashArray":null,"strokeLineCap":"round","strokeDashOffset":0,"strokeLineJoin":"round","strokeUniform":false,"strokeMiterLimit":10,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"path":[["M",785.996,438.496],["Q",786,438.5,786.5,438.5],["Q",787,438.5,790.5,440],["Q",794,441.5,798.5,443.5],["Q",803,445.5,808,448],["Q",813,450.5,820,453.5],["Q",827,456.5,880,480.5],["Q",933,504.5,940,508],["Q",947,511.5,952.5,514],["Q",958,516.5,962.5,518],["Q",967,519.5,970,521],["Q",973,522.5,974.5,523],["L",976.004,523.504]]},{"type":"path","version":"4.6.0","originX":"left","originY":"top","left":808,"top":417.5,"width":123.01,"height":124.01,"fill":null,"stroke":"#ffffff","strokeWidth":4,"strokeDashArray":null,"strokeLineCap":"round","strokeDashOffset":0,"strokeLineJoin":"round","strokeUniform":false,"strokeMiterLimit":10,"scaleX":1,"scaleY":1,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"path":[["M",933.004,419.496],["Q",933,419.5,931.5,420.5],["Q",930,421.5,928,425],["Q",926,428.5,921.5,433],["Q",917,437.5,914,441.5],["Q",911,445.5,905.5,450.5],["Q",900,455.5,894,462],["Q",888,468.5,883,473.5],["Q",878,478.5,872,484.5],["Q",866,490.5,861.5,495],["Q",857,499.5,851,504.5],["Q",845,509.5,841.5,513],["Q",838,516.5,833.5,520],["Q",829,523.5,826,526.5],["Q",823,529.5,820,533],["Q",817,536.5,815,538],["Q",813,539.5,812.5,540.5],["Q",812,541.5,811,542.5],["L",809.996,543.504]]}],"backgroundImage":{"type":"image","version":"4.6.0","originX":"left","originY":"top","left":0,"top":0,"width":889,"height":500,"fill":"rgb(0,0,0)","stroke":null,"strokeWidth":0,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":1.46,"scaleY":1.6,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"cropX":0,"cropY":0,"src":"https://cdn.colorhub.me/QgpUMkZxNhU/rs:auto:0:500:0/g:ce/fn:colorhub/bG9jYWw6Ly8vZmIvNmYvMjlkMTE1NjRkNmI5ZmRhOTczYmU3ZmUyNmMyMDkwM2MwZjU5ZmI2Zi5qcGVn.webp","crossOrigin":null,"filters":[]}}',
},
{
id: '1-3',
name: '风景图.jpg',
createTime: '2020-10-22 17:33:22',
type: 'img',
url: 'https://m.tuniucdn.com/fb2/t1/G5/M00/44/52/Cii-s1soezyIF2UxABn76u-yKl8AAIwBgB34jAAGfwC3020871',
imgtype: '原片2',
taskname: '佛山大火救援项目2',
airlineName: '火灾救援勘查航线2',
width: 2000,
height: 1334,
size: '1.63M',
photographFeiji: '救援机1008',
photographNumber: 'GD610',
photographMan: 'zachzhou',
photographTime: '2020-10-22 00:00:00',
label: ['人', '车'],
lat: 35.362625,
lng: 118.033886,
canvasJson:
'{"version":"4.6.0","objects":[],"backgroundImage":{"type":"image","version":"4.6.0","originX":"left","originY":"top","left":0,"top":0,"width":2000,"height":1334,"fill":"rgb(0,0,0)","stroke":null,"strokeWidth":0,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":0.65,"scaleY":0.6,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"cropX":0,"cropY":0,"src":"https://m.tuniucdn.com/fb2/t1/G5/M00/44/52/Cii-s1soezyIF2UxABn76u-yKl8AAIwBgB34jAAGfwC3020871","crossOrigin":null,"filters":[]}}',
},
{
id: '1-4',
name: '红外照片.jpg',
createTime: '2020-10-22 17:33:22',
type: 'img',
url: 'https://pic.rmb.bdstatic.com/bjh/gallery/8c885a0e3cf0647b60548535e2e9ca39.jpeg',
imgtype: '红外照片',
taskname: '佛山大火救援项目-红外照片',
airlineName: '火灾救援勘查航线',
width: 640,
height: 480,
size: '6.2M',
photographFeiji: '救援机1007',
photographNumber: 'GD610',
photographMan: 'zachzhou',
photographTime: '2020-10-22 00:00:00',
label: ['人', '车'],
lat: 35.362625,
lng: 118.033886,
},
{
id: '1-5',
name: '广角照片.jpg',
createTime: '2020-10-22 17:33:22',
type: 'img',
url: 'https://img2.baidu.com/it/u=2490853491,3226002419&fm=253&fmt=auto&app=138&f=JPEG?w=749&h=500',
imgtype: '广角照片',
taskname: '佛山大火救援项目-广角照片',
airlineName: '火灾救援勘查航线',
width: 749,
height: 500,
size: '6.2M',
photographFeiji: '救援机1007',
photographNumber: 'GD610',
photographMan: 'zachzhou',
photographTime: '2020-10-22 00:00:00',
label: ['人', '车'],
lat: 35.362625,
lng: 118.033886,
},
{
id: '1-6',
name: '变焦照片.jpg',
createTime: '2020-10-22 17:33:22',
type: 'img',
url: 'https://img2.baidu.com/it/u=3778652155,475195343&fm=253&fmt=auto&app=138&f=PNG?w=500&h=518',
imgtype: '变焦照片',
taskname: '佛山大火救援项目-变焦照片',
airlineName: '火灾救援勘查航线',
width: 500,
height: 518,
size: '6.2M',
photographFeiji: '救援机1007',
photographNumber: 'GD610',
photographMan: 'zachzhou',
photographTime: '2020-10-22 00:00:00',
label: ['人', '车'],
lat: 35.362625,
lng: 118.033886,
},
{
id: '41',
name: '视频',
createtime: '2025-03-24 18:13:17',
type: 'folder',
children: [
{
id: '4-4',
name: 'XZD153狼窝沟西南',
createTime: '',
type: 'video',
url: '74b95e6575d741489b9a9061bb646467',
manufacturer: '海康',
},
{
id: '4-5',
name: '费县马庄镇陈家鱼后村南斜坡后村',
createTime: '',
type: 'video',
url: 'http://111.36.45.20:18000/flv/hls/H-dcb1ea7388588111.flv',
manufacturer: '腾讯',
},
{
id: '4-6',
name: '可落',
createTime: '',
type: 'video',
url: '8H03AA1PAG8D9BF',
manufacturer: '乐橙',
},
{
id: '4-7',
name: '费县薛庄镇东张林村村南可见光',
createTime: '',
type: 'video',
url: '37130100181328000392',
manufacturer: '青犀',
},
],
},
]);
const tableType = ref('table');
const searchInfo = reactive<Recordable>({});
const searchParams = ref();
const [registerTable, { reload, getSelectRows, getDataSource, clearSelectedRowKeys }] = useTable({
// api: getOrgList,
dataSource: data,
rowKey: 'id',
columns,
formConfig: {
labelWidth: 120,
schemas: searchFormSchema,
},
rowSelection: {
type: 'checkbox',
// onChange: (selectedRowKeys, selectedRows) => {
// console.log(selectedRowKeys, selectedRows);
// },
// onSelect: (record, selected, selectedRows) => {
// console.log(record, selected, selectedRows);
// },
// onSelectAll: (selected, selectedRows, changeRows) => {
// console.log(selected, selectedRows, changeRows);
// },
},
striped: false,
bordered: false,
inset: false,
tableSetting: {
redo: false,
size: false,
setting: false,
},
useSearchForm: true,
showIndexColumn: false,
showTableSetting: true,
handleSearchInfoFn(info) {
console.log(info);
console.log(searchInfo.value);
searchParams.value = info;
return info;
},
beforeFetch: (data) => {
//
var temp = {
startTime: dayjs(data.startTime).startOf('month').format('YYYY-MM-DD'),
endTime: dayjs(data.endTime).endOf('month').format('YYYY-MM-DD HH:mm:ss'),
};
return temp;
},
afterFetch: (res) => {
console.log(res);
},
});
function handleSelect(orgId = '') {
searchInfo.orgId = orgId;
reload();
}
const childRef = ref<any>();
function handleSuccess() {
clearSelectedRowKeys();
childRef.value.fetch();
reload();
}
// ----------------------------------------------------------------------
//
const [addFolderModal, { openModal: openAddFolderModal }] = useModal();
//
const [moveFileModal, { openModal: openMoveFileModal }] = useModal();
//
const [compressFileModal, { openModal: openCompressFileModal }] = useModal();
//
const [renameModal, { openModal: openRenameModal }] = useModal();
//
function addFolder() {
let rows = getSelectRows();
let record: any = null;
if (rows.length == 1) {
if (rows[0].type == 'folder') {
record = rows[0];
} else {
record = findParentIdById(getDataSource(), rows[0].id);
}
}
openAddFolderModal(true, {
record,
});
}
//
function findParentIdById(tree, targetId) {
function recurse(nodes) {
for (let node of nodes) {
if (node.children) {
for (let child of node.children) {
if (child.id === targetId) {
return node;
}
const found = recurse([child]);
if (found) return found;
}
}
}
return null;
}
return recurse(tree);
}
//
function moveFolderOrFile() {
let rows = getSelectRows();
if (rows.length > 0) {
const record = rows;
openMoveFileModal(true, {
tableData: getDataSource(),
record,
});
} else {
return createMessage.warn('请选择一个或者多个文件/文件夹进行移动');
}
}
//
async function deleteFolderOrFile() {
let rows = getSelectRows();
if (rows.length == 0) {
return createMessage.warn('请选择一个或者多个文件/文件夹进行删除');
}
const query = rows.map((item) => item.id);
createConfirm({
iconType: 'info',
title: '删除',
content: '确定要删除当前部门吗',
onOk: async () => {
// const data = await deleteDept(query);
const data = null;
if (data) {
handleSuccess();
createMessage.success('删除成功');
} else {
createMessage.error('删除失败');
}
},
});
}
//
function compressFolderOrFile() {
let rows = getSelectRows();
if (rows.length > 0) {
const record = rows;
openCompressFileModal(true, {
tableData: getDataSource(),
record,
});
} else {
return createMessage.warn('请选择一个或者多个文件/文件夹压缩');
}
}
//
function renameRecord(record) {
openRenameModal(true, {
record,
});
}
// ----------------------------------------------------------------------
const open = ref(false);
//
const nowPreviewRecord: any = ref();
const previewRecordList: any = ref([]);
function lookRecord(record) {
open.value = true;
nowPreviewRecord.value = record;
previewRecordList.value = findParentIdById(getDataSource(), record.id)?.children;
if (!previewRecordList.value) {
previewRecordList.value = getDataSource();
}
}
//
function chooseNowPreviewRecord(value) {
nowPreviewRecord.value = value;
}
//
function closeModal() {
open.value = false;
}
</script>

@ -0,0 +1,192 @@
import { BasicColumn, FormSchema } from '@/components/Table';
import { h } from 'vue';
import { Tag } from 'ant-design-vue';
import { getPosGroupList } from '@/api/demo/system';
export const columns: BasicColumn[] = [
{
title: '文件名称',
dataIndex: 'name',
align: 'left',
width: 200,
},
{
title: '创建时间',
dataIndex: 'createtime',
align: 'left',
width: 150,
},
{
title: '大小',
dataIndex: 'size',
align: 'left',
width: 75,
},
{
title: '标签',
dataIndex: 'label',
align: 'left',
width: 200,
},
{
title: '设备名称',
dataIndex: 'equipmentName',
align: 'left',
width: 200,
},
{
title: '操作',
dataIndex: 'action',
align: 'left',
width: 120,
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'label',
label: '标签',
component: 'Select',
colProps: { span: 8 },
componentProps: {
mode: 'multiple',
options: [
{ label: '标签1', value: '标签1' },
{ label: '标签2', value: '标签2' },
{ label: '标签3', value: '标签3' },
],
},
},
{
field: 'equipmentName',
label: '设备名称',
component: 'Input',
colProps: { span: 5 },
},
{
field: '[startTime, endTime]',
label: '时间选择',
component: 'RangePicker',
colProps: { span: 6 },
},
{
field: 'name',
label: '文件名称',
component: 'Input',
colProps: { span: 5 },
},
];
export const formGroupSchema: FormSchema[] = [
{
field: 'posGroupId',
component: 'ApiSelect',
label: '职级组',
required: true,
componentProps: ({ formActionType, formModel }) => {
return {
api: getPosGroupList, // 接口
// 接口参数
resultField: 'result',
labelField: 'name',
valueField: 'id',
};
},
},
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: '部门id',
component: 'Input',
ifShow: true,
},
{
field: 'name',
label: '部门名称',
component: 'Input',
required: true,
},
{
field: 'parentId',
label: '上级部门',
component: 'TreeSelect',
componentProps: {
fieldNames: {
label: 'name',
key: 'id',
value: 'id',
},
onChange:(value)=>{
console.log(value)
},
getPopupContainer: () => document.body,
},
// required: true,
},
// {
// field: 'orderNo',
// label: '排序',
// component: 'InputNumber',
// required: true,
// },
{
field: 'status',
label: '状态',
component: 'RadioButtonGroup',
defaultValue: 0,
componentProps: {
options: [
{ label: '启用', value: 0 },
{ label: '停用', value: 1 },
],
},
required: true,
},
// {
// label: '备注',
// field: 'remark',
// component: 'InputTextArea',
// },
];
// 新建文件夹
export const addFolderSchema: FormSchema[] = [
{
field: 'name',
component: 'Input',
label: '上级目录',
required: false,
defaultValue: '/',
componentProps: {
disabled: true,
},
},
{
field: 'newName',
component: 'Input',
label: '新文件夹名称',
required: true,
},
];
// 压缩
export const compressFileSchema: FormSchema[] = [
{
field: 'compressName',
component: 'Input',
label: '压缩文件名称',
required: true,
defaultValue: '压缩文件',
}
];
// 重命名
export const renameSchema: FormSchema[] = [
{
field: 'name',
component: 'Input',
label: '新名称',
required: true,
}
];

@ -0,0 +1,58 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="新建文件夹" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { BasicForm, useForm } from '@/components/Form';
import { addFolderSchema } from '../modal.data';
import { orgPosGroup } from '@/api/demo/system';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
const emit = defineEmits(['success']);
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100,
baseColProps: { span: 24 },
schemas: addFolderSchema,
showActionButtonGroup: false,
});
// id
let folderId = ref();
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields();
setModalProps({ confirmLoading: false });
folderId.value = data.record?.id;
setFieldsValue({
...data.record,
});
});
//
async function handleSubmit() {
try {
const values = await validate();
let query = {
folderId: folderId.value,
newName: values.newName,
};
//
const data = await orgPosGroup(query);
if (data) {
setModalProps({ confirmLoading: true });
closeModal();
emit('success');
return createMessage.success('成功');
} else {
return createMessage.error('失败');
}
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

@ -0,0 +1,144 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="压缩" @ok="handleSubmit">
<div class="ziplist">
待压缩文件
<div class="zip" v-for="(zip, index) in zipS" :key="zip.id">
<span>{{ zip.name }}</span>
<CloseOutlined @click="deleteZip(index)" />
</div>
</div>
<div style="height: 200px; border: 1px solid black; padding-top: 10px">
压缩文件存放目录
<a-tree
:tree-data="treeData"
:field-names="{
key: 'id',
title: 'name',
}"
v-model:selectedKeys="selectedKeys"
default-expand-all
:height="200"
:showIcon="true"
:autoExpandParent="true"
:defaultExpandAll="true"
>
<template #icon="{ children }">
<template v-if="children">
<FolderOutlined />
</template>
</template>
</a-tree>
</div>
<div style="height: 80px; padding: 10px">
<BasicForm @register="registerForm" />
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { CloseOutlined, FolderOutlined } from '@ant-design/icons-vue';
import { BasicForm, useForm } from '@/components/Form';
import { compressFileSchema } from '../modal.data';
import { orgPosGroup } from '@/api/demo/system';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
const emit = defineEmits(['success']);
//
const compressName = ref('');
//
const zipS: any = ref([]);
//
function deleteZip(index: number) {
if (zipS.value.length !== 1) {
zipS.value.splice(index, 1);
} else {
createMessage.info('至少要保留一个文件');
}
}
// form
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100,
baseColProps: { span: 24 },
schemas: compressFileSchema,
showActionButtonGroup: false,
});
//
const selectedKeys: any = ref<string[]>();
//
const treeData = ref([]);
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields();
setModalProps({ confirmLoading: false });
//
treeData.value = filterTreeByType(data.tableData);
zipS.value = data.record.map((item) => item);
});
//
async function handleSubmit() {
try {
const values = await validate();
let query = {
zipS: zipS.value.map((item) => item.id),
key: selectedKeys.value[0],
compressName: values.compressName,
};
//
const data = await orgPosGroup(query);
if (data) {
setModalProps({ confirmLoading: true });
closeModal();
emit('success');
return createMessage.success('成功');
} else {
return createMessage.error('失败');
}
} finally {
setModalProps({ confirmLoading: false });
}
}
//
function filterTreeByType(tree) {
function recurse(nodes) {
const filteredNodes: any = [];
for (let node of nodes) {
// folder
if (node.type === 'folder') {
//
const newNode = { ...node };
// children
if (node.children && node.children.length > 0) {
const children = recurse(node.children);
newNode.children = children;
} else {
newNode.children = []; // children
}
// folder push
filteredNodes.push(newNode);
}
}
return filteredNodes;
}
return recurse(tree);
}
</script>
<style lang="scss" scoped>
.ziplist {
height: 200px;
border: 1px solid black;
}
.zip {
padding-left: 10px;
padding-right: 10px;
display: flex;
justify-content: space-between; /* 让子元素分布在两侧 */
align-items: center; /* 垂直居中对齐 */
width: 100%;
}
</style>

@ -0,0 +1,98 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="移动文件" @ok="handleSubmit">
<a-tree
:tree-data="treeData"
:field-names="{
key: 'id',
title: 'name',
}"
v-model:selectedKeys="selectedKeys"
default-expand-all
:height="300"
:showIcon="true"
:autoExpandParent="true"
:defaultExpandAll="true"
>
<template #icon="{ children }">
<template v-if="children">
<FolderOutlined />
</template>
</template>
</a-tree>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { FolderOutlined } from '@ant-design/icons-vue';
import { orgPosGroup } from '@/api/demo/system';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
const emit = defineEmits(['success']);
// id
const moveIds: any = ref([]);
const selectedKeys: any = ref<string[]>();
const treeData = ref([]);
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ confirmLoading: false });
//
treeData.value = filterTreeByType(data.tableData);
moveIds.value = data.record.map((item) => item);
});
//
async function handleSubmit() {
try {
let ids = moveIds.value.map((item) => item.id);
let key = selectedKeys.value[0];
if (!ids.includes(key)) {
let query = {
moveIds: ids,
key: key,
};
//
const data = await orgPosGroup(query);
if (data) {
setModalProps({ confirmLoading: true });
closeModal();
emit('success');
return createMessage.success('成功');
} else {
return createMessage.error('失败');
}
} else {
return createMessage.warn('目标目录不能包含选择目录');
}
} finally {
setModalProps({ confirmLoading: false });
}
}
//
function filterTreeByType(tree) {
function recurse(nodes) {
const filteredNodes: any = [];
for (let node of nodes) {
// folder
if (node.type === 'folder') {
//
const newNode = { ...node };
// children
if (node.children && node.children.length > 0) {
const children = recurse(node.children);
newNode.children = children;
} else {
newNode.children = []; // children
}
// folder push
filteredNodes.push(newNode);
}
}
return filteredNodes;
}
return recurse(tree);
}
</script>

@ -0,0 +1,64 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
title="重命名"
height="100"
@ok="handleSubmit"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { BasicForm, useForm } from '@/components/Form';
import { renameSchema } from '../modal.data';
import { orgPosGroup } from '@/api/demo/system';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
const emit = defineEmits(['success']);
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 100,
baseColProps: { span: 24 },
schemas: renameSchema,
showActionButtonGroup: false,
});
// id
let id = ref();
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields();
setModalProps({ confirmLoading: false });
id.value = data.record?.id;
setFieldsValue({
...data.record,
});
});
//
async function handleSubmit() {
try {
const values = await validate();
let query = {
id: id.value,
name: values.name,
};
//
const data = await orgPosGroup(query);
if (data) {
setModalProps({ confirmLoading: true });
closeModal();
emit('success');
return createMessage.success('重命名成功');
} else {
return createMessage.error('重命名失败');
}
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

@ -0,0 +1,4 @@
export { default as AddFolderModal } from './AddFolderModal.vue';
export { default as MoveFileModal } from './MoveFileModal.vue';
export { default as CompressFileModal } from './CompressFileModal.vue';
export { default as RenameModal } from './RenameModal.vue';

@ -0,0 +1,261 @@
<template>
<div
style="position: relative; height: 100%"
ref="vChartRef"
:id="'mars3d-container-' + name"
></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as mars3d from 'mars3d';
import { useModal } from '@/components/Modal';
import { EventBus } from '@/utils/eventBus';
import axios from 'axios';
const [editModal, { openModal: openEidtModal }] = useModal();
let map: mars3d.Map; //
let mapIns: any = null;
let markers: any = [];
let AMapIns: any = null;
const vChartRef = ref<HTMLElement>();
const props = defineProps(['nowPreviewRecord']);
onMounted(() => {
let options = {
scene: {
center: {
lat: props.nowPreviewRecord.lat,
lng: props.nowPreviewRecord.lng,
alt: 8306.3,
heading: 360,
pitch: -45,
},
scene3DOnly: false,
shadows: false,
removeDblClick: true,
sceneMode: 3,
showSun: true,
showMoon: true,
showSkyBox: true,
showSkyAtmosphere: true,
fog: true,
fxaa: true,
requestRenderMode: true,
contextOptions: {
requestWebgl1: false,
webgl: {
preserveDrawingBuffer: true,
alpha: false,
stencil: true,
powerPreference: 'high-performance',
},
},
globe: {
depthTestAgainstTerrain: false,
baseColor: '#546a53',
showGroundAtmosphere: true,
enableLighting: false,
},
cameraController: {
zoomFactor: 3,
minimumZoomDistance: 1,
maximumZoomDistance: 50000000,
enableRotate: true,
enableTranslate: true,
enableTilt: true,
enableZoom: true,
enableCollisionDetection: true,
minimumCollisionTerrainHeight: 15000,
},
},
control: {
homeButton: false,
baseLayerPicker: false,
sceneModePicker: false,
vrButton: false,
fullscreenButton: false,
navigationHelpButton: false,
animation: false,
timeline: false,
infoBox: false,
geocoder: false,
selectionIndicator: false,
showRenderLoopErrors: true,
contextmenu: {
hasDefault: true,
},
mouseDownView: true,
zoom: {
insertIndex: 1,
},
},
method: {
templateValues: {
dataServer: '//data.mars3d.cn',
gltfServerUrl: '//data.mars3d.cn/gltf',
},
},
terrain: {
url: '//data.mars3d.cn/terrain',
show: true,
clip: true,
},
basemaps: [
{
id: 10,
name: '地图底图',
type: 'group',
opacity: 1,
},
{
id: 2021,
pid: 10,
name: '天地图影像',
icon: 'https://data.mars3d.cn/img/thumbnail/basemap/tdt_img.png',
type: 'group',
layers: [
{
name: '底图',
type: 'tdt',
layer: 'img_d',
eventParent: {
id: 2021,
pid: 10,
name: '天地图影像',
icon: 'https://data.mars3d.cn/img/thumbnail/basemap/tdt_img.png',
type: 'group',
layers: [
{
name: '底图',
type: 'tdt',
layer: 'img_d',
show: true,
},
{
name: '注记',
type: 'tdt',
layer: 'img_z',
show: true,
},
],
show: true,
},
private: false,
id: 'm-a57ecb7d-ba05-47a3-b1be-2e28411a5954',
opacity: 1,
pid: 2021,
parent: {
id: 2021,
pid: 10,
name: '天地图影像',
icon: 'https://data.mars3d.cn/img/thumbnail/basemap/tdt_img.png',
type: 'group',
layers: [
{
name: '底图',
type: 'tdt',
layer: 'img_d',
show: true,
},
{
name: '注记',
type: 'tdt',
layer: 'img_z',
show: true,
},
],
show: true,
},
zIndex: 1,
},
{
name: '注记',
type: 'tdt',
layer: 'img_z',
eventParent: {
id: 2021,
pid: 10,
name: '天地图影像',
icon: 'https://data.mars3d.cn/img/thumbnail/basemap/tdt_img.png',
type: 'group',
layers: [
{
name: '底图',
type: 'tdt',
layer: 'img_d',
show: true,
},
{
name: '注记',
type: 'tdt',
layer: 'img_z',
show: true,
},
],
show: true,
},
private: false,
id: 'm-671f9d42-dda7-45ec-9d0f-4259c915b2cb',
opacity: 1,
pid: 2021,
parent: {
id: 2021,
pid: 10,
name: '天地图影像',
icon: 'https://data.mars3d.cn/img/thumbnail/basemap/tdt_img.png',
type: 'group',
layers: [
{
name: '底图',
type: 'tdt',
layer: 'img_d',
show: true,
},
{
name: '注记',
type: 'tdt',
layer: 'img_z',
show: true,
},
],
show: true,
},
zIndex: 2,
},
],
show: true,
opacity: 1,
},
],
layers: [],
};
initMap(options);
});
const isFirstLoad = ref(true);
const initMap = (newData: any) => {
//
if (isFirstLoad.value) {
map = new mars3d.Map(vChartRef.value, newData);
} else {
//
map.setSceneOptions(newData.scene);
}
isFirstLoad.value = false;
};
function flyToPoint(data) {
map.flyToPoint(data, {
radius: 5000, //
duration: 4,
});
}
defineExpose({
flyToPoint,
});
</script>
<style lang="less" scoped></style>

@ -0,0 +1,54 @@
<template>
<div id="my-three"></div>
</template>
<script lang="ts" setup>
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { onMounted } from 'vue';
//
const scene = new THREE.Scene();
//
const geometry = new THREE.SphereGeometry(300, 50, 50);
const texture = new THREE.TextureLoader().load('./天空盒全景图.png');
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.BackSide, //
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
//
const width = window.innerWidth,
height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
//
camera.position.set(300, 300, 300);
//
camera.lookAt(0, 0, 0);
//
const axesHelper = new THREE.AxesHelper(200); //200
scene.add(axesHelper);
//WebGL
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height); //
renderer.render(scene, camera); //
const controls = new OrbitControls(camera, renderer.domElement); //
controls.addEventListener('change', () => {
renderer.render(scene, camera); //
});
onMounted(() => {
document.getElementById('my-three')?.appendChild(renderer.domElement);
});
</script>
<style lang="scss">
body {
margin: 0;
padding: 0;
}
</style>

@ -0,0 +1,10 @@
export { default as PreviewImage } from './previewImage.vue';
export { default as PreviewCanvas } from './previewCanvas.vue';
export { default as PreviewVideo } from './previewVideo.vue';
export { default as PreviewInformation } from './previewInformation.vue';
export { default as MonitorHK } from './video/monitorHK.vue';
export { default as MonitorLC } from './video/monitorLC.vue';
export { default as MonitorQX } from './video/monitorQX.vue';
export { default as MonitorTX } from './video/monitorTX.vue';
export { default as PanoViewer } from './PanoViewer.vue';
export { default as Map } from './Map.vue';

@ -0,0 +1,138 @@
<template>
<div class="modal">
<div class="title">
<div class="title-1">{{ props.nowPreviewRecord.name }}</div>
<div class="title-2">
{{
props.nowPreviewRecord.createTime +
' ' +
props.nowPreviewRecord.size +
' ' +
props.nowPreviewRecord.id
}}
</div>
</div>
<div class="closeButton">
<CloseOutlined @click="closeModal" style="font-size: 25px; color: white" />
</div>
<div class="mainBody">
<div class="imgOrVideo">
<!-- 图片 -->
<div class="image" v-if="props.nowPreviewRecord.type == 'img'">
<!-- <PreviewCanvas
:nowPreviewRecord="props.nowPreviewRecord"
:previewRecordList="props.previewRecordList"
@chooseNowPreviewRecord="chooseNowPreviewRecord"
@reloadTable="reloadTable"
/> -->
<PreviewImage
:nowPreviewRecord="props.nowPreviewRecord"
:previewRecordList="props.previewRecordList"
@chooseNowPreviewRecord="chooseNowPreviewRecord"
@reloadTable="reloadTable"
/>
</div>
<!-- VR全景 -->
<!-- <div v-if="props.nowPreviewRecord.type == 'vr'">
<PanoViewer />
</div> -->
<!-- 视频 -->
<div class="video" v-if="props.nowPreviewRecord.type == 'video'">
<PreviewVideo :nowPreviewRecord="props.nowPreviewRecord" />
</div>
</div>
<div class="information">
<PreviewInformation :nowPreviewRecord="props.nowPreviewRecord" @reloadTable="reloadTable" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { CloseOutlined } from '@ant-design/icons-vue';
import { PreviewImage } from './preview';
import { PreviewCanvas } from './preview';
import { PreviewVideo } from './preview';
import { PreviewInformation } from './preview';
import { PanoViewer } from './preview';
const props = defineProps(['nowPreviewRecord', 'previewRecordList']);
const emit = defineEmits(['closeModal', 'chooseNowPreviewRecord', 'reloadTable']);
//
function chooseNowPreviewRecord(value) {
emit('chooseNowPreviewRecord', value);
}
//
function reloadTable() {
emit('reloadTable');
}
//
function closeModal() {
emit('closeModal');
}
</script>
<style lang="less">
.modal {
width: 100%;
height: 920px;
//
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE/Edge */
user-select: none;
}
.title {
pointer-events: none;
position: absolute;
top: 10px;
left: 15px;
z-index: 100;
.title-1 {
font-size: 20px;
color: #ffffff;
}
.title-2 {
font-size: 16px;
color: #f0f3f3;
}
}
.closeButton {
position: absolute;
top: 10px;
right: 15px;
z-index: 50;
}
.mainBody {
width: 100%;
height: 100%;
display: flex;
position: absolute;
top: 0px;
left: 0px;
.imgOrVideo {
width: 80%;
height: 920px;
background: #101010;
.image {
// display: flex;
// align-items: center;
position: relative;
justify-content: center;
width: 100%;
height: 100%;
}
}
.information {
position: relative;
width: 20%;
height: 100%;
background: #1c1c1c;
}
}
</style>

@ -0,0 +1,583 @@
<template>
<div class="image">
<div class="canvas">
<canvas
width="1300"
height="800"
id="canvas"
:style="{
transform: `scale(${scale}) rotate(${rotationAngle}deg)`,
transition: 'transform 0.2s',
}"
></canvas>
</div>
<div class="bottom">
<div class="buttonList">
<!-- 放大 -->
<div class="button"> <ZoomInOutlined @click="zoomIn" /> </div>
<!-- 缩小 -->
<div class="button"> <ZoomOutOutlined @click="zoomOut" /> </div>
<!-- 顺时针旋转 -->
<div class="button"> <RotateRightOutlined @click="rotateClockwise" /> </div>
<!-- 逆时针旋转 -->
<div class="button"> <RotateLeftOutlined @click="rotateCounterClockwise" /> </div>
|
<!-- 刷新 -->
<div class="button"> <RedoOutlined @click="refreshCanvas" /> </div>
<!-- 涂鸦 -->
<div class="tipChoose" v-if="graffitiFlag">
<n-color-picker size="small" :modes="['rgb']" v-model:value="graffitiColor" />
<a-slider v-model:value="graffitiWidth" :min="1" :max="10" />
</div>
<div class="button" :style="{ background: graffitiFlag ? 'blue' : '' }">
<HighlightOutlined
:style="{
color: graffitiColor,
}"
@click="
graffitiFlag = !graffitiFlag;
rectFlag = false;
textboxFlag = false;
"
/>
</div>
<!-- 标注 -->
<div class="tipChoose" v-if="rectFlag">
<n-color-picker size="small" :modes="['rgb']" v-model:value="rectColor" />
</div>
<div class="button" :style="{ background: rectFlag ? 'blue' : '' }">
<BorderOutlined
@click="
graffitiFlag = false;
rectFlag = !rectFlag;
textboxFlag = false;
"
/>
</div>
<!-- 文字 -->
<div class="tipChoose" v-if="textboxFlag">
<n-color-picker size="small" :modes="['rgb']" v-model:value="textboxColor" />
<a-slider v-model:value="textboxFontSize" :min="10" :max="50" />
</div>
<div class="button" :style="{ background: textboxFlag ? 'blue' : '' }">
<FontSizeOutlined
@click="
graffitiFlag = false;
rectFlag = false;
textboxFlag = !textboxFlag;
"
/>
</div>
<!-- 保存 -->
<div class="button"> <CheckOutlined @click="checkCanvas" /> </div>
<!-- 删除 -->
<div class="button"> <CloseOutlined @click="deleteCanvas" /> </div>
<!-- 隐藏or显示涂鸦和标签 -->
<div class="button">
<EyeOutlined @click="hideOrShowGraffiti(false)" v-if="hideOrShowGraffitiFlag" />
<EyeInvisibleOutlined @click="hideOrShowGraffiti(true)" v-if="!hideOrShowGraffitiFlag" />
<div style="position: absolute; bottom: 0px; right: 0px; font-size: 10px">涂鸦</div>
</div>
<div class="button">
<EyeOutlined @click="hideOrShowTextbox(false)" v-if="hideOrShowTextboxFlag" />
<EyeInvisibleOutlined @click="hideOrShowTextbox(true)" v-if="!hideOrShowTextboxFlag" />
<div style="position: absolute; bottom: 0px; right: 0px; font-size: 10px">标签</div>
</div>
|
<!-- 复制到剪贴板 -->
<div class="button">
<ExportOutlined @click="copyToClipboard(props.nowPreviewRecord.url)" />
</div>
<!-- 下载 -->
<div class="button">
<DownloadOutlined @click="fetchAndDownloadImage(props.nowPreviewRecord.url)" />
</div>
</div>
<div class="imageList">
<div v-for="li in props.previewRecordList" :key="li.id" @click="chooseNowPreviewRecord(li)">
<div
:class="li.id == props.nowPreviewRecord.id ? 'bottom_div_choose' : 'bottom_div'"
v-if="li.type == 'img'"
>
<img :src="li.url" :width="60" :height="40" />
</div>
</div>
</div>
</div>
<div class="leftButton" @click="clickLeftOrRightButton('left')">
<LeftOutlined style="color: #ffffff; font-size: 20px" />
</div>
<div class="rightButton" @click="clickLeftOrRightButton('right')">
<RightOutlined style="color: #ffffff; font-size: 20px" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, computed } from 'vue';
import { fabric } from 'fabric';
import {
CloseOutlined,
RightOutlined,
LeftOutlined,
ZoomOutOutlined,
ZoomInOutlined,
RotateLeftOutlined,
RotateRightOutlined,
RedoOutlined,
FileImageOutlined,
ExportOutlined,
DownloadOutlined,
DeleteOutlined,
HighlightOutlined,
CheckOutlined,
FontSizeOutlined,
BorderOutlined,
EyeOutlined,
EyeInvisibleOutlined,
} from '@ant-design/icons-vue';
import { useMessage } from '@/hooks/web/useMessage';
import { json } from 'stream/consumers';
const { createConfirm, createMessage } = useMessage();
const props = defineProps(['nowPreviewRecord', 'previewRecordList']);
const emit = defineEmits(['chooseNowPreviewRecord', 'reloadTable']);
//
function clickLeftOrRightButton(direction) {
const list = props.previewRecordList.filter((item) => item.type == 'img');
for (let index = 0; index < list.length; index++) {
if (list[index].id == props.nowPreviewRecord.id) {
if (direction == 'left') {
if (index == 0) {
chooseNowPreviewRecord(list[list.length - 1]);
} else {
chooseNowPreviewRecord(list[index - 1]);
}
}
if (direction == 'right') {
if (index == list.length - 1) {
chooseNowPreviewRecord(list[0]);
} else {
chooseNowPreviewRecord(list[index + 1]);
}
}
}
}
}
//
function chooseNowPreviewRecord(value) {
emit('chooseNowPreviewRecord', value);
//
scale.value = 1;
rotationAngle.value = 0;
setBackgroudUrl(value);
}
//
function reloadTable() {
emit('reloadTable');
}
//
const nowCanvasJson = ref(props.nowPreviewRecord.canvasJson);
let canvas: any = null;
//
function setBackgroudUrl(value) {
nowCanvasJson.value = value.canvasJson;
canvas.loadFromJSON(value.canvasJson);
fabric.Image.fromURL(value.url, (img) => {
//
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), {
scaleX: canvas.width / img.width,
scaleY: canvas.height / img.height,
});
});
}
// ---------------------------------------------------------------
const graffitiFlag = ref(false);
const graffitiColor = ref('#ffffff');
const graffitiWidth = ref(1);
watch(
() => (graffitiFlag.value, graffitiColor.value, graffitiWidth.value),
() => {
setGraffiti();
},
{
deep: true,
},
);
//
function setGraffiti() {
scale.value = 1;
rotationAngle.value = 0;
//
canvas.isDrawingMode = graffitiFlag.value;
//
canvas.freeDrawingBrush.color = graffitiColor.value;
//
canvas.freeDrawingBrush.width = graffitiWidth.value;
}
// ---------------------------------------------------------------
const textboxFlag = ref(false);
const textboxColor = ref('#ffffff');
const textboxFontSize = ref(20);
watch(
() => textboxFlag.value,
() => {
if (textboxFlag.value) {
setTextbox();
}
},
{
deep: true,
},
);
watch(
() => (textboxColor.value, textboxFontSize.value),
() => {
setTextboxStyle();
},
{
deep: true,
},
);
//
function setTextbox() {
scale.value = 1;
rotationAngle.value = 0;
addClickEvent();
}
//
function setTextboxStyle() {
const activeObj = canvas.getActiveObject();
if (activeObj) {
console.log(activeObj);
activeObj.fill = textboxColor.value;
activeObj.fontSize = textboxFontSize.value;
canvas.setActiveObject(activeObj);
}
}
// ---------------------------------------------------------------
const rectFlag = ref(false);
const rectColor = ref('#ffffff');
watch(
() => rectFlag.value,
() => {
if (rectFlag.value) {
setRect();
}
},
{
deep: true,
},
);
watch(
() => rectColor.value,
() => {
setRectStyle();
},
{
deep: true,
},
);
//
function setRect() {
scale.value = 1;
rotationAngle.value = 0;
addClickEvent();
}
//
function setRectStyle() {
const activeObj = canvas.getActiveObject();
if (activeObj) {
activeObj.stroke = rectColor.value;
canvas.setActiveObject(activeObj);
}
}
//
function addClickEvent() {
canvas.on('mouse:down', (options) => {
if (textboxFlag.value) {
//
const textbox = new fabric.Textbox('标注', {
width: 200,
fill: textboxColor.value,
fontSize: textboxFontSize.value,
top: options.absolutePointer.y,
left: options.absolutePointer.x,
});
canvas.add(textbox);
removeClickEvent();
}
if (rectFlag.value) {
//
const rect = new fabric.Rect({
top: options.absolutePointer.y,
left: options.absolutePointer.x,
fill: '#ffffff00',
stroke: rectColor.value,
width: 100,
height: 100,
});
canvas.add(rect);
removeClickEvent();
}
});
}
//
function removeClickEvent() {
canvas.off('mouse:down');
}
// -----------------------------------
const scale = ref(1);
//
function zoomIn() {
if (scale.value < 3) {
// 3
scale.value += 0.1;
}
}
//
function zoomOut() {
if (scale.value > 0.5) {
// 0.5
scale.value -= 0.1;
}
}
// -----------------------------------
const rotationAngle = ref(0);
//
function rotateClockwise() {
rotationAngle.value += 90; // 90
}
//
function rotateCounterClockwise() {
rotationAngle.value -= 90; // -90
}
// -----------------------------------
function checkCanvas() {
nowCanvasJson.value = JSON.stringify(canvas);
props.nowPreviewRecord.canvasJson = JSON.stringify(canvas);
textboxFlag.value = false;
graffitiFlag.value = false;
rectFlag.value = false;
console.log(JSON.stringify(canvas));
createMessage.success('保存成功!');
}
//
function deleteCanvas() {
const activeObj = canvas.getActiveObject();
canvas.remove(activeObj);
textboxFlag.value = false;
graffitiFlag.value = false;
rectFlag.value = false;
}
//
function refreshCanvas() {
scale.value = 1;
rotationAngle.value = 0;
setBackgroudUrl(props.nowPreviewRecord);
}
// -----------------------------------
const copyToClipboard = async (url) => {
try {
await navigator.clipboard.writeText(url);
createMessage.success('图片链接已复制到剪贴板');
} catch (err) {
createMessage.error('无法复制图片链接');
}
};
//
async function fetchAndDownloadImage(url) {
try {
const response = await fetch(url, {
mode: 'cors',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const urlObject = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = urlObject;
link.download = props.nowPreviewRecord.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(urlObject);
} catch (error) {
console.error('Error downloading image:', error);
}
}
//
const hideOrShowGraffitiFlag = ref(true);
function hideOrShowGraffiti(value) {
hideOrShowGraffitiFlag.value = value;
let json = JSON.parse(nowCanvasJson.value);
json.objects.forEach((item) => {
if (item.type === 'path') {
item.visible = value;
}
});
nowCanvasJson.value = JSON.stringify(json);
canvas.loadFromJSON(nowCanvasJson.value);
}
//
const hideOrShowTextboxFlag = ref(true);
function hideOrShowTextbox(value) {
hideOrShowTextboxFlag.value = value;
let json = JSON.parse(nowCanvasJson.value);
json.objects.forEach((item) => {
if (item.type === 'textbox' || item.type === 'rect') {
item.visible = value;
}
});
nowCanvasJson.value = JSON.stringify(json);
canvas.loadFromJSON(nowCanvasJson.value);
}
onMounted(() => {
canvas = new fabric.Canvas('canvas');
//
setBackgroudUrl(props.nowPreviewRecord);
// json
if (!nowCanvasJson.value) {
nowCanvasJson.value = JSON.stringify(canvas);
}
//
setGraffiti();
});
</script>
<style lang="less" scoped>
.image {
position: relative;
width: 100%;
height: 900px;
display: block;
}
.canvas {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 800px;
}
.bottom {
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
height: 100px;
background: #1c1c1c;
display: block;
.buttonList {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 40px;
.button {
position: relative;
padding-left: 10px;
padding-right: 10px;
color: #ffffff;
font-size: 22px;
}
}
.imageList {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60px;
.bottom_div {
padding: 5px;
margin-left: 5px;
margin-right: 5px;
}
.bottom_div_choose {
padding: 4px;
border: 1px solid yellow;
margin-left: 5px;
margin-right: 5px;
}
}
}
.leftButton {
position: absolute;
left: 40px;
top: 45%;
z-index: 1000;
background: #9c9c9c;
border-radius: 50px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.rightButton {
position: absolute;
right: 40px;
top: 45%;
z-index: 1000;
background: #9c9c9c;
border-radius: 50px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.eyeButton {
position: absolute;
right: 40px;
top: 45px;
background: #ffffff;
width: 50px;
height: 100px;
}
.tipChoose {
position: absolute;
background: #1c1c1c;
color: #ffffff;
padding: 5px;
top: 35px;
width: 100px;
}
</style>

@ -0,0 +1,893 @@
<template>
<div class="image">
<div class="canvas">
<div
ref="mouseCanvasRef"
@mousedown="onMouseDown"
:style="{
position: 'relative',
transform: `scale(${scale}) rotate(${rotationAngle}deg)`,
transition: 'transform 0.2s',
width: `${getImageWidthAndHeight[0]}px`,
height: `${getImageWidthAndHeight[1]}px`,
background: `url(${props.nowPreviewRecord.url}) no-repeat`,
}"
>
<!-- 创建的矩形 -->
<div
v-if="hideOrShowGraffitiFlag"
v-for="(rect, index) in graffitis"
:key="index"
:style="{
position: 'absolute',
left: rect.x + 'px',
top: rect.y + 'px',
width: rect.width + 'px',
height: rect.height + 'px',
zIndex: rect.status == 'edit' ? 1000 : 201,
}"
>
<!-- 文字标签 -->
<div
v-if="nowGraffiti == index || rect.text"
:style="{
position: 'absolute',
left: '0px',
top: rect.height + 'px',
width: '220px',
height: 30 + 'px',
background: nowGraffiti == index ? '#ffffff' : '#ffffff00',
}"
>
<div style="display: block" v-if="nowGraffiti == index">
<a-input
v-model:value="rect.text"
style="width: 110px; height: 30px; margin-right: 10px"
/>
<CheckOutlined
style="margin-right: 10px; color: green"
@click="
nowGraffiti = -1;
rect.status = 'success';
"
/>
<CloseOutlined
style="margin-right: 10px; color: red"
@click="
rect.text ? (rect.text = graffitisClone[index].text) : (rect.text = '');
nowGraffiti = -1;
rect.status = 'success';
"
/>
<DeleteOutlined
style="margin-right: 10px"
@click="
graffitis.splice(index, 1);
nowGraffiti = -1;
"
/>
<a-popover placement="top">
<template #content>
<div style="display: flex; gap: 5px">
<div
class="popoverClass"
v-for="color in ['#2D8CF0', '#19BE6B', '#FFBB00', '#E23C39', '#B620E0']"
:key="color"
:style="{ background: color }"
@click="rect.color = color"
>
<CheckOutlined v-if="rect.color == color" style="color: white" />
</div>
</div>
</template>
<FontColorsOutlined style="" />
</a-popover>
</div>
<div
v-if="nowGraffiti != index"
style="
font-size: 20px;
font-weight: bold;
-webkit-text-stroke: 1px white;
width: fit-content;
"
:style="{
textDecoration: rect.status == 'mouse' ? 'underline' : '',
}"
@mouseenter="rect.status != 'edit' ? (rect.status = 'mouse') : ''"
@mouseleave="rect.status == 'mouse' ? (rect.status = 'success') : ''"
@click="
rect.status = 'edit';
graffitisClone = cloneDeep(graffitis);
nowGraffiti = index;
"
>
{{ rect.text }}
</div>
</div>
<!-- -->
<div
:style="{
position: 'absolute',
left: rect.status == 'edit' ? '6px' : '0px',
top: '0px',
width: rect.status == 'edit' ? `${rect.width - 14}px` : `${rect.width}px`,
height: '4px',
background: `${rect.color}`,
outline: rect.status != 'success' ? `2px solid #ffffff` : '',
}"
@mouseenter="mouseenter(rect, 'top')"
@mouseleave="mouseleave(rect)"
@click="
rect.status = 'edit';
nowGraffiti = index;
"
@mousedown="funMouseDownEdit($event, index, 'top')"
/>
<!-- -->
<div
:style="{
position: 'absolute',
left: rect.status == 'edit' ? '6px' : '0px',
bottom: `0px`,
width: rect.status == 'edit' ? `${rect.width - 14}px` : `${rect.width}px`,
height: '4px',
background: `${rect.color}`,
outline: rect.status != 'success' ? `2px solid #ffffff` : '',
}"
@mouseenter="mouseenter(rect, 'bottom')"
@mouseleave="mouseleave(rect)"
@click="
rect.status = 'edit';
nowGraffiti = index;
"
@mousedown="funMouseDownEdit($event, index, 'bottom')"
/>
<!-- -->
<div
:style="{
position: 'absolute',
right: '0px',
top: rect.status == 'edit' ? '6px' : '0px',
width: '4px',
height: rect.status == 'edit' ? `${rect.height - 14}px` : `${rect.height}px`,
background: `${rect.color}`,
outline: rect.status != 'success' ? `2px solid #ffffff` : '',
}"
@mouseenter="mouseenter(rect, 'right')"
@mouseleave="mouseleave(rect)"
@click="
rect.status = 'edit';
nowGraffiti = index;
"
@mousedown="funMouseDownEdit($event, index, 'right')"
/>
<!-- -->
<div
:style="{
position: 'absolute',
left: '0px',
top: rect.status == 'edit' ? '6px' : '0px',
width: '4px',
height: rect.status == 'edit' ? `${rect.height - 14}px` : `${rect.height}px`,
background: `${rect.color}`,
outline: rect.status != 'success' ? `2px solid #ffffff` : '',
}"
@mouseenter="mouseenter(rect, 'left')"
@mouseleave="mouseleave(rect)"
@click="
rect.status = 'edit';
nowGraffiti = index;
"
@mousedown="funMouseDownEdit($event, index, 'left')"
/>
<!-- 左上 -->
<div
v-if="rect.status == 'edit'"
:style="{
position: 'absolute',
left: '-4px',
top: '-4px',
width: '12px',
height: '12px',
background: `#ffffff`,
}"
@mouseenter="mouseenter(rect, 'leftTop')"
@mouseleave="mouseleave(rect)"
@mousedown="funMouseDownEdit($event, index, 'leftTop')"
/>
<!-- 右下 -->
<div
v-if="rect.status == 'edit'"
:style="{
position: 'absolute',
right: '-4px',
bottom: '-4px',
width: '12px',
height: '12px',
background: `#ffffff`,
}"
@mouseenter="mouseenter(rect, 'rightBottom')"
@mouseleave="mouseleave(rect)"
@mousedown="funMouseDownEdit($event, index, 'rightBottom')"
/>
<!-- 右上 -->
<div
v-if="rect.status == 'edit'"
:style="{
position: 'absolute',
right: '-4px',
top: '-4px',
width: '12px',
height: '12px',
background: `#ffffff`,
}"
@mouseenter="mouseenter(rect, 'rightTop')"
@mouseleave="mouseleave(rect)"
@mousedown="funMouseDownEdit($event, index, 'rightTop')"
/>
<!-- 左下 -->
<div
v-if="rect.status == 'edit'"
:style="{
position: 'absolute',
left: '-4px',
bottom: '-4px',
width: '12px',
height: '12px',
background: `#ffffff`,
}"
@mouseenter="mouseenter(rect, 'leftBottom')"
@mouseleave="mouseleave(rect)"
@mousedown="funMouseDownEdit($event, index, 'leftBottom')"
/>
</div>
</div>
</div>
<div class="bottom">
<div class="buttonList">
<!-- 放大 -->
<div class="button"> <ZoomInOutlined @click="zoomIn" /> </div>
<!-- 缩小 -->
<div class="button"> <ZoomOutOutlined @click="zoomOut" /> </div>
<!-- 顺时针旋转 -->
<div class="button"> <RotateRightOutlined @click="rotateClockwise" /> </div>
<!-- 逆时针旋转 -->
<div class="button"> <RotateLeftOutlined @click="rotateCounterClockwise" /> </div>
<!-- 刷新 -->
<div class="button"> <RedoOutlined @click="refresh" /> </div>
|
<!-- 全屏 -->
<!-- <div class="button"> <RedoOutlined @click="refresh" /> </div> -->
|
<!-- 复制到剪贴板 -->
<div class="button">
<ExportOutlined @click="copyToClipboard(props.nowPreviewRecord.url)" />
</div>
<!-- 下载 -->
<div class="button">
<DownloadOutlined @click="fetchAndDownloadImage(props.nowPreviewRecord.url)" />
</div>
<!-- 删除 -->
<div class="button"> <DeleteOutlined @click="deleteCanvas" /> </div>
</div>
<div class="imageList">
<div v-for="li in props.previewRecordList" :key="li.id" @click="chooseNowPreviewRecord(li)">
<div
:class="li.id == props.nowPreviewRecord.id ? 'bottom_div_choose' : 'bottom_div'"
v-if="li.type == 'img'"
>
<img :src="li.url" :width="60" :height="40" />
</div>
</div>
</div>
</div>
<!-- 上一张下一张图片 -->
<div class="leftButton" @click="clickLeftOrRightButton('left')">
<LeftOutlined style="color: #ffffff; font-size: 20px" />
</div>
<div class="rightButton" @click="clickLeftOrRightButton('right')">
<RightOutlined style="color: #ffffff; font-size: 20px" />
</div>
<!-- 涂鸦颜色选择-提示 -->
<div
class="graffitiButton"
@click="setGraffiti"
:style="graffitiFlag ? 'outline: 2px solid #2B85E4' : ''"
>
<a-popover placement="left">
<template #content>
<div style="display: flex; gap: 5px">
<div
class="popoverClass"
v-for="color in ['#2D8CF0', '#19BE6B', '#FFBB00', '#E23C39', '#B620E0']"
:key="color"
:style="{ background: color }"
@click="graffitiColor = color"
>
<CheckOutlined v-if="graffitiColor == color" style="color: white" />
</div>
</div>
</template>
<EditOutlined
:style="{
color: graffitiColor,
fontSize: '20px',
}"
/>
</a-popover>
</div>
<!-- 隐藏or显示涂鸦和标签 -->
<div class="showTextboxClass">
<div class="button">
<EyeOutlined @click="hideOrShowGraffiti(false)" v-if="hideOrShowGraffitiFlag" />
<EyeInvisibleOutlined @click="hideOrShowGraffiti(true)" v-if="!hideOrShowGraffitiFlag" />
<div
style="
position: absolute;
bottom: 0px;
right: 0px;
font-size: 10px;
color: #000000;
pointer-events: none;
"
>
涂鸦
</div>
</div>
<div class="button">
<EyeOutlined @click="hideOrShowTextbox(false)" v-if="hideOrShowTextboxFlag" />
<EyeInvisibleOutlined @click="hideOrShowTextbox(true)" v-if="!hideOrShowTextboxFlag" />
<div
style="
position: absolute;
bottom: 0px;
right: 0px;
font-size: 10px;
color: #000000;
pointer-events: none;
"
>
标注
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, computed } from 'vue';
import {
CloseOutlined,
RightOutlined,
LeftOutlined,
ZoomOutOutlined,
ZoomInOutlined,
RotateLeftOutlined,
RotateRightOutlined,
ExportOutlined,
DownloadOutlined,
DeleteOutlined,
CheckOutlined,
EyeOutlined,
EyeInvisibleOutlined,
EditOutlined,
FontColorsOutlined,
RedoOutlined,
} from '@ant-design/icons-vue';
import { useMessage } from '@/hooks/web/useMessage';
import { cloneDeep } from 'lodash-es';
const { createConfirm, createMessage } = useMessage();
const props = defineProps(['nowPreviewRecord', 'previewRecordList']);
const emit = defineEmits(['chooseNowPreviewRecord', 'reloadTable']);
//
const getImageWidthAndHeight = computed(() => {
let width = 1300;
let height = 800;
if (props.nowPreviewRecord.width > 1300 || props.nowPreviewRecord.height > 800) {
if (props.nowPreviewRecord.width / 1300 > props.nowPreviewRecord.height / 800) {
width = 1300;
height = (props.nowPreviewRecord.height / props.nowPreviewRecord.width) * 1300;
} else {
height = 800;
width = (props.nowPreviewRecord.width / props.nowPreviewRecord.height) * 800;
}
} else {
width = props.nowPreviewRecord.width;
height = props.nowPreviewRecord.height;
}
return [width, height];
});
//
function clickLeftOrRightButton(direction) {
const list = props.previewRecordList.filter((item) => item.type == 'img');
graffitiFlag.value = false;
for (let index = 0; index < list.length; index++) {
if (list[index].id == props.nowPreviewRecord.id) {
if (direction == 'left') {
if (index == 0) {
chooseNowPreviewRecord(list[list.length - 1]);
} else {
chooseNowPreviewRecord(list[index - 1]);
}
}
if (direction == 'right') {
if (index == list.length - 1) {
chooseNowPreviewRecord(list[0]);
} else {
chooseNowPreviewRecord(list[index + 1]);
}
}
}
}
}
//
function chooseNowPreviewRecord(value) {
emit('chooseNowPreviewRecord', value);
//
scale.value = 1;
rotationAngle.value = 0;
}
//
function reloadTable() {
emit('reloadTable');
}
// -----------------------------------
const scale = ref(1);
//
function zoomIn() {
if (scale.value < 3) {
// 3
scale.value += 0.1;
}
}
//
function zoomOut() {
if (scale.value > 0.5) {
// 0.5
scale.value -= 0.1;
}
}
// -----------------------------------
const rotationAngle = ref(0);
//
function rotateClockwise() {
rotationAngle.value += 90; // 90
}
//
function rotateCounterClockwise() {
rotationAngle.value -= 90; // -90
}
//
function refresh() {
scale.value = 1;
rotationAngle.value = 0;
graffitiFlag.value = false;
}
//
function deleteCanvas() {}
// -----------------------------------
const copyToClipboard = async (url) => {
try {
await navigator.clipboard.writeText(url);
createMessage.success('图片链接已复制到剪贴板');
} catch (err) {
createMessage.error('无法复制图片链接');
}
};
//
async function fetchAndDownloadImage(url) {
try {
const response = await fetch(url, {
mode: 'cors',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const urlObject = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = urlObject;
link.download = props.nowPreviewRecord.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(urlObject);
} catch (error) {
console.error('Error downloading image:', error);
}
}
//
const hideOrShowGraffitiFlag = ref(true);
function hideOrShowGraffiti(value) {
hideOrShowGraffitiFlag.value = value;
}
//
const hideOrShowTextboxFlag = ref(true);
function hideOrShowTextbox(value) {
hideOrShowTextboxFlag.value = value;
}
// ---------------------------------------------------------------
const graffitiFlag = ref(false);
const graffitiColor = ref('#E23C39');
const nowGraffiti = ref(-1);
const nowMouseGraffiti = ref(0);
function setGraffiti() {
graffitiFlag.value = !graffitiFlag.value;
if (graffitiFlag.value) {
document.body.style.cursor = 'crosshair';
} else {
document.body.style.cursor = 'pointer';
}
}
//
const isDrawing = ref(false);
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
//
const graffitis: any = ref([]);
const graffitisClone: any = ref([]);
watch(
() => props.nowPreviewRecord.id,
() => {
graffitis.value = props.nowPreviewRecord.graffitiJson
? JSON.parse(props.nowPreviewRecord.graffitiJson)
: [];
},
{
deep: true,
immediate: true,
},
);
watch(
() => graffitis.value,
() => {
props.nowPreviewRecord.graffitiJson = JSON.stringify(graffitis.value);
},
{
deep: true,
},
);
const mouseCanvasRef = ref();
//
function onMouseDown(e) {
if (!graffitiFlag.value) return;
if (nowGraffiti.value != -1) return;
if (graffitis.value.findIndex((item) => item.status == 'mouse') != -1) return;
//
const rect = mouseCanvasRef.value.getBoundingClientRect();
startX = e.x - rect.x;
startY = e.y - rect.y;
isDrawing.value = true;
//
graffitis.value.push({
x: startX,
y: startY,
width: 0,
height: 0,
color: graffitiColor.value,
text: '',
status: 'edit',
});
graffitisClone.value = cloneDeep(graffitis.value);
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
//
function onMouseMove(e) {
const rect = mouseCanvasRef.value.getBoundingClientRect();
if (!isDrawing.value) return;
endX = e.x - rect.x;
endY = e.y - rect.y;
if (endX < 0 && endY < 0) {
endX = startX;
endY = startY;
}
setMouseData();
}
//
function onMouseUp(e) {
if (!isDrawing.value) return;
const rect = mouseCanvasRef.value.getBoundingClientRect();
endX = e.x - rect.x;
endY = e.y - rect.y;
if (endX < 0 && endY < 0) {
endX = startX;
endY = startY;
}
isDrawing.value = false;
if (e.x > rect.right || e.y > rect.bottom) {
graffitis.value.splice(graffitis.value.length - 1, 1);
nowGraffiti.value = -1;
} else {
setMouseData();
nowGraffiti.value = graffitis.value.length - 1;
}
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
}
//
function setMouseData() {
graffitis.value[graffitis.value.length - 1].x = Math.min(startX, endX);
graffitis.value[graffitis.value.length - 1].width = Math.abs(endX - startX);
graffitis.value[graffitis.value.length - 1].y = Math.min(startY, endY);
graffitis.value[graffitis.value.length - 1].height = Math.abs(endY - startY);
}
//------------------------------------------------------------------------
//
function mouseenter(rect, type) {
if (rect.status != 'edit') {
rect.status = 'mouse';
document.body.style.cursor = 'pointer';
}
if (rect.status == 'edit') {
if (type == 'top' || type == 'bottom') {
document.body.style.cursor = 'ns-resize';
}
if (type == 'left' || type == 'right') {
document.body.style.cursor = 'ew-resize';
}
if (type == 'leftTop' || type == 'rightBottom') {
document.body.style.cursor = 'nwse-resize';
}
if (type == 'leftBottom' || type == 'rightTop') {
document.body.style.cursor = 'nesw-resize';
}
}
}
//
function mouseleave(rect) {
if (rect.status == 'mouse' || rect.status == 'edit') {
if (rect.status == 'mouse') {
rect.status = 'success';
}
if (graffitiFlag.value) {
document.body.style.cursor = 'crosshair';
} else {
document.body.style.cursor = 'pointer';
}
}
}
//
const mouseEditType = ref('');
function funMouseDownEdit(e, index, type) {
//
graffitisClone.value = cloneDeep(graffitis.value);
const rect = mouseCanvasRef.value.getBoundingClientRect();
startX = e.x - rect.x;
startY = e.y - rect.y;
isDrawing.value = true;
nowGraffiti.value = index;
mouseEditType.value = type;
window.addEventListener('mousemove', funMouseMoveEdit);
window.addEventListener('mouseup', funMouseUpEdit);
}
//
function funMouseMoveEdit(e) {
const rect = mouseCanvasRef.value.getBoundingClientRect();
if (!isDrawing.value) return;
endX = e.x - rect.x;
endY = e.y - rect.y;
if (endX < 0 && endY < 0) {
endX = startX;
endY = startY;
}
funSetMouseDataEdit();
}
//
function funMouseUpEdit(e) {
if (!isDrawing.value) return;
const rect = mouseCanvasRef.value.getBoundingClientRect();
endX = e.x - rect.x;
endY = e.y - rect.y;
if (endX < 0 && endY < 0) {
endX = startX;
endY = startY;
}
isDrawing.value = false;
if (e.x > rect.right || e.y > rect.bottom) {
graffitis.value[nowGraffiti.value] = graffitisClone.value[nowGraffiti.value];
} else {
funSetMouseDataEdit();
}
window.removeEventListener('mousemove', funMouseMoveEdit);
window.removeEventListener('mouseup', funMouseUpEdit);
}
//
function funSetMouseDataEdit() {
if (['top', 'leftTop', 'rightTop'].includes(mouseEditType.value)) {
graffitis.value[nowGraffiti.value].height =
graffitis.value[nowGraffiti.value].height + graffitis.value[nowGraffiti.value].y - endY;
graffitis.value[nowGraffiti.value].y = endY;
}
if (['bottom', 'leftBottom', 'rightBottom'].includes(mouseEditType.value)) {
graffitis.value[nowGraffiti.value].height = endY - graffitis.value[nowGraffiti.value].y;
}
if (['left', 'leftTop', 'leftBottom'].includes(mouseEditType.value)) {
graffitis.value[nowGraffiti.value].width =
graffitis.value[nowGraffiti.value].width + graffitis.value[nowGraffiti.value].x - endX;
graffitis.value[nowGraffiti.value].x = endX;
}
if (['right', 'rightTop', 'rightBottom'].includes(mouseEditType.value)) {
graffitis.value[nowGraffiti.value].width = endX - graffitis.value[nowGraffiti.value].x;
}
}
onMounted(() => {});
</script>
<style lang="less" scoped>
.image {
position: relative;
width: 100%;
height: 900px;
display: block;
}
.canvas {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 820px;
}
.bottom {
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
height: 100px;
background: #1c1c1c;
display: block;
.buttonList {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 40px;
.button {
position: relative;
padding-left: 10px;
padding-right: 10px;
color: #ffffff;
font-size: 22px;
}
}
.imageList {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 60px;
.bottom_div {
padding: 5px;
margin-left: 5px;
margin-right: 5px;
}
.bottom_div_choose {
padding: 4px;
border: 1px solid yellow;
margin-left: 5px;
margin-right: 5px;
}
}
}
//
.leftButton {
position: absolute;
left: 40px;
top: 45%;
z-index: 200;
background: #9c9c9c;
border-radius: 50px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.rightButton {
position: absolute;
right: 40px;
top: 45%;
z-index: 200;
background: #9c9c9c;
border-radius: 50px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
// -
.graffitiButton {
background: #ffffff;
position: absolute;
right: 40px;
top: 5%;
z-index: 200;
width: 30px;
height: 30px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
}
.popoverClass {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
//
.showTextboxClass {
position: absolute;
right: 40px;
top: 25%;
width: 30px;
height: 70px;
display: block;
align-items: center;
justify-content: center;
.button {
border-radius: 3px;
position: relative;
color: #2d8cf0;
background: #ffffff;
font-size: 22px;
width: 30px;
height: 30px;
border: 1px solid #000000;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

@ -0,0 +1,271 @@
<template>
<div>
<div class="title"> 详细信息 </div>
<div class="info">
<a-row>
<a-col :span="7">
<span class="infotitle">图片名称</span>
</a-col>
<a-col :span="17">
<span class="infovalue" v-if="editNameFlag">
{{ props.nowPreviewRecord.name }}
<EditOutlined style="font-size: 20px; color: #07aaed" @click="editNameChange" />
</span>
<span class="infovalue" v-if="!editNameFlag">
<a-input
v-model:value="editName"
size="small"
@keypress.enter="pressEnterNameFunction"
@blur="editNameBlur"
/>
</span>
</a-col>
<a-col :span="7">
<span class="infotitle">照片类型</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.imgtype }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">任务名称</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.taskname }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">航线名称</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.airlineName }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">照片分辨率</span>
</a-col>
<a-col :span="17">
<span class="infovalue">
{{ props.nowPreviewRecord.width + '*' + props.nowPreviewRecord.height }}
</span>
</a-col>
<a-col :span="7">
<span class="infotitle">照片大小</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.size }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">拍摄飞机</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.photographFeiji }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">拍摄负载</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.photographNumber }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">拍摄人员</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.photographMan }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">拍摄时间</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.photographTime }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">标签</span>
</a-col>
<a-col :span="17">
<span class="infovalue" v-if="addLabelFlag">
<a-tag color="success" v-for="la in props.nowPreviewRecord.label" :key="la">
{{ la }}
</a-tag>
<PlusSquareOutlined style="font-size: 20px; color: #07aaed" @click="addLabelChange" />
</span>
<span class="infovalue" v-if="!addLabelFlag">
<a-tag
color="success"
v-for="la in props.nowPreviewRecord.label"
:key="la"
closable
@close="deleteLabel(la)"
>
{{ la }}
</a-tag>
<a-input
v-model:value="newLabelName"
size="small"
placeholder=""
@keypress.enter="pressEnterLabelFunction"
@blur="addLabelBlur"
/>
</span>
</a-col>
<a-col :span="7">
<span class="infotitle">涂鸦总数</span>
</a-col>
<a-col :span="17">
<span class="infovalue">{{ props.nowPreviewRecord.label }} </span>
</a-col>
<a-col :span="7">
<span class="infotitle">照片位置</span>
</a-col>
<a-col :span="17">
<span class="infobutton">
<EnvironmentOutlined style="font-size: 20px; color: #07aaed" @click="flyPoint" />
</span>
</a-col>
<a-col :span="24">
<div class="map">
<Map ref="mapRef" :nowPreviewRecord="props.nowPreviewRecord" />
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { EditOutlined, PlusSquareOutlined, EnvironmentOutlined } from '@ant-design/icons-vue';
import { orgPosGroup } from '@/api/demo/system';
import { Map } from './preview';
import { useMessage } from '@/hooks/web/useMessage';
const { createMessage } = useMessage();
const props = defineProps(['nowPreviewRecord']);
const emit = defineEmits(['reloadTable']);
// --------------------------------
const editNameFlag = ref(true);
const editName = ref(props.nowPreviewRecord.name.split('.').slice(0, -1).join('.'));
function editNameChange() {
editNameFlag.value = false;
}
//
async function pressEnterNameFunction() {
let query = {
id: props.nowPreviewRecord.id,
newName: editName.value + '.' + props.nowPreviewRecord.name.split('.').pop(),
};
props.nowPreviewRecord.name =
editName.value + '.' + props.nowPreviewRecord.name.split('.').pop();
editNameFlag.value = true;
emit('reloadTable');
return;
//
const data = await orgPosGroup(query);
if (data) {
editNameFlag.value = true;
emit('reloadTable');
return createMessage.success('修改名称成功');
} else {
return createMessage.error('修改名称失败');
}
}
function editNameBlur() {
editNameFlag.value = true;
editName.value = props.nowPreviewRecord.name.split('.').slice(0, -1).join('.');
}
// --------------------------------
const addLabelFlag = ref(true);
const newLabelName = ref('');
function addLabelChange() {
addLabelFlag.value = false;
}
//
async function pressEnterLabelFunction() {
if (!newLabelName.value) {
return;
}
if (!props.nowPreviewRecord.label.includes(newLabelName.value)) {
props.nowPreviewRecord.label.push(newLabelName.value);
let query = {
id: props.nowPreviewRecord.id,
newLabel: props.nowPreviewRecord.label,
};
addLabelFlag.value = true;
emit('reloadTable');
newLabelName.value = '';
return;
//
const data = await orgPosGroup(query);
if (data) {
addLabelFlag.value = true;
emit('reloadTable');
return createMessage.success('修改名称成功');
} else {
return createMessage.error('修改名称失败');
}
} else {
return createMessage.error('此标签已存在!');
}
}
function deleteLabel(value) {
props.nowPreviewRecord.label = props.nowPreviewRecord.label.filter((item) => item !== value);
emit('reloadTable');
}
function addLabelBlur() {
addLabelFlag.value = true;
newLabelName.value = '';
}
// --------------------------------
const mapRef = ref();
function flyPoint() {
mapRef.value?.flyToPoint([props.nowPreviewRecord.lng, props.nowPreviewRecord.lat]);
}
</script>
<style lang="less" scoped>
.title {
position: relative;
width: 50%;
height: 60px;
font-size: 15px;
color: white;
display: flex;
align-items: center;
}
.info {
position: relative;
width: 90%;
height: 480px;
.infotitle {
font-size: 15px;
color: #ffffff;
display: flex;
align-items: center;
height: 40px;
margin-left: 10px;
}
.infovalue {
font-size: 15px;
color: #ffffff;
display: flex;
align-items: center;
height: 40px;
}
.infobutton {
font-size: 10px;
color: #ffffff;
display: flex;
align-items: center;
justify-content: right;
height: 40px;
}
}
.map {
width: 100%;
height: 200px;
margin-left: 10px;
}
</style>

@ -0,0 +1,45 @@
<template>
<div>
<MonitorHK
v-if="props.nowPreviewRecord.manufacturer == '海康'"
:serialNumberValue="props.nowPreviewRecord.url"
:width="1380"
:height="900"
/>
<MonitorLC
v-if="props.nowPreviewRecord.manufacturer == '乐橙'"
:deviceId="props.nowPreviewRecord.url"
:channelId="0"
:width="1396"
:height="900"
:videoMuted="true"
/>
<MonitorTX
v-if="props.nowPreviewRecord.manufacturer == '腾讯'"
:serialNumberValue="props.nowPreviewRecord.url"
:width="1396"
:height="900"
:videoLoop="false"
:videoMuted="true"
:videoFit="'contain'"
/>
<MonitorQX
v-if="props.nowPreviewRecord.manufacturer == '青犀'"
:serialNumberValue="props.nowPreviewRecord.url"
:width="1396"
:height="900"
:videoLoop="false"
:videoMuted="true"
:videoFit="'contain'"
/>
</div>
</template>
<script lang="ts" setup>
import { MonitorHK } from './preview';
import { MonitorLC } from './preview';
import { MonitorTX } from './preview';
import { MonitorQX } from './preview';
const props = defineProps(['nowPreviewRecord']);
</script>
<style lang="less"></style>

@ -0,0 +1,267 @@
<template>
<div id="camera-box" class="camera-box">
<!-- 视口区域 -->
<div :id="'playWnd'" class="playWnd"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, watch, defineProps, defineExpose, ref, computed } from 'vue';
import { JSEncrypt } from 'jsencrypt';
const props = defineProps(['serialNumberValue', 'width', 'height']);
//
let initCount = 0;
let pubKey = '';
let oWebControl: any = null;
//
const initPlugin = () => {
oWebControl = new window.WebControl({
szPluginContainer: 'playWnd', // id
iServicePortStart: 15900, // 使
iServicePortEnd: 15909,
szClassId: '23BF3B0A-2C56-4D97-9C03-0CB103AA8F11', // IE10使ActiveXclsid
cbConnectSuccess: function () {
// WebControl
oWebControl
.JS_StartService('window', {
// WebControlVideoPluginPlugin.exe
dllPath: './VideoPluginConnect.dll', // "./VideoPluginConnect.dll"
})
.then(
function () {
//
oWebControl.JS_SetWindowControlCallback({
//
cbIntegrationCallBack: cbIntegrationCallBack,
});
//
let width = props.width;
let height = props.height;
const divElement = document.getElementById('camera-box');
if (divElement) {
const rect = divElement.getBoundingClientRect();
const rectWidth = rect.width;
const rectHeight = rect.height;
if (rectWidth < width) {
width = rectWidth;
}
if (rectHeight < height) {
height = rectHeight;
}
}
oWebControl.JS_CreateWnd('playWnd', width, height).then(function () {
init(); //
});
},
function () {
//
console.log('创建播放实例失败!!!');
},
);
},
cbConnectError: function () {
// WebControl
oWebControl = null;
let d = document.getElementById('playWnd');
if (d) {
d.innerHTML = '插件未启动,正在尝试启动,请稍候...';
}
window.WebControl.JS_WakeUp('VideoWebPlugin://'); // errorwakeup
initCount++;
if (initCount < 3) {
setTimeout(function () {
initPlugin();
}, 3000);
} else {
let d = document.getElementById('playWnd');
if (d) {
d.innerHTML = '插件启动失败,请检查插件是否安装!';
}
}
},
cbConnectClose: function (bNormalClose) {
// bNormalClose = false
// JS_DisconnectbNormalClose = true
oWebControl = null;
},
});
};
//
function init() {
getPubKey(function () {
let appkey = '23604396'; //appkey
let ip = '221.2.83.54'; //IP
let port = 1443;
let appSecret = 'NZJ8L3bxCOOV6rtTFjsx';
let secret = setEncrypt(appSecret); //secret
let layerOut = '1x1';
let playMode = 0; //0-1-
let snapDir = 'D:\\SnapDir'; //
let videoDir = 'D:\\VideoDir'; //
let layout = layerOut; //playMode 1*1
let enableHTTPS = 1; //HTTPS1
let encryptedFields = 'secret'; //secret
let showToolbar = 1; //0-0-
let showSmart = 1; //线0-0-
let buttonIDs = ''; //
oWebControl
.JS_RequestInterface({
funcName: 'init',
argument: JSON.stringify({
appkey: appkey, //APIappkey
secret: secret, //APIsecret
ip: ip, //APIIP
playMode: playMode, //
port: port, //
snapDir: snapDir, //
videoDir: videoDir, //
layout: layout, //
enableHTTPS: enableHTTPS, //HTTPS
encryptedFields: encryptedFields, //
showToolbar: showToolbar, //
showSmart: showSmart, //
buttonIDs: buttonIDs, //
}),
})
.then(function (oData) {
//
reSizeVideo();
});
//
oWebControl.JS_RequestInterface({
funcName: 'startPreview',
argument: {
cameraIndexCode: props.serialNumberValue,
streamMode: 0,
transMode: 1,
gpuMode: 0,
wndId: 1,
},
});
});
}
//
function closeHkVideo() {
if (oWebControl != null) {
//
oWebControl.JS_HideWnd();
//
oWebControl.JS_RequestInterface({ funcName: 'destroyWnd' });
//
oWebControl.JS_Disconnect();
}
}
//
function reSizeVideo() {
//
let width = props.width;
let height = props.height;
const divElement = document.getElementById('camera-box');
if (divElement) {
const rect = divElement.getBoundingClientRect();
const rectWidth = rect.width;
const rectHeight = rect.height;
if (rectWidth < width) {
width = rectWidth;
}
if (rectHeight < height) {
height = rectHeight;
}
}
oWebControl.JS_Resize(width, height); // resizefirefoxDIV
}
watch(
() => props.serialNumberValue,
() => {
init();
},
);
//
function getPubKey(callback) {
oWebControl
.JS_RequestInterface({
funcName: 'getRSAPubKey',
argument: JSON.stringify({
keyLength: 1024,
}),
})
.then(function (oData) {
if (oData.responseMsg.data) {
pubKey = oData.responseMsg.data;
callback();
}
});
}
// RSA
function setEncrypt(value) {
let encrypt = new JSEncrypt();
encrypt.setPublicKey(pubKey);
return encrypt.encrypt(value);
}
//
function cbIntegrationCallBack(oData) {
// showCBInfo(JSON.stringify(oData.responseMsg));
}
onMounted(() => {
//
initPlugin();
//
const elements = document.querySelectorAll('.ZhiGan_ModalVideo');
if (elements.length > 0) {
// scroll
elements.forEach((element) => {
element.addEventListener('scroll', reSizeVideo);
});
}
});
onUnmounted(() => {
//
closeHkVideo();
//
const elements = document.querySelectorAll('.ZhiGan_ModalVideo');
if (elements.length > 0) {
// scroll
elements.forEach((element) => {
element.removeEventListener('scroll', reSizeVideo);
});
}
});
defineExpose({
initPlugin,
init,
closeHkVideo,
});
</script>
<style scoped>
.camera-box {
width: v-bind('`${props.width}px`');
height: v-bind('`${props.height}px`');
border-radius: 5px;
position: relative;
z-index: 10 !important;
position: fixed;
left: 50%;
top: 50%;
z-index: 10;
transform: translate(-62%, -51%);
padding: 0px;
}
.playWnd {
width: v-bind('`${props.width}px`');
height: v-bind('`${props.height}px`');
}
</style>

@ -0,0 +1,207 @@
<template>
<div class="box">
<div class="box-container">
<div :id="'root' + props.timestamp"></div>
</div>
<div class="box-controls">
<div class="left-controls">
<div>
<n-button quaternary @click="playOrPauseClick">
<template #icon>
<n-icon>
<Pause
v-if="control_playOrPause"
:style="{
fontSize: '20px',
color: '#ffffff',
}"
/>
<CaretForward
v-if="!control_playOrPause"
:style="{
fontSize: '20px',
color: '#ffffff',
}"
/>
</n-icon>
</template>
</n-button>
</div>
<div>
<n-button quaternary @click="volumeClick">
<template #icon>
<n-icon>
<VolumeHigh
v-if="!control_volume"
:style="{
fontSize: '20px',
color: '#ffffff',
}"
/>
<VolumeMute
v-if="control_volume"
:style="{
fontSize: '20px',
color: '#ffffff',
}"
/>
</n-icon>
</template>
</n-button>
</div>
</div>
<div class="right-controls">
<n-button quaternary @click="fullScreenClick">
<template #icon>
<n-icon>
<ExpandOutlined
:style="{
fontSize: '20px',
color: '#ffffff',
}"
/>
</n-icon>
</template>
</n-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, watch, ref } from 'vue';
import axios from 'axios';
import { ExpandOutlined } from '@ant-design/icons-vue';
import { Pause, CaretForward, VolumeHigh, VolumeMute } from '@vicons/ionicons5';
let BASE_URL = 'http://111.17.207.220:9001/api';
const props = defineProps([
'deviceId',
'channelId',
'width',
'height',
'timestamp',
'videoMuted',
]);
let clPlayer: any = null;
// /
const control_playOrPause = ref(true);
// :true
const control_volume = ref(true);
//
const control_fullScreen = ref(false);
// /
function playOrPauseClick() {
control_playOrPause.value = !control_playOrPause.value;
if (control_playOrPause.value) {
clPlayer.play();
} else {
clPlayer.pause();
}
}
//
function volumeClick() {
control_volume.value = !control_volume.value;
if (control_volume.value) {
clPlayer.volume(0);
} else {
clPlayer.volume(1);
}
}
//
function fullScreenClick() {
clPlayer.fullScreen();
}
// kitToken
function getKitToken(deviceId, channelId) {
axios({
method: 'post',
url: BASE_URL + '/Camera/getKitToken?deviceId=' + deviceId + '&channelId=0' + '&type=0',
}).then((res) => {
let kitToken = res.data.result.data.kitToken;
loadMonitorVideo(deviceId, kitToken, channelId);
});
}
//
function loadMonitorVideo(deviceId, kitToken, channelId) {
//
closeMonitorVideo();
clPlayer = new imouPlayer({
id: 'root' + props.timestamp,
width: props.width,
height: props.height - 35,
deviceId: deviceId,
token: kitToken,
channelId: channelId,
type: 1,
streamId: 0,
recordType: 'cloud',
code: '',
controls: true,
});
clPlayer.volume(0);
}
//
function closeMonitorVideo() {
if (clPlayer != null) {
clPlayer.destroy();
clPlayer = null;
}
}
watch(
() => props.deviceId,
() => {
getKitToken(props.deviceId, props.channelId);
},
);
onMounted(() => {
getKitToken(props.deviceId, props.channelId);
control_volume.value = props.videoMuted;
});
onUnmounted(() => {
closeMonitorVideo();
});
</script>
<style scoped>
.box {
width: v-bind('`${props.width}px`');
height: v-bind('`${props.height}px`');
z-index: 99;
}
.box-container {
width: v-bind('`${props.width}px`');
height: v-bind('`${props.height-35}px`');
}
.box-controls {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
background: #000000;
height: 35px;
width: 100%;
gap: 10px;
}
.left-controls {
display: flex;
justify-self: start;
}
.right-controls {
justify-self: end;
}
</style>

@ -0,0 +1,40 @@
<template>
<div class="box">
<easy-player
:video-url="getUrl(props.serialNumberValue)"
live
autoplay
fluent="true"
:style="{ width: props.width + 'px', height: props.height + 'px' }"
:stretch="props.videoFit"
:muted="props.videoMuted"
:loop="props.videoLoop"
/>
</div>
</template>
<script setup lang="ts">
const props = defineProps([
'serialNumberValue',
'width',
'height',
'videoMuted',
'videoLoop',
'videoFit',
]);
function getUrl(value) {
return 'http://221.2.83.254:7012/live/' + value + '.m3u8';
}
</script>
<style lang="scss" scoped>
.box {
width: v-bind('`${props.width}px`');
height: v-bind('`${props.height}px`');
z-index: 99;
}
::v-deep .vjs-bitrate-control {
display: none !important;
}
</style>

@ -0,0 +1,84 @@
<template>
<div>
<video
:id="'ZhiGan_ModalVideo' + props.timestamp"
class="TCPlayer-video-container"
preload="auto"
crossOrigin="anonymous"
playsinline
autoplay
:loop="props.videoLoop"
:muted="props.videoMuted"
:style="{
width: props.width + 'px',
height: props.height + 'px',
}"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
const props = defineProps([
'serialNumberValue',
'width',
'height',
'timestamp',
'videoLoop',
'videoMuted',
'videoFit',
]);
//
let player: any = null;
function handlerPlayVideo() {
nextTick(() => {
if (player) {
player.src(props.serialNumberValue);
} else {
player = TCPlayer('ZhiGan_ModalVideo' + props.timestamp, {});
player.src(props.serialNumberValue);
}
});
}
function closePlayerVideo() {
if (player) {
player.dispose();
player = null;
}
}
watch(
() => props.serialNumberValue,
() => {
handlerPlayVideo();
},
);
onMounted(() => {
handlerPlayVideo();
});
onUnmounted(() => {
closePlayerVideo();
});
defineExpose({
handlerPlayVideo,
closePlayerVideo,
});
</script>
<style lang="scss" scoped>
video {
display: block;
object-fit: v-bind('props.videoFit');
}
::v-deep .vjs-live-control .vjs-live-display {
width: 100px !important;
}
</style>

@ -0,0 +1,10 @@
// export { default as PreviewImage } from './previewImage.vue';
// export { default as PreviewCanvas } from './previewCanvas.vue';
// export { default as PreviewVideo } from './previewVideo.vue';
// export { default as PreviewInformation } from './previewInformation.vue';
// export { default as MonitorHK } from './video/monitorHK.vue';
// export { default as MonitorLC } from './video/monitorLC.vue';
// export { default as MonitorQX } from './video/monitorQX.vue';
// export { default as MonitorTX } from './video/monitorTX.vue';
// export { default as PanoViewer } from './PanoViewer.vue';
// export { default as Map } from './Map.vue';

@ -0,0 +1,5 @@
<template>
<div> </div>
</template>
<script lang="ts" setup></script>
<style lang="less"></style>
Loading…
Cancel
Save