feat(iot): 优化 antd 里的整体代码风格。
parent
1bdc0d992f
commit
e7a61ce150
|
|
@ -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>;
|
||||
}
|
||||
|
||||
/** 查询场景联动规则分页 */
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 '按计划执行';
|
||||
},
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Recordable } from '@vben/types';
|
||||
|
||||
export * from './cron';
|
||||
export * from './rangePickerProps';
|
||||
export * from './routerHelper';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const methodOptions = computed(() => {
|
|||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'ts',
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import {
|
|||
ModbusModeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
defineOptions({ name: 'DeviceModbusConfigForm' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formData = ref<IotDeviceModbusConfigApi.ModbusConfig>();
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const eventThingModels = computed(() => {
|
|||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'reportTime',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
// 如果是图表模式且支持图表展示,等待渲染图表
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const serviceThingModels = computed(() => {
|
|||
});
|
||||
|
||||
/** Grid 列定义 */
|
||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'requestTime',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ const [Form, formApi] = useVbenForm({
|
|||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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="[
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/** 获取产品列表 */
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ async function copyToClipboard(text: string) {
|
|||
|
||||
/** 跳转到设备管理 */
|
||||
function goToDeviceList(productId: number) {
|
||||
// TODO @AI:在检查下,vben 里面有没其他也是这种路由情况的;要尽量使用 name;
|
||||
router.push({
|
||||
name: 'IoTDevice',
|
||||
query: { productId: String(productId) },
|
||||
|
|
|
|||
|
|
@ -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!,
|
||||
)
|
||||
"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/** 删除规则 */
|
||||
|
|
|
|||
|
|
@ -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: '数据定义',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<!-- dataType:array 数组类型 -->
|
||||
<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>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<!-- dataType:enum 数组类型 -->
|
||||
<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>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
<!-- dataType:number 数组类型 -->
|
||||
<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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
};
|
||||
|
||||
/** 物模型组标签常量 */
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ function isBoolean(value: unknown): value is boolean {
|
|||
* @param {T} value 要检查的值。
|
||||
* @returns {boolean} 如果值为空,返回true,否则返回false。
|
||||
*/
|
||||
function isEmpty<T = unknown>(value?: T): value is T {
|
||||
function isEmpty<T = unknown>(value?: T): boolean {
|
||||
if (value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue