媒体库-第一版
parent
4583fe0c4e
commit
e1c29bd6f7
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,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,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,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…
Reference in New Issue