index.vue 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320
  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 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. }
  356. const emit = defineEmits<{
  357. (e: 'success', value: void): void
  358. }>()
  359. const props = withDefaults(defineProps<Props>(), {
  360. devId: '',
  361. length: 0, // 区域宽度
  362. width: 0, // 区域高度
  363. })
  364. const deviceRoomId = ref('')
  365. const furnitureItems = ref<Furniture[]>([])
  366. // 检测区域宽度 length
  367. const areaWidth = computed(() => {
  368. return Math.abs(props.length)
  369. })
  370. // 检测区域高度 width
  371. const areaHeight = computed(() => {
  372. return Math.abs(props.width)
  373. })
  374. // 检测区域是否可用,小于50cm的区域,不可用
  375. const areaAvailable = computed(() => {
  376. return areaWidth.value < 50 || areaHeight.value < 50
  377. })
  378. const spinning = ref(false)
  379. // 获取房间布局
  380. const fetchRoomLayout = async () => {
  381. console.log('fetchRoomLayout', props, props.devId)
  382. if (!props.devId) {
  383. message.error('设备ID不能为空')
  384. return
  385. }
  386. try {
  387. spinning.value = true
  388. const res = await roomApi.queryRoomInfo({
  389. devId: props.devId,
  390. })
  391. console.log('✅获取到房间布局信息', res)
  392. if (!res) {
  393. spinning.value = false
  394. return
  395. }
  396. const { furnitures, roomId, subRegions } = res.data
  397. deviceRoomId.value = roomId || ''
  398. if (furnitures) {
  399. furnitureItems.value = furnitures!.map((item) => {
  400. // 将接口的家具,添加在家具画布上
  401. mapCanvasList.value.push({
  402. name: item.name,
  403. type: item.type,
  404. width: item.width,
  405. height: item.length,
  406. top: item.top,
  407. left: item.left,
  408. x: item.x,
  409. y: item.y,
  410. rotate: item.rotate,
  411. nanoid: nanoid(),
  412. })
  413. // 将接口的家具,添加在屏蔽子区域画布上
  414. return {
  415. ...item,
  416. width: item.width || 45,
  417. length: item.length || 45,
  418. top: item.top || 0,
  419. left: item.left || 0,
  420. rotate: item.rotate || 0,
  421. x: item.x || 0,
  422. y: item.y || 0,
  423. }
  424. })
  425. }
  426. if (subRegions) {
  427. // 将接口的子区域,添加在子区域画布上
  428. subRegions.forEach((item, index) => {
  429. blocks.value.push({
  430. // 本地需要使用的数据
  431. id: nanoid(),
  432. x: item.startXx + getOriginPosition().originX,
  433. y: getOriginPosition().originY - item.startYy,
  434. ox: item.startXx + getOriginPosition().originX - getOriginPosition().originX,
  435. oy: getOriginPosition().originY - item.startYy - getOriginPosition().originY,
  436. width: Math.abs(item.stopXx - item.startXx),
  437. height: Math.abs(item.stopYy - item.startYy),
  438. isDragging: false,
  439. isResizing: false,
  440. isActice: false,
  441. isTracking: Boolean(item.trackPresence),
  442. isFalling: Boolean(item.excludeFalling),
  443. isBed: index === 0 && mapCanvasList.value.some((item) => item.type === 'bed'),
  444. // 来自接口回显的数据
  445. startXx: item.startXx,
  446. stopXx: item.stopXx,
  447. startYy: item.startYy,
  448. stopYy: item.stopYy,
  449. startZz: item.startZz,
  450. stopZz: item.stopZz,
  451. isLowSnr: item.isLowSnr,
  452. isDoor: item.isDoor,
  453. presenceEnterDuration: item.presenceEnterDuration,
  454. presenceExitDuration: item.presenceExitDuration,
  455. trackPresence: item.trackPresence,
  456. excludeFalling: item.excludeFalling,
  457. })
  458. })
  459. console.log('🚀', blocks.value)
  460. }
  461. spinning.value = false
  462. } catch (error) {
  463. console.error('❌获取房间布局信息失败', error)
  464. spinning.value = false
  465. }
  466. }
  467. fetchRoomLayout().finally(() => {
  468. // 获取房间信息后,初始化雷达图标
  469. initRadarIcon()
  470. })
  471. interface CanvaseItem {
  472. name: string // 名称
  473. type: string // 类型,图标icon
  474. width: number // 家具宽度
  475. height: number // 家具长度
  476. top: number // 距离检测范围上方位置 cm
  477. left: number // 距离检测范围左边位置 cm
  478. rotate: number // 旋转角度: 0°,90°,180°,270°
  479. x?: number // 距离雷达的X距离
  480. y?: number // 距离雷达的Y距离
  481. nanoid?: string // 本地使用
  482. isActice?: boolean // 是否选中 本地使用
  483. isDragging?: boolean // 是否拖拽 本地使用
  484. }
  485. // 画布上的家具列表
  486. const mapCanvasList = ref<CanvaseItem[]>([])
  487. const isEditDraggable = ref(false)
  488. // 家具列表添加
  489. const addHnadler = (icon: FurnitureIconType) => {
  490. console.log('addHnadler', icon)
  491. // 检查画布上是否已经添加过了 icon 为 bed 的家具
  492. if (icon === 'bed') {
  493. const isExist = mapCanvasList.value.some((item) => item.type === icon)
  494. if (isExist) {
  495. message.error('床已经添加过了,不可重复添加')
  496. return
  497. }
  498. }
  499. const { originOffsetX, originOffsetY } = getOriginPosition()
  500. // 家具原始宽高
  501. const originWidth = furnitureIconSizeMap[icon].width || 30
  502. const originHeight = furnitureIconSizeMap[icon].height || 30
  503. mapCanvasList.value.push({
  504. name: furnitureIconNameMap[icon],
  505. type: icon,
  506. width: originWidth,
  507. height: originHeight,
  508. top: 0,
  509. left: 0,
  510. rotate: 0,
  511. x: originOffsetX,
  512. y: originOffsetY,
  513. nanoid: nanoid(),
  514. isActice: false,
  515. })
  516. message.success('已添加家具')
  517. if (icon === 'bed') {
  518. // 同步添加一个子区域
  519. blocks.value.unshift({
  520. // 本地用
  521. id: nanoid(),
  522. x: 20,
  523. y: 15,
  524. ox: -150,
  525. oy: 180,
  526. width: originWidth,
  527. height: originHeight,
  528. isDragging: false,
  529. isResizing: false,
  530. isActice: false,
  531. isTracking: false,
  532. isFalling: false,
  533. isBed: true,
  534. // 接口用
  535. startXx: -150,
  536. stopXx: -100,
  537. startYy: 180,
  538. stopYy: 120,
  539. startZz: 0,
  540. stopZz: 0,
  541. isLowSnr: 1,
  542. isDoor: 0,
  543. presenceEnterDuration: 3,
  544. presenceExitDuration: 3,
  545. trackPresence: 0,
  546. excludeFalling: 0,
  547. })
  548. console.log('blocks', blocks.value)
  549. message.success('已添加子区域')
  550. }
  551. }
  552. const contentEl = ref<HTMLElement>()
  553. const currentDragItem = ref<CanvaseItem | null>(null)
  554. // 内容区域放置处理
  555. useDropZone(contentEl, {
  556. onDrop(files: File[] | null, event: DragEvent) {
  557. if (contentEl.value && currentDragItem.value) {
  558. const rect = contentEl.value.getBoundingClientRect()
  559. // 计算基于画布容器的精确坐标
  560. const x = event.clientX - rect.left
  561. const y = event.clientY - rect.top
  562. // 中心点对齐计算
  563. currentDragItem.value.top = y - currentDragItem.value.height / 2
  564. currentDragItem.value.left = x - currentDragItem.value.width / 2
  565. // 添加边界检查
  566. // const maxX = contentEl.value.offsetWidth - currentDragItem.value.width
  567. // const maxY = contentEl.value.offsetHeight - currentDragItem.value.height
  568. // currentDragItem.value.left = Math.max(0, Math.min(x - currentDragItem.value.width / 2, maxX))
  569. // currentDragItem.value.top = Math.max(0, Math.min(y - currentDragItem.value.height / 2, maxY))
  570. }
  571. },
  572. })
  573. // 家具列表元素开始拖拽
  574. const onDragstartListItem = (event: DragEvent, item: CanvaseItem) => {
  575. console.log('🔥onDragstartListItem', event, item)
  576. currentDragItem.value = item
  577. item.isDragging = true
  578. // 创建拖拽镜像
  579. const target = event.currentTarget as HTMLElement
  580. const clone = target.cloneNode(true) as HTMLElement
  581. const rect = target.getBoundingClientRect()
  582. // 获取实际渲染样式
  583. const computedStyle = window.getComputedStyle(target)
  584. // 复制所有关键样式
  585. clone.style.cssText = `
  586. position: fixed;
  587. left: -9999px;
  588. opacity: 0.5;
  589. pointer-events: none;
  590. width: ${rect.width}px;
  591. height: ${rect.height}px;
  592. transform: ${computedStyle.transform || `rotate(${item.rotate}deg)`};
  593. transform-origin: ${computedStyle.transformOrigin || 'center center'};
  594. border: ${computedStyle.border};
  595. box-shadow: ${computedStyle.boxShadow};
  596. background: ${computedStyle.background};
  597. `
  598. console.log('克隆元素尺寸:', clone.style.cssText)
  599. document.body.appendChild(clone)
  600. // 计算中心点偏移量
  601. const offsetX = rect.width / 2 // 水平中心偏移
  602. const offsetY = rect.height / 2 // 垂直中心偏移
  603. // 设置拖拽镜像和偏移量
  604. event.dataTransfer?.setDragImage(clone, offsetX, offsetY)
  605. // 隐藏原元素
  606. target.style.opacity = '0'
  607. }
  608. // 家具列表元素结束拖拽
  609. const onDragendEndListItem = (event: DragEvent, item: CanvaseItem) => {
  610. item.isDragging = false
  611. const { originOffsetX, originOffsetY } = getOriginPosition()
  612. if (currentDragItem.value) {
  613. currentDragItem.value.x = originOffsetX
  614. currentDragItem.value.y = originOffsetY
  615. currentDragItem.value.isDragging = false
  616. }
  617. requestAnimationFrame(() => {
  618. item.isActice = true
  619. clickedDragItem.value = item
  620. })
  621. console.log('🔥onDragendEndListItem', event, item, {
  622. x: currentDragItem.value?.x,
  623. y: currentDragItem.value?.y,
  624. })
  625. if (event.currentTarget) {
  626. ;(event.currentTarget as HTMLElement).style.opacity = '1'
  627. }
  628. }
  629. const clickedDragItem = ref<CanvaseItem | null>()
  630. const onClickMapItem = (event: MouseEvent, item: CanvaseItem) => {
  631. if (!isEditDraggable.value || item.type === 'radar' || item.isDragging) return
  632. console.log('onClickMapItem', event, item)
  633. item.isActice = true
  634. clickedDragItem.value = item
  635. }
  636. const handleMouseDownMapCanvas = () => {
  637. mapCanvasList.value.forEach((item) => {
  638. item.isActice = false
  639. })
  640. clickedDragItem.value = null
  641. }
  642. // 家具旋转
  643. const rotateFurnitureIcon = (type: number, nanoid: string) => {
  644. console.log('rotateFurnitureIcon', type, nanoid, mapCanvasList.value)
  645. const rotateMap = [0, 90, 180, 270]
  646. if (nanoid) {
  647. mapCanvasList.value.forEach((item) => {
  648. if (item.nanoid === nanoid) {
  649. // 获取当前角度在rotateMap中的索引
  650. const currentIndex = rotateMap.indexOf(item.rotate)
  651. if (type === 1) {
  652. // 逆时针(索引递减)
  653. const newIndex = (currentIndex - 1 + rotateMap.length) % rotateMap.length
  654. item.rotate = rotateMap[newIndex]
  655. } else if (type === 2) {
  656. // 顺时针(索引递增)
  657. const newIndex = (currentIndex + 1) % rotateMap.length
  658. item.rotate = rotateMap[newIndex]
  659. }
  660. }
  661. })
  662. }
  663. }
  664. // 微调距离
  665. const distance = ref(5)
  666. // 家具位置微调
  667. const positonFurnitureIcon = (type: string, nanoid: string, distance: number) => {
  668. console.log('positonFurnitureIcon', type, nanoid, mapCanvasList.value)
  669. if (nanoid) {
  670. mapCanvasList.value.forEach((item) => {
  671. if (item.nanoid === nanoid) {
  672. if (type === 'up') {
  673. item.top -= distance
  674. }
  675. if (type === 'down') {
  676. item.top += distance
  677. }
  678. if (type === 'left') {
  679. item.left -= distance
  680. }
  681. if (type === 'right') {
  682. item.left += distance
  683. }
  684. }
  685. })
  686. }
  687. }
  688. // 删除家具
  689. const deleteFurnitureIcon = (nanoid: string) => {
  690. console.log('deleteFurnitureIcon', clickedDragItem.value)
  691. if (nanoid) {
  692. mapCanvasList.value = mapCanvasList.value.filter((item) => item.nanoid !== nanoid)
  693. clickedDragItem.value = null
  694. }
  695. }
  696. // 删除家具床
  697. const deleteFurnitureBed = (nanoid: string) => {
  698. console.log('deleteFurnitureBed', nanoid)
  699. // 先从家具画布移除床
  700. deleteFurnitureIcon(nanoid)
  701. // 再从子区域画布删除对应的子区域
  702. blocks.value.shift()
  703. }
  704. // 新增区块类型
  705. interface BlockItem {
  706. // 本地用
  707. id: string // 唯一标识
  708. x: number // 区块基于父元素的X偏移量,区块的左上角x坐标
  709. y: number // 区块基于父元素的Y偏移量,区块的左上角y坐标
  710. ox: number // 区块基于原点的X偏移量,区块的左上角x坐标
  711. oy: number // 区块基于原点的Y偏移量,区块的左上角y坐标
  712. width: number // 区块宽度
  713. height: number // 区块高度
  714. isDragging: boolean // 是否正在拖动
  715. isResizing: boolean // 是否正在调整大小
  716. isActice: boolean // 是否选中
  717. isTracking: boolean // 是否开启区域跟踪 0-否,1-是 对应 trackPresence 字段
  718. isFalling: boolean // 是否屏蔽区域跌倒检测 0-否,1-是 对应 excludeFalling 字段
  719. isBed?: boolean // 是否是床 本地判断使用
  720. // 接口用
  721. startXx: number // 屏蔽子区域X开始
  722. stopXx: number // 屏蔽子区域X结束
  723. startYy: number // 屏蔽子区域Y开始
  724. stopYy: number // 屏蔽子区域Y结束
  725. startZz: number // 屏蔽子区域Z开始
  726. stopZz: number // 屏蔽子区域Z结束
  727. isLowSnr: number // 是否为床 0-不是,1-是
  728. isDoor: number // 是否是门 0-否,1-是 默认0
  729. presenceEnterDuration: number // 人员进入时间 默认3
  730. presenceExitDuration: number // 人员离开时间 默认3
  731. trackPresence: number // 是否开启区域跟踪存在 0-否,1-是
  732. excludeFalling: number // 是否屏蔽区域跌倒检测 0-否,1-是
  733. }
  734. const blocks = ref<BlockItem[]>([])
  735. const isCreating = ref(false)
  736. const currentBlock = ref<{
  737. startX: number
  738. startY: number
  739. currentX: number
  740. currentY: number
  741. } | null>(null)
  742. const selectedBlock = ref<BlockItem | null>(null)
  743. // 新建区块处理
  744. const createNewBlock = () => {
  745. if (blocks.value && blocks.value.length > 5) {
  746. message.warn('最多只能创建6个区块')
  747. return
  748. }
  749. isCreating.value = true
  750. }
  751. // 获取容器边界
  752. const getContainerRect = () => {
  753. const container = document.querySelector('.blockArea') as HTMLElement
  754. return container?.getBoundingClientRect() || { left: 0, top: 0 }
  755. }
  756. // 鼠标事件处理
  757. const handleMouseDown = (e: MouseEvent) => {
  758. if (!isEditDraggable.value) return
  759. console.log('handleMouseDown', e)
  760. blocks.value.forEach((item) => {
  761. item.isActice = false
  762. })
  763. selectedBlock.value = null
  764. if (!isCreating.value) return
  765. const rect = getContainerRect()
  766. const startX = e.clientX - rect.left
  767. const startY = e.clientY - rect.top
  768. currentBlock.value = {
  769. startX,
  770. startY,
  771. currentX: startX,
  772. currentY: startY,
  773. }
  774. document.addEventListener('mousemove', handleMouseMove)
  775. document.addEventListener('mouseup', handleMouseUp)
  776. }
  777. // 鼠标移动处理
  778. const handleMouseMove = (e: MouseEvent) => {
  779. if (!currentBlock.value) return
  780. const rect = getContainerRect()
  781. currentBlock.value.currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width))
  782. currentBlock.value.currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height))
  783. }
  784. // 鼠标释放处理
  785. const handleMouseUp = () => {
  786. if (!currentBlock.value) return
  787. const { startX, startY, currentX, currentY } = currentBlock.value
  788. const width = Math.abs(currentX - startX)
  789. const height = Math.abs(currentY - startY)
  790. if (width > 10 && height > 10) {
  791. blocks.value.push({
  792. // 本地用
  793. id: nanoid(),
  794. x: Math.round(Math.min(startX, currentX)),
  795. y: Math.round(Math.min(startY, currentY)),
  796. ox: Math.round(Math.min(startX, currentX)) - getOriginPosition().originX,
  797. oy: Math.round(Math.min(startY, currentY)) - getOriginPosition().originY,
  798. width,
  799. height,
  800. isDragging: false,
  801. isResizing: false,
  802. isActice: false,
  803. isTracking: false,
  804. isFalling: false,
  805. // 接口用
  806. startXx: Math.round(Math.min(startX, currentX)) - getOriginPosition().originX,
  807. stopXx: Math.round(Math.min(startX, currentX)) - getOriginPosition().originX + width,
  808. startYy: Math.round(Math.min(startY, currentY)) - getOriginPosition().originY,
  809. stopYy: Math.round(Math.min(startY, currentY)) - getOriginPosition().originY + height,
  810. startZz: 0,
  811. stopZz: 0,
  812. isLowSnr: 0,
  813. isDoor: 0,
  814. presenceEnterDuration: 3,
  815. presenceExitDuration: 3,
  816. trackPresence: 0,
  817. excludeFalling: 0,
  818. })
  819. }
  820. currentBlock.value = null
  821. isCreating.value = false
  822. document.removeEventListener('mousemove', handleMouseMove)
  823. document.removeEventListener('mouseup', handleMouseUp)
  824. }
  825. // 区块拖动
  826. const startDrag = (block: BlockItem, e: MouseEvent) => {
  827. if (!isEditDraggable.value) return
  828. console.log('startDrag', block)
  829. e.stopPropagation()
  830. block.isDragging = true
  831. block.isActice = true
  832. const container = document.querySelector('.blockArea') as HTMLElement
  833. const rect = container.getBoundingClientRect()
  834. const offsetX = e.clientX - rect.left - block.x
  835. const offsetY = e.clientY - rect.top - block.y
  836. const moveHandler = (e: MouseEvent) => {
  837. const newX = e.clientX - rect.left - offsetX
  838. const newY = e.clientY - rect.top - offsetY
  839. const containerWidth = container.offsetWidth
  840. const containerHeight = container.offsetHeight
  841. block.x = Math.max(0, Math.min(newX, containerWidth - block.width))
  842. block.y = Math.max(0, Math.min(newY, containerHeight - block.height))
  843. block.ox = block.x - getOriginPosition().originX
  844. block.oy = getOriginPosition().originY - block.y
  845. block.startXx = block.ox
  846. block.stopXx = block.ox + block.width
  847. block.startYy = block.oy
  848. block.stopYy = block.oy - block.height
  849. }
  850. const upHandler = () => {
  851. block.isDragging = false
  852. block.isActice = false
  853. document.removeEventListener('mousemove', moveHandler)
  854. document.removeEventListener('mouseup', upHandler)
  855. }
  856. document.addEventListener('mousemove', moveHandler)
  857. document.addEventListener('mouseup', upHandler)
  858. }
  859. const selectBlock = (block: BlockItem) => {
  860. if (!isEditDraggable.value) return
  861. console.log('selectBlock', block)
  862. selectedBlock.value = block
  863. blocks.value.forEach((item) => {
  864. item.isActice = item === block
  865. })
  866. }
  867. // 保存子区域配置
  868. // const saveBlockConfig = () => {
  869. // const blockData = blocks.value.map((item) => {
  870. // return {
  871. // startXx: item.startXx,
  872. // stopXx: item.stopXx,
  873. // startYy: item.startYy,
  874. // stopYy: item.stopYy,
  875. // startZz: Number(item.startZz) || 0,
  876. // stopZz: Number(item.stopZz) || 0,
  877. // isLowSnr: item.isLowSnr,
  878. // isDoor: item.isDoor,
  879. // presenceEnterDuration: item.presenceEnterDuration,
  880. // presenceExitDuration: item.presenceExitDuration,
  881. // trackPresence: Number(item.isTracking),
  882. // excludeFalling: Number(item.isFalling),
  883. // }
  884. // })
  885. // console.log('当前所有区块配置:', blockData)
  886. // try {
  887. // const res = roomApi.saveRoomInfo({
  888. // roomId: deviceRoomId.value,
  889. // devId: props.devId,
  890. // subRegions: blockData,
  891. // })
  892. // console.log('saveBlockConfig 保存成功', res)
  893. // message.success('保存成功')
  894. // emit('success')
  895. // } catch (error) {
  896. // console.error('saveBlockConfig 保存失败', error)
  897. // }
  898. // }
  899. /**
  900. * 获取坐标位置
  901. * @returns
  902. * * width: 容器宽度
  903. * * heigt: 容器高度
  904. * * originX: 原点X坐标
  905. * * originY: 原点Y坐标
  906. * * offsetX: 元素基于父容器的偏移量 X坐标
  907. * * offsetY: 元素基于父容器的偏移量 Y坐标
  908. * * originOffsetX: 元素基于原点的偏移量 X坐标
  909. * * originOffsetY: 元素基于原点的偏移量 Y坐标
  910. * * radarX: 雷达X坐标
  911. * * radarY: 雷达Y坐标
  912. */
  913. const getOriginPosition = () => {
  914. // 地图尺寸
  915. const containerWidth = areaWidth.value
  916. const containerHeight = areaHeight.value
  917. // 地图原点坐标
  918. const originX = containerWidth / 2
  919. const originY = containerHeight / 2
  920. // 元素基于父容器的偏移量
  921. const offsetX = (currentDragItem.value && currentDragItem.value?.left) || 0
  922. const offsetY = (currentDragItem.value && currentDragItem.value?.top) || 0
  923. // 元素基于原点的偏移量
  924. const originOffsetX = offsetX - originX
  925. const originOffsetY = originY - offsetY
  926. // 雷达尺寸
  927. const radarWidth = furnitureIconSizeMap['radar']?.width || 0
  928. const radarHeight = furnitureIconSizeMap['radar']?.height || 0
  929. // 雷达基于原点的偏移量
  930. const radarX = Math.round(originX - radarWidth / 2)
  931. const radarY = Math.round(originY - radarHeight / 2)
  932. const data = {
  933. width: containerWidth,
  934. height: containerHeight,
  935. originX: Math.round(originX),
  936. originY: Math.round(originY),
  937. offsetX: Math.round(offsetX),
  938. offsetY: Math.round(offsetY),
  939. originOffsetX: Math.round(originOffsetX),
  940. originOffsetY: Math.round(originOffsetY),
  941. radarX,
  942. radarY,
  943. radarWidth,
  944. radarHeight,
  945. }
  946. console.log('getOriginPosition', data)
  947. return data
  948. }
  949. // 初始化添加雷达图标
  950. const initRadarIcon = () => {
  951. console.log('initRadarIcon', mapCanvasList.value, furnitureItems.value)
  952. const { radarX, radarY, originOffsetX, originOffsetY } = getOriginPosition()
  953. // 在家具地图添加雷达图标
  954. mapCanvasList.value.push({
  955. name: '雷达',
  956. type: 'radar',
  957. width: furnitureIconSizeMap['radar'].width,
  958. height: furnitureIconSizeMap['radar'].height,
  959. top: radarY,
  960. left: radarX,
  961. x: originOffsetX,
  962. y: originOffsetY,
  963. rotate: 0,
  964. nanoid: nanoid(),
  965. })
  966. // 在屏蔽子区域添加雷达图标
  967. furnitureItems.value.push({
  968. name: '雷达',
  969. type: 'radar',
  970. width: furnitureIconSizeMap['radar'].width,
  971. length: furnitureIconSizeMap['radar'].height,
  972. top: radarY,
  973. left: radarX,
  974. x: originOffsetX,
  975. y: originOffsetY,
  976. rotate: 0,
  977. })
  978. }
  979. // 保存家具配置
  980. // const saveFurnitureMapConfig = () => {
  981. // console.log('saveFurnitureMapConfig', mapCanvasList.value)
  982. // try {
  983. // const res = roomApi.saveRoomInfo({
  984. // roomId: deviceRoomId.value,
  985. // devId: props.devId,
  986. // furnitures: mapCanvasList.value
  987. // .filter((item) => item.type !== 'radar')
  988. // .map((item) => {
  989. // return {
  990. // name: item.name,
  991. // type: item.type as FurnitureType,
  992. // width: item.width,
  993. // length: item.height,
  994. // top: item.top,
  995. // left: item.left,
  996. // rotate: item.rotate as 0 | 90 | 180 | 270,
  997. // x: item?.x || 0,
  998. // y: item?.y || 0,
  999. // }
  1000. // }),
  1001. // })
  1002. // console.log('保存家具配置 成功', res)
  1003. // message.success('保存成功')
  1004. // emit('success')
  1005. // } catch (error) {
  1006. // console.error('保存家具配置 失败', error)
  1007. // }
  1008. // }
  1009. // 保存所有配置
  1010. const saveAllConfig = () => {
  1011. console.log('保存所有配置')
  1012. const blockData = blocks.value.map((item) => {
  1013. return {
  1014. startXx: item.startXx,
  1015. stopXx: item.stopXx,
  1016. startYy: item.startYy,
  1017. stopYy: item.stopYy,
  1018. startZz: Number(item.startZz) || 0,
  1019. stopZz: Number(item.stopZz) || 0,
  1020. isLowSnr: item.isLowSnr,
  1021. isDoor: item.isDoor,
  1022. presenceEnterDuration: item.presenceEnterDuration,
  1023. presenceExitDuration: item.presenceExitDuration,
  1024. trackPresence: Number(item.isTracking),
  1025. excludeFalling: Number(item.isFalling),
  1026. }
  1027. })
  1028. console.log('当前所有区块配置:', blockData)
  1029. try {
  1030. const res = roomApi.saveRoomInfo({
  1031. roomId: deviceRoomId.value,
  1032. devId: props.devId,
  1033. furnitures: mapCanvasList.value
  1034. .filter((item) => item.type !== 'radar')
  1035. .map((item) => {
  1036. return {
  1037. name: item.name,
  1038. type: item.type as FurnitureType,
  1039. width: item.width,
  1040. length: item.height,
  1041. top: item.top,
  1042. left: item.left,
  1043. rotate: item.rotate as 0 | 90 | 180 | 270,
  1044. x: item?.x || 0,
  1045. y: item?.y || 0,
  1046. }
  1047. }),
  1048. subRegions: blockData,
  1049. })
  1050. console.log('保存所有配置 成功', res)
  1051. message.success('保存成功')
  1052. emit('success')
  1053. } catch (error) {
  1054. console.error('保存所有配置 失败', error)
  1055. }
  1056. }
  1057. const startResize = (block: BlockItem, e: MouseEvent) => {
  1058. block.isResizing = true
  1059. selectedBlock.value = block
  1060. const startX = e.clientX
  1061. const startY = e.clientY
  1062. const initialWidth = block.width
  1063. const initialHeight = block.height
  1064. const moveHandler = (e: MouseEvent) => {
  1065. const rect = getContainerRect()
  1066. const deltaX = e.clientX - startX
  1067. const deltaY = e.clientY - startY
  1068. // 限制最小尺寸和容器边界
  1069. block.width = Math.max(50, Math.min(initialWidth + deltaX, rect.width - block.x))
  1070. block.height = Math.max(50, Math.min(initialHeight + deltaY, rect.height - block.y))
  1071. // 改变了区块的长款,元素的结束位置也相应变化
  1072. block.stopXx = block.ox + block.width
  1073. block.stopYy = block.oy + block.height
  1074. }
  1075. const upHandler = () => {
  1076. block.isResizing = false
  1077. selectedBlock.value = null
  1078. document.removeEventListener('mousemove', moveHandler)
  1079. document.removeEventListener('mouseup', upHandler)
  1080. }
  1081. document.addEventListener('mousemove', moveHandler)
  1082. document.addEventListener('mouseup', upHandler)
  1083. }
  1084. const blockInputPressEnter = (e: Event, el: BlockItem, attr: string) => {
  1085. if (attr === 'startXx') {
  1086. el.startXx = Number(el[attr as keyof BlockItem])
  1087. el.x = el.startXx + getOriginPosition().originX
  1088. }
  1089. if (attr === 'stopXx') {
  1090. el.stopXx = Number(el[attr as keyof BlockItem])
  1091. el.width = el.stopXx + getOriginPosition().originX - el.width
  1092. }
  1093. if (attr === 'startYy') {
  1094. el.startYy = Number(el[attr as keyof BlockItem])
  1095. el.x = el.startYy + getOriginPosition().originY
  1096. }
  1097. if (attr === 'stopYy') {
  1098. el.stopYy = Number(el[attr as keyof BlockItem])
  1099. el.height = el.stopYy + getOriginPosition().originY - el.height
  1100. }
  1101. }
  1102. const blockInputBlur = (e: Event, el: BlockItem, attr: string) => {
  1103. console.log('blockInputBlur', e, el, attr)
  1104. if (attr === 'startXx') {
  1105. el.startXx = Number(el[attr as keyof BlockItem])
  1106. el.x = el.startXx + getOriginPosition().originX
  1107. }
  1108. if (attr === 'stopXx') {
  1109. el.stopXx = Number(el[attr as keyof BlockItem])
  1110. el.width = el.stopXx + getOriginPosition().originX - el.width
  1111. }
  1112. if (attr === 'startYy') {
  1113. el.startYy = Number(el[attr as keyof BlockItem])
  1114. el.x = el.startYy + getOriginPosition().originY
  1115. }
  1116. if (attr === 'stopYy') {
  1117. el.stopYy = Number(el[attr as keyof BlockItem])
  1118. el.height = el.stopYy + getOriginPosition().originY - el.height
  1119. }
  1120. }
  1121. const deleteBlockArea = (id: string) => {
  1122. if (id) {
  1123. blocks.value = blocks.value.filter((item) => item.id !== id)
  1124. selectedBlock.value = null
  1125. }
  1126. }
  1127. </script>
  1128. <style scoped lang="less">
  1129. .viewer {
  1130. padding: 10px;
  1131. min-width: 500px;
  1132. flex-shrink: 0;
  1133. // margin-top: 10px;
  1134. &-header {
  1135. display: flex;
  1136. justify-content: space-between;
  1137. padding-bottom: 20px;
  1138. &-title {
  1139. font-size: 16px;
  1140. font-weight: 600;
  1141. line-height: 24px;
  1142. }
  1143. &-subtitle {
  1144. font-size: 14px;
  1145. color: #666;
  1146. }
  1147. }
  1148. &-content {
  1149. display: flex;
  1150. gap: 20px;
  1151. }
  1152. }
  1153. .mapBox {
  1154. background-color: #e0e0e0;
  1155. background-image:
  1156. linear-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px),
  1157. linear-gradient(to right, rgba(0, 0, 0, 0.1) 1px, transparent 1px);
  1158. background-size: 20px 20px;
  1159. position: relative;
  1160. flex-shrink: 0;
  1161. // 添加黑边框
  1162. &::before {
  1163. content: '';
  1164. position: absolute;
  1165. top: -5px;
  1166. left: -5px;
  1167. width: calc(100% + 10px);
  1168. height: calc(100% + 10px);
  1169. border: 5px solid rgba(0, 0, 0, 0.8);
  1170. box-sizing: border-box;
  1171. pointer-events: none;
  1172. }
  1173. }
  1174. .mapConfig {
  1175. background-color: #f5f5f5;
  1176. border-radius: 10px;
  1177. padding: 12px;
  1178. &-header {
  1179. font-size: 14px;
  1180. margin-bottom: 10px;
  1181. font-weight: 600;
  1182. }
  1183. &-item {
  1184. display: flex;
  1185. line-height: 30px;
  1186. &-label {
  1187. color: #888;
  1188. min-width: 80px;
  1189. }
  1190. &-content {
  1191. color: #555;
  1192. min-width: 100px;
  1193. }
  1194. }
  1195. }
  1196. .temp-block {
  1197. position: absolute;
  1198. background: rgba(24, 144, 255, 0.2);
  1199. border: 2px dashed #1890ff;
  1200. }
  1201. .block-item {
  1202. background: rgba(24, 144, 255, 0.1);
  1203. .resize-handle {
  1204. position: absolute;
  1205. right: -4px;
  1206. bottom: -4px;
  1207. width: 15px;
  1208. height: 15px;
  1209. background: #1890ff;
  1210. cursor: nwse-resize;
  1211. font-size: 12px;
  1212. color: #fff;
  1213. display: flex;
  1214. align-items: center;
  1215. justify-content: center;
  1216. }
  1217. }
  1218. .dragging-item {
  1219. opacity: 0.5;
  1220. transition: opacity 0.2s ease;
  1221. }
  1222. </style>