KonvaMap.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. const globSetting = useGlobSetting();
  49. const miniMap = useMiniMap();
  50. const { shopCameraList } = storeToRefs(miniMap);
  51. const emit = defineEmits(['changeDefaultCamera', 'sendCameraId']);
  52. const camImg = new Image();
  53. const stageConfig = ref({
  54. width: 800,
  55. height: 600,
  56. });
  57. const bgImg = new Image();
  58. const favoritesImg = new Image();
  59. favoritesImg.src = favoritesImgSrc;
  60. //右键点击是否出现
  61. const defaultShow = ref<boolean>(false);
  62. const posX = ref<number>();
  63. const posY = ref<number>();
  64. const posTipX = ref<number>();
  65. const posTipY = ref<number>();
  66. const pos = ref(TipPositionEnum.TOP);
  67. //上一次点击的相机
  68. const lastClickedGroupId = ref<string | null>(null);
  69. const bgImgUrl = ref<string | null>('');
  70. const cameras = ref<camerasGroupType[]>([]);
  71. //默认相机id
  72. const defaultCameraId = ref('');
  73. const tipShow = ref(false);
  74. const disabledSet = ref(false);
  75. //标签
  76. const layer = ref();
  77. const transformer = ref();
  78. const defaultIcon = ref();
  79. const bgConfig = ref({
  80. x: 0,
  81. y: 0,
  82. id: 'bgImg',
  83. width: bgImg.width,
  84. height: bgImg.height,
  85. image: bgImg,
  86. name: 'bgImg',
  87. backgroundSize: 'cover',
  88. });
  89. //待修改
  90. const transformerConfig = ref({
  91. keepRatio: true,
  92. rotateAnchorOffset: 30,
  93. enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
  94. });
  95. //背景大小变换
  96. const resizeContainer = (width, height) => {
  97. stageConfig.value.width = width;
  98. stageConfig.value.height = height;
  99. layer.value.getNode().draw();
  100. };
  101. const defaultIconConfig = ref({
  102. x: 18,
  103. y: -16,
  104. width: 16,
  105. height: 16,
  106. id: 'defaultIcon',
  107. image: favoritesImg,
  108. });
  109. //取消
  110. const handleMouseOver = (e) => {
  111. document.oncontextmenu = () => {
  112. return false;
  113. };
  114. const group = e.target.parent;
  115. if (group.id() === defaultCameraId.value) {
  116. tipShow.value = true;
  117. const stage = transformer.value.getNode().getStage();
  118. const defaultNode = stage.findOne('#defaultIcon');
  119. const tipPosition = defaultNode.absolutePosition();
  120. posTipX.value = Number(tipPosition?.x.toFixed(2)) || 0;
  121. posTipY.value = Number(tipPosition?.y.toFixed(2)) || 0;
  122. const angle = group.rotation() >= 0 ? group.rotation() : group.rotation() + 360;
  123. if (angle >= 30) {
  124. if (angle <= 150) {
  125. pos.value = TipPositionEnum.RIGHT;
  126. posTipX.value += 26;
  127. posTipY.value -= 17;
  128. } else if (angle <= 210) {
  129. pos.value = TipPositionEnum.BOTTOM;
  130. posTipY.value += 26;
  131. posTipX.value -= 50;
  132. } else {
  133. pos.value = TipPositionEnum.LEFT;
  134. posTipX.value -= 121;
  135. posTipY.value -= 25;
  136. }
  137. } else {
  138. posTipY.value -= 61;
  139. posTipX.value -= 43;
  140. }
  141. }
  142. };
  143. // 还原浏览器默认鼠标事件
  144. const handleMouseLeave = () => {
  145. document.oncontextmenu = () => {
  146. return true;
  147. };
  148. tipShow.value = false;
  149. };
  150. const handleDragStart = () => {
  151. tipShow.value = false;
  152. };
  153. const handleStageClick = (e: any) => {
  154. defaultShow.value = false;
  155. disabledSet.value = false;
  156. document.oncontextmenu = () => {
  157. return false;
  158. };
  159. const parent = e.target.parent;
  160. //判断是否点击相机组
  161. if (parent.hasName('group')) {
  162. lastClickedGroupId.value = parent.id();
  163. // 判断是否为右键点击
  164. if (e.evt.button === 2) {
  165. lastClickedGroupId.value = parent.id();
  166. posX.value = e.evt.offsetX + 20;
  167. posY.value = e.evt.offsetY;
  168. disabledSet.value = defaultCameraId.value === parent.id();
  169. defaultShow.value = true;
  170. }
  171. } else {
  172. lastClickedGroupId.value = null;
  173. //取消transformer选择
  174. const transformerNode = transformer.value.getNode();
  175. transformerNode.nodes([]);
  176. }
  177. };
  178. const handleCameraClick = (camera) => {
  179. tipShow.value = false;
  180. const transformerNode = transformer.value.getNode();
  181. const stage = transformerNode.getStage();
  182. const cameraNode = stage.findOne('#' + camera.id);
  183. // 将变换器附加到点击的相机
  184. transformerNode.nodes([cameraNode]);
  185. transformerNode.moveToTop();
  186. };
  187. //添加相机
  188. const addCamera = (id: string) => {
  189. const existingCamera = cameras.value.find((camera) => camera.id === id);
  190. if (existingCamera) return;
  191. const config = {
  192. width: 52,
  193. height: 37,
  194. image: camImg,
  195. name: 'image',
  196. id: id,
  197. };
  198. const groupConfig = {
  199. x: 50,
  200. y: 50,
  201. draggable: true,
  202. name: 'group',
  203. };
  204. const cameraDetail = {
  205. id,
  206. groupConfig,
  207. config,
  208. isDefault: false,
  209. };
  210. cameras.value.push(cameraDetail);
  211. //当只有一个相机时,自动设置默认相机
  212. if (cameras.value.length === 1) {
  213. cameras.value[0].isDefault = true;
  214. defaultCameraId.value = id;
  215. }
  216. };
  217. //设置默认相机
  218. const setDefaultCamera = () => {
  219. //选中的相机id号
  220. defaultCameraId.value = lastClickedGroupId.value!;
  221. cameras.value.forEach((item) => {
  222. if (item.id === defaultCameraId.value) {
  223. item.isDefault = true;
  224. } else {
  225. item.isDefault = false;
  226. }
  227. });
  228. const transformerNode = transformer.value.getNode();
  229. const stage = transformerNode.getStage();
  230. transformerNode.nodes([]);
  231. const cameraNode = stage.findOne('#' + defaultCameraId.value);
  232. // 将变换器附加到点击的相机
  233. transformerNode.nodes([cameraNode]);
  234. defaultShow.value = false;
  235. };
  236. watch(
  237. lastClickedGroupId,
  238. () => {
  239. emit('changeDefaultCamera', lastClickedGroupId.value);
  240. },
  241. { immediate: true },
  242. );
  243. watch(
  244. () => cameras.value,
  245. () => {
  246. emit('sendCameraId', cameras.value);
  247. },
  248. { immediate: true, deep: true },
  249. );
  250. const clearBg = () => {
  251. bgImg.src = null as any as string;
  252. layer.value.getNode().draw();
  253. };
  254. //添加背景
  255. const addBg = (imgBg) => {
  256. return new Promise((resolve) => {
  257. bgImgUrl.value = imgBg;
  258. bgImg.src = urlJoin(globSetting.imgUrl!, imgBg) as string;
  259. bgImg.onload = () => {
  260. bgConfig.value.width = bgImg.width;
  261. bgConfig.value.height = bgImg.height;
  262. resizeContainer(bgImg.width, bgImg.height);
  263. resolve(null);
  264. };
  265. });
  266. };
  267. //保存布局
  268. const saveLayout = () => {
  269. const stage = transformer.value.getNode().getStage();
  270. const groups = stage.find('.group');
  271. const tempList = cloneDeep(cameras.value);
  272. // console.log('cameras.value', cameras.value);
  273. const camerasLists = tempList.map((item, index) => {
  274. item.groupConfig.x = groups[index].attrs.x;
  275. item.groupConfig.y = groups[index].attrs.y;
  276. item.groupConfig.rotation = groups[index].attrs.rotation || 0;
  277. item.groupConfig.scaleX = groups[index].attrs.scaleX || 1;
  278. item.groupConfig.scaleY = groups[index].attrs.scaleY || 1;
  279. // item.config.url = cameraImgSrc;
  280. return item;
  281. });
  282. const layout = {
  283. stageConfig: stageConfig.value,
  284. bgConfig: bgConfig.value,
  285. bgImgUrl: bgImgUrl.value,
  286. defaultCameraId: defaultCameraId.value,
  287. cameraList: camerasLists,
  288. };
  289. return JSON.stringify(layout);
  290. };
  291. //删除相机
  292. const handleKeyDown = (e) => {
  293. if (e.keyCode === 46 || e.code === 'Delete' || e.keyCode === 8 || e.code === 'Backspace') {
  294. if (lastClickedGroupId.value === defaultCameraId.value) {
  295. // ElMessage.error({
  296. // message: '无法删除默认相机',
  297. // });
  298. // return;
  299. ElMessageBox.confirm('此相机为默认相机,您确认要删除此相机?', 'Warning', {
  300. confirmButtonText: '确认',
  301. cancelButtonText: '取消',
  302. type: 'warning',
  303. })
  304. .then(() => {
  305. const index = cameras.value.findIndex((item) => item.id === lastClickedGroupId.value);
  306. index >= 0 && cameras.value.splice(index, 1);
  307. lastClickedGroupId.value = '';
  308. //取消transformer
  309. const transformerNode = transformer.value.getNode();
  310. transformerNode.nodes([]);
  311. })
  312. .catch(() => {});
  313. } else {
  314. const index = cameras.value.findIndex((item) => item.id === lastClickedGroupId.value);
  315. index >= 0 && cameras.value.splice(index, 1);
  316. lastClickedGroupId.value = '';
  317. //取消transformer
  318. const transformerNode = transformer.value.getNode();
  319. transformerNode.nodes([]);
  320. }
  321. }
  322. };
  323. //重置
  324. const resetMap = () => {
  325. bgImgUrl.value = null;
  326. clearBg();
  327. cameras.value = [];
  328. lastClickedGroupId.value = null;
  329. defaultCameraId.value = '';
  330. };
  331. /** 导入布局json */
  332. const createMap = (layout) => {
  333. addBg(layout.bgImgUrl).then((_res) => {
  334. const unExitList = [] as any[];
  335. stageConfig.value = layout.stageConfig;
  336. defaultCameraId.value = layout.defaultCameraId;
  337. layout.cameraList = layout.cameraList
  338. ?.map((item) => {
  339. item.config.image = camImg;
  340. return item;
  341. })
  342. ?.filter((cam) => {
  343. if (shopCameraList.value.findIndex((x) => x.code === cam.id) >= 0) {
  344. return true;
  345. } else {
  346. unExitList.push(cam.code);
  347. return false;
  348. }
  349. });
  350. if (unExitList.length > 0) {
  351. ElMessage.warning('部分相机不存在,已为您删除!');
  352. }
  353. cameras.value = layout.cameraList;
  354. });
  355. };
  356. defineExpose({ addBg, createMap, resetMap, addCamera, resizeContainer, saveLayout });
  357. onMounted(() => {
  358. window.addEventListener('keydown', handleKeyDown);
  359. camImg.src = cameraImgSrc;
  360. });
  361. onBeforeUnmount(() => {
  362. window.removeEventListener('keydown', handleKeyDown);
  363. });
  364. </script>
  365. <style scoped lang="scss">
  366. .opt-container {
  367. width: 160px;
  368. padding: 10px;
  369. border-radius: 5px;
  370. background-color: #ffffff;
  371. box-shadow: 5px 5px 5px #a3a5a5;
  372. }
  373. .opt-item {
  374. height: 30px;
  375. font-size: 14px;
  376. color: #404040;
  377. display: flex;
  378. justify-content: flex-start;
  379. align-items: center;
  380. padding-left: 8px;
  381. border-radius: 3px;
  382. cursor: pointer;
  383. &:hover {
  384. background-color: #f1f2f5;
  385. }
  386. }
  387. .disabled {
  388. background-color: #f1f2f5;
  389. color: #bcbdc0;
  390. cursor: not-allowed;
  391. }
  392. </style>