useMapEditor.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import { computed, h, onBeforeUnmount, onMounted, ref, render } from 'vue';
  2. import Konva from 'konva';
  3. import cameraImg from '@/assets/camera/cameraImg.png';
  4. import favoritesImg from '@/assets/camera/favorites.png';
  5. import OptBar from '../components/CameraOptBar.vue';
  6. import DefaultTip from '../components/DefaultTip.vue';
  7. import { TipPositionEnum } from '../type';
  8. import { ElMessage } from 'element-plus';
  9. import { useGlobSetting } from '@/hooks/setting';
  10. import urlJoin from 'url-join';
  11. import useMiniMap from '../use-mini-map';
  12. import { storeToRefs } from 'pinia';
  13. export function useMapEditor() {
  14. const miniMap = useMiniMap();
  15. const { shopCameraList } = storeToRefs(miniMap);
  16. // let initWidth; // 默认宽度
  17. // let initHeight; // 默认高度
  18. let stage: Konva.Stage | null = null;
  19. let layer: Konva.Layer | null = null;
  20. let copyLayer: Konva.Layer | null = null;
  21. let defaultIcon: Konva.Image | null = null; // 默认相机的图标shape
  22. const addedCameras = ref<string[]>([]); // 已添加相机列表
  23. const activeGroup = ref<Konva.Group | null>(null); // transformer激活的相机
  24. const defaultCameraId = ref(''); // 默认相机的ID
  25. let optBlock: HTMLDivElement | null = null; // 鼠标右击弹出的选项组
  26. let defaultTip: HTMLDivElement | null = null; // 默认相机悬浮tip
  27. let isTransform = false; // 是否再变换中
  28. const activeCameraId = computed(() => activeGroup.value?.id()); // 当前选中相机ID
  29. const bgImgUrl = ref<string>('');
  30. const globSetting = useGlobSetting();
  31. /** 容器初始化 */
  32. const initContainer = (opt: Konva.StageConfig) => {
  33. // initWidth = opt.width || 0;
  34. // initHeight = opt.height || 0;
  35. stage = new Konva.Stage(opt);
  36. stage.on('click tap', handleStageClick);
  37. window.stage = stage;
  38. layer = new Konva.Layer();
  39. copyLayer = new Konva.Layer();
  40. stage.add(layer);
  41. stage.add(copyLayer);
  42. addDefaultIcon();
  43. };
  44. /** 初始生成默认相机的图标shape,但不可见 */
  45. const addDefaultIcon = () => {
  46. const favImg = new Image();
  47. favImg.onload = () => {
  48. defaultIcon = new Konva.Image({
  49. x: 18,
  50. y: -16,
  51. width: 16,
  52. height: 16,
  53. image: favImg,
  54. id: 'defaultIcon',
  55. visible: false,
  56. rotation: 0,
  57. });
  58. bindBaseEvt(defaultIcon);
  59. layer?.add(defaultIcon);
  60. layer?.batchDraw();
  61. };
  62. favImg.src = favoritesImg;
  63. };
  64. /** 更换背景图时根据图片大小重置容器宽高 */
  65. const resizeContainer = (width, height) => {
  66. // const newWidth = width > initWidth ? width : initWidth;
  67. // const newHeight = height > initHeight ? height : initHeight;
  68. // stage?.width(newWidth);
  69. // stage?.height(newHeight);
  70. stage?.width(width);
  71. stage?.height(height);
  72. };
  73. /** 添加背景 */
  74. const addBg = () => {
  75. const imgUrl = urlJoin(globSetting.imgUrl!, bgImgUrl.value);
  76. const bgNode = layer?.find('#bgImg')[0] as Konva.Image;
  77. const bgImg = new Image();
  78. bgImg.onload = () => {
  79. // 判断是否已有背景
  80. if (!bgNode) {
  81. const mapBg = new Konva.Image({
  82. x: 0,
  83. y: 0,
  84. image: bgImg,
  85. width: bgImg.width,
  86. height: bgImg.height,
  87. id: 'bgImg',
  88. });
  89. layer?.add(mapBg);
  90. mapBg.moveToBottom();
  91. } else {
  92. bgNode.width(bgImg.width);
  93. bgNode.height(bgImg.height);
  94. bgNode.image(bgImg);
  95. }
  96. resizeContainer(bgImg.width, bgImg.height);
  97. layer?.batchDraw();
  98. };
  99. bgImg.src = imgUrl;
  100. };
  101. /** 变更需要激活transform的相机 */
  102. const attachTransformer = (group: Konva.Group): Konva.Transformer => {
  103. activeGroup.value?.draggable(false);
  104. activeGroup.value = group;
  105. group.draggable(true);
  106. stage!.find('Transformer')[0]?.destroy(); // 清除现有transformer
  107. const id = group.id();
  108. const tr = new Konva.Transformer({
  109. keepRatio: true,
  110. rotateAnchorOffset: 30,
  111. rotationSnaps: [0, 45, 90, 135, 180, 225, 270, 315],
  112. enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
  113. id: 'tr_' + id,
  114. });
  115. tr.nodes([group]);
  116. layer?.add(tr);
  117. layer?.draw();
  118. group.on('dragstart', handleDragStart);
  119. group.on('dragstart', handleDragEnd);
  120. return tr;
  121. };
  122. /** 添加相机 */
  123. const addCamera = (id: string) => {
  124. const group = new Konva.Group({
  125. x: 50,
  126. y: 50,
  127. id,
  128. draggable: true,
  129. name: 'group',
  130. });
  131. const camImg = new Image();
  132. camImg.onload = () => {
  133. const cameraIcon = new Konva.Image({
  134. width: 52,
  135. height: 37,
  136. image: camImg,
  137. name: 'image',
  138. });
  139. group.add(cameraIcon);
  140. layer?.add(group);
  141. bindBaseEvt(cameraIcon);
  142. const tr = attachTransformer(group); // 添加的相机默认激活transformer
  143. addedCameras.value.push(id);
  144. // 如果是唯一相机,设置为默认相机
  145. if (addedCameras.value.length === 1) {
  146. defaultIcon?.show();
  147. setDefaultCamera(group, tr);
  148. tr.forceUpdate();
  149. }
  150. };
  151. camImg.src = cameraImg;
  152. };
  153. /** 变更默认相机 */
  154. const setDefaultCamera = (node: Konva.Group, tr?: Konva.Transformer) => {
  155. defaultIcon?.moveTo(node);
  156. tr?.forceUpdate();
  157. defaultCameraId.value = node.id();
  158. };
  159. /** 创建右键选项组 */
  160. const createOptBlock = (node: Konva.Group, x: number, y: number) => {
  161. const id = node.id();
  162. optBlock = document.createElement('div') as HTMLDivElement;
  163. optBlock.setAttribute('style', `position: absolute; left: ${x}px; top: ${y}px;`);
  164. const optBar = h(OptBar, {
  165. disabled: id === defaultCameraId.value,
  166. onSetDefault: () => {
  167. const tr = layer?.find(`#tr_${id}`)[0] as Konva.Transformer;
  168. setDefaultCamera(node, tr);
  169. destoryOptBlock();
  170. },
  171. });
  172. render(optBar, optBlock);
  173. const parentEl = document.getElementById('drawContainer') as HTMLDivElement;
  174. parentEl.append(optBlock);
  175. };
  176. /** 删除右键选项组 */
  177. const destoryOptBlock = () => {
  178. optBlock?.remove();
  179. optBlock = null;
  180. };
  181. /** 创建默认tip */
  182. const createDefaultTip = (x: number, y: number, pos: TipPositionEnum) => {
  183. if (isTransform) {
  184. return;
  185. }
  186. defaultTip = document.createElement('div') as HTMLDivElement;
  187. defaultTip.setAttribute('style', `position: absolute; left: ${x}px; top: ${y}px;`);
  188. const tipInstance = h(DefaultTip, { position: pos });
  189. render(tipInstance, defaultTip);
  190. const parentEl = document.getElementById('drawContainer') as HTMLDivElement;
  191. parentEl.append(defaultTip);
  192. };
  193. /** 删除默认tip */
  194. const destoryDefaultTip = () => {
  195. defaultTip?.remove();
  196. defaultTip = null;
  197. };
  198. /** 删除相机 */
  199. const deleteCamera = () => {
  200. // 判断是否为默认相机,默认相机不允许删除
  201. if (activeGroup.value?.id() === defaultCameraId.value) {
  202. ElMessage.error({
  203. message: '无法删除默认相机',
  204. });
  205. return;
  206. }
  207. const index = addedCameras.value.findIndex((item) => item === activeGroup.value?.id());
  208. index >= 0 && addedCameras.value.splice(index, 1);
  209. activeGroup.value?.destroy();
  210. stage!.find('Transformer')[0]?.destroy();
  211. layer?.draw();
  212. };
  213. /** 鼠标悬浮事件 */
  214. const handleMouseOver = (e) => {
  215. // 禁用浏览器默认鼠标事件
  216. document.oncontextmenu = () => {
  217. return false;
  218. };
  219. const group = e.target.parent;
  220. // 如果悬浮的相机是默认相机,弹出默认tip
  221. if (group.id() === defaultCameraId.value) {
  222. let pos = TipPositionEnum.TOP;
  223. const tipPosition = defaultIcon?.absolutePosition();
  224. let x = Number(tipPosition?.x.toFixed(2)) || 0;
  225. let y = Number(tipPosition?.y.toFixed(2)) || 0;
  226. const angle = group.rotation() >= 0 ? group.rotation() : group.rotation() + 360;
  227. if (angle >= 30) {
  228. if (angle <= 150) {
  229. pos = TipPositionEnum.RIGHT;
  230. x += 26;
  231. y -= 17;
  232. } else if (angle <= 210) {
  233. pos = TipPositionEnum.BOTTOM;
  234. y += 26;
  235. x -= 50;
  236. } else {
  237. pos = TipPositionEnum.LEFT;
  238. x -= 121;
  239. y -= 25;
  240. }
  241. } else {
  242. y -= 61;
  243. x -= 43;
  244. }
  245. createDefaultTip(x, y, pos);
  246. }
  247. };
  248. /** 鼠标离开事件 */
  249. const handleMouseLeave = () => {
  250. // 恢复浏览器默认事件
  251. document.oncontextmenu = () => {
  252. return true;
  253. };
  254. defaultTip && destoryDefaultTip();
  255. };
  256. /** 开始拖拽事件 */
  257. const handleDragStart = () => {
  258. isTransform = true;
  259. destoryDefaultTip();
  260. destoryOptBlock();
  261. };
  262. /** 结束拖拽事件 */
  263. const handleDragEnd = () => {
  264. isTransform = false;
  265. };
  266. /** 全局点击事件 */
  267. const handleStageClick = (e) => {
  268. // 点击舞台取消现有激活的transformer
  269. if (e.target === stage) {
  270. stage!.find('Transformer')[0].destroy();
  271. layer!.draw();
  272. return;
  273. }
  274. // 判断点击对象是否为相机
  275. if (!e.target.hasName('image')) {
  276. return;
  277. }
  278. const parent = e.target.parent;
  279. if (!parent.hasName('group')) {
  280. return;
  281. }
  282. const group = e.target.parent;
  283. attachTransformer(group);
  284. // 判断是否为右键点击
  285. if (e.evt.button === 2) {
  286. createOptBlock(group, e.evt.offsetX + 20, e.evt.offsetY);
  287. }
  288. };
  289. /** 键盘点击事件 */
  290. const handleKeyDown = (e) => {
  291. // 删除键
  292. if (e.keyCode === 46 || e.code === 'Delete') {
  293. deleteCamera();
  294. }
  295. };
  296. // 基础监听事件绑定
  297. const bindBaseEvt = (node: Konva.Node) => {
  298. // node.on('transform', handleDragStart);
  299. // node.on('transformend', handleDragEnd);
  300. node.on('mouseover', handleMouseOver);
  301. node.on('mouseleave', handleMouseLeave);
  302. };
  303. /** 输出布局json */
  304. const toJson = () => {
  305. const json = stage!.toJSON();
  306. const cameras = JSON.parse(json)
  307. .children[0].children.filter((node) => node.className === 'Group')
  308. .map((item) => {
  309. return {
  310. cameraId: item.attrs.id,
  311. rotation: Number((item.attrs.rotation | 0).toFixed(2)),
  312. x: Math.round(item.attrs.x | 0),
  313. y: Math.round(item.attrs.y | 0),
  314. scaleX: Number((item.attrs.scaleX | 1).toFixed(1)),
  315. scaleY: Number((item.attrs.scaleY | 1).toFixed(1)),
  316. url: shopCameraList.value.find((cam) => cam.code === item.attrs.id)?.pushstreamIp || '',
  317. };
  318. });
  319. const layout = {
  320. bgInfo: {
  321. bgImg: bgImgUrl.value,
  322. width: layer?.find('#bgImg')[0].width(),
  323. height: layer?.find('#bgImg')[0].height(),
  324. },
  325. defaultCameraId: defaultCameraId.value,
  326. cameraList: cameras,
  327. };
  328. return JSON.stringify(layout);
  329. };
  330. /** 导入布局json */
  331. const createMap = (layout) => {
  332. // const layout = JSON.parse(json);
  333. bgImgUrl.value = layout.bgInfo.bgImg;
  334. addBg();
  335. layout.cameraList.forEach((camera) => {
  336. const group = new Konva.Group({
  337. x: camera.x,
  338. y: camera.y,
  339. id: camera.cameraId,
  340. rotation: camera.rotation,
  341. scaleX: camera.scaleX,
  342. scaleY: camera.scaleY,
  343. draggable: false,
  344. name: 'group',
  345. });
  346. const camImg = new Image();
  347. camImg.onload = () => {
  348. const cameraIcon = new Konva.Image({
  349. width: 52,
  350. height: 37,
  351. image: camImg,
  352. name: 'image',
  353. });
  354. group.add(cameraIcon);
  355. layer?.add(group);
  356. bindBaseEvt(cameraIcon);
  357. addedCameras.value.push(camera.cameraId);
  358. if (camera.cameraId === layout.defaultCameraId) {
  359. setDefaultCamera(group);
  360. defaultIcon?.show();
  361. }
  362. if (addedCameras.value.length === layout.cameraList.length) {
  363. layer?.batchDraw();
  364. }
  365. };
  366. camImg.src = cameraImg;
  367. });
  368. };
  369. const resetMap = () => {
  370. defaultIcon?.moveTo(copyLayer);
  371. layer?.clear();
  372. layer?.removeChildren();
  373. defaultIcon?.moveTo(layer);
  374. addedCameras.value = [];
  375. activeGroup.value = null;
  376. defaultCameraId.value = '';
  377. isTransform = false;
  378. bgImgUrl.value = '';
  379. };
  380. onMounted(() => {
  381. window.addEventListener('keydown', handleKeyDown);
  382. });
  383. onBeforeUnmount(() => {
  384. window.removeEventListener('keydown', handleKeyDown);
  385. });
  386. return {
  387. defaultCameraId,
  388. activeCameraId,
  389. addedCameras,
  390. bgImgUrl,
  391. initContainer,
  392. addBg,
  393. addCamera,
  394. destoryOptBlock,
  395. toJson,
  396. createMap,
  397. resetMap,
  398. };
  399. }
  400. export default useMapEditor;