KonvaMap.vue 10 KB

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