KonvaMap.vue 14 KB

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