| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- <template>
- <div class="radarEditor" :class="{ disabled: props.disabled }">
- <DetectionAreaView
- :coordinates="coordinates"
- :direction="angle"
- :furniture-items="localFurniture"
- :canvas-size="500"
- mode="edit"
- >
- <template #furnitures>
- <EditableFurniture
- v-for="item in localFurniture"
- :key="item.nanoid"
- :item="item"
- :angle="angle"
- :coordinates="coordinates"
- :canvas-size="400"
- :disabled="disabled"
- @update="updateFurniture"
- @delete="deleteFurniture"
- />
- </template>
- <template #subregion>
- <EditableSubregion
- ref="editableSubregionRef"
- :ranges="coordinates"
- :angle="angle"
- :canvas-size="400"
- :subRegions="localSubRegions"
- :editable="!disabled"
- :has-bed="localFurniture.some((item) => item.type === 'bed')"
- @create="handleSubregionCreate"
- @update="handleSubregionUpdate"
- />
- </template>
- </DetectionAreaView>
- <div v-if="!disabled && showPanel" class="options">
- <div class="close" @click="showPanel = false">×</div>
- <div class="header">
- <a-radio-group
- v-model:value="modeRadio"
- button-style="solid"
- size="small"
- @change="modeRadioChange"
- >
- <a-radio-button :value="1">配置面板</a-radio-button>
- <a-radio-button :value="2">信息面板</a-radio-button>
- </a-radio-group>
- <a-button type="link" size="small" @click="syncCoordinates">同步坐标</a-button>
- </div>
- <div v-if="modeRadio === 1" class="config">
- <div class="panel">
- <div class="panel-hd">家具操作</div>
- <div class="panel-ct">
- <div class="furnitureTool">
- <a-radio-group v-model:value="sideRadio" size="small">
- <a-radio :style="radioStyle" :value="1">客厅</a-radio>
- <a-radio :style="radioStyle" :value="2">餐厅</a-radio>
- <a-radio :style="radioStyle" :value="3">卧室</a-radio>
- <a-radio :style="radioStyle" :value="4">卫生间</a-radio>
- </a-radio-group>
- <furniture-list
- v-if="sideRadio === 1"
- :icons="livingroomIcons"
- @add="add"
- ></furniture-list>
- <furniture-list
- v-if="sideRadio === 2"
- :icons="diningroomIcons"
- @add="add"
- ></furniture-list>
- <furniture-list
- v-if="sideRadio === 3"
- :icons="bedroomIocns"
- @add="add"
- ></furniture-list>
- <furniture-list
- v-if="sideRadio === 4"
- :icons="bathroomIocns"
- @add="add"
- ></furniture-list>
- </div>
- </div>
- <div class="panel-hd">区域操作</div>
- <div class="panel-ct">
- <div class="subregionTool">
- <div v-if="localSubRegions.length === 0">
- 暂无区域,<a-button type="link" size="small" @click="createSubregion"
- >新建区域</a-button
- >
- </div>
- <div v-else>
- <span>已创建 {{ localSubRegions.length }} 个区域</span>
- <a-button
- v-if="localSubRegions.length < 6"
- type="link"
- size="small"
- @click="createSubregion"
- >继续创建</a-button
- >
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-if="modeRadio === 2" class="info">
- <div class="panel">
- <div class="panel-hd">
- <div
- >家具列表 <span v-if="localFurniture.length">({{ localFurniture.length }})</span></div
- >
- <a-space>
- <a-popconfirm
- v-if="localFurniture.length"
- title="确定清空家具吗?"
- @confirm="clearFurniture"
- >
- <a-button size="small" type="link">清空</a-button>
- </a-popconfirm>
- <a-button size="small" type="link" @click="modeRadio = 1">添加</a-button>
- </a-space>
- </div>
- <div class="panel-ct">
- <template v-if="localFurniture.length">
- <div v-for="(furniture, index) in localFurniture" :key="index" class="list-item">
- <a-collapse v-model:activeKey="furnitureActiveKey" ghost>
- <a-collapse-panel :key="index + 1" :header="`${furniture.name} 属性`">
- <div class="mapConfig">
- <div class="mapConfig-item">
- <label class="mapConfig-item-label">家具尺寸:</label>
- <div class="mapConfig-item-content">
- <a-space>
- <a-input-number
- v-model:value="furniture.width"
- :min="10"
- size="small"
- :style="inputStyle"
- />
- <a-input-number
- v-model:value="furniture.length"
- :min="10"
- size="small"
- :style="inputStyle"
- />
- </a-space>
- </div>
- </div>
- <div class="mapConfig-item">
- <label class="mapConfig-item-label">家具旋转:</label>
- <div class="mapConfig-item-content">
- <a-select
- v-model:value="furniture.rotate"
- size="small"
- :style="inputStyle"
- >
- <a-select-option :value="0">0°</a-select-option>
- <a-select-option :value="90">90°</a-select-option>
- <a-select-option :value="180">180°</a-select-option>
- <a-select-option :value="270">270°</a-select-option>
- </a-select>
- </div>
- </div>
- <div class="mapConfig-item">
- <label class="mapConfig-item-label">位置微调:</label>
- <div class="mapConfig-item-content"></div>
- <a-space>
- <ArrowLeftOutlined @click="nudge('left')" />
- <ArrowUpOutlined @click="nudge('up')" />
- <ArrowDownOutlined @click="nudge('down')" />
- <ArrowRightOutlined @click="nudge('right')" />
- <a-input-number
- v-model:value="nudgeStep"
- :min="1"
- size="small"
- style="width: 60px"
- />
- </a-space>
- </div>
- <div class="mapConfig-item">
- <label class="mapConfig-item-label">left/top:</label>
- <div class="mapConfig-item-content">
- <a-space>
- <a-input-number
- v-model:value="furniture.left"
- disabled
- size="small"
- :style="inputStyle"
- />
- <a-input-number
- v-model:value="furniture.top"
- disabled
- size="small"
- :style="inputStyle"
- />
- </a-space>
- </div>
- </div>
- <div class="mapConfig-item">
- <label class="mapConfig-item-label">x/y:</label>
- <div class="mapConfig-item-content">
- <a-space>
- <a-input-number
- v-model:value="furniture.x"
- size="small"
- :style="inputStyle"
- />
- <a-input-number
- v-model:value="furniture.y"
- size="small"
- :style="inputStyle"
- />
- </a-space>
- </div>
- </div>
- <div class="mapConfig-item">
- <label class="mapConfig-item-label">操作:</label>
- <div class="mapConfig-item-content">
- <a-space>
- <a-popconfirm
- title="确定删除家具吗?"
- @confirm="deleteFurniture(furniture.nanoid!)"
- >
- <DeleteOutlined />
- </a-popconfirm>
- </a-space>
- </div>
- </div>
- </div>
- </a-collapse-panel>
- </a-collapse>
- </div>
- </template>
- <div v-else class="list-empty">暂无家具</div>
- </div>
- </div>
- <div class="panel">
- <div class="panel-hd">
- <div
- >区域列表
- <span v-if="localSubRegions.length">({{ localSubRegions.length }})</span></div
- >
- <a-space>
- <a-popconfirm
- v-if="localSubRegions.length"
- title="确定清空子区域吗?"
- @confirm="clearSubregions"
- >
- <a-button size="small" type="link">清空</a-button>
- </a-popconfirm>
- <a-button size="small" type="link" @click="createSubregion">新建</a-button>
- </a-space>
- </div>
- <div class="panel-ct">
- <template v-if="localSubRegions.length">
- <div v-for="(region, index) in localSubRegions" :key="index" class="list-item">
- <a-collapse v-model:activeKey="regionActiveKey" ghost>
- <a-collapse-panel :key="index + 1" :header="`子区域 ${index + 1} 属性`">
- <div class="mapConfig">
- <div class="mapConfig-item">
- <div class="mapConfig-item-label">X范围:</div>
- <div class="mapConfig-item-content">
- <a-space>
- <a-input
- v-model:value.trim="region.startXx"
- :style="inputStyle"
- size="small"
- />
- <a-input
- v-model:value.trim="region.stopXx"
- :style="inputStyle"
- size="small"
- />
- </a-space>
- </div>
- </div>
- <div class="mapConfig-item">
- <div class="mapConfig-item-label">Y范围:</div>
- <div class="mapConfig-item-content">
- <a-space>
- <a-input
- v-model:value.trim="region.startYy"
- :style="inputStyle"
- size="small"
- />
- <a-input
- v-model:value.trim="region.stopYy"
- :style="inputStyle"
- size="small"
- />
- </a-space>
- </div>
- </div>
- <div class="mapConfig-item">
- <div class="mapConfig-item-label">Z范围:</div>
- <div class="mapConfig-item-content">
- <a-space>
- <a-input
- v-model:value.trim="region.startZz"
- :style="inputStyle"
- size="small"
- />
- <a-input
- v-model:value.trim="region.stopZz"
- :style="inputStyle"
- size="small"
- />
- </a-space>
- </div>
- </div>
- <div class="mapConfig-item">
- <div class="mapConfig-item-label">区域跟踪:</div>
- <div class="mapConfig-item-content">
- <a-switch v-model:checked="region.isTracking" size="small" />
- </div>
- </div>
- <div class="mapConfig-item">
- <div class="mapConfig-item-label">区域跌倒:</div>
- <div class="mapConfig-item-content">
- <a-switch v-model:checked="region.isFalling" size="small" />
- </div>
- </div>
- <div v-if="region.isBed" class="mapConfig-item">
- <div class="mapConfig-item-label">呼吸检测:</div>
- <div class="mapConfig-item-content"> 默认开启 </div>
- </div>
- <div class="mapConfig-item">
- <div class="mapConfig-item-label">删除区域:</div>
- <div class="mapConfig-item-content">
- <a-popconfirm
- title="确定删除区域吗?"
- @confirm="deleteBlockArea(region.id || '')"
- >
- <DeleteOutlined />
- </a-popconfirm>
- </div>
- </div>
- </div>
- </a-collapse-panel>
- </a-collapse>
- </div>
- </template>
- <div v-else class="list-empty">暂无子区域</div>
- </div>
- </div>
- </div>
- </div>
- <a-button v-else type="link" @click="showPanel = true">开始配置</a-button>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, watch, computed, onUnmounted, reactive } from 'vue'
- // import type { FurnitureItem } from '@/types/radar'
- import DetectionAreaView from '../DetectionAreaView/index.vue'
- import EditableFurniture from '../EditableFurniture/index.vue'
- import EditableSubregion from '../EditableSubregion/index.vue'
- import type { FurnitureIconType } from '@/types/furniture'
- import { message } from 'ant-design-vue'
- import { nanoid } from 'nanoid'
- import { furnitureIconNameMap, furnitureIconSizeMap } from '@/const/furniture'
- import type {
- FurnitureItem,
- LocalFurnitureItem,
- LocalSubRegionItem,
- SubRegions,
- } from '@/api/room/types'
- import {
- DeleteOutlined,
- ArrowLeftOutlined,
- ArrowUpOutlined,
- ArrowDownOutlined,
- ArrowRightOutlined,
- } from '@ant-design/icons-vue'
- defineOptions({ name: 'RadarEditor' })
- interface Props {
- coordinates: [number, number, number, number]
- angle: number
- furnitureItems?: LocalFurnitureItem[]
- subRegions?: LocalSubRegionItem[]
- disabled?: boolean
- }
- const props = defineProps<Props>()
- const emit = defineEmits<{
- (e: 'update:furnitureItems', items: FurnitureItem[]): void
- (e: 'update:subRegions', regions: SubRegions[]): void
- }>()
- const localFurniture = ref<FurnitureItem[]>(props.furnitureItems ?? [])
- const localSubRegions = ref<SubRegions[]>(props.subRegions ?? [])
- console.log('props', props)
- const inputStyle = computed(() => ({ width: '70px' }))
- const pixelPosition = reactive({ left: 0, top: 0 })
- const nudgeStep = ref(5)
- // 微调功能
- const nudge = (direction: 'left' | 'right' | 'up' | 'down') => {
- const step = nudgeStep.value
- switch (direction) {
- case 'left':
- pixelPosition.left -= step
- break
- case 'right':
- pixelPosition.left += step
- break
- case 'up':
- pixelPosition.top -= step
- break
- case 'down':
- pixelPosition.top += step
- break
- }
- // updateGeoPosition()
- // emit('update', { ...localItem })
- }
- watch(
- () => props.furnitureItems,
- (newVal) => {
- if (newVal) localFurniture.value = [...newVal]
- },
- { deep: true }
- )
- watch(
- () => props.subRegions,
- (newVal) => {
- if (newVal) localSubRegions.value = [...newVal]
- },
- { deep: true }
- )
- // 监听本地子区域变化,通知父组件
- watch(
- localSubRegions,
- (newRegions) => {
- // emit('update:subRegions', newRegions)
- console.log('子区域变化', newRegions)
- // 缓存起来
- localStorage.setItem('subRegions', JSON.stringify(newRegions))
- },
- { deep: true }
- )
- function updateFurniture(item: FurnitureItem) {
- localFurniture.value = localFurniture.value.map((i) => (i.nanoid === item.nanoid ? item : i))
- emit('update:furnitureItems', localFurniture.value)
- }
- function deleteFurniture(nanoid: string) {
- localFurniture.value = localFurniture.value.filter((i) => i.nanoid !== nanoid)
- emit('update:furnitureItems', localFurniture.value)
- }
- function addFurniture(item: FurnitureItem) {
- localFurniture.value.push(item)
- emit('update:furnitureItems', [...localFurniture.value])
- }
- defineExpose({ addFurniture })
- const modeRadio = ref<1 | 2 | 3>(1)
- const sideRadio = ref<1 | 2 | 3 | 4>(1)
- const editableSubregionRef = ref<InstanceType<typeof EditableSubregion>>()
- const radioStyle = reactive({
- display: 'flex',
- height: '30px',
- lineHeight: '30px',
- })
- // 客厅图标
- const livingroomIcons = [
- 'living_sofa',
- 'living_sofa_single',
- 'living_tea_table',
- 'living_bookcase',
- 'living_tv_stand',
- ]
- // 餐厅图标
- const diningroomIcons = [
- 'dining_table',
- 'dining_table_rect',
- 'dining_fridge',
- 'dining_chair',
- 'bath_door',
- ]
- // 卧室图标
- const bedroomIocns = [
- 'bed',
- 'bed_table',
- 'bed_dressing_chair',
- 'bed_dressing_mirror',
- 'bed_cabinet',
- ]
- // 卫生间图标
- const bathroomIocns = ['bath_basin', 'bath_shower', 'bath_toilet', 'bath_floor']
- // 添加家具
- const add = (icon: FurnitureIconType) => {
- const originWidth = furnitureIconSizeMap[icon].width || 30
- const originHeight = furnitureIconSizeMap[icon].height || 30
- const newItem: FurnitureItem = {
- name: furnitureIconNameMap[icon],
- type: icon,
- width: originWidth,
- length: originHeight,
- top: 0,
- left: 0,
- rotate: 0,
- x: 0,
- y: 0,
- nanoid: nanoid(),
- }
- addFurniture(newItem)
- message.success('已添加家具')
- }
- // 创建子区域
- const createSubregion = () => {
- // modeRadio.value = 2
- // 通过ref调用EditableSubregion组件的createNewBlock方法
- if (editableSubregionRef.value) {
- editableSubregionRef.value?.createNewBlock()
- }
- }
- // 处理子区域创建事件
- const handleSubregionCreate = () => {
- // message.success('已创建子区域')
- }
- // 处理子区域更新事件
- const handleSubregionUpdate = (item: SubRegions[]) => {
- // 可以在这里添加更新后的逻辑
- const hasBed = localFurniture.value.some((furniture) => furniture.type === 'bed')
- console.log('子区域更新', item, hasBed)
- if (hasBed) {
- localSubRegions.value = item.map((region, index) => ({
- ...region,
- isBed: index === 0,
- }))
- } else {
- localSubRegions.value = item
- }
- // emit('update:subRegions', item)
- }
- const showPanel = ref(true)
- onUnmounted(() => {
- // 组件销毁时清除缓存
- localStorage.removeItem('subRegions')
- })
- const regionActiveKey = ref<number[]>([])
- const furnitureActiveKey = ref<number[]>([])
- const deleteBlockArea = (id: string) => {
- if (id) {
- localSubRegions.value = localSubRegions.value.filter((item) => item.id !== id)
- }
- }
- const modeRadioChange = () => {
- regionActiveKey.value = []
- furnitureActiveKey.value = []
- }
- const syncCoordinates = () => {
- console.log('同步坐标', localFurniture.value, localSubRegions.value)
- }
- const clearSubregions = () => {
- localSubRegions.value = []
- }
- const clearFurniture = () => {
- localFurniture.value = []
- }
- </script>
- <style scoped lang="less">
- .radarEditor.disabled {
- cursor: no-drop;
- :deep(*) {
- pointer-events: none;
- user-select: none;
- opacity: 0.85;
- }
- }
- .radarEditor {
- position: relative;
- display: flex;
- gap: 8px;
- .radar-view {
- flex-shrink: 0;
- }
- .options {
- width: 280px;
- background-color: #fefefe;
- border-radius: 10px;
- padding: 12px;
- position: relative;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- .close {
- position: absolute;
- top: 8px;
- right: 12px;
- font-size: 22px;
- font-weight: 600;
- line-height: 1;
- color: #999;
- cursor: pointer;
- transition: color 0.2s;
- }
- .header {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
- }
- }
- }
- .panel {
- margin-bottom: 12px;
- &-hd {
- font-size: 12px;
- font-weight: 600;
- color: #666;
- line-height: 1.5;
- padding: 5px 8px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- .ant-btn-link {
- font-size: 12px;
- line-height: 1.5;
- padding: 0;
- }
- }
- &-ct {
- display: flex;
- flex-direction: column;
- gap: 8px;
- min-height: 30px;
- max-height: 300px;
- overflow-y: auto;
- // 滚动条不遮挡内容
- ::-webkit-scrollbar {
- width: 4px;
- height: 4px;
- }
- ::-webkit-scrollbar-track {
- background-color: transparent;
- }
- ::-webkit-scrollbar-thumb {
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 2px;
- }
- .furnitureTool {
- display: flex;
- justify-content: space-between;
- background-color: #f5f5f5;
- padding: 8px;
- border-radius: 8px;
- .furnitureList {
- flex-grow: 1;
- }
- }
- .subregionTool {
- display: flex;
- align-items: center;
- background-color: #f5f5f5;
- padding: 10px 12px;
- border-radius: 8px;
- font-size: 14px;
- color: #333;
- }
- }
- &:last-child {
- margin-bottom: 0;
- }
- .list-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- border-radius: 8px;
- background-color: #f5f5f5;
- cursor: pointer;
- }
- .list-empty {
- padding: 8px 12px;
- font-size: 14px;
- color: #999;
- border-radius: 8px;
- background-color: #f5f5f5;
- }
- }
- .mapConfig {
- background-color: #f5f5f5;
- border-radius: 10px;
- padding: 0 12px;
- &-header {
- margin-bottom: 10px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- .title {
- font-size: 14px;
- font-weight: 600;
- line-height: 24px;
- }
- .close {
- font-size: 14px;
- color: #666;
- cursor: pointer;
- position: relative;
- top: -5px;
- }
- }
- &-item {
- display: flex;
- line-height: 30px;
- &-label {
- color: #888;
- min-width: 80px;
- }
- &-content {
- color: #555;
- }
- }
- }
- :deep(.ant-collapse) {
- width: 100%;
- }
- :deep(.ant-collapse .ant-collapse-content > .ant-collapse-content-box) {
- padding: 0;
- }
- :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
- padding: 5px;
- user-select: none;
- }
- </style>
|