feat(iot): 优化 antd 里的整体代码风格。

pull/345/head
YunaiV 2026-05-20 00:41:06 +08:00
parent 1bdc0d992f
commit e7a61ce150
52 changed files with 920 additions and 159 deletions

View File

@ -16,20 +16,14 @@ export namespace RuleSceneApi {
/** 场景联动规则的触发器 */
export interface Trigger {
type?: string;
type?: number;
productId?: number;
deviceId?: number;
identifier?: string;
operator?: string;
value?: any;
cronExpression?: string;
conditionGroups?: TriggerConditionGroup[];
}
/** 场景联动规则的触发条件组 */
export interface TriggerConditionGroup {
conditions?: TriggerCondition[];
operator?: string;
conditionGroups?: TriggerCondition[][];
}
/** 场景联动规则的触发条件 */
@ -39,17 +33,19 @@ export namespace RuleSceneApi {
identifier?: string;
operator?: string;
value?: any;
type?: string;
type?: number;
param?: string;
}
/** 场景联动规则的动作 */
export interface Action {
type?: string;
type?: number;
productId?: number;
deviceId?: number;
identifier?: string;
value?: any;
alertConfigId?: number;
params?: Record<string, any>;
}
}
@ -67,20 +63,15 @@ export interface IotSceneRule {
/** IoT 场景联动规则触发器 */
export interface Trigger {
type?: string;
type?: number;
productId?: number;
deviceId?: number;
identifier?: string;
operator?: string;
value?: any;
cronExpression?: string;
conditionGroups?: TriggerConditionGroup[];
}
/** IoT 场景联动规则触发条件组 */
export interface TriggerConditionGroup {
conditions?: TriggerCondition[];
operator?: string;
// 后端结构List<List<TriggerCondition>>;外层「或」、组内「且」
conditionGroups?: TriggerCondition[][];
}
/** IoT 场景联动规则触发条件 */
@ -90,19 +81,19 @@ export interface TriggerCondition {
identifier?: string;
operator?: string;
value?: any;
type?: string;
type?: number;
param?: string;
}
/** IoT 场景联动规则动作 */
export interface Action {
type?: string;
type?: number;
productId?: number;
deviceId?: number;
identifier?: string;
value?: any;
alertConfigId?: number;
params?: string;
params?: Record<string, any>;
}
/** 查询场景联动规则分页 */

View File

@ -1,3 +1,5 @@
import type { Rule } from 'ant-design-vue/es/form';
import type { PageParam, PageResult } from '@vben/request';
import { isEmpty } from '@vben/utils';
@ -123,7 +125,7 @@ export function buildIdentifierLikeNameValidator(label: string) {
}
/** IoT 物模型表单校验规则 */
export const ThingModelFormRules = {
export const ThingModelFormRules: Record<string, Rule[]> = {
name: [
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
{

View File

@ -0,0 +1,391 @@
// TODO @AI能不能弄到通用的基础 utils 里?方便 antd 和 ele 复用?
/**
* CRON CRON
*/
/** CRON 字段类型枚举 */
export enum CronFieldType {
DAY = 'day',
HOUR = 'hour',
MINUTE = 'minute',
MONTH = 'month',
SECOND = 'second',
WEEK = 'week',
YEAR = 'year',
}
/** CRON 字段配置 */
export interface CronFieldConfig {
key: CronFieldType;
label: string;
max: number;
min: number;
names?: Record<string, number>;
}
/** CRON 字段配置常量 */
export const CRON_FIELD_CONFIGS: Record<CronFieldType, CronFieldConfig> = {
[CronFieldType.SECOND]: {
key: CronFieldType.SECOND,
label: '秒',
min: 0,
max: 59,
},
[CronFieldType.MINUTE]: {
key: CronFieldType.MINUTE,
label: '分',
min: 0,
max: 59,
},
[CronFieldType.HOUR]: {
key: CronFieldType.HOUR,
label: '时',
min: 0,
max: 23,
},
[CronFieldType.DAY]: {
key: CronFieldType.DAY,
label: '日',
min: 1,
max: 31,
},
[CronFieldType.MONTH]: {
key: CronFieldType.MONTH,
label: '月',
min: 1,
max: 12,
names: {
JAN: 1,
FEB: 2,
MAR: 3,
APR: 4,
MAY: 5,
JUN: 6,
JUL: 7,
AUG: 8,
SEP: 9,
OCT: 10,
NOV: 11,
DEC: 12,
},
},
[CronFieldType.WEEK]: {
key: CronFieldType.WEEK,
label: '周',
min: 0,
max: 7,
names: {
SUN: 0,
MON: 1,
TUE: 2,
WED: 3,
THU: 4,
FRI: 5,
SAT: 6,
},
},
[CronFieldType.YEAR]: {
key: CronFieldType.YEAR,
label: '年',
min: 1970,
max: 2099,
},
};
/** 解析后的 CRON 字段 */
export interface ParsedCronField {
description: string;
original: string;
type:
| 'any'
| 'last'
| 'list'
| 'nth'
| 'range'
| 'specific'
| 'step'
| 'weekday';
values: number[];
}
/** 解析后的 CRON 表达式 */
export interface ParsedCronExpression {
day: ParsedCronField;
description: string;
hour: ParsedCronField;
isValid: boolean;
minute: ParsedCronField;
month: ParsedCronField;
nextExecutionTime?: Date;
second: ParsedCronField;
week: ParsedCronField;
year?: ParsedCronField;
}
/** CRON 表达式工具类 */
export const CronUtils = {
/** 验证 CRON 表达式格式 */
validate(cronExpression: string): boolean {
if (!cronExpression || typeof cronExpression !== 'string') {
return false;
}
const parts = cronExpression.trim().split(/\s+/);
if (parts.length < 5 || parts.length > 7) {
return false;
}
const cronRegex = /^[\d#*,/?LW\-]+$/;
return parts.every((part) => cronRegex.test(part));
},
/** 解析单个 CRON 字段 */
parseField(
fieldValue: string,
fieldType: CronFieldType,
config: CronFieldConfig,
): ParsedCronField {
const field: ParsedCronField = {
type: 'any',
values: [],
original: fieldValue,
description: '',
};
if (fieldValue === '*' || fieldValue === '?') {
field.type = 'any';
field.description = `${config.label}`;
return field;
}
if (fieldValue === 'L' && fieldType === CronFieldType.DAY) {
field.type = 'last';
field.description = '每月最后一天';
return field;
}
if (fieldValue.includes('-')) {
const [start, end] = fieldValue.split('-').map(Number);
if (
!Number.isNaN(start) &&
!Number.isNaN(end) &&
start! >= config.min &&
end! <= config.max
) {
field.type = 'range';
field.values = Array.from(
{ length: end! - start! + 1 },
(_, i) => start! + i,
);
field.description = `${config.label} ${start}-${end}`;
}
return field;
}
if (fieldValue.includes('/')) {
const [base, step] = fieldValue.split('/');
const stepNum = Number(step);
if (!Number.isNaN(stepNum) && stepNum > 0) {
field.type = 'step';
if (base === '*') {
field.description = `${stepNum} ${config.label}`;
} else {
const startNum = Number(base);
field.description = `${startNum} 开始每 ${stepNum} ${config.label}`;
}
}
return field;
}
if (fieldValue.includes(',')) {
const values = fieldValue
.split(',')
.map(Number)
.filter((n) => !Number.isNaN(n));
if (values.length > 0) {
field.type = 'list';
field.values = values;
field.description = `${config.label} ${values.join(',')}`;
}
return field;
}
const numValue = Number(fieldValue);
if (
!Number.isNaN(numValue) &&
numValue >= config.min &&
numValue <= config.max
) {
field.type = 'specific';
field.values = [numValue];
field.description = `${config.label} ${numValue}`;
}
return field;
},
/** 解析完整的 CRON 表达式 */
parse(cronExpression: string): ParsedCronExpression {
const result: ParsedCronExpression = {
second: { type: 'any', values: [], original: '*', description: '每秒' },
minute: { type: 'any', values: [], original: '*', description: '每分' },
hour: { type: 'any', values: [], original: '*', description: '每时' },
day: { type: 'any', values: [], original: '*', description: '每日' },
month: { type: 'any', values: [], original: '*', description: '每月' },
week: { type: 'any', values: [], original: '?', description: '任意周' },
isValid: false,
description: '',
};
if (!this.validate(cronExpression)) {
result.description = '无效的 CRON 表达式';
return result;
}
const parts = cronExpression.trim().split(/\s+/);
const fieldTypes = [
CronFieldType.SECOND,
CronFieldType.MINUTE,
CronFieldType.HOUR,
CronFieldType.DAY,
CronFieldType.MONTH,
CronFieldType.WEEK,
];
const startIndex = parts.length === 5 ? 1 : 0;
for (const [i, part] of parts.entries()) {
const fieldType = fieldTypes[i + startIndex];
if (fieldType && CRON_FIELD_CONFIGS[fieldType]) {
const config = CRON_FIELD_CONFIGS[fieldType];
(result as any)[fieldType] = this.parseField(part, fieldType, config);
}
}
if (parts.length === 7) {
const yearConfig = CRON_FIELD_CONFIGS[CronFieldType.YEAR];
result.year = this.parseField(parts[6]!, CronFieldType.YEAR, yearConfig);
}
result.isValid = true;
result.description = this.generateDescription(result);
return result;
},
/** 生成 CRON 表达式的可读描述 */
generateDescription(parsed: ParsedCronExpression): string {
const parts: string[] = [];
if (parsed.hour.type === 'specific' && parsed.minute.type === 'specific') {
const hour = parsed.hour.values[0]!;
const minute = parsed.minute.values[0]!;
parts.push(
`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`,
);
} else if (parsed.hour.type === 'specific') {
parts.push(`每天 ${parsed.hour.values[0]}`);
} else if (
parsed.minute.type === 'specific' &&
parsed.minute.values[0] === 0 &&
parsed.hour.type === 'any'
) {
parts.push('每小时整点');
} else if (parsed.minute.type === 'step') {
const step = parsed.minute.original.split('/')[1];
parts.push(`${step} 分钟`);
} else if (parsed.hour.type === 'step') {
const step = parsed.hour.original.split('/')[1];
parts.push(`${step} 小时`);
}
if (parsed.day.type === 'specific') {
parts.push(`每月 ${parsed.day.values[0]}`);
} else if (parsed.week.type === 'specific') {
const weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const weekDay = parsed.week.values[0]!;
if (weekDay >= 0 && weekDay <= 6) {
parts.push(`${weekNames[weekDay]}`);
}
} else if (parsed.week.type === 'range') {
parts.push('工作日');
}
if (parsed.month.type === 'specific') {
parts.push(`${parsed.month.values[0]}`);
}
return parts.length > 0 ? parts.join(' ') : '自定义时间规则';
},
/** 格式化 CRON 表达式为可读文本 */
format(cronExpression: string): string {
if (!cronExpression) return '';
const parsed = this.parse(cronExpression);
return parsed.isValid ? parsed.description : cronExpression;
},
/** 计算 CRON 表达式的下次执行时间(简化版,仅覆盖常见模式) */
getNextExecutionTime(
cronExpression: string,
fromDate?: Date,
): Date | null {
const parsed = this.parse(cronExpression);
if (!parsed.isValid) {
return null;
}
const now = fromDate || new Date();
const nextTime = new Date(now.getTime() + 1000);
if (parsed.second.type === 'specific' && parsed.minute.type === 'any') {
const targetSecond = parsed.second.values[0]!;
nextTime.setSeconds(targetSecond, 0);
if (nextTime <= now) {
nextTime.setMinutes(nextTime.getMinutes() + 1);
}
return nextTime;
}
if (
parsed.second.type === 'specific' &&
parsed.minute.type === 'specific' &&
parsed.hour.type === 'any'
) {
const targetSecond = parsed.second.values[0]!;
const targetMinute = parsed.minute.values[0]!;
nextTime.setMinutes(targetMinute, targetSecond, 0);
if (nextTime <= now) {
nextTime.setHours(nextTime.getHours() + 1);
}
return nextTime;
}
if (
parsed.second.type === 'specific' &&
parsed.minute.type === 'specific' &&
parsed.hour.type === 'specific'
) {
const targetSecond = parsed.second.values[0]!;
const targetMinute = parsed.minute.values[0]!;
const targetHour = parsed.hour.values[0]!;
nextTime.setHours(targetHour, targetMinute, targetSecond, 0);
if (nextTime <= now) {
nextTime.setDate(nextTime.getDate() + 1);
}
return nextTime;
}
if (parsed.minute.type === 'step') {
const step = Number.parseInt(parsed.minute.original.split('/')[1]!);
const currentMinute = nextTime.getMinutes();
const nextMinute = Math.ceil(currentMinute / step) * step;
if (nextMinute >= 60) {
nextTime.setHours(nextTime.getHours() + 1, 0, 0, 0);
} else {
nextTime.setMinutes(nextMinute, 0, 0);
}
return nextTime;
}
return new Date(now.getTime() + 60_000);
},
/** 获取 CRON 表达式的执行频率描述 */
getFrequencyDescription(cronExpression: string): string {
const parsed = this.parse(cronExpression);
if (!parsed.isValid) {
return '无效表达式';
}
if (parsed.second.type === 'any' && parsed.minute.type === 'any') {
return '每秒执行';
}
if (parsed.minute.type === 'any' && parsed.hour.type === 'any') {
return '每分钟执行';
}
if (parsed.hour.type === 'any' && parsed.day.type === 'any') {
return '每小时执行';
}
if (parsed.day.type === 'any' && parsed.month.type === 'any') {
return '每天执行';
}
if (parsed.month.type === 'any') {
return '每月执行';
}
return '按计划执行';
},
};

View File

@ -1,5 +1,6 @@
import type { Recordable } from '@vben/types';
export * from './cron';
export * from './rangePickerProps';
export * from './routerHelper';

View File

@ -1,5 +1,6 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertConfigApi } from '#/api/iot/alert/config';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@ -137,7 +138,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions<AlertConfigApi.AlertConfig>['columns'] {
return [
{ type: 'checkbox', width: 40 },
{

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertConfigApi } from '#/api/iot/alert/config';
@ -12,11 +12,11 @@ import { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config';
import { DictTag } from '#/components/dict-tag';
import { $t } from '#/locales';
import AlertConfigForm from './modules/form.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: AlertConfigForm,
connectedComponent: Form,
destroyOnClose: true,
});
@ -42,10 +42,8 @@ async function handleDelete(row: AlertConfigApi.AlertConfig) {
duration: 0,
});
try {
await deleteAlertConfig(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
});
await deleteAlertConfig(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { AlertConfigApi } from '#/api/iot/alert/config';
import { computed, ref } from 'vue';
@ -17,8 +17,6 @@ import { $t } from '#/locales';
import { useFormSchema } from '../data';
defineOptions({ name: 'IoTAlertConfigForm' });
const emit = defineEmits(['success']);
const formData = ref<AlertConfigApi.AlertConfig>();
const getTitle = computed(() => {
@ -32,9 +30,9 @@ const [Form, formApi] = useVbenForm({
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 140,
},
wrapperClass: 'grid-cols-2',
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,

View File

@ -1,5 +1,8 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertRecordApi } from '#/api/iot/alert/record';
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@ -9,6 +12,12 @@ import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
import { getRangePickerDefaultProps } from '#/utils';
/** 关联数据 */
let productList: IotProductApi.Product[] = [];
let deviceList: IotDeviceApi.Device[] = [];
getSimpleProductList().then((data) => (productList = data));
getSimpleDeviceList().then((data) => (deviceList = data));
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
@ -84,7 +93,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions<AlertRecordApi.AlertRecord>['columns'] {
return [
{ type: 'checkbox', width: 40 },
{
@ -106,17 +115,20 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
props: { type: DICT_TYPE.IOT_ALERT_LEVEL },
},
},
// TODO @AI非必要不缩写product、device
{
field: 'productId',
title: '产品名称',
minWidth: 120,
slots: { default: 'product' },
formatter: ({ cellValue }) =>
productList.find((p) => p.id === cellValue)?.name || '-',
},
{
field: 'deviceId',
title: '设备名称',
minWidth: 120,
slots: { default: 'device' },
formatter: ({ cellValue }) =>
deviceList.find((d) => d.id === cellValue)?.deviceName || '-',
},
{
field: 'deviceMessage',

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AlertRecordApi } from '#/api/iot/alert/record';
@ -14,6 +14,16 @@ import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
import { useGridColumns, useGridFormSchema } from './data';
/** 把设备消息序列化成可读字符串 */
function stringifyDeviceMessage(deviceMessage: any) {
if (!deviceMessage) {
return '';
}
return typeof deviceMessage === 'object'
? JSON.stringify(deviceMessage, null, 2)
: String(deviceMessage);
}
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
@ -44,7 +54,7 @@ function handleProcess(row: AlertRecordApi.AlertRecord) {
duration: 0,
});
try {
await processAlertRecord(row.id as number, processRemark.value);
await processAlertRecord(row.id!, processRemark.value);
message.success('处理成功');
handleRefresh();
} finally {
@ -83,16 +93,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
} as VxeTableGridOptions<AlertRecordApi.AlertRecord>,
});
/** 把设备消息序列化成可读字符串 */
function stringifyDeviceMessage(deviceMessage: any) {
if (!deviceMessage) {
return '';
}
return typeof deviceMessage === 'object'
? JSON.stringify(deviceMessage, null, 2)
: String(deviceMessage);
}
</script>
<template>

View File

@ -1,5 +1,6 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotDeviceApi } from '#/api/iot/device/device';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@ -268,7 +269,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions<IotDeviceApi.Device>['columns'] {
return [
{ type: 'checkbox', width: 40 },
{

View File

@ -10,8 +10,6 @@ import { Alert, Button, message, Textarea } from 'ant-design-vue';
import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device';
defineOptions({ name: 'DeviceDetailConfig' });
const props = defineProps<{
device: IotDeviceApi.Device;
}>();
@ -78,7 +76,7 @@ async function saveConfig() {
config.value = JSON.parse(configString.value);
} catch (error) {
console.error('JSON格式错误:', error);
message.error({ content: 'JSON格式错误请修正后再提交' });
message.error('JSON格式错误请修正后再提交');
return;
}
saveLoading.value = true;
@ -101,7 +99,7 @@ async function handleConfigPush() {
params: config.value,
});
//
message.success({ content: '配置推送成功!' });
message.success('配置推送成功!');
} finally {
pushLoading.value = false;
}
@ -116,7 +114,7 @@ async function updateDeviceConfig() {
id: props.device.id,
config: JSON.stringify(config.value),
} as IotDeviceApi.Device);
message.success({ content: '更新成功!' });
message.success('更新成功!');
// success
emit('success');
} finally {

View File

@ -44,7 +44,7 @@ const methodOptions = computed(() => {
});
/** Grid 列定义 */
function useGridColumns(): VxeTableGridOptions['columns'] {
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
return [
{
field: 'ts',

View File

@ -19,8 +19,6 @@ import {
ModbusModeEnum,
} from '#/views/iot/utils/constants';
defineOptions({ name: 'DeviceModbusConfigForm' });
const emit = defineEmits(['success']);
const formData = ref<IotDeviceModbusConfigApi.ModbusConfig>();

View File

@ -30,8 +30,6 @@ import { ModbusFunctionCodeOptions } from '#/views/iot/utils/constants';
import DeviceModbusConfigForm from './modbus-config-form.vue';
import DeviceModbusPointForm from './modbus-point-form.vue';
defineOptions({ name: 'DeviceModbusConfig' });
const props = defineProps<{
device: IotDeviceApi.Device;
product: IotProductApi.Product;
@ -174,7 +172,7 @@ function usePointFormSchema(): VbenFormSchema[] {
}
/** 点位列表列配置 */
function usePointColumns(): VxeTableGridOptions['columns'] {
function usePointColumns(): VxeTableGridOptions<IotDeviceModbusPointApi.ModbusPoint>['columns'] {
return [
{ field: 'name', title: '属性名称', minWidth: 100 },
{

View File

@ -26,8 +26,6 @@ import {
ModbusRawDataTypeOptions,
} from '#/views/iot/utils/constants';
defineOptions({ name: 'DeviceModbusPointForm' });
const emit = defineEmits(['success']);
const formData = ref<IotDeviceModbusPointApi.ModbusPoint>();
@ -224,10 +222,10 @@ const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
handleValuesChange: async (_values, changedFields) => {
handleValuesChange: async (values, changedFields) => {
// identifier name
if (changedFields.includes('thingModelId')) {
const thingModelId = await formApi.getFieldValue('thingModelId');
const thingModelId = values.thingModelId;
const thingModel = thingModelList.value.find(
(item) => item.id === thingModelId,
);
@ -238,7 +236,7 @@ const [Form, formApi] = useVbenForm({
}
//
if (changedFields.includes('rawDataType')) {
const rawDataType = await formApi.getFieldValue('rawDataType');
const rawDataType = values.rawDataType;
if (rawDataType) {
//
const option = ModbusRawDataTypeOptions.find(

View File

@ -204,7 +204,7 @@ async function handlePropertyPost() {
});
if (Object.keys(params).length === 0) {
message.warning({ content: '请至少输入一个属性值' });
message.warning('请至少输入一个属性值');
return;
}
@ -214,11 +214,11 @@ async function handlePropertyPost() {
params,
});
message.success({ content: '属性上报成功' });
message.success('属性上报成功');
//
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
} catch (error) {
message.error({ content: '属性上报失败' });
message.error('属性上报失败');
console.error(error);
}
}
@ -233,7 +233,7 @@ async function handleEventPost(row: ThingModelApi.ThingModel) {
try {
params = JSON.parse(valueStr);
} catch {
message.error({ content: '事件参数格式错误请输入有效的JSON格式' });
message.error('事件参数格式错误请输入有效的JSON格式');
return;
}
}
@ -247,11 +247,11 @@ async function handleEventPost(row: ThingModelApi.ThingModel) {
},
});
message.success({ content: '事件上报成功' });
message.success('事件上报成功');
//
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
} catch (error) {
message.error({ content: '事件上报失败' });
message.error('事件上报失败');
console.error(error);
}
}
@ -265,11 +265,11 @@ async function handleDeviceState(state: number) {
params: { state },
});
message.success({ content: '状态变更成功' });
message.success('状态变更成功');
//
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
} catch (error) {
message.error({ content: '状态变更失败' });
message.error('状态变更失败');
console.error(error);
}
}
@ -286,7 +286,7 @@ async function handlePropertySet() {
});
if (Object.keys(params).length === 0) {
message.warning({ content: '请至少输入一个属性值' });
message.warning('请至少输入一个属性值');
return;
}
@ -296,11 +296,11 @@ async function handlePropertySet() {
params,
});
message.success({ content: '属性设置成功' });
message.success('属性设置成功');
//
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
} catch (error) {
message.error({ content: '属性设置失败' });
message.error('属性设置失败');
console.error(error);
}
}
@ -315,7 +315,7 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
try {
params = JSON.parse(valueStr);
} catch {
message.error({ content: '服务参数格式错误请输入有效的JSON格式' });
message.error('服务参数格式错误请输入有效的JSON格式');
return;
}
}
@ -329,11 +329,11 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
},
});
message.success({ content: '服务调用成功' });
message.success('服务调用成功');
//
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
} catch (error) {
message.error({ content: '服务调用失败' });
message.error('服务调用失败');
console.error(error);
}
}
@ -595,7 +595,7 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
>
<IconifyIcon
v-if="!messageCollapsed"
icon="lucide:chevron-down"
icon="lucide:chevron-up"
/>
<IconifyIcon
v-if="messageCollapsed"

