From ce3e57e3989d96d56acbbc2d8d95b56548315a65 Mon Sep 17 00:00:00 2001 From: lzh Date: Tue, 3 Feb 2026 22:43:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(@vben/web-antd):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=E8=A7=86?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建区域管理页面目录结构 - 支持区域层级管理(建筑/楼层/功能区) - 设备绑定抽屉:支持区域内IoT设备管理 - 区域表单:基础版本和增强版本 - 设备配置模态框和选择模态框 - 修复ESLint警告:使用严格相等运算符 Co-Authored-By: Claude Sonnet 4.5 --- .../src/views/ops/area/data-enhanced.ts | 632 ++++++++++++++++++ apps/web-antd/src/views/ops/area/data.ts | 458 +++++++++++++ .../src/views/ops/area/index-enhanced.vue | 296 ++++++++ apps/web-antd/src/views/ops/area/index.vue | 223 ++++++ .../modules/device-bind-drawer-enhanced.vue | 549 +++++++++++++++ .../ops/area/modules/device-bind-drawer.vue | 219 ++++++ .../ops/area/modules/device-config-modal.vue | 90 +++ .../ops/area/modules/device-select-modal.vue | 240 +++++++ .../views/ops/area/modules/form-enhanced.vue | 286 ++++++++ .../src/views/ops/area/modules/form.vue | 99 +++ 10 files changed, 3092 insertions(+) create mode 100644 apps/web-antd/src/views/ops/area/data-enhanced.ts create mode 100644 apps/web-antd/src/views/ops/area/data.ts create mode 100644 apps/web-antd/src/views/ops/area/index-enhanced.vue create mode 100644 apps/web-antd/src/views/ops/area/index.vue create mode 100644 apps/web-antd/src/views/ops/area/modules/device-bind-drawer-enhanced.vue create mode 100644 apps/web-antd/src/views/ops/area/modules/device-bind-drawer.vue create mode 100644 apps/web-antd/src/views/ops/area/modules/device-config-modal.vue create mode 100644 apps/web-antd/src/views/ops/area/modules/device-select-modal.vue create mode 100644 apps/web-antd/src/views/ops/area/modules/form-enhanced.vue create mode 100644 apps/web-antd/src/views/ops/area/modules/form.vue diff --git a/apps/web-antd/src/views/ops/area/data-enhanced.ts b/apps/web-antd/src/views/ops/area/data-enhanced.ts new file mode 100644 index 000000000..fc6a65cd0 --- /dev/null +++ b/apps/web-antd/src/views/ops/area/data-enhanced.ts @@ -0,0 +1,632 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { + AreaLevel, + AreaType, + FunctionType, + OpsAreaApi, + RelationType, +} from '#/api/ops/area'; + +import { h } from 'vue'; + +import { handleTree } from '@vben/utils'; + +import { Badge, Tag } from 'ant-design-vue'; + +import { z } from '#/adapter/form'; +import { getSimpleDeviceList } from '#/api/iot/device/device'; +import { getAreaTree } from '#/api/ops/area'; + +/** 区域类型选项 */ +export const AREA_TYPE_OPTIONS: { + color: string; + icon: string; + label: string; + value: AreaType; +}[] = [ + { label: '园区', value: 'PARK', color: 'blue', icon: '🏢' }, + { label: '楼栋', value: 'BUILDING', color: 'cyan', icon: '🏛️' }, + { label: '楼层', value: 'FLOOR', color: 'geekblue', icon: '📐' }, + { label: '功能区域', value: 'FUNCTION', color: 'purple', icon: '🎯' }, +]; + +/** 功能类型选项 */ +export const FUNCTION_TYPE_OPTIONS: { + color: string; + icon: string; + label: string; + value: FunctionType; +}[] = [ + { label: '男厕', value: 'MALE_TOILET', color: 'blue', icon: '🚹' }, + { label: '女厕', value: 'FEMALE_TOILET', color: 'magenta', icon: '🚺' }, + { label: '公共区域', value: 'PUBLIC', color: 'green', icon: '🌍' }, + { label: '电梯', value: 'ELEVATOR', color: 'orange', icon: '🛗' }, +]; + +/** 区域等级选项 */ +export const AREA_LEVEL_OPTIONS: { + color: string; + label: string; + value: AreaLevel; +}[] = [ + { label: '高', value: 'HIGH', color: 'red' }, + { label: '中', value: 'MEDIUM', color: 'orange' }, + { label: '低', value: 'LOW', color: 'green' }, +]; + +/** 关联类型选项 */ +export const RELATION_TYPE_OPTIONS: { + color: string; + icon: string; + label: string; + value: RelationType; +}[] = [ + { label: '客流计数', value: 'TRAFFIC_COUNTER', color: 'blue', icon: '👥' }, + { label: '信标', value: 'BEACON', color: 'purple', icon: '📡' }, + { label: '工牌', value: 'BADGE', color: 'orange', icon: '🎫' }, +]; + +/** 启用状态选项(筛选用) */ +export const ACTIVE_OPTIONS = [ + { label: '全部', value: undefined }, + { label: '启用', value: true }, + { label: '停用', value: false }, +]; + +/** 列表搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '区域名称/编码', + component: 'Input', + componentProps: { + placeholder: '请输入区域名称或编码', + allowClear: true, + }, + }, + { + fieldName: 'areaType', + label: '区域类型', + component: 'Select', + componentProps: { + placeholder: '请选择区域类型', + allowClear: true, + options: AREA_TYPE_OPTIONS, + }, + }, + { + fieldName: 'isActive', + label: '启用状态', + component: 'Select', + componentProps: { + placeholder: '请选择', + allowClear: true, + options: [ + { label: '启用', value: true }, + { label: '停用', value: false }, + ], + }, + }, + ]; +} + +/** 区域新增/编辑表单 schema */ +export function useAreaFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { triggerFields: [''], show: () => false }, + }, + { + fieldName: 'parentId', + label: '上级区域', + component: 'ApiTreeSelect', + componentProps: { + allowClear: true, + api: async () => { + const data = await getAreaTree(); + const list = ( + Array.isArray(data) ? data : ((data as any)?.list ?? []) + ) as any[]; + list.unshift({ + id: 0, + areaName: '根节点', + parentId: undefined, + }); + return handleTree(list, 'id', 'parentId'); + }, + labelField: 'areaName', + valueField: 'id', + childrenField: 'children', + placeholder: '请选择上级区域', + treeDefaultExpandAll: true, + }, + dependencies: { + triggerFields: ['id', 'parentId'], + disabled: (values) => + !!( + values?.parentId !== null && + values?.parentId !== 0 && + !values?.id + ), + }, + rules: 'selectRequired', + }, + { + fieldName: 'areaName', + label: '区域名称', + component: 'Input', + componentProps: { placeholder: '请输入区域名称' }, + rules: 'required', + }, + { + fieldName: 'areaCode', + label: '区域编码', + component: 'Input', + componentProps: { placeholder: '请输入区域编码' }, + }, + { + fieldName: 'areaType', + label: '区域类型', + component: 'Select', + componentProps: { + placeholder: '请选择区域类型', + options: AREA_TYPE_OPTIONS, + }, + rules: 'selectRequired', + }, + { + fieldName: 'functionType', + label: '功能类型', + component: 'Select', + componentProps: { + placeholder: '请选择功能类型', + allowClear: true, + options: FUNCTION_TYPE_OPTIONS, + }, + }, + { + fieldName: 'floorNo', + label: '楼层号', + component: 'InputNumber', + componentProps: { + min: -10, + placeholder: '请输入楼层号', + class: 'w-full', + }, + }, + { + fieldName: 'cleaningFrequency', + label: '保洁频率', + component: 'Input', + componentProps: { placeholder: '如:每日 2 次' }, + }, + { + fieldName: 'standardDuration', + label: '标准时长(分钟)', + component: 'InputNumber', + componentProps: { + min: 0, + placeholder: '请输入标准时长', + class: 'w-full', + }, + }, + { + fieldName: 'areaLevel', + label: '区域等级', + component: 'Select', + componentProps: { + placeholder: '请选择区域等级', + allowClear: true, + options: AREA_LEVEL_OPTIONS, + }, + }, + { + fieldName: 'isActive', + label: '启用状态', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '启用', value: true }, + { label: '停用', value: false }, + ], + optionType: 'button', + buttonStyle: 'solid', + }, + rules: z.boolean().default(true), + }, + { + fieldName: 'sort', + label: '排序', + component: 'InputNumber', + componentProps: { + min: 0, + placeholder: '请输入排序', + class: 'w-full', + }, + rules: z.number().min(0).default(0), + }, + ]; +} + +/** 区域列表列 - 增强版 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'areaName', + title: '区域名称', + minWidth: 200, + align: 'left', + fixed: 'left', + treeNode: true, + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + return h('div', { class: 'flex items-center gap-2' }, [ + h( + 'span', + { class: 'font-medium text-gray-900 dark:text-white' }, + row.areaName, + ), + ]); + }, + }, + }, + { + field: 'areaCode', + title: '区域编码', + minWidth: 120, + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + return row.areaCode + ? h( + 'code', + { + class: + 'px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 rounded', + }, + row.areaCode, + ) + : h('span', { class: 'text-gray-400' }, '-'); + }, + }, + }, + { + field: 'areaType', + title: '区域类型', + minWidth: 120, + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + const option = AREA_TYPE_OPTIONS.find( + (o) => o.value === row.areaType, + ); + return option + ? h( + Tag, + { color: option.color }, + () => `${option.icon} ${option.label}`, + ) + : h('span', row.areaType || '-'); + }, + }, + }, + { + field: 'functionType', + title: '功能类型', + minWidth: 120, + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + const option = FUNCTION_TYPE_OPTIONS.find( + (o) => o.value === row.functionType, + ); + return option + ? h( + Tag, + { color: option.color }, + () => `${option.icon} ${option.label}`, + ) + : h('span', { class: 'text-gray-400' }, '-'); + }, + }, + }, + { + field: 'floorNo', + title: '楼层号', + minWidth: 88, + align: 'center', + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + return row.floorNo === null + ? h('span', { class: 'text-gray-400' }, '-') + : h( + 'span', + { + class: + 'inline-flex items-center justify-center px-2 py-1 bg-blue-50 dark:bg-blue-950/30 text-blue-700 dark:text-blue-300 rounded-md font-medium', + }, + `${row.floorNo}F`, + ); + }, + }, + }, + { + field: 'cleaningFrequency', + title: '保洁频率', + minWidth: 120, + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + return row.cleaningFrequency + ? h( + 'span', + { class: 'text-gray-700 dark:text-gray-300' }, + row.cleaningFrequency, + ) + : h('span', { class: 'text-gray-400' }, '-'); + }, + }, + }, + { + field: 'standardDuration', + title: '标准时长', + minWidth: 110, + align: 'center', + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + return row.standardDuration === null + ? h('span', { class: 'text-gray-400' }, '-') + : h( + 'span', + { + class: + 'inline-flex items-center gap-1 px-2 py-1 bg-purple-50 dark:bg-purple-950/30 text-purple-700 dark:text-purple-300 rounded-md', + }, + [ + h( + 'svg', + { + xmlns: 'http://www.w3.org/2000/svg', + class: 'h-4 w-4', + fill: 'none', + viewBox: '0 0 24 24', + stroke: 'currentColor', + }, + [ + h('path', { + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + 'stroke-width': '2', + d: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', + }), + ], + ), + h('span', `${row.standardDuration}分钟`), + ], + ); + }, + }, + }, + { + field: 'areaLevel', + title: '区域等级', + minWidth: 100, + align: 'center', + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + const option = AREA_LEVEL_OPTIONS.find( + (o) => o.value === row.areaLevel, + ); + return option + ? h( + Badge, + { + status: + option.color === 'red' ? 'error' : ( + option.color === 'orange' + ? 'warning' + : 'success' + ), + text: option.label, + }, + ) + : h('span', { class: 'text-gray-400' }, '-'); + }, + }, + }, + { + field: 'isActive', + title: '状态', + minWidth: 90, + align: 'center', + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + return h(Badge, { + status: row.isActive ? 'success' : 'error', + text: row.isActive ? '启用' : '停用', + }); + }, + }, + }, + { + field: 'sort', + title: '排序', + minWidth: 72, + align: 'center', + slots: { + default: ({ row }: { row: OpsAreaApi.BusArea }) => { + return h( + 'span', + { class: 'text-gray-600 dark:text-gray-400 font-mono' }, + row.sort ?? 0, + ); + }, + }, + }, + { + title: '操作', + width: 360, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 绑定设备表单 schema */ +export function useBindFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'deviceId', + label: '设备', + component: 'ApiSelect', + componentProps: { + api: () => getSimpleDeviceList(), + labelField: 'nickname', + valueField: 'id', + placeholder: '请选择设备', + }, + rules: 'selectRequired', + }, + { + fieldName: 'relationType', + label: '关联类型', + component: 'Select', + componentProps: { + placeholder: '请选择关联类型', + options: RELATION_TYPE_OPTIONS, + }, + rules: 'selectRequired', + }, + ]; +} + +/** 已绑定设备列表列 */ +export function useDeviceRelationColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'deviceName', + title: '设备名称', + minWidth: 140, + formatter: ({ row }) => row?.deviceName || row?.deviceKey || '-', + }, + { field: 'deviceKey', title: '设备 Key', minWidth: 140 }, + { + field: 'productName', + title: '产品', + minWidth: 120, + formatter: ({ cellValue, row }) => cellValue || row?.productKey || '-', + }, + { + field: 'relationType', + title: '关联类型', + minWidth: 100, + formatter: ({ cellValue }) => + RELATION_TYPE_OPTIONS.find((o) => o.value === cellValue)?.label ?? + cellValue ?? + '-', + }, + { + field: 'enabled', + title: '状态', + minWidth: 84, + formatter: ({ cellValue }) => (cellValue ? '启用' : '停用'), + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'device-actions' }, + }, + ]; +} + +/** 按 relation_type 获取保洁配置表单 schema */ +export function useDeviceConfigFormSchema( + relationType: RelationType, +): VbenFormSchema[] { + switch (relationType) { + case 'BADGE': { + return [ + { + fieldName: 'notifyType', + label: '通知方式', + component: 'Select', + componentProps: { + options: [ + { label: '震动', value: 'VIBRATE' }, + { label: '语音', value: 'VOICE' }, + ], + }, + }, + { + fieldName: 'voiceTemplate', + label: '语音内容模板', + component: 'Input', + componentProps: { placeholder: '语音播报内容模板' }, + }, + { + fieldName: 'batteryAlertThreshold', + label: '电量告警阈值(%)', + component: 'InputNumber', + componentProps: { + min: 0, + max: 100, + placeholder: '低于该值告警', + class: 'w-full', + }, + }, + ]; + } + case 'BEACON': { + return [ + { + fieldName: 'rssiThreshold', + label: 'RSSI 阈值', + component: 'InputNumber', + componentProps: { placeholder: '如 -70', class: 'w-full' }, + }, + { + fieldName: 'attendanceRule', + label: '到岗判定规则', + component: 'Input', + componentProps: { placeholder: '简要描述到岗判定规则' }, + }, + ]; + } + case 'TRAFFIC_COUNTER': { + return [ + { + fieldName: 'trafficThreshold', + label: '客流阈值', + component: 'InputNumber', + componentProps: { + min: 1, + placeholder: '超过该人数触发', + class: 'w-full', + }, + }, + { + fieldName: 'statPeriodMinutes', + label: '统计周期(分钟)', + component: 'InputNumber', + componentProps: { min: 1, placeholder: '统计周期', class: 'w-full' }, + }, + { + fieldName: 'triggerAction', + label: '触发动作', + component: 'Select', + componentProps: { + placeholder: '请选择', + options: [ + { label: '生成工单', value: 'CREATE_ORDER' }, + { label: '仅告警', value: 'ALERT_ONLY' }, + ], + }, + }, + ]; + } + default: { + return []; + } + } +} diff --git a/apps/web-antd/src/views/ops/area/data.ts b/apps/web-antd/src/views/ops/area/data.ts new file mode 100644 index 000000000..72e1b9b8e --- /dev/null +++ b/apps/web-antd/src/views/ops/area/data.ts @@ -0,0 +1,458 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { + AreaLevel, + AreaType, + FunctionType, + OpsAreaApi, + RelationType, +} from '#/api/ops/area'; + +import { handleTree } from '@vben/utils'; + +import { z } from '#/adapter/form'; +import { getSimpleDeviceList } from '#/api/iot/device/device'; +import { getAreaTree } from '#/api/ops/area'; + +/** 区域类型选项 */ +export const AREA_TYPE_OPTIONS: { label: string; value: AreaType }[] = [ + { label: '园区', value: 'PARK' }, + { label: '楼栋', value: 'BUILDING' }, + { label: '楼层', value: 'FLOOR' }, + { label: '功能区域', value: 'FUNCTION' }, +]; + +/** 功能类型选项 */ +export const FUNCTION_TYPE_OPTIONS: { label: string; value: FunctionType }[] = [ + { label: '男厕', value: 'MALE_TOILET' }, + { label: '女厕', value: 'FEMALE_TOILET' }, + { label: '公共区域', value: 'PUBLIC' }, + { label: '电梯', value: 'ELEVATOR' }, +]; + +/** 区域等级选项 */ +export const AREA_LEVEL_OPTIONS: { label: string; value: AreaLevel }[] = [ + { label: '高', value: 'HIGH' }, + { label: '中', value: 'MEDIUM' }, + { label: '低', value: 'LOW' }, +]; + +/** 关联类型选项 */ +export const RELATION_TYPE_OPTIONS: { label: string; value: RelationType }[] = [ + { label: '客流计数', value: 'TRAFFIC_COUNTER' }, + { label: '信标', value: 'BEACON' }, + { label: '工牌', value: 'BADGE' }, +]; + +/** 启用状态选项(筛选用) */ +export const ACTIVE_OPTIONS = [ + { label: '全部', value: undefined }, + { label: '启用', value: true }, + { label: '停用', value: false }, +]; + +/** 列表搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '区域名称/编码', + component: 'Input', + componentProps: { + placeholder: '请输入区域名称或编码', + allowClear: true, + }, + }, + { + fieldName: 'areaType', + label: '区域类型', + component: 'Select', + componentProps: { + placeholder: '请选择区域类型', + allowClear: true, + options: AREA_TYPE_OPTIONS, + }, + }, + { + fieldName: 'isActive', + label: '启用状态', + component: 'Select', + componentProps: { + placeholder: '请选择', + allowClear: true, + options: [ + { label: '启用', value: true }, + { label: '停用', value: false }, + ], + }, + }, + ]; +} + +/** 区域新增/编辑表单 schema */ +export function useAreaFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { triggerFields: [''], show: () => false }, + }, + { + fieldName: 'parentId', + label: '上级区域', + component: 'ApiTreeSelect', + componentProps: { + allowClear: true, + api: async () => { + const data = await getAreaTree(); + const list = ( + Array.isArray(data) ? data : ((data as any)?.list ?? []) + ) as any[]; + list.unshift({ + id: 0, + areaName: '根节点', + parentId: undefined, + }); + return handleTree(list, 'id', 'parentId'); + }, + labelField: 'areaName', + valueField: 'id', + childrenField: 'children', + placeholder: '请选择上级区域', + treeDefaultExpandAll: true, + }, + dependencies: { + triggerFields: ['id', 'parentId'], + disabled: (values) => + !!( + values?.parentId !== null && + values?.parentId !== 0 && + !values?.id + ), + }, + rules: 'selectRequired', + }, + { + fieldName: 'areaName', + label: '区域名称', + component: 'Input', + componentProps: { placeholder: '请输入区域名称' }, + rules: 'required', + }, + { + fieldName: 'areaCode', + label: '区域编码', + component: 'Input', + componentProps: { placeholder: '请输入区域编码' }, + }, + { + fieldName: 'areaType', + label: '区域类型', + component: 'Select', + componentProps: { + placeholder: '请选择区域类型', + options: AREA_TYPE_OPTIONS, + }, + rules: 'selectRequired', + }, + { + fieldName: 'functionType', + label: '功能类型', + component: 'Select', + componentProps: { + placeholder: '请选择功能类型', + allowClear: true, + options: FUNCTION_TYPE_OPTIONS, + }, + }, + { + fieldName: 'floorNo', + label: '楼层号', + component: 'InputNumber', + componentProps: { + min: -10, + placeholder: '请输入楼层号', + class: 'w-full', + }, + }, + { + fieldName: 'cleaningFrequency', + label: '保洁频率', + component: 'Input', + componentProps: { placeholder: '如:每日 2 次' }, + }, + { + fieldName: 'standardDuration', + label: '标准时长(分钟)', + component: 'InputNumber', + componentProps: { + min: 0, + placeholder: '请输入标准时长', + class: 'w-full', + }, + }, + { + fieldName: 'areaLevel', + label: '区域等级', + component: 'Select', + componentProps: { + placeholder: '请选择区域等级', + allowClear: true, + options: AREA_LEVEL_OPTIONS, + }, + }, + { + fieldName: 'isActive', + label: '启用状态', + component: 'RadioGroup', + componentProps: { + options: [ + { label: '启用', value: true }, + { label: '停用', value: false }, + ], + optionType: 'button', + buttonStyle: 'solid', + }, + rules: z.boolean().default(true), + }, + { + fieldName: 'sort', + label: '排序', + component: 'InputNumber', + componentProps: { + min: 0, + placeholder: '请输入排序', + class: 'w-full', + }, + rules: z.number().min(0).default(0), + }, + ]; +} + +/** 区域列表列 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'areaName', + title: '区域名称', + minWidth: 160, + align: 'left', + fixed: 'left', + treeNode: true, + }, + { field: 'areaCode', title: '区域编码', minWidth: 120 }, + { + field: 'areaType', + title: '区域类型', + minWidth: 100, + formatter: ({ cellValue }) => + AREA_TYPE_OPTIONS.find((o) => o.value === cellValue)?.label ?? + cellValue ?? + '-', + }, + { + field: 'functionType', + title: '功能类型', + minWidth: 100, + formatter: ({ cellValue }) => + FUNCTION_TYPE_OPTIONS.find((o) => o.value === cellValue)?.label ?? + cellValue ?? + '-', + }, + { + field: 'floorNo', + title: '楼层号', + minWidth: 88, + formatter: ({ cellValue }) => cellValue ?? '-', + }, + { field: 'cleaningFrequency', title: '保洁频率', minWidth: 110 }, + { + field: 'standardDuration', + title: '标准时长', + minWidth: 96, + formatter: ({ cellValue }) => + cellValue === null ? '-' : `${cellValue} 分钟`, + }, + { + field: 'areaLevel', + title: '区域等级', + minWidth: 90, + formatter: ({ cellValue }) => + AREA_LEVEL_OPTIONS.find((o) => o.value === cellValue)?.label ?? + cellValue ?? + '-', + }, + { + field: 'isActive', + title: '状态', + minWidth: 84, + formatter: ({ cellValue }) => (cellValue ? '启用' : '停用'), + }, + { field: 'sort', title: '排序', minWidth: 72 }, + { + title: '操作', + width: 360, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 绑定设备表单 schema */ +export function useBindFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'deviceId', + label: '设备', + component: 'ApiSelect', + componentProps: { + api: () => getSimpleDeviceList(), + labelField: 'nickname', + valueField: 'id', + placeholder: '请选择设备', + }, + rules: 'selectRequired', + }, + { + fieldName: 'relationType', + label: '关联类型', + component: 'Select', + componentProps: { + placeholder: '请选择关联类型', + options: RELATION_TYPE_OPTIONS, + }, + rules: 'selectRequired', + }, + ]; +} + +/** 已绑定设备列表列 */ +export function useDeviceRelationColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'deviceName', + title: '设备名称', + minWidth: 140, + formatter: ({ row }) => row?.deviceName || row?.deviceKey || '-', + }, + { field: 'deviceKey', title: '设备 Key', minWidth: 140 }, + { + field: 'productName', + title: '产品', + minWidth: 120, + formatter: ({ cellValue, row }) => cellValue || row?.productKey || '-', + }, + { + field: 'relationType', + title: '关联类型', + minWidth: 100, + formatter: ({ cellValue }) => + RELATION_TYPE_OPTIONS.find((o) => o.value === cellValue)?.label ?? + cellValue ?? + '-', + }, + { + field: 'enabled', + title: '状态', + minWidth: 84, + formatter: ({ cellValue }) => (cellValue ? '启用' : '停用'), + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'device-actions' }, + }, + ]; +} + +/** 按 relation_type 获取保洁配置表单 schema */ +export function useDeviceConfigFormSchema( + relationType: RelationType, +): VbenFormSchema[] { + switch (relationType) { + case 'BADGE': { + return [ + { + fieldName: 'notifyType', + label: '通知方式', + component: 'Select', + componentProps: { + options: [ + { label: '震动', value: 'VIBRATE' }, + { label: '语音', value: 'VOICE' }, + ], + }, + }, + { + fieldName: 'voiceTemplate', + label: '语音内容模板', + component: 'Input', + componentProps: { placeholder: '语音播报内容模板' }, + }, + { + fieldName: 'batteryAlertThreshold', + label: '电量告警阈值(%)', + component: 'InputNumber', + componentProps: { + min: 0, + max: 100, + placeholder: '低于该值告警', + class: 'w-full', + }, + }, + ]; + } + case 'BEACON': { + return [ + { + fieldName: 'rssiThreshold', + label: 'RSSI 阈值', + component: 'InputNumber', + componentProps: { placeholder: '如 -70', class: 'w-full' }, + }, + { + fieldName: 'attendanceRule', + label: '到岗判定规则', + component: 'Input', + componentProps: { placeholder: '简要描述到岗判定规则' }, + }, + ]; + } + case 'TRAFFIC_COUNTER': { + return [ + { + fieldName: 'trafficThreshold', + label: '客流阈值', + component: 'InputNumber', + componentProps: { + min: 1, + placeholder: '超过该人数触发', + class: 'w-full', + }, + }, + { + fieldName: 'statPeriodMinutes', + label: '统计周期(分钟)', + component: 'InputNumber', + componentProps: { min: 1, placeholder: '统计周期', class: 'w-full' }, + }, + { + fieldName: 'triggerAction', + label: '触发动作', + component: 'Select', + componentProps: { + placeholder: '请选择', + options: [ + { label: '生成工单', value: 'CREATE_ORDER' }, + { label: '仅告警', value: 'ALERT_ONLY' }, + ], + }, + }, + ]; + } + default: { + return []; + } + } +} diff --git a/apps/web-antd/src/views/ops/area/index-enhanced.vue b/apps/web-antd/src/views/ops/area/index-enhanced.vue new file mode 100644 index 000000000..31c0b73c3 --- /dev/null +++ b/apps/web-antd/src/views/ops/area/index-enhanced.vue @@ -0,0 +1,296 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/area/index.vue b/apps/web-antd/src/views/ops/area/index.vue new file mode 100644 index 000000000..669de5a4e --- /dev/null +++ b/apps/web-antd/src/views/ops/area/index.vue @@ -0,0 +1,223 @@ + + + diff --git a/apps/web-antd/src/views/ops/area/modules/device-bind-drawer-enhanced.vue b/apps/web-antd/src/views/ops/area/modules/device-bind-drawer-enhanced.vue new file mode 100644 index 000000000..77b892ebe --- /dev/null +++ b/apps/web-antd/src/views/ops/area/modules/device-bind-drawer-enhanced.vue @@ -0,0 +1,549 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/area/modules/device-bind-drawer.vue b/apps/web-antd/src/views/ops/area/modules/device-bind-drawer.vue new file mode 100644 index 000000000..e97ca2e9c --- /dev/null +++ b/apps/web-antd/src/views/ops/area/modules/device-bind-drawer.vue @@ -0,0 +1,219 @@ + + + diff --git a/apps/web-antd/src/views/ops/area/modules/device-config-modal.vue b/apps/web-antd/src/views/ops/area/modules/device-config-modal.vue new file mode 100644 index 000000000..3c4a4bebb --- /dev/null +++ b/apps/web-antd/src/views/ops/area/modules/device-config-modal.vue @@ -0,0 +1,90 @@ + + + diff --git a/apps/web-antd/src/views/ops/area/modules/device-select-modal.vue b/apps/web-antd/src/views/ops/area/modules/device-select-modal.vue new file mode 100644 index 000000000..c83578cdc --- /dev/null +++ b/apps/web-antd/src/views/ops/area/modules/device-select-modal.vue @@ -0,0 +1,240 @@ + + + diff --git a/apps/web-antd/src/views/ops/area/modules/form-enhanced.vue b/apps/web-antd/src/views/ops/area/modules/form-enhanced.vue new file mode 100644 index 000000000..38b32409e --- /dev/null +++ b/apps/web-antd/src/views/ops/area/modules/form-enhanced.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/apps/web-antd/src/views/ops/area/modules/form.vue b/apps/web-antd/src/views/ops/area/modules/form.vue new file mode 100644 index 000000000..95bf2dbe0 --- /dev/null +++ b/apps/web-antd/src/views/ops/area/modules/form.vue @@ -0,0 +1,99 @@ + + +