feat(iot): 优化 antd 里的整体代码风格。
parent
1bdc0d992f
commit
e7a61ce150
|
|
@ -16,20 +16,14 @@ export namespace RuleSceneApi {
|
||||||
|
|
||||||
/** 场景联动规则的触发器 */
|
/** 场景联动规则的触发器 */
|
||||||
export interface Trigger {
|
export interface Trigger {
|
||||||
type?: string;
|
type?: number;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
deviceId?: number;
|
deviceId?: number;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
operator?: string;
|
operator?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
cronExpression?: string;
|
cronExpression?: string;
|
||||||
conditionGroups?: TriggerConditionGroup[];
|
conditionGroups?: TriggerCondition[][];
|
||||||
}
|
|
||||||
|
|
||||||
/** 场景联动规则的触发条件组 */
|
|
||||||
export interface TriggerConditionGroup {
|
|
||||||
conditions?: TriggerCondition[];
|
|
||||||
operator?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 场景联动规则的触发条件 */
|
/** 场景联动规则的触发条件 */
|
||||||
|
|
@ -39,17 +33,19 @@ export namespace RuleSceneApi {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
operator?: string;
|
operator?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
type?: string;
|
type?: number;
|
||||||
|
param?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 场景联动规则的动作 */
|
/** 场景联动规则的动作 */
|
||||||
export interface Action {
|
export interface Action {
|
||||||
type?: string;
|
type?: number;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
deviceId?: number;
|
deviceId?: number;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
alertConfigId?: number;
|
alertConfigId?: number;
|
||||||
|
params?: Record<string, any>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,20 +63,15 @@ export interface IotSceneRule {
|
||||||
|
|
||||||
/** IoT 场景联动规则触发器 */
|
/** IoT 场景联动规则触发器 */
|
||||||
export interface Trigger {
|
export interface Trigger {
|
||||||
type?: string;
|
type?: number;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
deviceId?: number;
|
deviceId?: number;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
operator?: string;
|
operator?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
cronExpression?: string;
|
cronExpression?: string;
|
||||||
conditionGroups?: TriggerConditionGroup[];
|
// 后端结构:List<List<TriggerCondition>>;外层「或」、组内「且」
|
||||||
}
|
conditionGroups?: TriggerCondition[][];
|
||||||
|
|
||||||
/** IoT 场景联动规则触发条件组 */
|
|
||||||
export interface TriggerConditionGroup {
|
|
||||||
conditions?: TriggerCondition[];
|
|
||||||
operator?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IoT 场景联动规则触发条件 */
|
/** IoT 场景联动规则触发条件 */
|
||||||
|
|
@ -90,19 +81,19 @@ export interface TriggerCondition {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
operator?: string;
|
operator?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
type?: string;
|
type?: number;
|
||||||
param?: string;
|
param?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IoT 场景联动规则动作 */
|
/** IoT 场景联动规则动作 */
|
||||||
export interface Action {
|
export interface Action {
|
||||||
type?: string;
|
type?: number;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
deviceId?: number;
|
deviceId?: number;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
value?: any;
|
value?: any;
|
||||||
alertConfigId?: number;
|
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 type { PageParam, PageResult } from '@vben/request';
|
||||||
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
@ -123,7 +125,7 @@ export function buildIdentifierLikeNameValidator(label: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IoT 物模型表单校验规则 */
|
/** IoT 物模型表单校验规则 */
|
||||||
export const ThingModelFormRules = {
|
export const ThingModelFormRules: Record<string, Rule[]> = {
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '功能名称不能为空', trigger: 'blur' },
|
{ 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';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
export * from './cron';
|
||||||
export * from './rangePickerProps';
|
export * from './rangePickerProps';
|
||||||
export * from './routerHelper';
|
export * from './routerHelper';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AlertConfigApi } from '#/api/iot/alert/config';
|
||||||
|
|
||||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
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 [
|
return [
|
||||||
{ type: 'checkbox', width: 40 },
|
{ 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 { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { AlertConfigApi } from '#/api/iot/alert/config';
|
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 { DictTag } from '#/components/dict-tag';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import AlertConfigForm from './modules/form.vue';
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: AlertConfigForm,
|
connectedComponent: Form,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -42,10 +42,8 @@ async function handleDelete(row: AlertConfigApi.AlertConfig) {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await deleteAlertConfig(row.id as number);
|
await deleteAlertConfig(row.id!);
|
||||||
message.success({
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
content: $t('ui.actionMessage.deleteSuccess', [row.name]),
|
|
||||||
});
|
|
||||||
handleRefresh();
|
handleRefresh();
|
||||||
} finally {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { AlertConfigApi } from '#/api/iot/alert/config';
|
import type { AlertConfigApi } from '#/api/iot/alert/config';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
@ -17,8 +17,6 @@ import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTAlertConfigForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<AlertConfigApi.AlertConfig>();
|
const formData = ref<AlertConfigApi.AlertConfig>();
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
|
|
@ -32,9 +30,9 @@ const [Form, formApi] = useVbenForm({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
labelWidth: 140,
|
labelWidth: 140,
|
||||||
},
|
},
|
||||||
wrapperClass: 'grid-cols-2',
|
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: useFormSchema(),
|
schema: useFormSchema(),
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
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 { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
@ -9,6 +12,12 @@ import { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
import { getRangePickerDefaultProps } from '#/utils';
|
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[] {
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
|
|
@ -84,7 +93,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表的字段 */
|
/** 列表的字段 */
|
||||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
export function useGridColumns(): VxeTableGridOptions<AlertRecordApi.AlertRecord>['columns'] {
|
||||||
return [
|
return [
|
||||||
{ type: 'checkbox', width: 40 },
|
{ type: 'checkbox', width: 40 },
|
||||||
{
|
{
|
||||||
|
|
@ -106,17 +115,20 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
props: { type: DICT_TYPE.IOT_ALERT_LEVEL },
|
props: { type: DICT_TYPE.IOT_ALERT_LEVEL },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO @AI:非必要,不缩写;product、device
|
||||||
{
|
{
|
||||||
field: 'productId',
|
field: 'productId',
|
||||||
title: '产品名称',
|
title: '产品名称',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
slots: { default: 'product' },
|
formatter: ({ cellValue }) =>
|
||||||
|
productList.find((p) => p.id === cellValue)?.name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceId',
|
field: 'deviceId',
|
||||||
title: '设备名称',
|
title: '设备名称',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
slots: { default: 'device' },
|
formatter: ({ cellValue }) =>
|
||||||
|
deviceList.find((d) => d.id === cellValue)?.deviceName || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceMessage',
|
field: 'deviceMessage',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { AlertRecordApi } from '#/api/iot/alert/record';
|
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';
|
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() {
|
function handleRefresh() {
|
||||||
gridApi.query();
|
gridApi.query();
|
||||||
|
|
@ -44,7 +54,7 @@ function handleProcess(row: AlertRecordApi.AlertRecord) {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await processAlertRecord(row.id as number, processRemark.value);
|
await processAlertRecord(row.id!, processRemark.value);
|
||||||
message.success('处理成功');
|
message.success('处理成功');
|
||||||
handleRefresh();
|
handleRefresh();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -83,16 +93,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions<AlertRecordApi.AlertRecord>,
|
} as VxeTableGridOptions<AlertRecordApi.AlertRecord>,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 把设备消息序列化成可读字符串 */
|
|
||||||
function stringifyDeviceMessage(deviceMessage: any) {
|
|
||||||
if (!deviceMessage) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return typeof deviceMessage === 'object'
|
|
||||||
? JSON.stringify(deviceMessage, null, 2)
|
|
||||||
: String(deviceMessage);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
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 [
|
return [
|
||||||
{ type: 'checkbox', width: 40 },
|
{ 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';
|
import { sendDeviceMessage, updateDevice } from '#/api/iot/device/device';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceDetailConfig' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
device: IotDeviceApi.Device;
|
device: IotDeviceApi.Device;
|
||||||
}>();
|
}>();
|
||||||
|
|
@ -78,7 +76,7 @@ async function saveConfig() {
|
||||||
config.value = JSON.parse(configString.value);
|
config.value = JSON.parse(configString.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('JSON格式错误:', error);
|
console.error('JSON格式错误:', error);
|
||||||
message.error({ content: 'JSON格式错误,请修正后再提交!' });
|
message.error('JSON格式错误,请修正后再提交!');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saveLoading.value = true;
|
saveLoading.value = true;
|
||||||
|
|
@ -101,7 +99,7 @@ async function handleConfigPush() {
|
||||||
params: config.value,
|
params: config.value,
|
||||||
});
|
});
|
||||||
// 提示成功
|
// 提示成功
|
||||||
message.success({ content: '配置推送成功!' });
|
message.success('配置推送成功!');
|
||||||
} finally {
|
} finally {
|
||||||
pushLoading.value = false;
|
pushLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +114,7 @@ async function updateDeviceConfig() {
|
||||||
id: props.device.id,
|
id: props.device.id,
|
||||||
config: JSON.stringify(config.value),
|
config: JSON.stringify(config.value),
|
||||||
} as IotDeviceApi.Device);
|
} as IotDeviceApi.Device);
|
||||||
message.success({ content: '更新成功!' });
|
message.success('更新成功!');
|
||||||
// 触发 success 事件
|
// 触发 success 事件
|
||||||
emit('success');
|
emit('success');
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const methodOptions = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Grid 列定义 */
|
/** Grid 列定义 */
|
||||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'ts',
|
field: 'ts',
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ import {
|
||||||
ModbusModeEnum,
|
ModbusModeEnum,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceModbusConfigForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const formData = ref<IotDeviceModbusConfigApi.ModbusConfig>();
|
const formData = ref<IotDeviceModbusConfigApi.ModbusConfig>();
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ import { ModbusFunctionCodeOptions } from '#/views/iot/utils/constants';
|
||||||
import DeviceModbusConfigForm from './modbus-config-form.vue';
|
import DeviceModbusConfigForm from './modbus-config-form.vue';
|
||||||
import DeviceModbusPointForm from './modbus-point-form.vue';
|
import DeviceModbusPointForm from './modbus-point-form.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceModbusConfig' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
device: IotDeviceApi.Device;
|
device: IotDeviceApi.Device;
|
||||||
product: IotProductApi.Product;
|
product: IotProductApi.Product;
|
||||||
|
|
@ -174,7 +172,7 @@ function usePointFormSchema(): VbenFormSchema[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 点位列表列配置 */
|
/** 点位列表列配置 */
|
||||||
function usePointColumns(): VxeTableGridOptions['columns'] {
|
function usePointColumns(): VxeTableGridOptions<IotDeviceModbusPointApi.ModbusPoint>['columns'] {
|
||||||
return [
|
return [
|
||||||
{ field: 'name', title: '属性名称', minWidth: 100 },
|
{ field: 'name', title: '属性名称', minWidth: 100 },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,6 @@ import {
|
||||||
ModbusRawDataTypeOptions,
|
ModbusRawDataTypeOptions,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceModbusPointForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const formData = ref<IotDeviceModbusPointApi.ModbusPoint>();
|
const formData = ref<IotDeviceModbusPointApi.ModbusPoint>();
|
||||||
|
|
@ -224,10 +222,10 @@ const [Form, formApi] = useVbenForm({
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: useFormSchema(),
|
schema: useFormSchema(),
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
handleValuesChange: async (_values, changedFields) => {
|
handleValuesChange: async (values, changedFields) => {
|
||||||
// 物模型属性变化:自动填充 identifier 和 name
|
// 物模型属性变化:自动填充 identifier 和 name
|
||||||
if (changedFields.includes('thingModelId')) {
|
if (changedFields.includes('thingModelId')) {
|
||||||
const thingModelId = await formApi.getFieldValue('thingModelId');
|
const thingModelId = values.thingModelId;
|
||||||
const thingModel = thingModelList.value.find(
|
const thingModel = thingModelList.value.find(
|
||||||
(item) => item.id === thingModelId,
|
(item) => item.id === thingModelId,
|
||||||
);
|
);
|
||||||
|
|
@ -238,7 +236,7 @@ const [Form, formApi] = useVbenForm({
|
||||||
}
|
}
|
||||||
// 数据类型变化:自动设置寄存器数量和字节序
|
// 数据类型变化:自动设置寄存器数量和字节序
|
||||||
if (changedFields.includes('rawDataType')) {
|
if (changedFields.includes('rawDataType')) {
|
||||||
const rawDataType = await formApi.getFieldValue('rawDataType');
|
const rawDataType = values.rawDataType;
|
||||||
if (rawDataType) {
|
if (rawDataType) {
|
||||||
// 根据数据类型自动设置寄存器数量
|
// 根据数据类型自动设置寄存器数量
|
||||||
const option = ModbusRawDataTypeOptions.find(
|
const option = ModbusRawDataTypeOptions.find(
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ async function handlePropertyPost() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(params).length === 0) {
|
if (Object.keys(params).length === 0) {
|
||||||
message.warning({ content: '请至少输入一个属性值' });
|
message.warning('请至少输入一个属性值');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,11 +214,11 @@ async function handlePropertyPost() {
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success({ content: '属性上报成功' });
|
message.success('属性上报成功');
|
||||||
// 延迟刷新设备消息列表
|
// 延迟刷新设备消息列表
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error({ content: '属性上报失败' });
|
message.error('属性上报失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +233,7 @@ async function handleEventPost(row: ThingModelApi.ThingModel) {
|
||||||
try {
|
try {
|
||||||
params = JSON.parse(valueStr);
|
params = JSON.parse(valueStr);
|
||||||
} catch {
|
} catch {
|
||||||
message.error({ content: '事件参数格式错误,请输入有效的JSON格式' });
|
message.error('事件参数格式错误,请输入有效的JSON格式');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,11 +247,11 @@ async function handleEventPost(row: ThingModelApi.ThingModel) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success({ content: '事件上报成功' });
|
message.success('事件上报成功');
|
||||||
// 延迟刷新设备消息列表
|
// 延迟刷新设备消息列表
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error({ content: '事件上报失败' });
|
message.error('事件上报失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -265,11 +265,11 @@ async function handleDeviceState(state: number) {
|
||||||
params: { state },
|
params: { state },
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success({ content: '状态变更成功' });
|
message.success('状态变更成功');
|
||||||
// 延迟刷新设备消息列表
|
// 延迟刷新设备消息列表
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error({ content: '状态变更失败' });
|
message.error('状态变更失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +286,7 @@ async function handlePropertySet() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(params).length === 0) {
|
if (Object.keys(params).length === 0) {
|
||||||
message.warning({ content: '请至少输入一个属性值' });
|
message.warning('请至少输入一个属性值');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,11 +296,11 @@ async function handlePropertySet() {
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success({ content: '属性设置成功' });
|
message.success('属性设置成功');
|
||||||
// 延迟刷新设备消息列表
|
// 延迟刷新设备消息列表
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error({ content: '属性设置失败' });
|
message.error('属性设置失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +315,7 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
||||||
try {
|
try {
|
||||||
params = JSON.parse(valueStr);
|
params = JSON.parse(valueStr);
|
||||||
} catch {
|
} catch {
|
||||||
message.error({ content: '服务参数格式错误,请输入有效的JSON格式' });
|
message.error('服务参数格式错误,请输入有效的JSON格式');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -329,11 +329,11 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success({ content: '服务调用成功' });
|
message.success('服务调用成功');
|
||||||
// 延迟刷新设备消息列表
|
// 延迟刷新设备消息列表
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error({ content: '服务调用失败' });
|
message.error('服务调用失败');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -595,7 +595,7 @@ async function handleServiceInvoke(row: ThingModelApi.ThingModel) {
|
||||||
>
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
v-if="!messageCollapsed"
|
v-if="!messageCollapsed"
|
||||||
icon="lucide:chevron-down"
|
icon="lucide:chevron-up"
|
||||||
/>
|
/>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
v-if="messageCollapsed"
|
v-if="messageCollapsed"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ const props = defineProps<Props>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
/** 子设备列表表格列配置 */
|
/** 子设备列表表格列配置 */
|
||||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
function useGridColumns(): VxeTableGridOptions<IotDeviceApi.Device>['columns'] {
|
||||||
return [
|
return [
|
||||||
{ type: 'checkbox', width: 40 },
|
{ type: 'checkbox', width: 40 },
|
||||||
{
|
{
|
||||||
|
|
@ -190,7 +190,7 @@ function useAddGridFormSchema(): VbenFormSchema[] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function useAddGridColumns(): VxeTableGridOptions['columns'] {
|
function useAddGridColumns(): VxeTableGridOptions<IotDeviceApi.Device>['columns'] {
|
||||||
return [
|
return [
|
||||||
{ type: 'checkbox', width: 40 },
|
{ type: 'checkbox', width: 40 },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const eventThingModels = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Grid 列定义 */
|
/** Grid 列定义 */
|
||||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'reportTime',
|
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 ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
/** IoT 设备属性历史数据详情 */
|
|
||||||
defineOptions({ name: 'DeviceDetailsThingModelPropertyHistory' });
|
|
||||||
|
|
||||||
defineProps<{ deviceId: number }>();
|
defineProps<{ deviceId: number }>();
|
||||||
|
|
||||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
|
|
@ -155,11 +152,9 @@ const paginationConfig = computed(() => ({
|
||||||
async function getList() {
|
async function getList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
|
// 后端直接返回数组
|
||||||
const data = await getHistoryDevicePropertyList(queryParams);
|
const data = await getHistoryDevicePropertyList(queryParams);
|
||||||
// 后端直接返回数组,不是 { list: [] } 格式
|
list.value = (data || []) as IotDeviceApi.DevicePropertyDetail[];
|
||||||
list.value = (
|
|
||||||
Array.isArray(data) ? data : data?.list || []
|
|
||||||
) as IotDeviceApi.DevicePropertyDetail[];
|
|
||||||
total.value = list.value.length;
|
total.value = list.value.length;
|
||||||
|
|
||||||
// 如果是图表模式且支持图表展示,等待渲染图表
|
// 如果是图表模式且支持图表展示,等待渲染图表
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ let autoRefreshTimer: any = null; // 定时器
|
||||||
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
|
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
|
||||||
|
|
||||||
/** Grid 列定义 */
|
/** Grid 列定义 */
|
||||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
function useGridColumns(): VxeTableGridOptions<IotDeviceApi.DevicePropertyDetail>['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'identifier',
|
field: 'identifier',
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ const serviceThingModels = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Grid 列定义 */
|
/** Grid 列定义 */
|
||||||
function useGridColumns(): VxeTableGridOptions['columns'] {
|
function useGridColumns(): VxeTableGridOptions<Record<string, any>>['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'requestTime',
|
field: 'requestTime',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { PageParam } from '@vben/request';
|
import type { PageParam } from '@vben/request';
|
||||||
|
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
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 { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
|
@ -16,8 +16,6 @@ import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
|
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<IotDeviceApi.Device>();
|
const formData = ref<IotDeviceApi.Device>();
|
||||||
const products = ref<IotProductApi.Product[]>([]);
|
const products = ref<IotProductApi.Product[]>([]);
|
||||||
|
|
@ -35,8 +33,9 @@ const [Form, formApi] = useVbenForm({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 100,
|
||||||
},
|
},
|
||||||
wrapperClass: 'grid-cols-1',
|
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: useBasicFormSchema(),
|
schema: useBasicFormSchema(),
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
|
|
@ -62,8 +61,9 @@ const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 100,
|
||||||
},
|
},
|
||||||
wrapperClass: 'grid-cols-1',
|
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: useAdvancedFormSchema(),
|
schema: useAdvancedFormSchema(),
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGroupFormSchema } from '../data';
|
import { useGroupFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceGroupForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const deviceIds = ref<number[]>([]);
|
const deviceIds = ref<number[]>([]);
|
||||||
const getTitle = computed(() => '添加设备到分组');
|
const getTitle = computed(() => '添加设备到分组');
|
||||||
|
|
@ -24,6 +22,8 @@ const [Form, formApi] = useVbenForm({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 100,
|
||||||
},
|
},
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: useGroupFormSchema(),
|
schema: useGroupFormSchema(),
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,13 @@ import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useImportFormSchema } from '../data';
|
import { useImportFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceImportForm' });
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const [Form, formApi] = useVbenForm({
|
const [Form, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
formItemClass: 'col-span-2',
|
formItemClass: 'col-span-2',
|
||||||
labelWidth: 120,
|
labelWidth: 120,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||||
|
|
||||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: 'id',
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ async function handleDelete(row: IotDeviceGroupApi.DeviceGroup) {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await deleteDeviceGroup(row.id as number);
|
await deleteDeviceGroup(row.id!);
|
||||||
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
handleRefresh();
|
handleRefresh();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ const [Form, formApi] = useVbenForm({
|
||||||
componentProps: {
|
componentProps: {
|
||||||
class: 'w-full',
|
class: 'w-full',
|
||||||
},
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
},
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
schema: useFormSchema(),
|
schema: useFormSchema(),
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { StatsData } from './data';
|
import type { StatsData } from './data';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
@ -18,22 +18,16 @@ import MessageTrendCard from './modules/message-trend-card.vue';
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const statsData = ref<StatsData>(defaultStatsData);
|
const statsData = ref<StatsData>(defaultStatsData);
|
||||||
|
|
||||||
/** 加载统计数据 */
|
|
||||||
async function loadStatisticsData(): Promise<StatsData> {
|
|
||||||
return await getStatisticsSummary();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 加载数据 */
|
/** 加载数据 */
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
statsData.value = await loadStatisticsData();
|
statsData.value = await getStatisticsSummary();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 组件挂载时加载数据 */
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
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 type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
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 { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotProductCategoryApi } from '#/api/iot/product/category';
|
||||||
|
|
||||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
type: 'seq',
|
field: 'id',
|
||||||
title: 'ID',
|
title: 'ID',
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
<template>
|
<template>
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<FormModal @success="handleRefresh" />
|
<FormModal @success="handleRefresh" />
|
||||||
<Grid>
|
<Grid table-title="产品分类列表">
|
||||||
<template #toolbar-tools>
|
<template #toolbar-tools>
|
||||||
<TableAction
|
<TableAction
|
||||||
:actions="[
|
:actions="[
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,10 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
if (!data || !data.id) {
|
if (!data || !data.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 编辑模式:加载数据
|
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
formData.value = await getProductCategory(data.id);
|
formData.value = await getProductCategory(data.id);
|
||||||
|
// 设置到 values
|
||||||
await formApi.setValues(formData.value);
|
await formApi.setValues(formData.value);
|
||||||
} finally {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,9 @@ const loading = ref(false);
|
||||||
const productList = ref<IotProductApi.Product[]>([]);
|
const productList = ref<IotProductApi.Product[]>([]);
|
||||||
|
|
||||||
/** 处理选择变化 */
|
/** 处理选择变化 */
|
||||||
function handleChange(value?: number) {
|
function handleChange(value: any) {
|
||||||
emit('update:modelValue', value);
|
emit('update:modelValue', value as number | undefined);
|
||||||
emit('change', value);
|
emit('change', value as number | undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取产品列表 */
|
/** 获取产品列表 */
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DeviceTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
@ -109,8 +110,7 @@ export function useBasicFormSchema(
|
||||||
buttonStyle: 'solid',
|
buttonStyle: 'solid',
|
||||||
optionType: 'button',
|
optionType: 'button',
|
||||||
},
|
},
|
||||||
// TODO @AI:枚举值。或者这里不要枚举值?对齐 vue3 + ep 版本
|
defaultValue: DeviceTypeEnum.DEVICE,
|
||||||
defaultValue: 0,
|
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: ['id'],
|
triggerFields: ['id'],
|
||||||
componentProps: (values) => ({
|
componentProps: (values) => ({
|
||||||
|
|
@ -131,8 +131,7 @@ export function useBasicFormSchema(
|
||||||
// 网关子设备走网关联网,不需要联网方式
|
// 网关子设备走网关联网,不需要联网方式
|
||||||
dependencies: {
|
dependencies: {
|
||||||
triggerFields: ['deviceType'],
|
triggerFields: ['deviceType'],
|
||||||
// TODO @AI:枚举值。或者这里不要枚举值?(也看看 vben 里,其它是不是也漏了枚举值。)
|
show: (values) => values.deviceType !== DeviceTypeEnum.GATEWAY,
|
||||||
show: (values) => values.deviceType !== 2,
|
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
|
|
@ -199,7 +198,7 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表的字段 */
|
/** 列表的字段 */
|
||||||
export function useGridColumns(): VxeTableGridOptions['columns'] {
|
export function useGridColumns(): VxeTableGridOptions<IotProductApi.Product>['columns'] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
field: 'id',
|
field: 'id',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { onMounted, provide, ref } from 'vue';
|
import { onMounted, provide, ref } from 'vue';
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ async function copyToClipboard(text: string) {
|
||||||
|
|
||||||
/** 跳转到设备管理 */
|
/** 跳转到设备管理 */
|
||||||
function goToDeviceList(productId: number) {
|
function goToDeviceList(productId: number) {
|
||||||
// TODO @AI:在检查下,vben 里面有没其他也是这种路由情况的;要尽量使用 name;
|
|
||||||
router.push({
|
router.push({
|
||||||
name: 'IoTDevice',
|
name: 'IoTDevice',
|
||||||
query: { productId: String(productId) },
|
query: { productId: String(productId) },
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ async function copyToClipboard(text: string) {
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
v-if="
|
v-if="
|
||||||
[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(
|
([DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY] as number[]).includes(
|
||||||
product.deviceType!,
|
product.deviceType!,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useAccess } from '@vben/access';
|
import { useAccess } from '@vben/access';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, ProductStatusEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -190,8 +190,10 @@ onMounted(() => {
|
||||||
物模型
|
物模型
|
||||||
</Button>
|
</Button>
|
||||||
<template v-if="hasAccessByCodes(['iot:product:delete'])">
|
<template v-if="hasAccessByCodes(['iot:product:delete'])">
|
||||||
<!-- TODO @AI:使用枚举 -->
|
<Tooltip
|
||||||
<Tooltip v-if="item.status === 1" title="已发布的产品不能删除">
|
v-if="item.status === ProductStatusEnum.PUBLISHED"
|
||||||
|
title="已发布的产品不能删除"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
|
|
|
||||||
|
|
@ -41,21 +41,23 @@ const getTitle = computed(() => {
|
||||||
const [Form, formApi] = useVbenForm({
|
const [Form, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
componentProps: { class: 'w-full' },
|
componentProps: { class: 'w-full' },
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 100,
|
||||||
},
|
},
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: [],
|
schema: [],
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
wrapperClass: 'grid-cols-1',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
const [AdvancedForm, advancedFormApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
componentProps: { class: 'w-full' },
|
componentProps: { class: 'w-full' },
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 100,
|
||||||
},
|
},
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: useAdvancedFormSchema(),
|
schema: useAdvancedFormSchema(),
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
wrapperClass: 'grid-cols-1',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */
|
/** 基础表单需要 formApi 引用,所以通过 setState 设置 schema */
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,7 @@ import { $t } from '#/locales';
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
import DataRuleForm from './data-rule-form.vue';
|
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 数据流转规则列表 */
|
/** IoT 数据流转规则列表 */
|
||||||
defineOptions({ name: 'IotDataRule' });
|
|
||||||
|
|
||||||
const [FormModal, formModalApi] = useVbenModal({
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
connectedComponent: DataRuleForm,
|
connectedComponent: DataRuleForm,
|
||||||
destroyOnClose: true,
|
destroyOnClose: true,
|
||||||
|
|
@ -29,12 +25,12 @@ function handleRefresh() {
|
||||||
|
|
||||||
/** 创建规则 */
|
/** 创建规则 */
|
||||||
function handleCreate() {
|
function handleCreate() {
|
||||||
formModalApi.setData({ type: 'create' }).open();
|
formModalApi.setData(null).open();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 编辑规则 */
|
/** 编辑规则 */
|
||||||
function handleEdit(row: any) {
|
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 { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
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 [
|
return [
|
||||||
{ type: 'checkbox', width: 40 },
|
{ type: 'checkbox', width: 40 },
|
||||||
{
|
{
|
||||||
|
|
@ -49,7 +50,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
title: '数据类型',
|
title: '数据类型',
|
||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
formatter: ({ row }) =>
|
formatter: ({ row }) =>
|
||||||
getDataTypeOptionsLabel(row.property?.dataType) || '-',
|
getDataTypeOptionsLabel(row.property?.dataType ?? '') || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '数据定义',
|
title: '数据定义',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
|
@ -96,7 +97,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
refresh: true,
|
refresh: true,
|
||||||
search: true,
|
search: true,
|
||||||
},
|
},
|
||||||
},
|
} as VxeTableGridOptions<ThingModelApi.ThingModel>,
|
||||||
});
|
});
|
||||||
</script>
|
</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 { Ref } from 'vue';
|
||||||
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
|
||||||
|
|
@ -142,25 +142,25 @@ export const getDataTypeName = (dataType: string): string => {
|
||||||
return typeMap[dataType] || dataType;
|
return typeMap[dataType] || dataType;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 获取数据类型标签类型(用于 tag 的 type 属性) */
|
/** 获取数据类型标签颜色(antd Tag `color`) */
|
||||||
export const getDataTypeTagType = (
|
export const getDataTypeTagColor = (
|
||||||
dataType: string,
|
dataType: string,
|
||||||
): 'danger' | 'info' | 'primary' | 'success' | 'warning' => {
|
): 'default' | 'error' | 'processing' | 'success' | 'warning' => {
|
||||||
const tagMap: Record<
|
const tagMap: Record<
|
||||||
string,
|
string,
|
||||||
'danger' | 'info' | 'primary' | 'success' | 'warning'
|
'default' | 'error' | 'processing' | 'success' | 'warning'
|
||||||
> = {
|
> = {
|
||||||
[IoTDataSpecsDataTypeEnum.INT]: 'primary',
|
[IoTDataSpecsDataTypeEnum.INT]: 'processing',
|
||||||
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
|
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
|
||||||
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
|
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
|
||||||
[IoTDataSpecsDataTypeEnum.TEXT]: 'info',
|
[IoTDataSpecsDataTypeEnum.TEXT]: 'default',
|
||||||
[IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
|
[IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
|
||||||
[IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
|
[IoTDataSpecsDataTypeEnum.ENUM]: 'error',
|
||||||
[IoTDataSpecsDataTypeEnum.DATE]: 'primary',
|
[IoTDataSpecsDataTypeEnum.DATE]: 'processing',
|
||||||
[IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
|
[IoTDataSpecsDataTypeEnum.STRUCT]: 'default',
|
||||||
[IoTDataSpecsDataTypeEnum.ARRAY]: 'warning',
|
[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 要检查的值。
|
* @param {T} value 要检查的值。
|
||||||
* @returns {boolean} 如果值为空,返回true,否则返回false。
|
* @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) {
|
if (value === null || value === undefined) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue