feat(iot): 将 antd 的 rule scene 迁移到 ele 里。

pull/345/head
YunaiV 2026-05-21 08:53:22 +08:00
parent b3462c3286
commit c11db17376
2 changed files with 1 additions and 393 deletions

View File

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

View File

@ -13,7 +13,7 @@ import {
IotRuleSceneTriggerTypeEnum,
} from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { formatDateTime } from '@vben/utils';
import { CronUtils, formatDateTime } from '@vben/utils';
import { Card, Col, message, Row, Tag, Tooltip } from 'ant-design-vue';
@ -25,7 +25,6 @@ import {
} from '#/api/iot/rule/scene';
import { DictTag } from '#/components/dict-tag';
import { $t } from '#/locales';
import { CronUtils } from '#/utils/cron';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';