Browse Source

告警设置

lipenghui 1 tuần trước cách đây
mục cha
commit
8b9d0b07d0

+ 50 - 0
src/api/pms/alarm/index.ts

@@ -0,0 +1,50 @@
+import request from '@/config/axios'
+
+// 告警设置 VO
+export interface IotAlarmSettingVO {
+  id: number // 主键id
+  classifyId: number // 分类id
+  classifyName: string // 分类名称
+  deviceId: number // 设备id
+  deviceName: string // 设备名称
+  maxValue: string // 上限
+  minValue: string // 下限
+  alarmProperty: number // 告警属性
+}
+
+// 告警设置 API
+export const IotAlarmSettingApi = {
+  // 查询告警设置分页
+  getIotAlarmSettingPage: async (params: any, ifdevice:string) => {
+    return await request.get({ url: `/rq/iot-alarm-setting/page/` + ifdevice, params })
+  },
+  batchUpdateAlarmSettings: async (alarms: any) => {
+    debugger
+    console.log(JSON.stringify(alarms))
+    return await request.post({ url: `/rq/iot-alarm-setting/batch/update`, alarms })
+  },
+  // 查询告警设置详情
+  getIotAlarmSetting: async (id: number) => {
+    return await request.get({ url: `/rq/iot-alarm-setting/get?id=` + id })
+  },
+
+  // 新增告警设置
+  createIotAlarmSetting: async (data: IotAlarmSettingVO) => {
+    return await request.post({ url: `/rq/iot-alarm-setting/create`, data })
+  },
+
+  // 修改告警设置
+  updateIotAlarmSetting: async (data: IotAlarmSettingVO) => {
+    return await request.put({ url: `/rq/iot-alarm-setting/update`, data })
+  },
+
+  // 删除告警设置
+  deleteIotAlarmSetting: async (id: number) => {
+    return await request.delete({ url: `/rq/iot-alarm-setting/delete?id=` + id })
+  },
+
+  // 导出告警设置 Excel
+  exportIotAlarmSetting: async (params) => {
+    return await request.download({ url: `/rq/iot-alarm-setting/export-excel`, params })
+  },
+}

+ 3 - 0
src/api/pms/device/index.ts