View File

@ -29,7 +29,7 @@ const props = defineProps<Props>();
const router = useRouter();
/** 子设备列表表格列配置 */
function useGridColumns(): VxeTableGridOptions['columns'] {
function useGridColumns(): VxeTableGridOptions<IotDeviceApi.Device>['columns'] {
return [
{ type: 'checkbox', width: 40 },
{
@ -190,7 +190,7 @@ function useAddGridFormSchema(): VbenFormSchema[] {
];
}
function useAddGridColumns(): VxeTableGridOptions['columns'] {
function useAddGridColumns(): VxeTableGridOptions<IotDeviceApi.Device>['columns'] {
return [
{ type: 'checkbox', width: 40 },
{

View File

@ -39,7 +39,7 @@ const eventThingModels = computed(() => {
});
/** Grid 列定义 */
function useGridColumns(): VxeTableGridOptions['columns'] {
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
return [
{
field: 'reportTime',

View File

@ -28,9 +28,6 @@ import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
/** IoT 设备属性历史数据详情 */
defineOptions({ name: 'DeviceDetailsThingModelPropertyHistory' });
defineProps<{ deviceId: number }>();
const dialogVisible = ref(false); //
@ -155,11 +152,9 @@ const paginationConfig = computed(() => ({
async function getList() {
loading.value = true;
try {
//
const data = await getHistoryDevicePropertyList(queryParams);
// { list: [] }
list.value = (
Array.isArray(data) ? data : data?.list || []
) as IotDeviceApi.DevicePropertyDetail[];
list.value = (data || []) as IotDeviceApi.DevicePropertyDetail[];
total.value = list.value.length;
//

View File

@ -45,7 +45,7 @@ let autoRefreshTimer: any = null; // 定时器
const viewMode = ref<'card' | 'list'>('card'); //
/** Grid 列定义 */
function useGridColumns(): VxeTableGridOptions['columns'] {
function useGridColumns(): VxeTableGridOptions<IotDeviceApi.DevicePropertyDetail>['columns'] {
return [
{
field: 'identifier',

View File

@ -39,7 +39,7 @@ const serviceThingModels = computed(() => {
});
/** Grid 列定义 */
function useGridColumns(): VxeTableGridOptions['columns'] {
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
return [
{
field: 'requestTime',

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { PageParam } from '@vben/request';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
import type { IotProductApi } from '#/api/iot/product/product';

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
@ -16,8 +16,6 @@ import { $t } from '#/locales';
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
defineOptions({ name: 'IoTDeviceForm' });
const emit = defineEmits(['success']);
const formData = ref<IotDeviceApi.Device>();
const products = ref<IotProductApi.Product[]>([]);
@ -35,8 +33,9 @@ const [Form, formApi] = useVbenForm({
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 100,
},
wrapperClass: 'grid-cols-1',
layout: 'horizontal',
schema: useBasicFormSchema(),
showDefaultActions: false,
@ -62,8 +61,9 @@ const [AdvancedForm, advancedFormApi] = useVbenForm({
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 100,
},
wrapperClass: 'grid-cols-1',
layout: 'horizontal',
schema: useAdvancedFormSchema(),
showDefaultActions: false,

View File

@ -13,8 +13,6 @@ import { $t } from '#/locales';
import { useGroupFormSchema } from '../data';
defineOptions({ name: 'IoTDeviceGroupForm' });
const emit = defineEmits(['success']);
const deviceIds = ref<number[]>([]);
const getTitle = computed(() => '添加设备到分组');
@ -24,6 +22,8 @@ const [Form, formApi] = useVbenForm({
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useGroupFormSchema(),

View File

@ -14,12 +14,13 @@ import { $t } from '#/locales';
import { useImportFormSchema } from '../data';
defineOptions({ name: 'IoTDeviceImportForm' });
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 120,
},

View File

@ -1,5 +1,6 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@ -78,7 +79,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions<IotDeviceGroupApi.DeviceGroup>['columns'] {
return [
{
field: 'id',

View File

@ -40,7 +40,7 @@ async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
duration: 0,
});
try {
await deleteDeviceGroup(row.id as number);
await deleteDeviceGroup(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {

View File

@ -30,7 +30,10 @@ const [Form, formApi] = useVbenForm({
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
},
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false,
});

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { StatsData } from './data';
import { onMounted, ref } from 'vue';
@ -18,22 +18,16 @@ import MessageTrendCard from './modules/message-trend-card.vue';
const loading = ref(true);
const statsData = ref<StatsData>(defaultStatsData);
/** 加载统计数据 */
async function loadStatisticsData(): Promise<StatsData> {
return await getStatisticsSummary();
}
/** 加载数据 */
async function loadData() {
loading.value = true;
try {
statsData.value = await loadStatisticsData();
statsData.value = await getStatisticsSummary();
} finally {
loading.value = false;
}
}
/** 组件挂载时加载数据 */
onMounted(() => {
loadData();
});

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { IotStatisticsApi } from '#/api/iot/statistics';
import { computed, nextTick, onMounted, ref, watch } from 'vue';

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { IotStatisticsApi } from '#/api/iot/statistics';
import { computed, nextTick, onMounted, ref, watch } from 'vue';

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { Dayjs } from 'dayjs';
import type { IotStatisticsApi } from '#/api/iot/statistics';

View File

@ -1,5 +1,6 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductCategoryApi } from '#/api/iot/product/category';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@ -91,10 +92,10 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions<IotProductCategoryApi.ProductCategory>['columns'] {
return [
{
type: 'seq',
field: 'id',
title: 'ID',
width: 80,
},

View File

@ -85,7 +85,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
<Grid>
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[

View File

@ -70,10 +70,10 @@ const [Modal, modalApi] = useVbenModal({
if (!data || !data.id) {
return;
}
//
modalApi.lock();
try {
formData.value = await getProductCategory(data.id);
// values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();

View File

@ -24,9 +24,9 @@ const loading = ref(false);
const productList = ref<IotProductApi.Product[]>([]);
/** 处理选择变化 */
function handleChange(value?: number) {
emit('update:modelValue', value);
emit('change', value);
function handleChange(value: any) {
emit('update:modelValue', value as number | undefined);
emit('change', value as number | undefined);
}
/** 获取产品列表 */

View File

@ -1,9 +1,10 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductApi } from '#/api/iot/product/product';
import { h } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@ -109,8 +110,7 @@ export function useBasicFormSchema(
buttonStyle: 'solid',
optionType: 'button',
},
// TODO @AI枚举值。或者这里不要枚举值对齐 vue3 + ep 版本
defaultValue: 0,
defaultValue: DeviceTypeEnum.DEVICE,
dependencies: {
triggerFields: ['id'],
componentProps: (values) => ({
@ -131,8 +131,7 @@ export function useBasicFormSchema(
// 网关子设备走网关联网,不需要联网方式
dependencies: {
triggerFields: ['deviceType'],
// TODO @AI枚举值。或者这里不要枚举值也看看 vben 里,其它是不是也漏了枚举值。)
show: (values) => values.deviceType !== 2,
show: (values) => values.deviceType !== DeviceTypeEnum.GATEWAY,
},
rules: 'required',
},
@ -199,7 +198,7 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions<IotProductApi.Product>['columns'] {
return [
{
field: 'id',

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { IotProductApi } from '#/api/iot/product/product';
import { onMounted, provide, ref } from 'vue';

View File

@ -49,7 +49,6 @@ async function copyToClipboard(text: string) {
/** 跳转到设备管理 */
function goToDeviceList(productId: number) {
// TODO @AIvben 使 name
router.push({
name: 'IoTDevice',
query: { productId: String(productId) },

View File

@ -74,7 +74,7 @@ async function copyToClipboard(text: string) {
</Descriptions.Item>
<Descriptions.Item
v-if="
[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(
([DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY] as number[]).includes(
product.deviceType!,
)
"

View File

@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue';
import { useAccess } from '@vben/access';
import { DICT_TYPE } from '@vben/constants';
import { DICT_TYPE, ProductStatusEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import {
@ -190,8 +190,10 @@ onMounted(() => {
物模型
</Button>
<template v-if="hasAccessByCodes(['iot:product:delete'])">
<!-- TODO @AI使用枚举 -->
<Tooltip v-if="item.status === 1" title="已发布的产品不能删除">
<Tooltip
v-if="item.status === ProductStatusEnum.PUBLISHED"
title="已发布的产品不能删除"
>
<Button
size="small"
danger

View File

@ -41,21 +41,23 @@ const getTitle = computed(() => {
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
});
const [AdvancedForm, advancedFormApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useAdvancedFormSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-1',
});
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */

View File

@ -12,11 +12,7 @@ import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import DataRuleForm from './data-rule-form.vue';
// TODO @haohao apps/web-antd/src/views/iot/rule/data/index.vue apps/web-antd/src/views/iot/rule/data/index.vue tabs
/** IoT 数据流转规则列表 */
defineOptions({ name: 'IotDataRule' });
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: DataRuleForm,
destroyOnClose: true,
@ -29,12 +25,12 @@ function handleRefresh() {
/** 创建规则 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
formModalApi.setData(null).open();
}
/** 编辑规则 */
function handleEdit(row: any) {
formModalApi.setData({ type: 'update', id: row.id }).open();
formModalApi.setData({ id: row.id }).open();
}
/** 删除规则 */

View File

@ -1,5 +1,6 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ThingModelApi } from '#/api/iot/thingmodel';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@ -23,7 +24,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(): VxeTableGridOptions<ThingModelApi.ThingModel>['columns'] {
return [
{ type: 'checkbox', width: 40 },
{
@ -49,7 +50,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
title: '数据类型',
minWidth: 100,
formatter: ({ row }) =>
getDataTypeOptionsLabel(row.property?.dataType) || '-',
getDataTypeOptionsLabel(row.property?.dataType ?? '') || '-',
},
{
title: '数据定义',

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { IotProductApi } from '#/api/iot/product/product';
import type { ThingModelApi } from '#/api/iot/thingmodel';
@ -96,7 +97,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
refresh: true,
search: true,
},
},
} as VxeTableGridOptions<ThingModelApi.ThingModel>,
});
</script>

View File

@ -0,0 +1,73 @@
<!-- dataTypearray 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Form, Input, Radio } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import {
getDataTypeOptions,
IoTDataSpecsDataTypeEnum,
} from '#/views/iot/utils/constants';
import ThingModelStructDataSpecs from './thing-model-struct-data-specs.vue';
/** 数组型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelArrayDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
/** 元素类型改变时间。当值为 struct 时,对 dataSpecs 中的 dataSpecsList 进行初始化 */
function handleChange(val: any) {
if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
return;
}
dataSpecs.value.dataSpecsList = [];
}
</script>
<template>
<Form.Item
:name="['property', 'dataSpecs', 'childDataType']"
:rules="ThingModelFormRules.childDataType"
label="元素类型"
>
<Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
<template v-for="item in getDataTypeOptions()" :key="item.value">
<Radio
v-if="
!(
[
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.DATE,
] as any[]
).includes(item.value)
"
:value="item.value"
class="w-1/3"
>
{{ `${item.value}(${item.label})` }}
</Radio>
</template>
</Radio.Group>
</Form.Item>
<Form.Item
:name="['property', 'dataSpecs', 'size']"
:rules="ThingModelFormRules.size"
label="元素个数"
>
<Input
v-model:value="dataSpecs.size"
placeholder="请输入数组中的元素个数"
/>
</Form.Item>
<!-- Struct 型配置-->
<ThingModelStructDataSpecs
v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"
v-model="dataSpecs.dataSpecsList"
/>
</template>

View File

@ -0,0 +1,67 @@
<!-- dataTypeenum 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Button, Form, Input, message } from 'ant-design-vue';
/** 枚举型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelEnumDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
/** 添加枚举项 */
function addEnum() {
dataSpecsList.value.push({
name: '', //
value: '', //
} as any);
}
/** 删除枚举项 */
function deleteEnum(index: number) {
if (dataSpecsList.value.length === 1) {
message.warning('至少需要一个枚举项');
return;
}
dataSpecsList.value.splice(index, 1);
}
</script>
<template>
<Form.Item label="枚举项">
<div class="flex flex-col">
<div class="flex items-center">
<span class="flex-1"> 参数值 </span>
<span class="flex-1"> 参数描述 </span>
</div>
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="mb-5px flex items-center justify-between"
>
<div class="flex-1">
<Input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />
</div>
<span class="mx-2">~</span>
<div class="flex-1">
<Input v-model:value="item.name" placeholder="对该枚举项的描述" />
</div>
<Button class="ml-10px" type="link" @click="deleteEnum(index)">
删除
</Button>
</div>
<Button type="link" @click="addEnum">+</Button>
</div>
</Form.Item>
</template>
<style lang="scss" scoped>
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,78 @@
<!-- dataTypenumber 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { DataSpecsNumberData } from '#/api/iot/thingmodel';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'ant-design-vue';
/** 数值型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelNumberDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(
props,
'modelValue',
emits,
) as Ref<DataSpecsNumberData>;
/** 单位发生变化时触发 */
const unitChange = (UnitSpecs: any) => {
if (!UnitSpecs) return;
const [unitName, unit] = String(UnitSpecs).split('-');
dataSpecs.value.unitName = unitName;
dataSpecs.value.unit = unit;
};
</script>
<template>
<Form.Item label="取值范围">
<div class="flex items-center justify-between">
<div class="flex-1">
<Input v-model:value="dataSpecs.min" placeholder="请输入最小值" />
</div>
<span class="mx-2">~</span>
<div class="flex-1">
<Input v-model:value="dataSpecs.max" placeholder="请输入最大值" />
</div>
</div>
</Form.Item>
<Form.Item label="步长">
<Input v-model:value="dataSpecs.step" placeholder="请输入步长" />
</Form.Item>
<Form.Item label="单位">
<Select
:model-value="
dataSpecs.unit ? `${dataSpecs.unitName}-${dataSpecs.unit}` : ''
"
show-search
placeholder="请选择单位"
class="w-1/1"
@change="unitChange"
>
<Select.Option
v-for="(item, index) in getDictOptions(
DICT_TYPE.IOT_THING_MODEL_UNIT,
'string',
)"
:key="index"
:value="`${item.label}-${item.value}`"
>
{{ `${item.label}-${item.value}` }}
</Select.Option>
</Select>
</Form.Item>
</template>
<style lang="scss" scoped>
:deep(.ant-form-item) {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,160 @@
<script lang="ts" setup>
import type { Ref } from 'vue';
import { onMounted, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { isEmpty } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import { Button, Divider, Form, Input } from 'ant-design-vue';
import { ThingModelFormRules } from '#/api/iot/thingmodel';
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
import ThingModelProperty from '../thing-model-property.vue';
/** Struct 型的 dataSpecs 配置 */
defineOptions({ name: 'ThingModelStructDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>;
const structFormRef = ref();
const formData = ref<any>(buildEmptyFormData());
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
try {
await structFormRef.value?.validate();
} catch {
return;
}
const data = formData.value;
const item = {
identifier: data.identifier,
name: data.name,
description: data.description,
dataType: IoTDataSpecsDataTypeEnum.STRUCT,
childDataType: data.property.dataType,
dataSpecs:
!isEmpty(data.property.dataSpecs) &&
Object.keys(data.property.dataSpecs).length > 1
? data.property.dataSpecs
: undefined,
dataSpecsList: isEmpty(data.property.dataSpecsList)
? undefined
: data.property.dataSpecsList,
};
const existingIndex = dataSpecsList.value.findIndex(
(spec) => spec.identifier === data.identifier,
);
if (existingIndex === -1) {
dataSpecsList.value.push(item);
} else {
dataSpecsList.value[existingIndex] = item;
}
await modalApi.close();
},
onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
formData.value = buildEmptyFormData();
structFormRef.value?.clearValidate?.();
const data = modalApi.getData<any>();
if (isEmpty(data)) {
return;
}
formData.value = {
identifier: data.identifier ?? '',
name: data.name ?? '',
description: data.description ?? '',
property: {
dataType: data.childDataType ?? IoTDataSpecsDataTypeEnum.INT,
dataSpecs: data.dataSpecs ?? {},
dataSpecsList: data.dataSpecsList ?? [],
},
};
},
});
/** 构造空白结构体表单 */
function buildEmptyFormData() {
return {
identifier: '',
name: '',
description: '',
property: {
dataType: IoTDataSpecsDataTypeEnum.INT,
dataSpecs: { dataType: IoTDataSpecsDataTypeEnum.INT },
dataSpecsList: [],
},
};
}
/** 打开结构体表单 */
function openStructForm(val: any) {
modalApi.setData(val).open();
}
/** 删除结构体项 */
function deleteStructItem(index: number) {
dataSpecsList.value.splice(index, 1);
}
onMounted(() => {
if (isEmpty(dataSpecsList.value)) {
dataSpecsList.value = [];
}
});
</script>
<template>
<Form.Item label="属性对象">
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="mb-2.5 flex w-full justify-between bg-gray-100 px-2.5 dark:bg-gray-800"
>
<span>参数{{ item.name }}</span>
<div>
<Button type="link" @click="openStructForm(item)"></Button>
<Divider type="vertical" />
<Button danger type="link" @click="deleteStructItem(index)">
删除
</Button>
</div>
</div>
<Button type="link" @click="openStructForm(null)">+ </Button>
</Form.Item>
<!-- 结构体参数表单 -->
<Modal class="w-2/5" title="结构体参数">
<Form
ref="structFormRef"
:label-col="{ span: 6 }"
:model="formData"
:wrapper-col="{ span: 18 }"
class="mx-4"
>
<Form.Item
:rules="ThingModelFormRules.name"
label="参数名称"
name="name"
>
<Input v-model:value="formData.name" placeholder="请输入参数名称" />
</Form.Item>
<Form.Item
:rules="ThingModelFormRules.identifier"
label="标识符"
name="identifier"
>
<Input v-model:value="formData.identifier" placeholder="请输入标识符" />
</Form.Item>
<!-- 属性配置 -->
<ThingModelProperty v-model="formData.property" is-struct-data-specs />
</Form>
</Modal>
</template>

View File

@ -1,4 +1,4 @@
<script setup lang="ts">
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { IotProductApi } from '#/api/iot/product/product';

View File

@ -142,25 +142,25 @@ export const getDataTypeName = (dataType: string): string => {
return typeMap[dataType] || dataType;
};
/** 获取数据类型标签类型(用于 tag 的 type 属性 */
export const getDataTypeTagType = (
/** 获取数据类型标签颜色antd Tag `color` */
export const getDataTypeTagColor = (
dataType: string,
): 'danger' | 'info' | 'primary' | 'success' | 'warning' => {
): 'default' | 'error' | 'processing' | 'success' | 'warning' => {
const tagMap: Record<
string,
'danger' | 'info' | 'primary' | 'success' | 'warning'
'default' | 'error' | 'processing' | 'success' | 'warning'
> = {
[IoTDataSpecsDataTypeEnum.INT]: 'primary',
[IoTDataSpecsDataTypeEnum.INT]: 'processing',
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
[IoTDataSpecsDataTypeEnum.TEXT]: 'info',
[IoTDataSpecsDataTypeEnum.TEXT]: 'default',
[IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
[IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
[IoTDataSpecsDataTypeEnum.DATE]: 'primary',
[IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
[IoTDataSpecsDataTypeEnum.ENUM]: 'error',
[IoTDataSpecsDataTypeEnum.DATE]: 'processing',
[IoTDataSpecsDataTypeEnum.STRUCT]: 'default',
[IoTDataSpecsDataTypeEnum.ARRAY]: 'warning',
};
return tagMap[dataType] || 'info';
return tagMap[dataType] || 'default';
};
/** 物模型组标签常量 */

View File

@ -33,7 +33,7 @@ function isBoolean(value: unknown): value is boolean {
* @param {T} value
* @returns {boolean} truefalse
*/
function isEmpty<T = unknown>(value?: T): value is T {
function isEmpty<T = unknown>(value?: T): boolean {
if (value === null || value === undefined) {
return true;
}