index.vue 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322
  1. <template>
  2. <a-spin :spinning="spinning">
  3. <a-alert
  4. v-if="areaAvailable"
  5. message="检测区域范围未配置或数值较小,请在设备配置调整参数!"
  6. banner
  7. />
  8. <furnitureCard
  9. v-if="isEditDraggable"
  10. v-model:is-edit="isEditDraggable"
  11. :style="{ marginTop: '30px' }"
  12. @add="addHnadler"
  13. ></furnitureCard>
  14. <div class="viewer">
  15. <div class="viewer-header">
  16. <div>
  17. <div class="viewer-header-title">家具配置</div>
  18. <div class="viewer-header-subtitle">
  19. <span>检测范围 {{ areaWidth }} x {{ areaHeight }} cm</span>
  20. </div>
  21. </div>
  22. <div class="viewer-header-extra">
  23. <a-space>
  24. <a-switch
  25. :checked="isEditDraggable"
  26. checked-children="启用"
  27. un-checked-children="禁用"
  28. @change="isEditDraggable = !isEditDraggable"
  29. />
  30. <a-button
  31. type="primary"
  32. size="small"
  33. :disabled="!isEditDraggable"
  34. @click="saveAllConfig"
  35. >保存配置</a-button
  36. >
  37. </a-space>
  38. </div>
  39. </div>
  40. <div v-if="!areaAvailable" class="viewer-content">
  41. <div
  42. ref="contentEl"
  43. class="mapBox"
  44. :style="{
  45. width: `${areaWidth}px`,
  46. height: `${areaHeight}px`,
  47. cursor: !isEditDraggable ? 'no-drop' : 'default',
  48. }"
  49. @mousedown="handleMouseDownMapCanvas"
  50. >
  51. <furniture-icon
  52. v-for="(item, index) in mapCanvasList"
  53. :key="index"
  54. :icon="item.type"
  55. :width="item.width"
  56. :height="item.height"
  57. :style="{
  58. left: `${item.left}px`,
  59. top: `${item.top}px`,
  60. position: 'absolute',
  61. transform: `rotate(${item.rotate}deg)`,
  62. cursor: item.type === 'radar' ? 'default' : isEditDraggable ? 'move' : 'default',
  63. pointerEvents: item.type === 'radar' ? 'none' : isEditDraggable ? 'auto' : 'none',
  64. border: `${item.isActice && item.type !== 'radar' ? '2px solid yellow' : 'none'}`,
  65. }"
  66. :class="{ 'dragging-item': item.isDragging }"
  67. tabindex="0"
  68. :draggable="item.type === 'radar' ? false : isEditDraggable"
  69. @dragstart="onDragstartListItem($event, item)"
  70. @dragend="onDragendEndListItem($event, item)"
  71. @click="onClickMapItem($event, item)"
  72. />
  73. </div>
  74. <div v-if="clickedDragItem" class="mapConfig">
  75. <div class="mapConfig-header">家具属性</div>
  76. <div class="mapConfig-item">
  77. <div class="mapConfig-item-label">家具名称:</div>
  78. <div class="mapConfig-item-content">
  79. <a-input
  80. v-if="clickedDragItem"
  81. v-model:value.trim="clickedDragItem.name"
  82. size="small"
  83. style="width: 128px"
  84. />
  85. </div>
  86. </div>
  87. <div class="mapConfig-item">
  88. <div class="mapConfig-item-label">家具大小:</div>
  89. <div class="mapConfig-item-content">
  90. <a-space v-if="clickedDragItem">
  91. <a-input-number
  92. v-model:value.trim="clickedDragItem.width"
  93. size="small"
  94. style="width: 60px"
  95. />
  96. <a-input-number
  97. v-model:value.trim="clickedDragItem.height"
  98. size="small"
  99. style="width: 60px"
  100. />
  101. </a-space>
  102. </div>
  103. </div>
  104. <div class="mapConfig-item">
  105. <div class="mapConfig-item-label">家具旋转:</div>
  106. <div class="mapConfig-item-content">
  107. <a-space>
  108. <UndoOutlined
  109. :rotate="-270"
  110. @click="rotateFurnitureIcon(1, clickedDragItem?.nanoid || '')"
  111. />
  112. <RedoOutlined
  113. :rotate="-90"
  114. @click="rotateFurnitureIcon(2, clickedDragItem?.nanoid || '')"
  115. />
  116. </a-space>
  117. </div>
  118. </div>
  119. <div class="mapConfig-item">
  120. <div class="mapConfig-item-label">微调位置:</div>
  121. <div class="mapConfig-item-content">
  122. <a-space>
  123. <ArrowLeftOutlined
  124. @click="positonFurnitureIcon('left', clickedDragItem?.nanoid || '', distance)"
  125. />
  126. <ArrowUpOutlined
  127. @click="positonFurnitureIcon('up', clickedDragItem?.nanoid || '', distance)"
  128. />
  129. <ArrowDownOutlined
  130. @click="positonFurnitureIcon('down', clickedDragItem?.nanoid || '', distance)"
  131. />
  132. <ArrowRightOutlined
  133. @click="positonFurnitureIcon('right', clickedDragItem?.nanoid || '', distance)"
  134. />
  135. <a-input-number v-model:value.trim="distance" size="small" style="width: 60px" />
  136. </a-space>
  137. </div>
  138. </div>
  139. <div class="mapConfig-item">
  140. <div class="mapConfig-item-label">删除家具:</div>
  141. <div class="mapConfig-item-content">
  142. <a-popconfirm
  143. v-if="clickedDragItem.type === 'bed'"
  144. placement="bottom"
  145. @confirm="deleteFurnitureBed(clickedDragItem?.nanoid || '')"
  146. >
  147. <template #icon><question-circle-outlined style="color: red" /></template>
  148. <template #title>
  149. <div>删除 “床”也会删除子区域,</div>
  150. <div>是否继续删除?</div>
  151. </template>
  152. <DeleteOutlined />
  153. </a-popconfirm>
  154. <DeleteOutlined v-else @click="deleteFurnitureIcon(clickedDragItem?.nanoid || '')" />
  155. </div>
  156. </div>
  157. </div>
  158. </div>
  159. </div>
  160. <div class="viewer">
  161. <div class="viewer-header">
  162. <div>
  163. <div class="viewer-header-title">屏蔽子区域配置</div>
  164. <div class="viewer-header-subtitle">检测范围 {{ areaWidth }} x {{ areaHeight }} cm</div>
  165. </div>
  166. <div class="viewer-header-extra">
  167. <a-space>
  168. <a-button size="small" :disabled="!isEditDraggable" @click="createNewBlock">{{
  169. isCreating ? '创建中...' : '新建区域'
  170. }}</a-button>
  171. </a-space>
  172. </div>
  173. </div>
  174. <div v-if="!areaAvailable" class="viewer-content">
  175. <div
  176. class="mapBox blockArea"
  177. :style="{
  178. width: `${areaWidth}px`,
  179. height: `${areaHeight}px`,
  180. cursor: !isEditDraggable ? 'no-drop' : isCreating ? 'crosshair' : 'default',
  181. }"
  182. @mousedown="handleMouseDown"
  183. >
  184. <furniture-icon
  185. v-for="(item, index) in furnitureItems"
  186. :key="index"
  187. :icon="item.type"
  188. :width="item.width"
  189. :height="item.length"
  190. :style="{
  191. left: `${item.left}px`,
  192. top: `${item.top}px`,
  193. position: 'absolute',
  194. transform: `rotate(${item.rotate}deg)`,
  195. cursor: 'default',
  196. }"
  197. :draggable="false"
  198. />
  199. <!-- 绘制临时选区 -->
  200. <div
  201. v-if="currentBlock"
  202. class="temp-block"
  203. :style="{
  204. left: `${Math.min(currentBlock.startX, currentBlock.currentX)}px`,
  205. top: `${Math.min(currentBlock.startY, currentBlock.currentY)}px`,
  206. width: `${Math.abs(currentBlock.currentX - currentBlock.startX)}px`,
  207. height: `${Math.abs(currentBlock.currentY - currentBlock.startY)}px`,
  208. }"
  209. ></div>
  210. <!-- 已创建区块 -->
  211. <div
  212. v-for="(block, blockIndex) in blocks"
  213. :key="block.id"
  214. class="block-item"
  215. :style="{
  216. left: `${block.x}px`,
  217. top: `${block.y}px`,
  218. width: `${block.width}px`,
  219. height: `${block.height}px`,
  220. border: `2px solid ${block?.isBed ? '#1abc1a' : block.isActice ? 'yellow' : '#1890ff'}`,
  221. position: 'absolute',
  222. cursor: !isEditDraggable ? 'no-drop' : 'move',
  223. backgroundColor: block.isBed ? 'rgba(26, 188, 26, 0.1)' : 'rgba(24, 144, 255, 0.1)',
  224. }"
  225. @mousedown="startDrag(block, $event)"
  226. @click="selectBlock(block)"
  227. >
  228. <div
  229. class="resize-handle"
  230. :style="{
  231. backgroundColor: block.isBed ? '#1abc1a' : '#1890ff',
  232. }"
  233. @mousedown.stop="startResize(block, $event)"
  234. >
  235. {{ blockIndex + 1 }}
  236. </div>
  237. </div>
  238. </div>
  239. <div v-if="selectedBlock" class="mapConfig">
  240. <div class="mapConfig-header">子区域属性</div>
  241. <div class="mapConfig-item">
  242. <div class="mapConfig-item-label">X范围:</div>
  243. <div class="mapConfig-item-content">
  244. <a-space>
  245. <a-input
  246. v-model:value.trim="selectedBlock.startXx"
  247. :style="{ width: '50px' }"
  248. size="small"
  249. @pressEnter="blockInputPressEnter($event, selectedBlock, 'startXx')"
  250. @blur="blockInputBlur($event, selectedBlock, 'startXx')"
  251. />
  252. <a-input
  253. v-model:value.trim="selectedBlock.stopXx"
  254. :style="{ width: '50px' }"
  255. size="small"
  256. @pressEnter="blockInputPressEnter($event, selectedBlock, 'stopXx')"
  257. @blur="blockInputBlur($event, selectedBlock, 'stopXx')"
  258. />
  259. </a-space>
  260. </div>
  261. </div>
  262. <div class="mapConfig-item">
  263. <div class="mapConfig-item-label">Y范围:</div>
  264. <div class="mapConfig-item-content">
  265. <a-space>
  266. <a-input
  267. v-model:value.trim="selectedBlock.startYy"
  268. :style="{ width: '50px' }"
  269. size="small"
  270. @pressEnter="blockInputPressEnter($event, selectedBlock, 'startYy')"
  271. @blur="blockInputBlur($event, selectedBlock, 'startYy')"
  272. />
  273. <a-input
  274. v-model:value.trim="selectedBlock.stopYy"
  275. :style="{ width: '50px' }"
  276. size="small"
  277. @pressEnter="blockInputPressEnter($event, selectedBlock, 'stopYy')"
  278. @blur="blockInputBlur($event, selectedBlock, 'stopYy')"
  279. />
  280. </a-space>
  281. </div>
  282. </div>
  283. <div class="mapConfig-item">
  284. <div class="mapConfig-item-label">Z范围:</div>
  285. <div class="mapConfig-item-content">
  286. <a-space>
  287. <a-input
  288. v-model:value.trim="selectedBlock.startZz"
  289. :style="{ width: '50px' }"
  290. size="small"
  291. />
  292. <a-input
  293. v-model:value.trim="selectedBlock.stopZz"
  294. :style="{ width: '50px' }"
  295. size="small"
  296. />
  297. </a-space>
  298. </div>
  299. </div>
  300. <div class="mapConfig-item">
  301. <div class="mapConfig-item-label">区域跟踪:</div>
  302. <div class="mapConfig-item-content">
  303. <a-switch v-model:checked="selectedBlock.isTracking" size="small" />
  304. </div>
  305. </div>
  306. <div class="mapConfig-item">
  307. <div class="mapConfig-item-label">区域跌倒:</div>
  308. <div class="mapConfig-item-content">
  309. <a-switch v-model:checked="selectedBlock.isFalling" size="small" />
  310. </div>
  311. </div>
  312. <div v-if="selectedBlock.isBed" class="mapConfig-item">
  313. <div class="mapConfig-item-label">呼吸检测:</div>
  314. <div class="mapConfig-item-content"> 默认开启 </div>
  315. </div>
  316. <div class="mapConfig-item">
  317. <div class="mapConfig-item-label">删除区域:</div>
  318. <div class="mapConfig-item-content">
  319. <DeleteOutlined @click="deleteBlockArea(selectedBlock.id || '')" />
  320. </div>
  321. </div>
  322. </div>
  323. </div>
  324. </div>
  325. </a-spin>
  326. </template>
  327. <script setup lang="ts">
  328. import { ref, computed } from 'vue'
  329. import * as roomApi from '@/api/room'
  330. import type { FurnitureType, Furniture } from '@/api/room/types'
  331. import { message } from 'ant-design-vue'
  332. import { nanoid } from 'nanoid'
  333. import { useDropZone } from '@vueuse/core'
  334. import { furnitureIconNameMap, furnitureIconSizeMap } from '@/const/furniture'
  335. import type { FurnitureIconType } from '@/types/furniture'
  336. import furnitureCard from '../furnitureCard/index.vue'
  337. import {
  338. RedoOutlined,
  339. UndoOutlined,
  340. ArrowUpOutlined,
  341. ArrowDownOutlined,
  342. ArrowLeftOutlined,
  343. ArrowRightOutlined,
  344. DeleteOutlined,
  345. QuestionCircleOutlined,
  346. } from '@ant-design/icons-vue'
  347. defineOptions({
  348. name: 'deviceAreaConfig',
  349. })
  350. type Props = {
  351. devId: string // 设备id,用于获取房间布局
  352. // x,y的范围,用于初始化画布的大小
  353. length: number
  354. width: number
  355. ranges: number[] // 区域范围
  356. }
  357. const emit = defineEmits<{
  358. (e: 'success', value: void): void
  359. }>()
  360. const props = withDefaults(defineProps<Props>(), {
  361. devId: '',
  362. length: 0, // 区域宽度
  363. width: 0, // 区域高度
  364. ranges: () => [], // 区域范围
  365. })
  366. const deviceRoomId = ref('')
  367. const furnitureItems = ref<Furniture[]>([])
  368. // 检测区域宽度 length
  369. const areaWidth = computed(() => {
  370. return Math.abs(props.length)
  371. })
  372. // 检测区域高度 width
  373. const areaHeight = computed(() => {
  374. return Math.abs(props.width)
  375. })
  376. // 检测区域是否可用,小于50cm的区域,不可用
  377. const areaAvailable = computed(() => {
  378. return areaWidth.value < 50 || areaHeight.value < 50
  379. })
  380. const spinning = ref(false)
  381. // 获取房间布局
  382. const fetchRoomLayout = async () => {
  383. console.log('fetchRoomLayout', props, props.devId)
  384. if (!props.devId) {
  385. message.error('设备ID不能为空')
  386. return
  387. }
  388. try {
  389. spinning.value = true
  390. const res = await roomApi.queryRoomInfo({
  391. devId: props.devId,
  392. })
  393. console.log('✅获取到房间布局信息', res)
  394. if (!res) {
  395. spinning.value = false
  396. return
  397. }
  398. const { furnitures, roomId, subRegions } = res.data
  399. deviceRoomId.value = roomId || ''
  400. if (furnitures) {
  401. furnitureItems.value = furnitures!.map((item) => {
  402. // 将接口的家具,添加在家具画布上
  403. mapCanvasList.value.push({
  404. name: item.name,
  405. type: item.type,
  406. width: item.width,
  407. height: item.length,
  408. top: item.top,
  409. left: item.left,
  410. x: item.x,
  411. y: item.y,
  412. rotate: item.rotate,
  413. nanoid: nanoid(),
  414. })
  415. // 将接口的家具,添加在屏蔽子区域画布上
  416. return {
  417. ...item,
  418. width: item.width || 45,
  419. length: item.length || 45,
  420. top: item.top || 0,
  421. left: item.left || 0,
  422. rotate: item.rotate || 0,
  423. x: item.x || 0,
  424. y: item.y || 0,
  425. }
  426. })
  427. }
  428. if (subRegions) {
  429. // 将接口的子区域,添加在子区域画布上
  430. subRegions.forEach((item, index) => {
  431. blocks.value.push({
  432. // 本地需要使用的数据
  433. id: nanoid(),
  434. x: item.startXx + getOriginPosition().originX,
  435. y: getOriginPosition().originY - item.startYy,
  436. ox: item.startXx + getOriginPosition().originX - getOriginPosition().originX,
  437. oy: getOriginPosition().originY - item.startYy - getOriginPosition().originY,
  438. width: Math.abs(item.stopXx - item.startXx),
  439. height: Math.abs(item.stopYy - item.startYy),
  440. isDragging: false,
  441. isResizing: false,
  442. isActice: false,
  443. isTracking: Boolean(item.trackPresence),
  444. isFalling: Boolean(item.excludeFalling),
  445. isBed: index === 0 && mapCanvasList.value.some((item) => item.type === 'bed'),
  446. // 来自接口回显的数据
  447. startXx: item.startXx,
  448. stopXx: item.stopXx,
  449. startYy: item.startYy,
  450. stopYy: item.stopYy,
  451. startZz: item.startZz,
  452. stopZz: item.stopZz,
  453. isLowSnr: item.isLowSnr,
  454. isDoor: item.isDoor,
  455. presenceEnterDuration: item.presenceEnterDuration,
  456. presenceExitDuration: item.presenceExitDuration,
  457. trackPresence: item.trackPresence,
  458. excludeFalling: item.excludeFalling,
  459. })
  460. })
  461. console.log('🚀', blocks.value)
  462. }
  463. spinning.value = false
  464. } catch (error) {
  465. console.error('❌获取房间布局信息失败', error)
  466. spinning.value = false
  467. }
  468. }
  469. fetchRoomLayout().finally(() => {
  470. // 获取房间信息后,初始化雷达图标
  471. initRadarIcon()
  472. })
  473. interface CanvaseItem {
  474. name: string // 名称
  475. type: string // 类型,图标icon
  476. width: number // 家具宽度
  477. height: number // 家具长度
  478. top: number // 距离检测范围上方位置 cm
  479. left: number // 距离检测范围左边位置 cm
  480. rotate: number // 旋转角度: 0°,90°,180°,270°
  481. x?: number // 距离雷达的X距离
  482. y?: number // 距离雷达的Y距离
  483. nanoid?: string // 本地使用
  484. isActice?: boolean // 是否选中 本地使用
  485. isDragging?: boolean // 是否拖拽 本地使用
  486. }
  487. // 画布上的家具列表
  488. const mapCanvasList = ref<CanvaseItem[]>([])
  489. const isEditDraggable = ref(false)
  490. // 家具列表添加
  491. const addHnadler = (icon: FurnitureIconType) => {
  492. console.log('addHnadler', icon)
  493. // 检查画布上是否已经添加过了 icon 为 bed 的家具
  494. if (icon === 'bed') {
  495. const isExist = mapCanvasList.value.some((item) => item.type === icon)
  496. if (isExist) {
  497. message.error('床已经添加过了,不可重复添加')
  498. return
  499. }
  500. }
  501. const { originOffsetX, originOffsetY } = getOriginPosition()
  502. // 家具原始宽高
  503. const originWidth = furnitureIconSizeMap[icon].width || 30
  504. const originHeight = furnitureIconSizeMap[icon].height || 30
  505. mapCanvasList.value.push({
  506. name: furnitureIconNameMap[icon],
  507. type: icon,
  508. width: originWidth,
  509. height: originHeight,
  510. top: 0,
  511. left: 0,
  512. rotate: 0,
  513. x: originOffsetX,
  514. y: originOffsetY,
  515. nanoid: nanoid(),
  516. isActice: false,
  517. })
  518. message.success('已添加家具')
  519. if (icon === 'bed') {
  520. // 同步添加一个子区域
  521. blocks.value.unshift({
  522. // 本地用
  523. id: nanoid(),
  524. x: 20,
  525. y: 15,
  526. ox: -150,
  527. oy: 180,
  528. width: originWidth,
  529. height: originHeight,
  530. isDragging: false,
  531. isResizing: false,
  532. isActice: false,
  533. isTracking: false,
  534. isFalling: false,
  535. isBed: true,
  536. // 接口用
  537. startXx: -150,
  538. stopXx: -100,
  539. startYy: 180,
  540. stopYy: 120,
  541. startZz: 0,
  542. stopZz: 0,
  543. isLowSnr: 1,
  544. isDoor: 0,
  545. presenceEnterDuration: 3,
  546. presenceExitDuration: 3,
  547. trackPresence: 0,
  548. excludeFalling: 0,
  549. })
  550. console.log('blocks', blocks.value)
  551. message.success('已添加子区域')
  552. }
  553. }
  554. const contentEl = ref<HTMLElement>()
  555. const currentDragItem = ref<CanvaseItem | null>(null)
  556. // 内容区域放置处理
  557. useDropZone(contentEl, {
  558. onDrop(files: File[] | null, event: DragEvent) {
  559. if (contentEl.value && currentDragItem.value) {
  560. const rect = contentEl.value.getBoundingClientRect()
  561. // 计算基于画布容器的精确坐标
  562. const x = event.clientX - rect.left
  563. const y = event.clientY - rect.top
  564. // 中心点对齐计算
  565. currentDragItem.value.top = y - currentDragItem.value.height / 2
  566. currentDragItem.value.left = x - currentDragItem.value.width / 2
  567. // 添加边界检查
  568. // const maxX = contentEl.value.offsetWidth - currentDragItem.value.width
  569. // const maxY = contentEl.value.offsetHeight - currentDragItem.value.height
  570. // currentDragItem.value.left = Math.max(0, Math.min(x - currentDragItem.value.width / 2, maxX))
  571. // currentDragItem.value.top = Math.max(0, Math.min(y - currentDragItem.value.height / 2, maxY))
  572. }
  573. },
  574. })
  575. // 家具列表元素开始拖拽
  576. const onDragstartListItem = (event: DragEvent, item: CanvaseItem) => {
  577. console.log('🔥onDragstartListItem', event, item)
  578. currentDragItem.value = item
  579. item.isDragging = true
  580. // 创建拖拽镜像
  581. const target = event.currentTarget as HTMLElement
  582. const clone = target.cloneNode(true) as HTMLElement
  583. const rect = target.getBoundingClientRect()
  584. // 获取实际渲染样式
  585. const computedStyle = window.getComputedStyle(target)
  586. // 复制所有关键样式
  587. clone.style.cssText = `
  588. position: fixed;
  589. left: -9999px;
  590. opacity: 0.5;
  591. pointer-events: none;
  592. width: ${rect.width}px;
  593. height: ${rect.height}px;
  594. transform: ${computedStyle.transform || `rotate(${item.rotate}deg)`};
  595. transform-origin: ${computedStyle.transformOrigin || 'center center'};
  596. border: ${computedStyle.border};
  597. box-shadow: ${computedStyle.boxShadow};
  598. background: ${computedStyle.background};
  599. `
  600. console.log('克隆元素尺寸:', clone.style.cssText)
  601. document.body.appendChild(clone)
  602. // 计算中心点偏移量
  603. const offsetX = rect.width / 2 // 水平中心偏移
  604. const offsetY = rect.height / 2 // 垂直中心偏移
  605. // 设置拖拽镜像和偏移量
  606. event.dataTransfer?.setDragImage(clone, offsetX, offsetY)
  607. // 隐藏原元素
  608. target.style.opacity = '0'
  609. }
  610. // 家具列表元素结束拖拽
  611. const onDragendEndListItem = (event: DragEvent, item: CanvaseItem) => {
  612. item.isDragging = false
  613. const { originOffsetX, originOffsetY } = getOriginPosition()
  614. if (currentDragItem.value) {
  615. currentDragItem.value.x = originOffsetX
  616. currentDragItem.value.y = originOffsetY
  617. currentDragItem.value.isDragging = false
  618. }
  619. requestAnimationFrame(() => {
  620. item.isActice = true
  621. clickedDragItem.value = item
  622. })
  623. console.log('🔥onDragendEndListItem', event, item, {
  624. x: currentDragItem.value?.x,
  625. y: currentDragItem.value?.y,
  626. })
  627. if (event.currentTarget) {
  628. ;(event.currentTarget as HTMLElement).style.opacity = '1'
  629. }
  630. }
  631. const clickedDragItem = ref<CanvaseItem | null>()
  632. const onClickMapItem = (event: MouseEvent, item: CanvaseItem) => {
  633. if (!isEditDraggable.value || item.type === 'radar' || item.isDragging) return
  634. console.log('onClickMapItem', event, item)
  635. item.isActice = true
  636. clickedDragItem.value = item
  637. }
  638. const handleMouseDownMapCanvas = () => {
  639. mapCanvasList.value.forEach((item) => {
  640. item.isActice = false
  641. })
  642. clickedDragItem.value = null
  643. }
  644. // 家具旋转
  645. const rotateFurnitureIcon = (type: number, nanoid: string) => {
  646. console.log('rotateFurnitureIcon', type, nanoid, mapCanvasList.value)
  647. const rotateMap = [0, 90, 180, 270]
  648. if (nanoid) {
  649. mapCanvasList.value.forEach((item) => {
  650. if (item.nanoid === nanoid) {
  651. // 获取当前角度在rotateMap中的索引
  652. const currentIndex = rotateMap.indexOf(item.rotate)
  653. if (type === 1) {
  654. // 逆时针(索引递减)
  655. const newIndex = (currentIndex - 1 + rotateMap.length) % rotateMap.length
  656. item.rotate = rotateMap[newIndex]
  657. } else if (type === 2) {
  658. // 顺时针(索引递增)
  659. const newIndex = (currentIndex + 1) % rotateMap.length
  660. item.rotate = rotateMap[newIndex]
  661. }
  662. }
  663. })
  664. }
  665. }
  666. // 微调距离
  667. const distance = ref(5)
  668. // 家具位置微调
  669. const positonFurnitureIcon = (type: string, nanoid: string, distance: number) => {
  670. console.log('positonFurnitureIcon', type, nanoid, mapCanvasList.value)
  671. if (nanoid) {
  672. mapCanvasList.value.forEach((item) => {
  673. if (item.nanoid === nanoid) {
  674. if (type === 'up') {
  675. item.top -= distance
  676. }
  677. if (type === 'down') {
  678. item.top += distance
  679. }
  680. if (type === 'left') {
  681. item.left -= distance
  682. }
  683. if (type === 'right') {
  684. item.left += distance
  685. }
  686. }
  687. })
  688. }
  689. }
  690. // 删除家具
  691. const deleteFurnitureIcon = (nanoid: string) => {
  692. console.log('deleteFurnitureIcon', clickedDragItem.value)
  693. if (nanoid) {
  694. mapCanvasList.value = mapCanvasList.value.filter((item) => item.nanoid !== nanoid)
  695. clickedDragItem.value = null
  696. }
  697. }
  698. // 删除家具床
  699. const deleteFurnitureBed = (nanoid: string) => {
  700. console.log('deleteFurnitureBed', nanoid)
  701. // 先从家具画布移除床
  702. deleteFurnitureIcon(nanoid)
  703. // 再从子区域画布删除对应的子区域
  704. blocks.value.shift()
  705. }
  706. // 新增区块类型
  707. interface BlockItem {
  708. // 本地用
  709. id: string // 唯一标识
  710. x: number // 区块基于父元素的X偏移量,区块的左上角x坐标
  711. y: number // 区块基于父元素的Y偏移量,区块的左上角y坐标
  712. ox: number // 区块基于原点的X偏移量,区块的左上角x坐标
  713. oy: number // 区块基于原点的Y偏移量,区块的左上角y坐标
  714. width: number // 区块宽度
  715. height: number // 区块高度
  716. isDragging: boolean // 是否正在拖动
  717. isResizing: boolean // 是否正在调整大小
  718. isActice: boolean // 是否选中
  719. isTracking: boolean // 是否开启区域跟踪 0-否,1-是 对应 trackPresence 字段
  720. isFalling: boolean // 是否屏蔽区域跌倒检测 0-否,1-是 对应 excludeFalling 字段
  721. isBed?: boolean // 是否是床 本地判断使用
  722. // 接口用
  723. startXx: number // 屏蔽子区域X开始
  724. stopXx: number // 屏蔽子区域X结束
  725. startYy: number // 屏蔽子区域Y开始
  726. stopYy: number // 屏蔽子区域Y结束
  727. startZz: number // 屏蔽子区域Z开始
  728. stopZz: number // 屏蔽子区域Z结束
  729. isLowSnr: number // 是否为床 0-不是,1-是
  730. isDoor: number // 是否是门 0-否,1-是 默认0
  731. presenceEnterDuration: number // 人员进入时间 默认3
  732. presenceExitDuration: number // 人员离开时间 默认3
  733. trackPresence: number // 是否开启区域跟踪存在 0-否,1-是
  734. excludeFalling: number // 是否屏蔽区域跌倒检测 0-否,1-是
  735. }
  736. const blocks = ref<BlockItem[]>([])
  737. const isCreating = ref(false)
  738. const currentBlock = ref<{
  739. startX: number
  740. startY: number
  741. currentX: number
  742. currentY: number
  743. } | null>(null)
  744. const selectedBlock = ref<BlockItem | null>(null)
  745. // 新建区块处理
  746. const createNewBlock = () => {
  747. if (blocks.value && blocks.value.length > 5) {
  748. message.warn('最多只能创建6个区块')
  749. return
  750. }
  751. isCreating.value = true
  752. }
  753. // 获取容器边界
  754. const getContainerRect = () => {
  755. const container = document.querySelector('.blockArea') as HTMLElement
  756. return container?.getBoundingClientRect() || { left: 0, top: 0 }
  757. }
  758. // 鼠标事件处理
  759. const handleMouseDown = (e: MouseEvent) => {
  760. if (!isEditDraggable.value) return
  761. console.log('handleMouseDown', e)
  762. blocks.value.forEach((item) => {
  763. item.isActice = false
  764. })
  765. selectedBlock.value = null
  766. if (!isCreating.value) return
  767. const rect = getContainerRect()
  768. const startX = e.clientX - rect.left
  769. const startY = e.clientY - rect.top
  770. currentBlock.value = {
  771. startX,
  772. startY,
  773. currentX: startX,
  774. currentY: startY,
  775. }
  776. document.addEventListener('mousemove', handleMouseMove)
  777. document.addEventListener('mouseup', handleMouseUp)
  778. }
  779. // 鼠标移动处理
  780. const handleMouseMove = (e: MouseEvent) => {
  781. if (!currentBlock.value) return
  782. const rect = getContainerRect()
  783. currentBlock.value.currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width))
  784. currentBlock.value.currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height))
  785. }
  786. // 鼠标释放处理
  787. const handleMouseUp = () => {
  788. if (!currentBlock.value) return
  789. const { startX, startY, currentX, currentY } = currentBlock.value
  790. const width = Math.abs(currentX - startX)
  791. const height = Math.abs(currentY - startY)
  792. if (width > 10 && height > 10) {
  793. blocks.value.push({
  794. // 本地用
  795. id: nanoid(),
  796. x: Math.round(Math.min(startX, currentX)),
  797. y: Math.round(Math.min(startY, currentY)),
  798. ox: Math.round(Math.min(startX, currentX)) - getOriginPosition().originX,
  799. oy: Math.round(Math.min(startY, currentY)) - getOriginPosition().originY,
  800. width,
  801. height,
  802. isDragging: false,
  803. isResizing: false,
  804. isActice: false,
  805. isTracking: false,
  806. isFalling: false,
  807. // 接口用
  808. startXx: Math.round(Math.min(startX, currentX)) - getOriginPosition().originX,
  809. stopXx: Math.round(Math.min(startX, currentX)) - getOriginPosition().originX + width,
  810. startYy: Math.round(Math.min(startY, currentY)) - getOriginPosition().originY,
  811. stopYy: Math.round(Math.min(startY, currentY)) - getOriginPosition().originY + height,
  812. startZz: 0,
  813. stopZz: 0,
  814. isLowSnr: 0,
  815. isDoor: 0,
  816. presenceEnterDuration: 3,
  817. presenceExitDuration: 3,
  818. trackPresence: 0,
  819. excludeFalling: 0,
  820. })
  821. }
  822. currentBlock.value = null
  823. isCreating.value = false
  824. document.removeEventListener('mousemove', handleMouseMove)
  825. document.removeEventListener('mouseup', handleMouseUp)
  826. }
  827. // 区块拖动
  828. const startDrag = (block: BlockItem, e: MouseEvent) => {
  829. if (!isEditDraggable.value) return
  830. console.log('startDrag', block)
  831. e.stopPropagation()
  832. block.isDragging = true
  833. block.isActice = true
  834. const container = document.querySelector('.blockArea') as HTMLElement
  835. const rect = container.getBoundingClientRect()
  836. const offsetX = e.clientX - rect.left - block.x
  837. const offsetY = e.clientY - rect.top - block.y
  838. const moveHandler = (e: MouseEvent) => {
  839. const newX = e.clientX - rect.left - offsetX
  840. const newY = e.clientY - rect.top - offsetY
  841. const containerWidth = container.offsetWidth
  842. const containerHeight = container.offsetHeight
  843. block.x = Math.max(0, Math.min(newX, containerWidth - block.width))
  844. block.y = Math.max(0, Math.min(newY, containerHeight - block.height))
  845. block.ox = block.x - getOriginPosition().originX
  846. block.oy = getOriginPosition().originY - block.y
  847. block.startXx = block.ox
  848. block.stopXx = block.ox + block.width
  849. block.startYy = block.oy
  850. block.stopYy = block.oy - block.height
  851. }
  852. const upHandler = () => {
  853. block.isDragging = false
  854. block.isActice = false
  855. document.removeEventListener('mousemove', moveHandler)
  856. document.removeEventListener('mouseup', upHandler)
  857. }
  858. document.addEventListener('mousemove', moveHandler)
  859. document.addEventListener('mouseup', upHandler)
  860. }
  861. const selectBlock = (block: BlockItem) => {
  862. if (!isEditDraggable.value) return
  863. console.log('selectBlock', block)
  864. selectedBlock.value = block
  865. blocks.value.forEach((item) => {
  866. item.isActice = item === block
  867. })
  868. }
  869. // 保存子区域配置
  870. // const saveBlockConfig = () => {
  871. // const blockData = blocks.value.map((item) => {
  872. // return {
  873. // startXx: item.startXx,
  874. // stopXx: item.stopXx,
  875. // startYy: item.startYy,
  876. // stopYy: item.stopYy,
  877. // startZz: Number(item.startZz) || 0,
  878. // stopZz: Number(item.stopZz) || 0,
  879. // isLowSnr: item.isLowSnr,
  880. // isDoor: item.isDoor,
  881. // presenceEnterDuration: item.presenceEnterDuration,
  882. // presenceExitDuration: item.presenceExitDuration,
  883. // trackPresence: Number(item.isTracking),
  884. // excludeFalling: Number(item.isFalling),
  885. // }
  886. // })
  887. // console.log('当前所有区块配置:', blockData)
  888. // try {
  889. // const res = roomApi.saveRoomInfo({
  890. // roomId: deviceRoomId.value,
  891. // devId: props.devId,
  892. // subRegions: blockData,
  893. // })
  894. // console.log('saveBlockConfig 保存成功', res)
  895. // message.success('保存成功')
  896. // emit('success')
  897. // } catch (error) {
  898. // console.error('saveBlockConfig 保存失败', error)
  899. // }
  900. // }
  901. /**
  902. * 获取坐标位置
  903. * @returns
  904. * * width: 容器宽度
  905. * * heigt: 容器高度
  906. * * originX: 原点X坐标
  907. * * originY: 原点Y坐标
  908. * * offsetX: 元素基于父容器的偏移量 X坐标
  909. * * offsetY: 元素基于父容器的偏移量 Y坐标
  910. * * originOffsetX: 元素基于原点的偏移量 X坐标
  911. * * originOffsetY: 元素基于原点的偏移量 Y坐标
  912. * * radarX: 雷达X坐标
  913. * * radarY: 雷达Y坐标
  914. */
  915. const getOriginPosition = () => {
  916. // 地图尺寸
  917. const containerWidth = areaWidth.value
  918. const containerHeight = areaHeight.value
  919. // 地图原点坐标
  920. const originX = containerWidth / 2
  921. const originY = containerHeight / 2
  922. // 元素基于父容器的偏移量
  923. const offsetX = (currentDragItem.value && currentDragItem.value?.left) || 0
  924. const offsetY = (currentDragItem.value && currentDragItem.value?.top) || 0
  925. // 元素基于原点的偏移量
  926. const originOffsetX = offsetX - originX
  927. const originOffsetY = originY - offsetY
  928. // 雷达尺寸
  929. const radarWidth = furnitureIconSizeMap['radar']?.width || 0
  930. const radarHeight = furnitureIconSizeMap['radar']?.height || 0
  931. // 雷达基于原点的偏移量
  932. const radarX = Math.round(originX - radarWidth / 2)
  933. const radarY = Math.round(originY - radarHeight / 2)
  934. const data = {
  935. width: containerWidth,
  936. height: containerHeight,
  937. originX: Math.round(originX),
  938. originY: Math.round(originY),
  939. offsetX: Math.round(offsetX),
  940. offsetY: Math.round(offsetY),
  941. originOffsetX: Math.round(originOffsetX),
  942. originOffsetY: Math.round(originOffsetY),
  943. radarX,
  944. radarY,
  945. radarWidth,
  946. radarHeight,
  947. }
  948. console.log('getOriginPosition', data)
  949. return data
  950. }
  951. // 初始化添加雷达图标
  952. const initRadarIcon = () => {
  953. console.log('initRadarIcon', mapCanvasList.value, furnitureItems.value)
  954. const { radarX, radarY, originOffsetX, originOffsetY } = getOriginPosition()
  955. // 在家具地图添加雷达图标
  956. mapCanvasList.value.push({
  957. name: '雷达',
  958. type: 'radar',
  959. width: furnitureIconSizeMap['radar'].width,
  960. height: furnitureIconSizeMap['radar'].height,
  961. top: radarY,
  962. left: radarX,
  963. x: originOffsetX,
  964. y: originOffsetY,
  965. rotate: 0,
  966. nanoid: nanoid(),
  967. })
  968. // 在屏蔽子区域添加雷达图标
  969. furnitureItems.value.push({
  970. name: '雷达',
  971. type: 'radar',
  972. width: furnitureIconSizeMap['radar'].width,
  973. length: furnitureIconSizeMap['radar'].height,
  974. top: radarY,
  975. left: radarX,
  976. x: originOffsetX,
  977. y: originOffsetY,
  978. rotate: 0,
  979. })
  980. }
  981. // 保存家具配置
  982. // const saveFurnitureMapConfig = () => {
  983. // console.log('saveFurnitureMapConfig', mapCanvasList.value)
  984. // try {
  985. // const res = roomApi.saveRoomInfo({
  986. // roomId: deviceRoomId.value,
  987. // devId: props.devId,
  988. // furnitures: mapCanvasList.value
  989. // .filter((item) => item.type !== 'radar')
  990. // .map((item) => {
  991. // return {
  992. // name: item.name,
  993. // type: item.type as FurnitureType,
  994. // width: item.width,
  995. // length: item.height,
  996. // top: item.top,
  997. // left: item.left,
  998. // rotate: item.rotate as 0 | 90 | 180 | 270,
  999. // x: item?.x || 0,
  1000. // y: item?.y || 0,
  1001. // }
  1002. // }),
  1003. // })
  1004. // console.log('保存家具配置 成功', res)
  1005. // message.success('保存成功')
  1006. // emit('success')
  1007. // } catch (error) {
  1008. // console.error('保存家具配置 失败', error)
  1009. // }
  1010. // }
  1011. // 保存所有配置
  1012. const saveAllConfig = () => {
  1013. console.log('保存所有配置')
  1014. const blockData = blocks.value.map((item) => {
  1015. return {
  1016. startXx: item.startXx,
  1017. stopXx: item.stopXx,
  1018. startYy: item.startYy,
  1019. stopYy: item.stopYy,
  1020. startZz: Number(item.startZz) || 0,
  1021. stopZz: Number(item.stopZz) || 0,
  1022. isLowSnr: item.isLowSnr,
  1023. isDoor: item.isDoor,
  1024. presenceEnterDuration: item.presenceEnterDuration,
  1025. presenceExitDuration: item.presenceExitDuration,
  1026. trackPresence: Number(item.isTracking),
  1027. excludeFalling: Number(item.isFalling),
  1028. }
  1029. })
  1030. console.log('当前所有区块配置:', blockData)
  1031. try {
  1032. const res = roomApi.saveRoomInfo({
  1033. roomId: deviceRoomId.value,
  1034. devId: props.devId,
  1035. furnitures: mapCanvasList.value
  1036. .filter((item) => item.type !== 'radar')
  1037. .map((item) => {
  1038. return {
  1039. name: item.name,
  1040. type: item.type as FurnitureType,
  1041. width: item.width,
  1042. length: item.height,
  1043. top: item.top,
  1044. left: item.left,
  1045. rotate: item.rotate as 0 | 90 | 180 | 270,
  1046. x: item?.x || 0,
  1047. y: item?.y || 0,
  1048. }
  1049. }),
  1050. subRegions: blockData,
  1051. })
  1052. console.log('保存所有配置 成功', res)
  1053. message.success('保存成功')
  1054. emit('success')
  1055. } catch (error) {
  1056. console.error('保存所有配置 失败', error)
  1057. }
  1058. }
  1059. const startResize = (block: BlockItem, e: MouseEvent) => {
  1060. block.isResizing = true
  1061. selectedBlock.value = block
  1062. const startX = e.clientX
  1063. const startY = e.clientY
  1064. const initialWidth = block.width
  1065. const initialHeight = block.height
  1066. const moveHandler = (e: MouseEvent) => {
  1067. const rect = getContainerRect()
  1068. const deltaX = e.clientX - startX
  1069. const deltaY = e.clientY - startY
  1070. // 限制最小尺寸和容器边界
  1071. block.width = Math.max(50, Math.min(initialWidth + deltaX, rect.width - block.x))
  1072. block.height = Math.max(50, Math.min(initialHeight + deltaY, rect.height - block.y))
  1073. // 改变了区块的长款,元素的结束位置也相应变化
  1074. block.stopXx = block.ox + block.width
  1075. block.stopYy = block.oy + block.height
  1076. }
  1077. const upHandler = () => {
  1078. block.isResizing = false
  1079. selectedBlock.value = null
  1080. document.removeEventListener('mousemove', moveHandler)
  1081. document.removeEventListener('mouseup', upHandler)
  1082. }
  1083. document.addEventListener('mousemove', moveHandler)
  1084. document.addEventListener('mouseup', upHandler)
  1085. }
  1086. const blockInputPressEnter = (e: Event, el: BlockItem, attr: string) => {
  1087. if (attr === 'startXx') {
  1088. el.startXx = Number(el[attr as keyof BlockItem])
  1089. el.x = el.startXx + getOriginPosition().originX
  1090. }
  1091. if (attr === 'stopXx') {
  1092. el.stopXx = Number(el[attr as keyof BlockItem])
  1093. el.width = el.stopXx + getOriginPosition().originX - el.width
  1094. }
  1095. if (attr === 'startYy') {
  1096. el.startYy = Number(el[attr as keyof BlockItem])
  1097. el.x = el.startYy + getOriginPosition().originY
  1098. }
  1099. if (attr === 'stopYy') {
  1100. el.stopYy = Number(el[attr as keyof BlockItem])
  1101. el.height = el.stopYy + getOriginPosition().originY - el.height
  1102. }
  1103. }
  1104. const blockInputBlur = (e: Event, el: BlockItem, attr: string) => {
  1105. console.log('blockInputBlur', e, el, attr)
  1106. if (attr === 'startXx') {
  1107. el.startXx = Number(el[attr as keyof BlockItem])
  1108. el.x = el.startXx + getOriginPosition().originX
  1109. }
  1110. if (attr === 'stopXx') {
  1111. el.stopXx = Number(el[attr as keyof BlockItem])
  1112. el.width = el.stopXx + getOriginPosition().originX - el.width
  1113. }
  1114. if (attr === 'startYy') {
  1115. el.startYy = Number(el[attr as keyof BlockItem])
  1116. el.x = el.startYy + getOriginPosition().originY
  1117. }
  1118. if (attr === 'stopYy') {
  1119. el.stopYy = Number(el[attr as keyof BlockItem])
  1120. el.height = el.stopYy + getOriginPosition().originY - el.height
  1121. }
  1122. }
  1123. const deleteBlockArea = (id: string) => {
  1124. if (id) {
  1125. blocks.value = blocks.value.filter((item) => item.id !== id)
  1126. selectedBlock.value = null
  1127. }
  1128. }
  1129. </script>
  1130. <style scoped lang="less">
  1131. .viewer {
  1132. padding: 10px;
  1133. min-width: 500px;
  1134. flex-shrink: 0;
  1135. // margin-top: 10px;
  1136. &-header {
  1137. display: flex;
  1138. justify-content: space-between;
  1139. padding-bottom: 20px;
  1140. &-title {
  1141. font-size: 16px;
  1142. font-weight: 600;
  1143. line-height: 24px;
  1144. }
  1145. &-subtitle {
  1146. font-size: 14px;
  1147. color: #666;
  1148. }
  1149. }
  1150. &-content {
  1151. display: flex;
  1152. gap: 20px;
  1153. }
  1154. }
  1155. .mapBox {
  1156. background-color: #e0e0e0;
  1157. background-image:
  1158. linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
  1159. linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
  1160. background-size: 20px 20px;
  1161. position: relative;
  1162. flex-shrink: 0;
  1163. // 添加黑边框
  1164. &::before {
  1165. content: '';
  1166. position: absolute;
  1167. top: -5px;
  1168. left: -5px;
  1169. width: calc(100% + 10px);
  1170. height: calc(100% + 10px);
  1171. border: 5px solid rgba(0, 0, 0, 0.8);
  1172. box-sizing: border-box;
  1173. pointer-events: none;
  1174. }
  1175. }
  1176. .mapConfig {
  1177. background-color: #f5f5f5;
  1178. border-radius: 10px;
  1179. padding: 12px;
  1180. &-header {
  1181. font-size: 14px;
  1182. margin-bottom: 10px;
  1183. font-weight: 600;
  1184. }
  1185. &-item {
  1186. display: flex;
  1187. line-height: 30px;
  1188. &-label {
  1189. color: #888;
  1190. min-width: 80px;
  1191. }
  1192. &-content {
  1193. color: #555;
  1194. min-width: 100px;
  1195. }
  1196. }
  1197. }
  1198. .temp-block {
  1199. position: absolute;
  1200. background: rgba(24, 144, 255, 0.2);
  1201. border: 2px dashed #1890ff;
  1202. }
  1203. .block-item {
  1204. background: rgba(24, 144, 255, 0.1);
  1205. .resize-handle {
  1206. position: absolute;
  1207. right: -4px;
  1208. bottom: -4px;
  1209. width: 15px;
  1210. height: 15px;
  1211. background: #1890ff;
  1212. cursor: nwse-resize;
  1213. font-size: 12px;
  1214. color: #fff;
  1215. display: flex;
  1216. align-items: center;
  1217. justify-content: center;
  1218. }
  1219. }
  1220. .dragging-item {
  1221. opacity: 0.5;
  1222. transition: opacity 0.2s ease;
  1223. }
  1224. </style>