KonvaMap.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. <template>
  2. <div>
  3. <v-stage ref="stageAll" :config="stageConfig" @click="handleStageClick">
  4. <v-layer ref="layer">
  5. <v-image :config="bgConfig" v-show="bgImgUrl" />
  6. <v-group
  7. v-for="camera in cameras"
  8. :key="camera.id"
  9. :id="camera.id"
  10. :config="camera.groupConfig"
  11. @click="handleCameraClick(camera)"
  12. @mouseover="(e) => handleMouseOver(e)"
  13. @mouseleave="handleMouseLeave()"
  14. @dragstart="handleDragStart()"
  15. >
  16. <v-image :config="camera.config" />
  17. <v-image v-if="camera.isDefault" ref="defaultIcon" :config="defaultIconConfig" />
  18. </v-group>
  19. <v-transformer :config="transformerConfig" ref="transformer" />
  20. </v-layer>
  21. </v-stage>
  22. <div
  23. v-show="defaultShow"
  24. class="opt-container"
  25. :style="{ position: 'absolute', left: posX + 'px', top: posY + 'px' }"
  26. >
  27. <div class="opt-item" :class="{ disabled: disabledSet }" @click="setDefaultCamera"
  28. >设为默认相机</div
  29. >
  30. <div class="opt-item" @click="previewCamera">预览相机</div>
  31. </div>
  32. <CameraPreview
  33. v-if="isShow"
  34. :last-pos-x="posX!"
  35. :last-pos-y="posY!"
  36. :video-url="videoUrl"
  37. @close="closePreview"
  38. />
  39. <DefaultTip
  40. v-show="tipShow"
  41. :position="pos"
  42. :style="{ position: 'absolute', left: posTipX + 'px', top: posTipY + 'px' }"
  43. />
  44. </div>
  45. </template>
  46. <script setup lang="ts">
  47. import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
  48. import { ElMessage, ElMessageBox } from 'element-plus';
  49. import { useGlobSetting } from '@/hooks/setting';
  50. import DefaultTip from '../components/DefaultTip.vue';
  51. import urlJoin from 'url-join';
  52. import cameraImgSrc from '@/assets/camera/cameraImg.png';
  53. import favoritesImgSrc from '@/assets/camera/favorites.png';
  54. import { TipPositionEnum, camerasGroupType } from '../type';
  55. import { cloneDeep } from 'lodash-es';
  56. import useMiniMap from '../use-mini-map';
  57. import { storeToRefs } from 'pinia';
  58. import { updateMinMapViewLayoutApi } from '@/api/scene/scene';
  59. import CameraPreview from './CameraPreview.vue';
  60. const globSetting = useGlobSetting();
  61. const miniMap = useMiniMap();
  62. const { shopCameraList } = storeToRefs(miniMap);
  63. interface DataType {
  64. name: string;
  65. code: string;
  66. cameraIp: string;
  67. remark: string;
  68. status: number;
  69. pushstreamIp: string;
  70. isSet: number;
  71. workSpaceName: string;
  72. integrationState?: number;
  73. }
  74. const emit = defineEmits(['changeDefaultCamera', 'sendCameraId', 'change']);
  75. const props = defineProps<{ filterData: DataType[] }>();
  76. const camImg = new Image();
  77. const stageConfig = ref({
  78. width: 800,
  79. height: 600,
  80. });
  81. const bgImg = new Image();
  82. const favoritesImg = new Image();
  83. favoritesImg.src = favoritesImgSrc;
  84. //右键点击是否出现
  85. const defaultShow = ref<boolean>(false);
  86. const posX = ref<number>();
  87. const posY = ref<number>();
  88. const posTipX = ref<number>();
  89. const posTipY = ref<number>();
  90. const pos = ref(TipPositionEnum.TOP);
  91. //上一次点击的相机
  92. const lastClickedGroupId = ref<string | null>(null);
  93. const lastClickedVideoUrl = ref<string | null>(null);
  94. const bgImgUrl = ref<string | null>('');
  95. const cameras = ref<camerasGroupType[]>([]);
  96. //默认相机id
  97. const defaultCameraId = ref('');
  98. const tipShow = ref(false);
  99. const disabledSet = ref(false);
  100. const videoUrl = ref<string>('');
  101. const cameraIconSize = { width: 52, height: 52 };
  102. //标签
  103. const layer = ref();
  104. const transformer = ref();
  105. const defaultIcon = ref();
  106. function addIntegrationState(data: camerasGroupType[], updateData: DataType[]) {
  107. for (let i = 0; i < data.length; i++) {
  108. const camera = data[i];
  109. const matchedCamera = updateData.find((item) => item.code === camera.id);
  110. if (matchedCamera) {
  111. camera.groupConfig.visible = matchedCamera.integrationState === 0 ? true : false;
  112. }
  113. }
  114. }
  115. const bgConfig = ref({
  116. x: 0,
  117. y: 0,
  118. id: 'bgImg',
  119. width: bgImg.width,
  120. height: bgImg.height,
  121. image: bgImg,
  122. name: 'bgImg',
  123. backgroundSize: 'cover',
  124. });
  125. //待修改
  126. const transformerConfig = ref({
  127. keepRatio: true,
  128. rotateAnchorOffset: 30,
  129. enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
  130. });
  131. //背景大小变换
  132. const resizeContainer = (width, height) => {
  133. stageConfig.value.width = width;
  134. stageConfig.value.height = height;
  135. layer.value.getNode().draw();
  136. };
  137. const defaultIconConfig = ref({
  138. x: 18,
  139. y: -16,
  140. width: 16,
  141. height: 16,
  142. id: 'defaultIcon',
  143. image: favoritesImg,
  144. });
  145. //取消
  146. const handleMouseOver = (e) => {
  147. document.oncontextmenu = () => {
  148. return false;
  149. };
  150. const group = e.target.parent;
  151. if (group.id() === defaultCameraId.value) {
  152. tipShow.value = true;
  153. const stage = transformer.value.getNode().getStage();
  154. const defaultNode = stage.findOne('#defaultIcon');
  155. const tipPosition = defaultNode.absolutePosition();
  156. posTipX.value = Number(tipPosition?.x.toFixed(2)) || 0;
  157. posTipY.value = Number(tipPosition?.y.toFixed(2)) || 0;
  158. const angle = group.rotation() >= 0 ? group.rotation() : group.rotation() + 360;
  159. if (angle >= 30) {
  160. if (angle <= 150) {
  161. pos.value = TipPositionEnum.RIGHT;
  162. posTipX.value += 26;
  163. posTipY.value -= 17;
  164. } else if (angle <= 210) {
  165. pos.value = TipPositionEnum.BOTTOM;
  166. posTipY.value += 26;
  167. posTipX.value -= 50;
  168. } else {
  169. pos.value = TipPositionEnum.LEFT;
  170. posTipX.value -= 121;
  171. posTipY.value -= 25;
  172. }
  173. } else {
  174. posTipY.value -= 61;
  175. posTipX.value -= 43;
  176. }
  177. }
  178. };
  179. // 还原浏览器默认鼠标事件
  180. const handleMouseLeave = () => {
  181. document.oncontextmenu = () => {
  182. return true;
  183. };
  184. tipShow.value = false;
  185. };
  186. const handleDragStart = () => {
  187. tipShow.value = false;
  188. emit('change', true);
  189. };
  190. const handleStageClick = (e: any) => {
  191. defaultShow.value = false;
  192. disabledSet.value = false;
  193. document.oncontextmenu = () => {
  194. return false;
  195. };
  196. const parent = e.target.parent;
  197. //判断是否点击相机组
  198. if (parent.hasName('group')) {
  199. lastClickedGroupId.value = parent.id();
  200. emit('change', true);
  201. // 判断是否为右键点击
  202. if (e.evt.button === 2) {
  203. lastClickedGroupId.value = parent.id();
  204. const clickedVideoUrl = props.filterData.find(
  205. (item) => item.code === lastClickedGroupId.value,
  206. );
  207. if (clickedVideoUrl) {
  208. lastClickedVideoUrl.value = clickedVideoUrl.pushstreamIp;
  209. }
  210. isShow.value = false;
  211. posX.value = e.evt.offsetX + 20;
  212. posY.value = e.evt.offsetY;
  213. disabledSet.value = defaultCameraId.value === parent.id();
  214. defaultShow.value = true;
  215. }
  216. } else {
  217. lastClickedGroupId.value = null;
  218. //取消transformer选择
  219. const transformerNode = transformer.value.getNode();
  220. transformerNode.nodes([]);
  221. }
  222. };
  223. const handleCameraClick = (camera) => {
  224. tipShow.value = false;
  225. const transformerNode = transformer.value.getNode();
  226. const stage = transformerNode.getStage();
  227. const cameraNode = stage.findOne('#' + camera.id);
  228. // 将变换器附加到点击的相机
  229. transformerNode.nodes([cameraNode]);
  230. transformerNode.moveToTop();
  231. emit('change', true);
  232. };
  233. //添加相机
  234. const addCamera = (id: string) => {
  235. const existingCamera = cameras.value.find((camera) => camera.id === id);
  236. if (existingCamera) return;
  237. const config = {
  238. width: cameraIconSize.width,
  239. height: cameraIconSize.height,
  240. image: camImg,
  241. name: 'image',
  242. id: id,
  243. };
  244. const groupConfig = {
  245. x: 50,
  246. y: 50,
  247. draggable: true,
  248. name: 'group',
  249. };
  250. const cameraDetail = {
  251. id,
  252. groupConfig,
  253. config,
  254. isDefault: false,
  255. };
  256. cameras.value.push(cameraDetail);
  257. //当只有一个相机时,自动设置默认相机
  258. if (cameras.value.length === 1) {
  259. cameras.value[0].isDefault = true;
  260. defaultCameraId.value = id;
  261. }
  262. emit('change', true);
  263. };
  264. //设置默认相机
  265. const setDefaultCamera = () => {
  266. //选中的相机id号
  267. defaultCameraId.value = lastClickedGroupId.value!;
  268. cameras.value.forEach((item) => {
  269. if (item.id === defaultCameraId.value) {
  270. item.isDefault = true;
  271. } else {
  272. item.isDefault = false;
  273. }
  274. });
  275. const transformerNode = transformer.value.getNode();
  276. const stage = transformerNode.getStage();
  277. transformerNode.nodes([]);
  278. const cameraNode = stage.findOne('#' + defaultCameraId.value);
  279. // 将变换器附加到点击的相机
  280. transformerNode.nodes([cameraNode]);
  281. defaultShow.value = false;
  282. emit('change', true);
  283. };
  284. const isShow = ref<boolean>(false);
  285. const previewCamera = () => {
  286. videoUrl.value = lastClickedVideoUrl.value!;
  287. isShow.value = true;
  288. defaultShow.value = false;
  289. };
  290. const closePreview = () => {
  291. isShow.value = false;
  292. defaultShow.value = false;
  293. };
  294. watch(
  295. lastClickedGroupId,
  296. () => {
  297. emit('changeDefaultCamera', lastClickedGroupId.value);
  298. },
  299. { immediate: true },
  300. );
  301. watch(
  302. () => cameras.value,
  303. () => {
  304. emit('sendCameraId', cameras.value);
  305. },
  306. { immediate: true, deep: true },
  307. );
  308. watch(
  309. () => props.filterData,
  310. () => {
  311. addIntegrationState(cameras.value, props.filterData);
  312. },
  313. { immediate: true, deep: true },
  314. );
  315. const clearBg = () => {
  316. bgImg.src = null as any as string;
  317. layer.value.getNode().draw();
  318. };
  319. //添加背景
  320. const addBg = (imgBg) => {
  321. return new Promise((resolve) => {
  322. bgImgUrl.value = imgBg;
  323. bgImg.src = urlJoin(globSetting.imgUrl!, imgBg) as string;
  324. bgImg.onload = () => {
  325. bgConfig.value.width = bgImg.width;
  326. bgConfig.value.height = bgImg.height;
  327. resizeContainer(bgImg.width, bgImg.height);
  328. resolve(null);
  329. };
  330. });
  331. };
  332. //根据id找到对应的相机
  333. const findCamera = (cameraId: string) => {
  334. const camera = cameras.value.find((item) => item.id === cameraId);
  335. return camera;
  336. };
  337. //保存布局
  338. const saveLayout = () => {
  339. const stage = transformer.value.getNode().getStage();
  340. const groups = stage.find('.group');
  341. const tempList = cloneDeep(cameras.value);
  342. const camerasLists = tempList.map((item, index) => {
  343. item.groupConfig.x = groups[index].attrs.x;
  344. item.groupConfig.y = groups[index].attrs.y;
  345. item.groupConfig.rotation = groups[index].attrs.rotation || 0;
  346. item.groupConfig.scaleX = groups[index].attrs.scaleX || 1;
  347. item.groupConfig.scaleY = groups[index].attrs.scaleY || 1;
  348. // item.config.url = cameraImgSrc;
  349. return item;
  350. });
  351. const layout = {
  352. stageConfig: stageConfig.value,
  353. bgConfig: bgConfig.value,
  354. bgImgUrl: bgImgUrl.value,
  355. defaultCameraId: defaultCameraId.value,
  356. cameraList: camerasLists,
  357. };
  358. emit('change', false);
  359. return JSON.stringify(layout);
  360. };
  361. //删除相机
  362. const handleKeyDown = (e) => {
  363. if (e.keyCode === 46 || e.code === 'Delete' || e.keyCode === 8 || e.code === 'Backspace') {
  364. if (lastClickedGroupId.value === defaultCameraId.value) {
  365. // ElMessage.error({
  366. // message: '无法删除默认相机',
  367. // });
  368. // return;
  369. ElMessageBox.confirm('此相机为默认相机,您确认要删除此相机?', 'Warning', {
  370. confirmButtonText: '确认',
  371. cancelButtonText: '取消',
  372. type: 'warning',
  373. })
  374. .then(() => {
  375. const index = cameras.value.findIndex((item) => item.id === lastClickedGroupId.value);
  376. index >= 0 && cameras.value.splice(index, 1);
  377. lastClickedGroupId.value = '';
  378. //取消transformer
  379. const transformerNode = transformer.value.getNode();
  380. transformerNode.nodes([]);
  381. })
  382. .catch(() => {});
  383. } else {
  384. const index = cameras.value.findIndex((item) => item.id === lastClickedGroupId.value);
  385. index >= 0 && cameras.value.splice(index, 1);
  386. lastClickedGroupId.value = '';
  387. //取消transformer
  388. const transformerNode = transformer.value.getNode();
  389. transformerNode.nodes([]);
  390. }
  391. emit('change', true);
  392. }
  393. };
  394. //重置
  395. const resetMap = () => {
  396. bgImgUrl.value = null;
  397. clearBg();
  398. cameras.value = [];
  399. lastClickedGroupId.value = null;
  400. lastClickedGroupId.value = null;
  401. videoUrl.value = '';
  402. isShow.value = false;
  403. defaultCameraId.value = '';
  404. };
  405. /** 导入布局json */
  406. const createMap = (layout, selectId) => {
  407. addBg(layout.bgImgUrl).then((_res) => {
  408. const unExitList = [] as any[];
  409. stageConfig.value = layout.stageConfig;
  410. defaultCameraId.value = layout.defaultCameraId;
  411. layout.cameraList = layout.cameraList
  412. ?.map((item) => {
  413. item.config.image = camImg;
  414. const width = item.config.width;
  415. const height = item.config.height;
  416. const ratio = width / height;
  417. /** 老版本的相机icon比例在1.4左右,新icon已经更新,比例是1:1,为了避免新icon渲染出来变形,需要调整icon大小
  418. * 如果图标的比例在该范围,那么会将它设置为新图标的icon大小 */
  419. if (1.3 < ratio && ratio < 1.5) {
  420. item.config.width = cameraIconSize.width;
  421. item.config.height = cameraIconSize.height;
  422. }
  423. return item;
  424. })
  425. ?.filter((cam) => {
  426. if (shopCameraList.value.findIndex((x) => x.code === cam.id) >= 0) {
  427. return true;
  428. } else {
  429. unExitList.push(cam.code);
  430. return false;
  431. }
  432. });
  433. if (unExitList.length > 0) {
  434. ElMessage.warning('部分相机不存在,已为您删除!');
  435. const layoutNew = cloneDeep(layout);
  436. updateMinMapViewLayoutApi({
  437. layout: JSON.stringify({ ...layoutNew, isUploadBg: true }),
  438. targetId: String(selectId),
  439. });
  440. }
  441. cameras.value = layout.cameraList;
  442. });
  443. };
  444. defineExpose({
  445. addBg,
  446. createMap,
  447. handleCameraClick,
  448. findCamera,
  449. resetMap,
  450. addCamera,
  451. resizeContainer,
  452. saveLayout,
  453. });
  454. onMounted(() => {
  455. window.addEventListener('keydown', handleKeyDown);
  456. camImg.src = cameraImgSrc;
  457. });
  458. onBeforeUnmount(() => {
  459. window.removeEventListener('keydown', handleKeyDown);
  460. });
  461. </script>
  462. <style scoped lang="scss">
  463. .opt-container {
  464. width: 160px;
  465. padding: 10px;
  466. border-radius: 5px;
  467. background-color: #ffffff;
  468. box-shadow: 5px 5px 5px #a3a5a5;
  469. }
  470. .opt-item {
  471. height: 30px;
  472. font-size: 14px;
  473. color: #404040;
  474. display: flex;
  475. justify-content: flex-start;
  476. align-items: center;
  477. padding-left: 8px;
  478. border-radius: 3px;
  479. cursor: pointer;
  480. &:hover {
  481. background-color: #f1f2f5;
  482. }
  483. }
  484. .disabled {
  485. background-color: #f1f2f5;
  486. color: #bcbdc0;
  487. cursor: not-allowed;
  488. }
  489. </style>