DrawTools.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. <template>
  2. <div class="draw-tools-container">
  3. <div class="draw-item" @mouseover="showColorSelect = true" @mouseleave="showColorSelect = false">
  4. <div class="item-label">画笔选择</div>
  5. <i class="color-box" :style="{ backgroundColor: colorList[drawerColorIndex]?.value }" />
  6. <!--弹窗选择器-->
  7. <div v-show="showColorSelect" ref="selectBoxRef" class="select-box" style="top: -242px; padding-bottom: 20px">
  8. <div class="box-content">
  9. <div
  10. v-for="(item, index) in colorList"
  11. :key="index"
  12. :class="mouseToolState.drawType === item.value ? 'select-item active' : 'select-item'"
  13. @click="selectColor(index)"
  14. >
  15. <i class="color-box" :style="{ backgroundColor: item.value }" />
  16. <span>{{ item.label }}</span>
  17. </div>
  18. </div>
  19. </div>
  20. </div>
  21. <div class="draw-item" @mouseover="showDrawTypeSelect = true" @mouseleave="showDrawTypeSelect = false">
  22. <div class="item-label">框面类型</div>
  23. <!--弹窗选择器-->
  24. <div v-show="showDrawTypeSelect" ref="selectBoxRef2" class="select-box" style="top: -127px; padding-bottom: 20px">
  25. <div class="box-content">
  26. <div
  27. v-for="(item, index) in drawTypeList"
  28. :key="index"
  29. :class="mouseToolState.drawType === item.value ? 'select-item active' : 'select-item'"
  30. @click="selectDrawType(index)"
  31. >
  32. <span>{{ item.label }}</span>
  33. </div>
  34. </div>
  35. </div>
  36. </div>
  37. <div class="draw-item" @mouseover="showGraphicsTypeSelect = true" @mouseleave="showGraphicsTypeSelect = false">
  38. <div class="item-label">图像类型</div>
  39. <!--弹窗选择器-151-->
  40. <div v-show="showGraphicsTypeSelect" ref="selectBoxRef2" class="select-box" style="top: -184.5px; padding-bottom: 20px">
  41. <div class="box-content">
  42. <div
  43. v-for="(item, index) in graphicsTypeList"
  44. :key="index"
  45. :class="mouseToolState.graphicsType === item.value ? 'select-item active' : 'select-item'"
  46. @click="selectGraphicsType(index)"
  47. >
  48. <span>{{ item.label }}</span>
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. <div class="draw-item" @click="openDraw">{{ mouseToolState.drawing ? '关闭绘制' : '开启绘制' }}</div>
  54. <div class="draw-item" @click="handleUndo">撤销</div>
  55. <div id="menu-box">
  56. <div class="menu-item">删除</div>
  57. </div>
  58. </div>
  59. </template>
  60. <script lang="ts" setup name="DrawTools">
  61. import { useHistory } from '@/hooks/useHistory';
  62. import { nanoid } from 'nanoid';
  63. import { deepClone } from '@/utils';
  64. import * as turf from '@turf/turf';
  65. interface ListItem {
  66. label: string;
  67. value: string;
  68. }
  69. const props = defineProps({
  70. activeMap: String
  71. });
  72. const AMapType = ['vectorgraph', 'satellite'];
  73. const getDrawTool = inject('getDrawTool');
  74. const getMap = inject('getMap');
  75. const emits = defineEmits(['handleAnalysisData']);
  76. const { currentState, commit, undo, history, future } = useHistory();
  77. const mouseToolState = reactive({
  78. drawing: false,
  79. color: '#f80102',
  80. // 1线框 2区域
  81. drawType: '1',
  82. // 图形形状 circle圆形 rectangle矩形 polygon多边形
  83. graphicsType: 'circle'
  84. });
  85. watch(
  86. mouseToolState,
  87. () => {
  88. const drawTool = getDrawTool();
  89. if (mouseToolState.drawing) {
  90. drawOptions.color = mouseToolState.color;
  91. drawOptions.drawType = mouseToolState.drawType;
  92. drawOptions.graphicsType = mouseToolState.graphicsType;
  93. drawTool.drawGraphics2(drawOptions);
  94. if (AMapType.includes(props.activeMap)) {
  95. drawTool.getMouseTool().on('draw', onDraw);
  96. } else {
  97. drawTool.getMouseTool().on('drawend', onDraw2);
  98. }
  99. } else {
  100. mouseToolState.drawing = false;
  101. drawTool.closeDraw();
  102. }
  103. },
  104. {
  105. deep: true
  106. }
  107. );
  108. const drawOptions = reactive({
  109. color: '#f80102',
  110. // 1线框 2区域
  111. drawType: '1',
  112. // 图形形状 circle圆形 rectangle矩形 polygon多边形
  113. graphicsType: 'circle'
  114. });
  115. const selectBoxRef = ref(null);
  116. const selectBoxRef2 = ref(null);
  117. const selectBoxRef3 = ref(null);
  118. // 显示画笔选择弹窗
  119. let showColorSelect = ref(false);
  120. // 画笔颜色索引
  121. let drawerColorIndex = ref(0);
  122. // 画笔颜色列表
  123. const colorList: ListItem[] = reactive([
  124. { label: '红色', value: '#f80102' },
  125. { label: '橙色', value: '#f4511e' },
  126. { label: '蓝色', value: '#0197f8' },
  127. { label: '绿色', value: '#42b884' }
  128. ]);
  129. // 选中颜色
  130. const selectColor = (index) => {
  131. drawerColorIndex.value = index;
  132. mouseToolState.color = colorList[index].value;
  133. showColorSelect.value = false;
  134. };
  135. const overlays = [];
  136. const overlaysData = [];
  137. // 点击颜色选择之外
  138. onClickOutside(selectBoxRef, () => {
  139. if (!showColorSelect.value) return;
  140. showColorSelect.value = false;
  141. });
  142. // 绘制类型列表
  143. const drawTypeList: ListItem[] = reactive([
  144. { label: '线框', value: '1' },
  145. { label: '面框', value: '2' }
  146. ]);
  147. // 显示隐藏绘制类型弹窗
  148. let showDrawTypeSelect = ref(false);
  149. // 选择
  150. const selectDrawType = (index) => {
  151. mouseToolState.drawType = drawTypeList[index].value;
  152. showDrawTypeSelect.value = false;
  153. };
  154. // 点击绘制类型选择之外
  155. onClickOutside(selectBoxRef2, () => {
  156. if (!showDrawTypeSelect.value) return;
  157. showDrawTypeSelect.value = false;
  158. });
  159. // 绘制图形列表
  160. const graphicsTypeList: ListItem[] = reactive([
  161. { label: '圆形', value: 'circle' },
  162. { label: '矩形', value: 'rectangle' },
  163. { label: '多边形', value: 'polygon' }
  164. // { label: '索套', value: 'freePolygon' }
  165. ]);
  166. // 显隐图像类型弹窗
  167. let showGraphicsTypeSelect = ref(false);
  168. // 选择
  169. const selectGraphicsType = (index) => {
  170. mouseToolState.graphicsType = graphicsTypeList[index].value;
  171. showGraphicsTypeSelect.value = false;
  172. };
  173. // 点击绘制类型选择之外
  174. onClickOutside(selectBoxRef3, () => {
  175. if (!showGraphicsTypeSelect.value) return;
  176. showGraphicsTypeSelect.value = false;
  177. });
  178. // 开启结束绘制
  179. const openDraw = () => {
  180. mouseToolState.drawing = !mouseToolState.drawing;
  181. };
  182. // 高德地图绘制结束
  183. const onDraw = (event) => {
  184. const obj = event.obj;
  185. const id = nanoid();
  186. obj._opts.extData = {
  187. id: id
  188. };
  189. const data: any = {
  190. id: id,
  191. type: drawOptions.graphicsType,
  192. color: obj._opts.strokeColor,
  193. drawType: obj._opts.fillOpacity === 0 ? '1' : '2'
  194. };
  195. if (data.type === 'circle') {
  196. data.center = [obj.getCenter().lng, obj.getCenter().lat];
  197. data.radius = obj.getRadius();
  198. }
  199. const path = obj.getPath();
  200. // 将AMap.LngLat对象数组转换为经纬度数组
  201. const pathArr = path.map((lngLat) => {
  202. // 返回经度和纬度的数组
  203. return [lngLat.lng, lngLat.lat];
  204. });
  205. data.path = pathArr;
  206. overlays.push(obj);
  207. overlaysData.push(data);
  208. commit(deepClone(overlaysData));
  209. // 右击进入编辑
  210. obj.on('rightclick', handleRightClick);
  211. if (overlaysData.length === 1) {
  212. analysisSpatial(data);
  213. }
  214. // 点击空间分析
  215. obj.on('click', function () {
  216. // 没在编辑时
  217. if (!mouseToolState.drawing) {
  218. analysisSpatial(data);
  219. }
  220. });
  221. };
  222. // 粤政图绘制结束
  223. const onDraw2 = (event) => {
  224. const feature = event.feature;
  225. const geometry = feature.getGeometry();
  226. const geometryType = geometry.getType();
  227. const id = nanoid();
  228. feature.set('id', id);
  229. const data: any = {
  230. id: id,
  231. type: drawOptions.graphicsType,
  232. // color: feature._opts.strokeColor,
  233. // drawType: feature._opts.fillOpacity === 0 ? '1' : '2'
  234. };
  235. // 获取绘制的几何信息(包括经纬度)
  236. if (geometryType === 'Point') {
  237. const coordinates = geometry.getCoordinates();
  238. console.log('绘制了一个点:', coordinates);
  239. } else if (geometryType === 'LineString') {
  240. const coordinates = geometry.getCoordinates();
  241. console.log('绘制了一条线:', coordinates);
  242. // 线的坐标是点的数组,每个点都是一个经纬度数组
  243. } else if (geometryType === 'Polygon') {
  244. const coordinates = geometry.getCoordinates();
  245. console.log('绘制了一个多边形:', coordinates);
  246. // 多边形的坐标是环的数组,每个环是点的数组,每个点都是一个经纬度数组
  247. } else if (geometryType === 'Circle') {
  248. data.center = geometry.getCenter();
  249. data.radius = geometry.getRadius();
  250. const data2 = turf.sector(data.center, data.radius, 0, 360);
  251. const pathArr = data2.geometry.coordinates[0];
  252. data.path = pathArr;
  253. console.log('绘制了一个圆:', data);
  254. }
  255. // const path = geometry.getCoordinates();
  256. // debugger
  257. // // 将AMap.LngLat对象数组转换为经纬度数组
  258. // const pathArr = path.map((lngLat) => {
  259. // // 返回经度和纬度的数组
  260. // return [lngLat.lng, lngLat.lat];
  261. // });
  262. // data.path = pathArr;
  263. overlays.push(feature);
  264. overlaysData.push(data);
  265. commit(deepClone(overlaysData));
  266. // 右击进入编辑
  267. feature.on('contextmenu', handleRightClick);
  268. if (overlaysData.length === 1) {
  269. analysisSpatial(data);
  270. }
  271. // // 点击空间分析
  272. // feature.on('click', function () {
  273. // // 没在编辑时
  274. // if (!mouseToolState.drawing) {
  275. // analysisSpatial(data);
  276. // }
  277. // });
  278. };
  279. let rightClickObj;
  280. // 图形右击事件
  281. let initContextMenu = false;
  282. const handleRightClick = (event) => {
  283. debugger
  284. rightClickObj = event.target;
  285. const contextMenu = getDrawTool().getContextMenu();
  286. if (!initContextMenu) {
  287. // 右键删除按钮
  288. contextMenu.addItem(
  289. '删除',
  290. function () {
  291. contextMenu.close();
  292. deleteGraphics();
  293. },
  294. 0
  295. );
  296. initContextMenu = true;
  297. }
  298. contextMenu.open(getMap(), event.lnglat);
  299. };
  300. // 删除图形
  301. const deleteGraphics = () => {
  302. const id = rightClickObj.getExtData()?.id;
  303. if (id) {
  304. for (let i = 0; i < overlays.length; i++) {
  305. const overlay = Array.isArray(overlays[i]) ? overlays[i][0] : overlays[i];
  306. if (overlay?.getExtData().id === id) {
  307. removeOverlayByIndex(i);
  308. commit(deepClone(overlaysData));
  309. rightClickObj = null;
  310. }
  311. }
  312. }
  313. };
  314. // 撤销绘制
  315. const handleUndo = () => {
  316. mouseToolState.drawing = false;
  317. const previous = history.value[history.value.length - 2];
  318. if (history.value.length > 1) {
  319. if (currentState.value.length > previous.length) {
  320. // 撤销新增
  321. removeOverlayByIndex(currentState.value.length - 1);
  322. } else {
  323. let restoreData;
  324. for (let i = 0; i < previous.length; i++) {
  325. let index = 0;
  326. for (let k = 0; k < currentState.value.length; k++) {
  327. if (previous[i].id !== currentState.value[k].id) {
  328. index++;
  329. } else {
  330. break;
  331. }
  332. }
  333. if (index === previous.length - 1) {
  334. restoreData = previous[i];
  335. break;
  336. }
  337. }
  338. if (restoreData) {
  339. const newData = {
  340. type: restoreData.type,
  341. path: restoreData.path,
  342. strokeColor: restoreData.color,
  343. strokeOpacity: 1,
  344. strokeWeight: '1',
  345. fillColor: restoreData.color,
  346. fillOpacity: restoreData.drawType === '1' ? 0 : 0.5
  347. };
  348. if (restoreData.type === 'circle') {
  349. newData.center = restoreData.center;
  350. newData.radius = restoreData.radius;
  351. }
  352. const obj = getDrawTool().createGraphics(newData);
  353. overlays.push(obj);
  354. }
  355. }
  356. undo();
  357. console.log(history.value, future.value, currentState.value);
  358. }
  359. };
  360. // 根据索引移除覆盖物
  361. const removeOverlayByIndex = (index: number) => {
  362. const map = getMap();
  363. if (Array.isArray(overlays[index])) {
  364. overlays[index].forEach((overlay) => {
  365. // 移除地图上覆盖物
  366. map.remove(overlay);
  367. });
  368. } else {
  369. // 移除地图上覆盖物
  370. map.remove(overlays[index]);
  371. }
  372. overlays.splice(index, 1);
  373. overlaysData.splice(index, 1);
  374. };
  375. let selectedScope = reactive({});
  376. // 空间分析数据
  377. const analysisSpatial = (data) => {
  378. // 已选中的范围
  379. if (selectedScope[data.id]) {
  380. delete selectedScope[data.id];
  381. if (JSON.stringify(selectedScope) === '{}') {
  382. // rightMenuRef.value.clickContractMenu();
  383. return;
  384. }
  385. } else {
  386. selectedScope[data.id] = data;
  387. }
  388. let location = [];
  389. for (let key in selectedScope) {
  390. let itemLocation = [];
  391. if (selectedScope[key].path && selectedScope[key].path.length > 1) {
  392. selectedScope[key].path.forEach((item) => {
  393. itemLocation.push({
  394. x: item[0],
  395. y: item[1]
  396. });
  397. });
  398. itemLocation.push(itemLocation[0]);
  399. location.push(itemLocation);
  400. }
  401. }
  402. emits('handleAnalysisData', location);
  403. };
  404. onMounted(() => {
  405. // 监听右击事件
  406. getMap().on('contextmenu', (event) => {
  407. event.preventDefault(); // 阻止默认上下文菜单
  408. // 获取右击位置的要素
  409. const featuresAtPixel = getMap().getFeaturesAtPixel(event.pixel);
  410. // 检查是否有要素在右击位置
  411. if (featuresAtPixel && featuresAtPixel.length > 0) {
  412. // 遍历要素并检查条件(这里可以根据需要添加条件)
  413. featuresAtPixel.forEach(function(feature) {
  414. // 执行你想要的动作,例如显示一个信息框或弹出菜单
  415. console.log('右击了要素:', feature.get('id')); // 假设要素有一个'id'属性
  416. // 你可以在这里显示一个自定义的右击菜单
  417. // 例如,创建一个<div>元素,设置其内容和样式,然后添加到DOM中
  418. // 并监听菜单项的点击事件来执行相应的动作
  419. });
  420. }
  421. });
  422. })
  423. onBeforeUnmount(() => {
  424. //
  425. });
  426. </script>
  427. <style lang="scss" scoped>
  428. .draw-tools-container {
  429. position: absolute;
  430. right: 250px;
  431. bottom: 460px;
  432. background-color: #304468;
  433. border-radius: 5px;
  434. display: flex;
  435. color: #fff;
  436. font-size: 36px;
  437. padding: 10px 0;
  438. .draw-item {
  439. display: flex;
  440. align-items: center;
  441. position: relative;
  442. padding: 0 10px;
  443. cursor: pointer;
  444. border-left: 1px solid #3a74be;
  445. &:first-child {
  446. border-left: none;
  447. }
  448. .item-label {
  449. margin-right: 10px;
  450. }
  451. .select-box {
  452. position: absolute;
  453. left: 0;
  454. z-index: 7;
  455. width: 100%;
  456. .box-content {
  457. background-color: #102341;
  458. }
  459. .select-item {
  460. display: flex;
  461. align-items: center;
  462. cursor: pointer;
  463. padding: 5px 10px;
  464. &:hover {
  465. background-color: rgba(22, 73, 142, 0.5);
  466. }
  467. &:first-child {
  468. margin-left: 0;
  469. }
  470. }
  471. .color-box {
  472. margin-right: 5px;
  473. }
  474. }
  475. }
  476. .color-box {
  477. display: block;
  478. width: 20px;
  479. height: 20px;
  480. cursor: pointer;
  481. border-radius: 3px;
  482. }
  483. }
  484. .active {
  485. background-color: rgba(22, 73, 142, 0.5);
  486. }
  487. .menu-box {
  488. .menu-item {
  489. font-size: 32px;
  490. }
  491. }
  492. </style>