@@ -49,6 +49,9 @@ export const IotDeviceApi = {
   getMapDevice: async (params:any) => {
     return await request.get({ url: `/rq/iot-device/map`, params})
   },
+  getAllDeviceParams: async (params:any) => {
+    return await request.get({ url: `/rq/iot-device/all/params`, params})
+  },
   // 查询设备台账分页
   getIotDevicePage: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/page`, params })

+ 1 - 0
src/assets/svgs/iot/arrange.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1759112595197" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7721" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M864.064 3.392a152.192 152.192 0 0 0-152 152c0 42.624 17.696 81.184 46.048 108.768l-71.008 146.816a151.072 151.072 0 0 0-19.52-1.376 150.72 150.72 0 0 0-82.624 24.608l-78.72-58.432c1.376-8.224 2.272-16.64 2.272-25.248 0-83.808-68.192-152-152-152s-152 68.192-152 152c0 42.624 17.696 81.152 46.048 108.768l-71.04 146.816a151.072 151.072 0 0 0-19.52-1.408c-83.808 0-152 68.192-152 152s68.192 152 152 152 152-68.192 152-152a151.456 151.456 0 0 0-46.048-108.768l71.008-146.816c6.4 0.832 12.896 1.376 19.52 1.376a151.04 151.04 0 0 0 103.776-41.28l64.736 48.064a150.784 150.784 0 0 0-9.472 52.288c0 83.808 68.192 152 152 152s152-68.192 152-152a151.424 151.424 0 0 0-46.08-108.8l71.008-146.816c6.4 0.832 12.928 1.376 19.552 1.376 83.808 0 152-68.192 152-152S947.808 3.36 864 3.36zM160.032 812.736c-30.88 0-56-25.12-56-56s25.12-56 56-56 56 25.12 56 56-25.12 56-56 56z m196.48-406.208c-30.88 0-56-25.12-56-56s25.12-56 56-56 56 25.12 56 56-25.12 56-56 56z m311.072 211.072c-30.88 0-56-25.12-56-56s25.12-56 56-56 56 25.12 56 56-25.12 56-56 56z m196.48-406.208c-30.88 0-56-25.12-56-56s25.12-56 56-56 56 25.12 56 56-25.12 56-56 56z" p-id="7722" fill="#13227a"></path></svg>

+ 131 - 0
src/views/pms/device/monitor/IotAlarmSettingForm.vue

@@ -0,0 +1,131 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="分类名称" prop="classifyName">
+        <el-input v-model="formData.classifyName" disabled />
+      </el-form-item>
+      <el-form-item label="设备编码" prop="deviceName" v-if="ifDevice">
+        <el-input v-model="formData.deviceName" disabled />
+      </el-form-item>
+      <el-form-item label="告警属性" prop="alarmProperty">
+        <el-input v-model="formData.alarmProperty" placeholder="请输入告警属性" />
+      </el-form-item>
+      <el-form-item label="属性标识" prop="propertyCode">
+        <el-input v-model="formData.propertyCode" placeholder="请输入属性标识" />
+      </el-form-item>
+      <el-form-item label="上限值" prop="maxValue">
+        <el-input v-model="formData.maxValue" placeholder="请输入上限" />
+      </el-form-item>
+      <el-form-item label="下限值" prop="minValue">
+        <el-input v-model="formData.minValue" placeholder="请输入下限" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { IotAlarmSettingApi, IotAlarmSettingVO } from '@/api/pms/alarm'
+
+/** 告警设置 表单 */
+defineOptions({ name: 'IotAlarmSettingForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  classifyId: undefined,
+  classifyName: undefined,
+  deviceId: undefined,
+  deviceName: undefined,
+  maxValue: undefined,
+  minValue: undefined,
+  alarmProperty: undefined,
+})
+const formRules = reactive({
+  classifyName: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
+  alarmProperty: [{ required: true, message: '告警属性不能为空', trigger: 'blur' }],
+  propertyCode: [{ required: true, message: '属性标识不能为空', trigger: 'blur' }],
+  maxValue: [{ required: true, message: '上限不能为空', trigger: 'blur' }],
+  minValue: [{ required: true, message: '下限不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+const ifDevice = ref(false)
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number,classId:any, className:any,deviceId:any, deviceName:any) => {
+  debugger
+  dialogVisible.value = true
+  dialogTitle.value = '新增设置'
+  formType.value = type
+  resetForm()
+  formData.value.classifyId = classId;
+  formData.value.classifyName = className;
+  formData.value.deviceId = deviceId;
+  formData.value.deviceName = deviceName;
+  ifDevice.value = !(deviceId === '' || deviceId === undefined);
+  // 修改时,设置数据
+  if (id) {
+    dialogTitle.value = '编辑设置'
+    formLoading.value = true
+    try {
+      formData.value = await IotAlarmSettingApi.getIotAlarmSetting(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as IotAlarmSettingVO
+    if (formType.value === 'create') {
+      await IotAlarmSettingApi.createIotAlarmSetting(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotAlarmSettingApi.updateIotAlarmSetting(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success',ifDevice.value)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    classifyId: undefined,
+    classifyName: undefined,
+    deviceId: undefined,
+    deviceName: undefined,
+    maxValue: undefined,
+    minValue: undefined,
+    alarmProperty: undefined,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 532 - 0
src/views/pms/device/monitor/RangeSet.vue

@@ -0,0 +1,532 @@
+<template>
+  <el-row :gutter="20">
+    <!-- 左侧 设备分类 树 -->
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1">
+        <DeviceCategoryTree @node-click="handleDeviceCategoryTreeNodeClick" />
+      </ContentWrap>
+    </el-col>
+    <el-col :span="20" :xs="24">
+      <!-- 搜索 -->
+      <ContentWrap>
+        <ContentWrap>
+          <ContentWrap v-if="selectedNode">
+            <div style="display: flex;flex-direction: row; justify-content: space-between; vertical-align: center">
+              <div style="display: flex;flex-direction: row; ">
+                <el-radio style="background-color: #f4dcaf" v-model="ifDevice" label="1" size="large" @input="handleClassifyRadio" border>分类区间</el-radio>
+                <el-radio style="background-color: #f4f3f3" v-model="ifDevice" label="2" size="large" @input="handleDeviceRadio" border>设备区间</el-radio>
+              </div>
+<!--              <el-select-->
+<!--                style="width: 200px;"-->
+<!--                v-if="selectedNode&&ifDevice==='2'"-->
+<!--                :placeholder="t('faultForm.choose')"-->
+<!--                clearable-->
+<!--                @change="handleDeviceSelected"-->
+<!--                @clear="handleClear"-->
+<!--              />-->
+              <el-select style="width: 250px" v-model="selectedDeviceId" v-if="selectedNode&&ifDevice==='2'" :placeholder="t('iotMaintain.deviceHolder')"
+                         @clear="handleClear" @change="handleDeviceSelected" :class="{ 'shake-highlight': isDeviceSelectShake }" >
+                <el-option
+                  v-for="item in devices"
+                  :key="item.id"
+                  :label="item.deviceCode"
+                  :value="item.id">
+                  <span style="float: left">{{ item.deviceCode }}</span>
+                  <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deviceName }}</span>
+                </el-option>
+              </el-select>
+            </div>
+          </ContentWrap>
+          <!-- 选中项信息与操作 -->
+          <div v-if="selectedNode" class="selected-info-bar">
+            <div class="selected-info">
+              <Icon icon="fa:foursquare" />
+              <h2 class="selected-name">{{ifDevice==='1'?selectedName:selectedDeviceName }}</h2>
+              <el-tag
+                :type="ifDevice === '1' ? 'success' : 'primary'"
+                class="node-type-tag"
+              >
+                {{ ifDevice === '1' ? '分类' : '设备' }}
+              </el-tag>
+            </div>
+
+            <div class="action-buttons">
+              <el-button
+                type="primary"
+                @click="openForm('create')"
+                class="add-property-btn"
+                size="default"
+              >
+                <template #icon>
+                  <Plus />
+                </template>
+                添加属性
+              </el-button>
+              <el-button
+                type="success"
+                @click="saveAllProperties"
+                :loading="saveLoading"
+                size="default"
+              >
+                <template #icon>
+                  <Check />
+                </template>
+                保存配置
+              </el-button>
+            </div>
+          </div>
+          <!-- 未选择任何项时的空状态 -->
+          <div v-else class="empty-state">
+            <el-empty description="请从左侧选择一个设备或分类" class="main-empty-state">
+              <template #image>
+                <div class="empty-image-container">
+                  <Icon :size="40" icon="fa-solid:empty-set"/>
+                </div>
+              </template>
+            </el-empty>
+          </div>
+        </ContentWrap>
+
+        <ContentWrap>
+          <el-row v-if="selectedNode&&list.length>0" :gutter="16">
+            <el-col v-for="item in list" :key="item.id" :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
+              <el-card :body-style="{ padding: '0' }" class="h-full transition-colors">
+                <!-- 内容区域 -->
+                <div class="p-4">
+                  <!-- 标题区域 -->
+                  <div class="flex items-center mb-3">
+                    <div class="mr-2.5 flex items-center">
+                      <el-image :src="defaultIconUrl" class="w-[35px] h-[35px]" />
+                    </div>
+                    <div class="text-[16px] font-600">{{ item.alarmProperty }}</div>
+                  </div>
+
+                  <!-- 信息区域 -->
+                  <div class="flex items-center text-[14px]">
+                    <div class="flex-1">
+                      <div class="mb-2.5 last:mb-0">
+                        <span class="text-[#717c8e] mr-2.5">属性标识</span>
+                        <el-tag><span class="text-[#0070ff]" style="font-size: 15px">{{ item.propertyCode }}</span></el-tag>
+                      </div>
+                      <div class="mb-2.5 last:mb-0">
+                        <span class="text-[#717c8e] mr-2.5">上限数值</span>
+                        <el-input
+                          v-model.number="item.maxValue"
+                          type="number"
+                          class="text-[#0070ff] inline-input"
+                          size="small"
+                          step="any"
+                        :min="item.minValue !== null ? item.minValue : -Infinity"
+                        />
+                      </div>
+                      <div class="mb-2.5 last:mb-0">
+                        <span class="text-[#717c8e] mr-2.5">下限数值</span>
+                        <el-input
+                          v-model.number="item.minValue"
+                          type="number"
+                          class="text-[#0070ff] inline-input"
+                          size="small"
+                          step="any"
+                        :max="item.maxValue !== null ? item.maxValue : Infinity"
+                        />
+                      </div>
+                    </div>
+                    <div class="w-[100px] h-[80px]">
+                      <el-image :src="defaultPicUrl" class="w-full h-full" />
+                    </div>
+                  </div>
+
+                  <!-- 分隔线 -->
+                  <el-divider class="!my-3" />
+
+                  <!-- 按钮组 -->
+                  <div class="flex items-center px-0">
+                    <el-button
+                      v-hasPermi="['iot:product:update']"
+                      class="flex-1 !px-2 !h-[32px] text-[13px]"
+                      plain
+                      type="primary"
+                      @click="openForm('update', item.id)"
+                    >
+                      <Icon class="mr-1" icon="ep:edit-pen" />
+                      编辑
+                    </el-button>
+                    <el-button
+                      class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
+                      plain
+                      type="warning"
+                      @click="openDetail(item.id)"
+                    >
+                      <Icon class="mr-1" icon="ep:view" />
+                      详情
+                    </el-button>
+                    <el-button
+                      class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
+                      plain
+                      type="success"
+                      @click="openObjectModel(item)"
+                    >
+                      <Icon class="mr-1" icon="ep:scale-to-original" />
+                      物模型
+                    </el-button>
+                    <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
+                    <el-button
+                      v-hasPermi="['iot:product:delete']"
+                      :disabled="item.status === 1"
+                      class="!px-2 !h-[32px] text-[13px]"
+                      plain
+                      type="danger"
+                      @click="handleDelete(item.id)"
+                    >
+                      <Icon icon="ep:delete" />
+                    </el-button>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+          <Pagination
+            v-if="selectedNode&&list.length>0"
+            v-model:limit="queryParams.pageSize"
+            v-model:page="queryParams.pageNo"
+            :total="total"
+            @pagination="getList"
+          />
+        </ContentWrap>
+        <!-- 有选中项但无属性时的空状态 -->
+        <div v-if="selectedNode && list.length === 0" class="no-properties-state">
+          <el-empty description="暂无属性,请点击添加属性按钮创建">
+            <template #image>
+              <div class="no-properties-image">
+                <el-icon class="no-properties-icon"><Icon icon="ep:add" /></el-icon>
+              </div>
+            </template>
+            <template #bottom>
+              <el-button type="primary" @click="showAddPropertyDialog" size="default">
+                <template #icon>
+                  <Plus />
+                </template>
+                添加第一个属性
+              </el-button>
+            </template>
+          </el-empty>
+        </div>
+      </ContentWrap>
+    </el-col>
+  </el-row>
+  <IotAlarmSettingForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import * as DeviceTemplateApi from '@/api/pms/devicetemplate'
+import DeviceCategoryTree from '../../bom/DeviceCategoryTree.vue'
+import { useTreeStore } from '@/store/modules/attrTemplateTreeStore'
+import { Check, Plus } from '@element-plus/icons-vue'
+import { nextTick, reactive, ref } from 'vue'
+import { IotAlarmSettingApi } from '@/api/pms/alarm'
+import IotAlarmSettingForm from "@/views/pms/device/monitor/IotAlarmSettingForm.vue";
+import defaultIconUrl from '@/assets/svgs/iot/cube.svg'
+import defaultPicUrl from '@/assets/svgs/iot/arrange.svg'
+import {IotDeviceApi} from "@/api/pms/device";
+
+defineOptions({ name: 'IotAlarmSet' })
+
+const treeStore = useTreeStore()
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const { push } = useRouter()
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const saveLoading = ref(false) // 保存的加载状态
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: undefined,
+  code: undefined,
+  status: undefined,
+  classifyId: undefined,
+  deviceId: undefined,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+// 从 Store 中获取左侧 设备分类树 选中的 节点ID
+const selectedId = ref<string>()
+const selectedDeviceId = ref<string>()
+const selectedName = ref<string>()
+const selectedDeviceName = ref<string>()
+const selectedNode = ref(null)
+const ifDevice = ref('')
+const devices = ref([])
+const isDeviceSelectShake = ref(false)
+
+const handleDeviceSelected = async (row) =>{
+  debugger
+  // selectedDeviceId.value = row.id
+  const device = devices.value.find(item => item.id === row)
+  if (device) {
+    selectedDeviceName.value = device.deviceCode+device.deviceName
+  }
+  queryParams.deviceId = row
+  await getList("2");
+}
+const handleClear = () =>{
+  selectedDeviceId.value = ''
+  selectedDeviceName.value = ''
+}
+const handleDeviceRadio = async () =>{
+  //置空分类名称
+  // selectedName.value = ''
+  const deviceParams = {
+    assetClass: selectedId.value,
+    pageNo:1,
+  }
+  // queryParams.deviceId  = selectedDeviceId.value
+  const data = await IotDeviceApi.getAllDeviceParams(deviceParams)
+  debugger
+  devices.value = data;
+  // if (selectedDeviceId.value===''||selectedDeviceId.value===undefined) {
+  //   list.value = []
+  // } else {
+    await getList("2")
+  // }
+};
+const handleClassifyRadio = async () =>{
+  queryParams.classifyId = selectedId.value
+  queryParams.deviceId = null
+  selectedDeviceId.value = ''
+  await getList("1")
+};
+/** 查询 设备属性模板 列表 */
+const getList = async (ifdevice:string) => {
+  loading.value = true
+  try {
+    const data = await IotAlarmSettingApi.getIotAlarmSettingPage(queryParams, ifdevice)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 搜索按钮操作 */
+const handleAllQuery = () => {
+  queryParams.pageNo = 1
+  queryParams.deviceCategoryId = ''
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 处理 设备分类树 被点击 */
+const handleDeviceCategoryTreeNodeClick = async (row) => {
+  selectedNode.value = row
+  selectedId.value = row.id
+  selectedName.value = row.name
+  queryParams.classifyId = row.id
+  ifDevice.value = '1'
+  await getList("1")
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  debugger
+  if (ifDevice.value === '2'&&(selectedDeviceId.value === ''||selectedDeviceId.value === null||selectedDeviceId.value === undefined)) {
+    message.error('请先选择设备');
+    // 触发抖动效果
+    isDeviceSelectShake.value = true
+    // 1秒后移除抖动类名,以便下次可以重新触发
+    setTimeout(() => {
+      isDeviceSelectShake.value = false
+    }, 1000)
+    return
+  }
+  formRef.value.open(type, id, selectedId.value, selectedName.value, selectedDeviceId.value, selectedDeviceName.value)
+}
+
+/** 打开详情 */
+const openDetail = (row) => {
+  push({
+    name: 'DeviceAttrTemplateModel',
+    params: {
+      id: row.deviceCategoryId,
+      // 添加额外参数
+      templateName: row.name,
+      categoryName: row.deviceCategoryName
+    }
+  })
+}
+
+/** 修改 设备属性模板 状态 */
+const handleStatusChange = async (row: DeviceTemplateApi.DeviceAttrTemplateVO) => {
+  try {
+    // 修改状态的二次确认
+    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
+    await message.confirm('确认要"' + text + '""' + row.name + '"属性模板吗?')
+    // 发起修改状态
+    await DeviceTemplateApi.updateDeviceTemplateStatus(row.id, row.status)
+    // 刷新列表
+    await getList()
+  } catch {
+    // 取消后,进行恢复按钮
+    row.status =
+      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+  }
+}
+
+/** 操作分发 */
+const handleCommand = (command: string, row: DeviceTemplateApi.DeviceAttrTemplateVO) => {
+  switch (command) {
+    case 'handleDelete':
+      handleDelete(row.id)
+      break
+    default:
+      break
+  }
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await DeviceTemplateApi.deleteDeviceTemplate(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 保存所有属性配置 */
+const saveAllProperties = async () => {
+  if (!selectedNode.value) {
+    message.warning('请先选择设备或分类')
+    return
+  }
+
+  // 筛选出有修改的属性
+  const modifiedItems = list.value.filter(item =>
+    item.maxValue !== undefined || item.minValue !== undefined
+  )
+
+  if (modifiedItems.length === 0) {
+    message.info('没有需要保存的修改')
+    return
+  }
+
+  saveLoading.value = true
+  try {
+    debugger
+
+    // 调用保存接口
+    await IotAlarmSettingApi.batchUpdateAlarmSettings(modifiedItems)
+    message.success('保存成功')
+    // 重新获取列表刷新数据
+    await getList(ifDevice.value)
+  } catch (error) {
+    message.error('保存失败')
+  } finally {
+    saveLoading.value = false
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  //一进来先查询分类的属性
+  getList("1")
+})
+</script>
+<style scoped>
+.property-config-area {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.selected-info-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  margin-bottom: 20px;
+}
+
+.selected-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.selected-name {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 500;
+  color: #1d2129;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 12px;
+}
+
+.empty-state {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+.empty-image-container {
+  width: 120px;
+  height: 120px;
+  border-radius: 50%;
+  background-color: #f0f7ff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16px;
+}
+
+/* 新增样式:调整输入框显示效果 */
+.inline-input {
+  width: 100px;
+  display: inline-block;
+  vertical-align: middle;
+}
+.shake-highlight {
+  animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;
+  background-color: red !important;
+  box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.2) !important;
+}
+
+@keyframes shake {
+  10%, 90% { transform: translateX(-1px); }
+  20%, 80% { transform: translateX(2px); }
+  30%, 50%, 70% { transform: translateX(-3px); }
+  40%, 60% { transform: translateX(3px); }
+}
+:deep(.el-select__selection) {
+  height: 32px; /* 自定义高度 */
+}
+:deep(.el-select__placeholder.is-transparent){
+  color: #409EFF;
+}
+</style>

+ 1388 - 0
src/views/pms/device/monitor/RangeSetting.vue

@@ -0,0 +1,1388 @@
+<template>
+  <div class="property-manager-container">
+    <!-- 页面标题与导航 -->
+    <el-page-header
+      @back="handleBack"
+      content="设备属性管理"
+      class="page-header"
+    >
+      <template #extra>
+        <el-breadcrumb separator="/" class="breadcrumb">
+          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
+          <el-breadcrumb-item :to="{ path: '/devices' }">设备管理</el-breadcrumb-item>
+          <el-breadcrumb-item>属性配置</el-breadcrumb-item>
+        </el-breadcrumb>
+      </template>
+    </el-page-header>
+
+    <div class="main-content">
+      <!-- 左侧设备/分类选择面板 -->
+      <div class="sidebar">
+        <el-card class="sidebar-card">
+          <template #header>
+            <div class="sidebar-header">
+              <h3 class="sidebar-title">设备与分类</h3>
+              <el-input
+                v-model="searchQuery"
+                placeholder="搜索设备或分类..."
+                size="small"
+                class="search-input"
+              >
+                <template #prefix>
+                  <el-icon class="search-icon"><Search /></el-icon>
+                </template>
+              </el-input>
+            </div>
+          </template>
+
+          <el-tree
+            ref="deviceTree"
+            :data="deviceTreeData"
+            :props="treeProps"
+            :filter-node-method="filterNode"
+            node-key="id"
+            default-expand-all
+            @node-click="handleNodeSelect"
+            class="device-tree"
+            :highlight-current="true"
+          >
+            <!-- 树形节点自定义插槽 -->
+            <template #default="{ node, data }">
+              <div class="tree-node">
+                <el-icon :class="data.type === 'category' ? 'category-icon' : 'device-icon'">
+                  <template v-if="data.type === 'category'">
+                    <FolderOpened />
+                  </template>
+                  <template v-else>
+                    <Monitor />
+                  </template>
+                </el-icon>
+                <span class="node-label">{{ node.label }}</span>
+                <el-badge
+                  v-if="data.propertyCount"
+                  :value="data.propertyCount"
+                  class="property-count-badge"
+                  size="small"
+                />
+              </div>
+            </template>
+          </el-tree>
+        </el-card>
+      </div>
+
+      <!-- 右侧属性配置区域 -->
+      <div class="property-config-area">
+        <!-- 选中项信息与操作 -->
+        <div v-if="selectedNode" class="selected-info-bar">
+          <div class="selected-info">
+            <el-icon :class="selectedNode.type === 'category' ? 'category-icon' : 'device-icon'">
+              <template v-if="selectedNode.type === 'category'">
+                <FolderOpened />
+              </template>
+              <template v-else>
+                <Monitor />
+              </template>
+            </el-icon>
+            <h2 class="selected-name">{{ selectedNode.label }}</h2>
+            <el-tag :type="selectedNode.type === 'category' ? 'info' : 'primary'" class="node-type-tag">
+              {{ selectedNode.type === 'category' ? '分类' : '设备' }}
+            </el-tag>
+          </div>
+
+          <div class="action-buttons">
+            <el-button
+              type="primary"
+              @click="showAddPropertyDialog"
+              class="add-property-btn"
+              size="default"
+            >
+              <template #icon>
+                <Plus />
+              </template>
+              添加属性
+            </el-button>
+            <el-button
+              type="success"
+              @click="saveAllProperties"
+              :loading="saveLoading"
+              size="default"
+            >
+              <template #icon>
+                <Check />
+              </template>
+              保存配置
+            </el-button>
+          </div>
+        </div>
+
+        <!-- 未选择任何项时的空状态 -->
+        <div v-else class="empty-state">
+          <el-empty
+            description="请从左侧选择一个设备或分类"
+            class="main-empty-state"
+          >
+            <template #image>
+              <div class="empty-image-container">
+                <el-icon class="empty-icon"><Icon icon="ep:add" /></el-icon>
+              </div>
+            </template>
+          </el-empty>
+        </div>
+
+        <!-- 属性卡片列表 -->
+        <div v-if="selectedNode && properties.length > 0" class="properties-grid">
+          <!-- 属性卡片组件,使用v-for渲染 -->
+          <property-card
+            v-for="(property, index) in properties"
+            :key="property.id"
+            :property="property"
+            @update-property="handlePropertyUpdate(index, $event)"
+            @delete-property="handlePropertyDelete(index)"
+            class="property-card-item"
+          >
+            <!-- 卡片底部插槽示例 - 可以根据需要自定义内容 -->
+            <template #footer-actions>
+              <el-tooltip content="查看历史记录">
+                <el-button icon="Clock" size="mini" class="history-btn" />
+              </el-tooltip>
+            </template>
+          </property-card>
+        </div>
+
+        <!-- 有选中项但无属性时的空状态 -->
+        <div v-if="selectedNode && properties.length === 0" class="no-properties-state">
+          <el-empty
+            description="暂无属性,请点击添加属性按钮创建"
+          >
+            <template #image>
+              <div class="no-properties-image">
+                <el-icon class="no-properties-icon"><Icon icon="ep:add" /></el-icon>
+              </div>
+            </template>
+            <template #bottom>
+              <el-button
+                type="primary"
+                @click="showAddPropertyDialog"
+                size="default"
+              >
+                <template #icon>
+                  <Plus />
+                </template>
+                添加第一个属性
+              </el-button>
+            </template>
+          </el-empty>
+        </div>
+      </div>
+    </div>
+
+    <!-- 添加/编辑属性对话框 -->
+    <el-dialog
+      v-model="propertyDialogVisible"
+      :title="isEditing ? '编辑属性' : '添加新属性'"
+      :width="500"
+      @close="resetPropertyForm"
+      class="property-dialog"
+    >
+      <el-form
+        ref="propertyForm"
+        :model="currentProperty"
+        :rules="propertyRules"
+        label-width="120px"
+        class="property-form"
+      >
+        <el-form-item label="属性名称" prop="name">
+          <el-input
+            v-model="currentProperty.name"
+            placeholder="请输入属性名称"
+            maxlength="50"
+          />
+        </el-form-item>
+
+        <el-form-item label="属性标识" prop="code">
+          <el-input
+            v-model="currentProperty.code"
+            placeholder="请输入属性唯一标识"
+            maxlength="30"
+            :disabled="isEditing"
+          />
+          <div class="form-hint">标识用于系统内部识别,添加后不可修改</div>
+        </el-form-item>
+
+        <el-form-item label="数据类型" prop="dataType">
+          <el-select
+            v-model="currentProperty.dataType"
+            placeholder="请选择数据类型"
+            @change="handleDataTypeChange"
+          >
+            <el-option label="整数" value="integer" />
+            <el-option label="浮点数" value="float" />
+            <el-option label="布尔值" value="boolean" />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="单位" prop="unit">
+          <el-input
+            v-model="currentProperty.unit"
+            placeholder="例如:℃、%、m"
+            maxlength="10"
+          />
+        </el-form-item>
+
+        <el-row :gutter="16" class="limit-inputs-row">
+          <el-col :span="12">
+            <el-form-item label="最小值" prop="minValue">
+              <el-input-number
+                v-model.number="currentProperty.minValue"
+                :precision="currentProperty.dataType === 'float' ? 2 : 0"
+                :step="currentProperty.dataType === 'float' ? 0.1 : 1"
+                placeholder="请输入最小值"
+                :disabled="currentProperty.dataType === 'boolean'"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="最大值" prop="maxValue">
+              <el-input-number
+                v-model.number="currentProperty.maxValue"
+                :precision="currentProperty.dataType === 'float' ? 2 : 0"
+                :step="currentProperty.dataType === 'float' ? 0.1 : 1"
+                placeholder="请输入最大值"
+                :disabled="currentProperty.dataType === 'boolean'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="描述信息" prop="description">
+          <el-input
+            v-model="currentProperty.description"
+            placeholder="请输入属性描述(可选)"
+            type="textarea"
+            :rows="3"
+            maxlength="200"
+          />
+        </el-form-item>
+
+        <el-form-item>
+          <el-checkbox v-model="currentProperty.required">设为必填属性</el-checkbox>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="propertyDialogVisible = false">取消</el-button>
+        <el-button
+          type="primary"
+          @click="confirmPropertyAction"
+          :loading="dialogLoading"
+        >
+          {{ isEditing ? '更新属性' : '创建属性' }}
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, nextTick, computed } from 'vue';
+import {
+  Search, FolderOpened, Monitor, Plus, Check,
+  Edit, Delete, InfoFilled, Clock
+} from '@element-plus/icons-vue';
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus';
+
+// 属性卡片组件(使用slot)
+const PropertyCard = {
+  props: {
+    property: {
+      type: Object,
+      required: true,
+      default: () => ({})
+    }
+  },
+  emits: ['update-property', 'delete-property'],
+  template: `
+    <el-card class="property-card">
+      <!-- 卡片头部 -->
+      <template #header>
+        <div class="property-card-header">
+          <div class="property-name">
+            <span>{{ property.name }}</span>
+            <el-tag
+              v-if="property.required"
+              size="mini"
+              type="danger"
+              class="required-tag"
+            >
+              必填
+            </el-tag>
+          </div>
+          <div class="property-actions">
+            <el-tooltip content="编辑属性" placement="top">
+              <el-button
+                icon="Edit"
+                size="mini"
+                @click="$emit('update-property', { ...property })"
+                class="action-btn edit-btn"
+              />
+            </el-tooltip>
+            <el-tooltip content="删除属性" placement="top">
+              <el-button
+                icon="Delete"
+                size="mini"
+                @click="$emit('delete-property')"
+                class="action-btn delete-btn"
+              />
+            </el-tooltip>
+          </div>
+        </div>
+      </template>
+
+      <!-- 卡片内容区 -->
+      <div class="property-content">
+        <div class="property-info-item">
+          <span class="info-label">标识:</span>
+          <span class="info-value">{{ property.code }}</span>
+        </div>
+
+        <div class="property-info-item">
+          <span class="info-label">类型:</span>
+          <span class="info-value">
+            <el-tag size="small" :type="getTypeTagType(property.dataType)">
+              {{ getTypeName(property.dataType) }}
+            </el-tag>
+          </span>
+        </div>
+
+        <!-- 上下限配置区域 -->
+        <div v-if="property.dataType !== 'boolean'" class="property-limits">
+          <div class="limits-label">数值范围:</div>
+          <div class="limits-inputs">
+            <el-input-number
+              v-model.number="property.minValue"
+              :precision="property.dataType === 'float' ? 2 : 0"
+              :step="property.dataType === 'float' ? 0.1 : 1"
+              :placeholder="'最小'"
+              size="small"
+              class="limit-input min-input"
+              @change="handleLimitChange"
+            />
+            <span class="limit-separator">-</span>
+            <el-input-number
+              v-model.number="property.maxValue"
+              :precision="property.dataType === 'float' ? 2 : 0"
+              :step="property.dataType === 'float' ? 0.1 : 1"
+              :placeholder="'最大'"
+              size="small"
+              class="limit-input max-input"
+              :min="property.minValue"
+              @change="handleLimitChange"
+            />
+            <span v-if="property.unit" class="property-unit">{{ property.unit }}</span>
+          </div>
+        </div>
+
+        <!-- 布尔值特殊处理 -->
+        <div v-else class="boolean-value">
+          <span class="info-label">值:</span>
+          <el-switch
+            v-model="property.booleanValue"
+            active-text="真"
+            inactive-text="假"
+            @change="handleBooleanChange"
+          />
+        </div>
+
+        <!-- 描述信息 -->
+        <div v-if="property.description" class="property-description">
+          <el-icon class="description-icon"><InfoFilled /></el-icon>
+          <span>{{ property.description }}</span>
+        </div>
+      </div>
+
+      <!-- 卡片底部,包含默认内容和插槽 -->
+      <template #footer>
+        <div class="property-card-footer">
+          <span class="last-updated">
+            最后更新:{{ formatDate(property.updatedAt) }}
+          </span>
+          <div class="footer-actions">
+            <!-- 插槽:允许父组件添加额外的操作按钮 -->
+            <slot name="footer-actions"></slot>
+          </div>
+        </div>
+      </template>
+    </el-card>
+  `,
+  methods: {
+    // 获取数据类型显示名称
+    getTypeName(type) {
+      const types = {
+        'integer': '整数',
+        'float': '浮点数',
+        'boolean': '布尔值'
+      };
+      return types[type] || type;
+    },
+    // 获取数据类型标签样式
+    getTypeTagType(type) {
+      const types = {
+        'integer': 'primary',
+        'float': 'success',
+        'boolean': 'warning'
+      };
+      return types[type] || 'info';
+    },
+    // 处理上下限变化
+    handleLimitChange() {
+      this.$emit('update-property', { ...this.property });
+    },
+    // 处理布尔值变化
+    handleBooleanChange() {
+      this.$emit('update-property', { ...this.property });
+    },
+    // 格式化日期
+    formatDate(timestamp) {
+      if (!timestamp) return '未更新';
+      const date = new Date(timestamp);
+      return date.toLocaleString();
+    }
+  }
+};
+
+// 生成唯一ID
+const generateId = () => {
+  return Date.now().toString(36) + Math.random().toString(36).substr(2, 8);
+};
+
+// 设备树形结构数据
+const deviceTreeData = ref([
+  {
+    id: 'cat1',
+    label: '温度设备',
+    type: 'category',
+    propertyCount: 3,
+    children: [
+      {
+        id: 'dev11',
+        label: '室内温度计',
+        type: 'device',
+        propertyCount: 5
+      },
+      {
+        id: 'dev12',
+        label: '室外温度计',
+        type: 'device',
+        propertyCount: 4
+      }
+    ]
+  },
+  {
+    id: 'cat2',
+    label: '湿度设备',
+    type: 'category',
+    propertyCount: 2,
+    children: [
+      {
+        id: 'dev21',
+        label: '车间湿度计',
+        type: 'device',
+        propertyCount: 3
+      },
+      {
+        id: 'dev22',
+        label: '仓库湿度计',
+        type: 'device',
+        propertyCount: 3
+      }
+    ]
+  }
+]);
+
+// 树形结构配置
+const treeProps = {
+  children: 'children',
+  label: 'label'
+};
+
+// 状态管理
+const searchQuery = ref('');
+const deviceTree = ref(null);
+const selectedNode = ref(null);
+const properties = ref([]);
+const saveLoading = ref(false);
+const dialogLoading = ref(false);
+const propertyDialogVisible = ref(false);
+const isEditing = ref(false);
+const currentEditIndex = ref(-1);
+
+// 属性表单数据
+const currentProperty = reactive({
+  id: '',
+  name: '',
+  code: '',
+  dataType: 'float',
+  unit: '',
+  minValue: null,
+  maxValue: null,
+  booleanValue: false,
+  description: '',
+  required: false,
+  updatedAt: null
+});
+
+// 属性表单验证规则
+const propertyRules = {
+  name: [
+    { required: true, message: '请输入属性名称', trigger: 'blur' },
+    { max: 50, message: '属性名称不能超过50个字符', trigger: 'blur' }
+  ],
+  code: [
+    { required: true, message: '请输入属性标识', trigger: 'blur' },
+    { pattern: /^[a-zA-Z0-9_]+$/, message: '标识只能包含字母、数字和下划线', trigger: 'blur' },
+    { max: 30, message: '属性标识不能超过30个字符', trigger: 'blur' }
+  ],
+  dataType: [
+    { required: true, message: '请选择数据类型', trigger: 'change' }
+  ],
+  minValue: [
+    {
+      required: () => currentProperty.dataType !== 'boolean',
+      message: '请输入最小值',
+      trigger: 'blur'
+    },
+    {
+      type: 'number',
+      message: '请输入有效的数字',
+      trigger: 'blur',
+      validator: (rule, value, callback) => {
+        if (currentProperty.dataType === 'boolean') {
+          return callback();
+        }
+        if (value === null || value === undefined) {
+          return callback(new Error('请输入最小值'));
+        }
+        if (currentProperty.maxValue !== null && value > currentProperty.maxValue) {
+          return callback(new Error('最小值不能大于最大值'));
+        }
+        callback();
+      }
+    }
+  ],
+  maxValue: [
+    {
+      required: () => currentProperty.dataType !== 'boolean',
+      message: '请输入最大值',
+      trigger: 'blur'
+    },
+    {
+      type: 'number',
+      message: '请输入有效的数字',
+      trigger: 'blur',
+      validator: (rule, value, callback) => {
+        if (currentProperty.dataType === 'boolean') {
+          return callback();
+        }
+        if (value === null || value === undefined) {
+          return callback(new Error('请输入最大值'));
+        }
+        if (currentProperty.minValue !== null && value < currentProperty.minValue) {
+          return callback(new Error('最大值不能小于最小值'));
+        }
+        callback();
+      }
+    }
+  ]
+};
+
+// 过滤节点方法
+const filterNode = (value, data) => {
+  if (!value) return true;
+  return data.label.toLowerCase().includes(value.toLowerCase());
+};
+
+// 监听搜索关键词变化
+watch(searchQuery, (value) => {
+  deviceTree.value?.filter(value);
+});
+
+// 处理节点选择
+const handleNodeSelect = (data) => {
+  // 检查是否有未保存的修改
+  if (hasUnsavedChanges.value) {
+    ElMessageBox.confirm(
+      '当前有未保存的修改,切换设备/分类将丢失这些更改,是否继续?',
+      '确认切换',
+      {
+        confirmButtonText: '继续',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    ).then(() => {
+      loadNodeProperties(data);
+    }).catch(() => {
+      // 取消切换,恢复之前的选择
+      nextTick(() => {
+        deviceTree.value.setCurrentKey(selectedNode.value?.id);
+      });
+    });
+  } else {
+    loadNodeProperties(data);
+  }
+};
+
+// 加载节点属性
+const loadNodeProperties = (data) => {
+  selectedNode.value = data;
+  // 模拟API加载属性数据
+  saveLoading.value = true;
+
+  setTimeout(() => {
+    // 根据不同节点加载不同的属性示例数据
+    if (data.id === 'dev11') { // 室内温度计
+      properties.value = [
+        {
+          id: generateId(),
+          name: '测量温度',
+          code: 'measure_temp',
+          dataType: 'float',
+          unit: '℃',
+          minValue: -10,
+          maxValue: 50,
+          required: true,
+          description: '设备测量的环境温度',
+          updatedAt: Date.now() - 86400000
+        },
+        {
+          id: generateId(),
+          name: '测量精度',
+          code: 'measure_precision',
+          dataType: 'float',
+          unit: '℃',
+          minValue: 0,
+          maxValue: 0.5,
+          required: false,
+          description: '温度测量的允许误差范围',
+          updatedAt: Date.now() - 3600000
+        },
+        {
+          id: generateId(),
+          name: '采样频率',
+          code: 'sample_rate',
+          dataType: 'integer',
+          unit: '次/分钟',
+          minValue: 1,
+          maxValue: 60,
+          required: true,
+          updatedAt: Date.now()
+        }
+      ];
+    } else if (data.id === 'cat1') { // 温度设备分类
+      properties.value = [
+        {
+          id: generateId(),
+          name: '工作温度',
+          code: 'working_temp',
+          dataType: 'float',
+          unit: '℃',
+          minValue: -20,
+          maxValue: 70,
+          required: true,
+          description: '设备正常工作的环境温度范围',
+          updatedAt: Date.now() - 86400000 * 2
+        },
+        {
+          id: generateId(),
+          name: '存储温度',
+          code: 'storage_temp',
+          dataType: 'float',
+          unit: '℃',
+          minValue: -40,
+          maxValue: 85,
+          required: true,
+          description: '设备存储的环境温度范围',
+          updatedAt: Date.now() - 86400000
+        }
+      ];
+    } else {
+      // 其他节点的默认属性
+      properties.value = [];
+    }
+
+    saveLoading.value = false;
+    ElNotification({
+      title: '已加载',
+      message: `已加载 ${data.label} 的 ${properties.value.length} 个属性`,
+      duration: 1500,
+      position: 'bottom-right'
+    });
+  }, 600);
+};
+
+// 检查是否有未保存的更改
+const hasUnsavedChanges = ref(false);
+
+// 监听属性变化
+watch(properties, (newVal) => {
+  hasUnsavedChanges.value = true;
+}, { deep: true });
+
+// 显示添加属性对话框
+const showAddPropertyDialog = () => {
+  resetPropertyForm();
+  isEditing.value = false;
+  currentProperty.id = generateId();
+  propertyDialogVisible.value = true;
+
+  // 自动聚焦到第一个输入框
+  nextTick(() => {
+    const firstInput = document.querySelector('.property-dialog .el-input__inner');
+    if (firstInput) firstInput.focus();
+  });
+};
+
+// 显示编辑属性对话框
+const showEditPropertyDialog = (index, property) => {
+  resetPropertyForm();
+  isEditing.value = true;
+  currentEditIndex.value = index;
+
+  // 复制属性数据到表单
+  Object.assign(currentProperty, { ...property });
+
+  propertyDialogVisible.value = true;
+  nextTick(() => {
+    const firstInput = document.querySelector('.property-dialog .el-input__inner');
+    if (firstInput) firstInput.focus();
+  });
+};
+
+// 重置属性表单
+const resetPropertyForm = () => {
+  currentProperty.name = '';
+  currentProperty.code = '';
+  currentProperty.dataType = 'float';
+  currentProperty.unit = '';
+  currentProperty.minValue = null;
+  currentProperty.maxValue = null;
+  currentProperty.booleanValue = false;
+  currentProperty.description = '';
+  currentProperty.required = false;
+
+  // 重置表单验证
+  nextTick(() => {
+    propertyForm.value?.clearValidate();
+  });
+};
+
+// 处理数据类型变化
+const handleDataTypeChange = (type) => {
+  // 根据数据类型重置相关值
+  if (type === 'boolean') {
+    currentProperty.minValue = null;
+    currentProperty.maxValue = null;
+  } else if (type === 'integer') {
+    currentProperty.minValue = currentProperty.minValue !== null ? Math.round(currentProperty.minValue) : 0;
+    currentProperty.maxValue = currentProperty.maxValue !== null ? Math.round(currentProperty.maxValue) : 100;
+  }
+};
+
+// 确认属性操作(添加或编辑)
+const propertyForm = ref(null);
+const confirmPropertyAction = () => {
+  propertyForm.value.validate((valid) => {
+    if (valid) {
+      dialogLoading.value = true;
+
+      // 模拟API请求延迟
+      setTimeout(() => {
+        const newProperty = { ...currentProperty };
+        newProperty.updatedAt = Date.now();
+
+        if (isEditing.value) {
+          // 编辑现有属性
+          properties.value.splice(currentEditIndex.value, 1, newProperty);
+          ElMessage.success('属性已更新');
+        } else {
+          // 添加新属性
+          properties.value.push(newProperty);
+          // 更新节点的属性计数
+          if (selectedNode.value) {
+            selectedNode.value.propertyCount = (selectedNode.value.propertyCount || 0) + 1;
+          }
+          ElMessage.success('新属性已添加');
+        }
+
+        dialogLoading.value = false;
+        propertyDialogVisible.value = false;
+      }, 500);
+    }
+  });
+};
+
+// 处理属性更新
+const handlePropertyUpdate = (index, updatedProperty) => {
+  updatedProperty.updatedAt = Date.now();
+  properties.value.splice(index, 1, updatedProperty);
+};
+
+// 处理属性删除
+const handlePropertyDelete = (index) => {
+  const property = properties.value[index];
+  ElMessageBox.confirm(
+    `确定要删除属性"${property.name}"吗?此操作不可撤销。`,
+    '确认删除',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'danger'
+    }
+  ).then(() => {
+    properties.value.splice(index, 1);
+    // 更新节点的属性计数
+    if (selectedNode.value) {
+      selectedNode.value.propertyCount = Math.max(0, (selectedNode.value.propertyCount || 0) - 1);
+    }
+    ElMessage.success('属性已删除');
+  }).catch(() => {
+    // 取消删除
+  });
+};
+
+// 保存所有属性配置
+const saveAllProperties = () => {
+  if (properties.value.length === 0) {
+    ElMessage.warning('没有可保存的属性');
+    return;
+  }
+
+  saveLoading.value = true;
+
+  // 模拟API保存
+  setTimeout(() => {
+    saveLoading.value = false;
+    hasUnsavedChanges.value = false;
+
+    ElNotification({
+      title: '保存成功',
+      message: `已成功保存 ${properties.value.length} 个属性配置`,
+      type: 'success',
+      duration: 2000,
+      position: 'bottom-right'
+    });
+  }, 800);
+};
+
+// 返回操作
+const handleBack = () => {
+  if (hasUnsavedChanges.value) {
+    ElMessageBox.confirm(
+      '当前有未保存的修改,离开页面将丢失这些更改,是否继续?',
+      '确认离开',
+      {
+        confirmButtonText: '离开',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    ).then(() => {
+      // 实际应用中这里会执行路由跳转
+      ElMessage.info('已返回上一页');
+    });
+  } else {
+    // 实际应用中这里会执行路由跳转
+    ElMessage.info('已返回上一页');
+  }
+};
+
+// 页面加载时默认选中第一个设备
+watch(() => deviceTreeData.value.length, (length) => {
+  if (length > 0 && !selectedNode.value) {
+    // 尝试选择第一个设备节点
+    const firstDevice = deviceTreeData.value[0]?.children?.[0];
+    if (firstDevice) {
+      nextTick(() => {
+        handleNodeSelect(firstDevice);
+        deviceTree.value.setCurrentKey(firstDevice.id);
+      });
+    }
+  }
+}, { immediate: true });
+</script>
+
+<style scoped>
+/* 全局样式 */
+.property-manager-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: 100vh;
+}
+
+/* 页面头部 */
+.page-header {
+  margin-bottom: 24px;
+  --el-page-header-text-color: #1d2129;
+  --el-page-header-font-size: 20px;
+}
+
+.breadcrumb {
+  font-size: 13px;
+  color: #86909c;
+}
+
+/* 主内容区 */
+.main-content {
+  display: flex;
+  gap: 24px;
+  height: calc(100vh - 120px);
+}
+
+/* 左侧边栏 */
+.sidebar {
+  width: 320px;
+  flex-shrink: 0;
+}
+
+.sidebar-card {
+  height: 100%;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  border-radius: 8px;
+  overflow: hidden;
+  border: none;
+  display: flex;
+  flex-direction: column;
+}
+
+.sidebar-header {
+  padding: 16px;
+  background-color: #f7f8fa;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.sidebar-title {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  font-weight: 500;
+  color: #1d2129;
+}
+
+.search-input {
+  width: 100%;
+  --el-input-bg-color: #fff;
+}
+
+.search-icon {
+  color: #86909c;
+}
+
+.device-tree {
+  flex: 1;
+  overflow-y: auto;
+  padding: 8px 0;
+  --el-tree-node-content-hover-bg-color: #f5f7fa;
+}
+
+.tree-node {
+  display: flex;
+  align-items: center;
+  padding: 4px 8px;
+}
+
+.category-icon {
+  color: #409eff;
+  margin-right: 8px;
+  font-size: 16px;
+}
+
+.device-icon {
+  color: #00b42a;
+  margin-right: 8px;
+  font-size: 16px;
+}
+
+.node-label {
+  font-size: 14px;
+  flex: 1;
+  transition: color 0.2s;
+}
+
+.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content .node-label {
+  color: #409eff;
+  font-weight: 500;
+}
+
+.property-count-badge {
+  background-color: #f2f3f5;
+  color: #86909c;
+  --el-badge-font-size: 12px;
+}
+
+/* 右侧属性配置区 */
+.property-config-area {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+/* 选中项信息栏 */
+.selected-info-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  margin-bottom: 20px;
+}
+
+.selected-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.selected-name {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 500;
+  color: #1d2129;
+}
+
+.node-type-tag {
+  margin-left: 8px;
+}
+
+.action-buttons {
+  display: flex;
+  gap: 12px;
+}
+
+.add-property-btn {
+  transition: all 0.2s;
+}
+
+.add-property-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+}
+
+/* 属性卡片网格 */
+.properties-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
+  gap: 20px;
+  padding: 4px;
+  overflow-y: auto;
+  flex: 1;
+}
+
+.property-card-item {
+  transition: all 0.3s ease;
+}
+
+.property-card-item:hover {
+  transform: translateY(-4px);
+}
+
+/* 属性卡片样式 */
+.property-card {
+  height: 100%;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
+  transition: all 0.2s;
+  border: none;
+  overflow: hidden;
+}
+
+.property-card:hover {
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
+}
+
+.property-card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  background-color: #f7f8fa;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.property-name {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-weight: 500;
+  color: #1d2129;
+}
+
+.required-tag {
+  font-size: 12px;
+  padding: 0 4px;
+  height: 18px;
+  line-height: 18px;
+}
+
+.property-actions {
+  display: flex;
+  gap: 4px;
+}
+
+.action-btn {
+  padding: 0 4px;
+  height: 24px;
+  border-radius: 4px;
+  transition: all 0.2s;
+}
+
+.edit-btn {
+  color: #409eff;
+  background-color: #ecf5ff;
+}
+
+.edit-btn:hover {
+  background-color: #d9ecff;
+}
+
+.delete-btn {
+  color: #f56c6c;
+  background-color: #fef0f0;
+}
+
+.delete-btn:hover {
+  background-color: #fee4e4;
+}
+
+.property-content {
+  padding: 16px;
+}
+
+.property-info-item {
+  margin-bottom: 12px;
+  font-size: 14px;
+}
+
+.info-label {
+  color: #86909c;
+  display: inline-block;
+  width: 60px;
+}
+
+.info-value {
+  color: #1d2129;
+}
+
+.property-limits {
+  margin: 16px 0;
+  padding: 12px;
+  background-color: #f7f8fa;
+  border-radius: 6px;
+}
+
+.limits-label {
+  color: #86909c;
+  margin-bottom: 8px;
+  font-size: 14px;
+}
+
+.limits-inputs {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.limit-input {
+  flex: 1;
+  --el-input-number-input-height: 32px;
+}
+
+.min-input {
+  --el-input-number-bg-color: #f0f7ff;
+}
+
+.max-input {
+  --el-input-number-bg-color: #f0fff4;
+}
+
+.limit-separator {
+  color: #86909c;
+  font-weight: 500;
+}
+
+.property-unit {
+  color: #86909c;
+  white-space: nowrap;
+  padding-left: 4px;
+}
+
+.boolean-value {
+  margin: 16px 0;
+  padding: 12px;
+  background-color: #f7f8fa;
+  border-radius: 6px;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.property-description {
+  margin-top: 12px;
+  padding: 8px 12px;
+  background-color: #f0f7ff;
+  border-radius: 4px;
+  font-size: 13px;
+  color: #4e5969;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.description-icon {
+  color: #409eff;
+  font-size: 14px;
+}
+
+.property-card-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 16px;
+  background-color: #f7f8fa;
+  border-top: 1px solid #f0f0f0;
+  font-size: 12px;
+  color: #86909c;
+}
+
+.last-updated {
+  flex: 1;
+}
+
+.footer-actions {
+  display: flex;
+  gap: 4px;
+}
+
+.history-btn {
+  color: #86909c;
+  background-color: #f2f3f5;
+  padding: 0 4px;
+  height: 24px;
+}
+
+/* 空状态样式 */
+.empty-state {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.empty-image-container {
+  width: 120px;
+  height: 120px;
+  border-radius: 50%;
+  background-color: #f0f7ff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 16px;
+}
+
+.empty-icon {
+  font-size: 60px;
+  color: #409eff;
+}
+
+.no-properties-state {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  padding: 40px;
+}
+
+.no-properties-image {
+  width: 100px;
+  height: 100px;
+  margin-bottom: 16px;
+}
+
+.no-properties-icon {
+  font-size: 100px;
+  color: #c9cdD4;
+}
+
+/* 对话框样式 */
+.property-dialog {
+  --el-dialog-border-radius: 8px;
+}
+
+.property-form {
+  margin-top: 12px;
+}
+
+.form-hint {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #86909c;
+  line-height: 1.4;
+}
+
+.limit-inputs-row {
+  margin-bottom: 8px;
+}
+
+/* 滚动条美化 */
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+/* 动画效果 */
+.el-collapse-transition {
+  transition: height 0.3s ease, opacity 0.3s ease;
+}
+
+/* 响应式调整 */
+@media (max-width: 1200px) {
+  .properties-grid {
+    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+  }
+}
+
+@media (max-width: 992px) {
+  .main-content {
+    flex-direction: column;
+    height: auto;
+  }
+
+  .sidebar {
+    width: 100%;
+    height: 300px;
+  }
+
+  .properties-grid {
+    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  }
+}
+</style>