From 6bb3688279ffe9ad348f4d0b34d446b090008ddd Mon Sep 17 00:00:00 2001 From: lzq <2495532633@qq.com> Date: Wed, 24 Dec 2025 01:19:09 +0800 Subject: [PATCH] 1 --- src/common/app/app-user-store.ts | 4 +- src/common/utils/strings.ts | 25 +- src/pages/sys/task/TaskForm.vue | 31 +- src/pages/sys/task/cron/Cron.vue | 31 +- src/pages/sys/task/cron/cron-expression.ts | 241 ++++++++++ .../sys/task/cron/cron-panel/CronPanel.vue | 69 +-- .../sys/task/cron/cron-panel/DayPanel.vue | 99 +--- .../sys/task/cron/cron-panel/HourPanel.vue | 76 +-- .../sys/task/cron/cron-panel/MinutePanel.vue | 77 +-- .../sys/task/cron/cron-panel/MonthPanel.vue | 99 +--- .../sys/task/cron/cron-panel/SecondPanel.vue | 76 +-- .../sys/task/cron/cron-panel/WeekPanel.vue | 109 +---- .../sys/task/cron/cron-panel/YearPanel.vue | 57 +-- src/pages/sys/task/cron/cron-store.ts | 453 ++++++++++++++++++ 14 files changed, 864 insertions(+), 583 deletions(-) create mode 100644 src/pages/sys/task/cron/cron-expression.ts create mode 100644 src/pages/sys/task/cron/cron-store.ts diff --git a/src/common/app/app-user-store.ts b/src/common/app/app-user-store.ts index 9bc2e6f..3cc1257 100644 --- a/src/common/app/app-user-store.ts +++ b/src/common/app/app-user-store.ts @@ -3,7 +3,7 @@ import { computed, ref, } from 'vue' -import { isEmpty } from '@/common/utils/strings.ts' +import Strings from '@/common/utils/strings.ts' import Evt from '@/common/utils/evt.ts' export const useAppUserStore = defineStore('AppUser', () => { @@ -15,7 +15,7 @@ export const useAppUserStore = defineStore('AppUser', () => { const token = ref(null) const tenantId = ref(null) const tenantName = ref(null) - const isAuthenticated = computed(() => !isEmpty(token.value)) + const isAuthenticated = computed(() => !Strings.isEmpty(token.value)) function $reset() { userId.value = null diff --git a/src/common/utils/strings.ts b/src/common/utils/strings.ts index f254d36..0da4d1b 100644 --- a/src/common/utils/strings.ts +++ b/src/common/utils/strings.ts @@ -3,7 +3,7 @@ * * @param str 待测字符串 */ -export function isBlank(str?: string | null) { +function isBlank(str?: string | null) { return str == null || str.trim().length === 0 } @@ -12,7 +12,7 @@ export function isBlank(str?: string | null) { * * @param str 待测字符串 */ -export function isEmpty(str?: string | null) { +function isEmpty(str?: string | null) { return str == null || str === '' } @@ -21,7 +21,7 @@ export function isEmpty(str?: string | null) { * * @param str 待处理字符串 */ -export function capitalize(str?: string | null) { +function capitalize(str?: string | null) { if (isBlank(str)) return '' return str!.charAt(0).toUpperCase() + str!.slice(1) } @@ -34,7 +34,7 @@ let splitCharPattern = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g * @param str 待处理字符串 * @param joiner 连接符处理函数 (当前单词, 单词索引)=>单词处理后的结果 */ -export function processWords(str: string | null | undefined, joiner: (word: string, index: number) => string) { +function processWords(str: string | null | undefined, joiner: (word: string, index: number) => string) { if (isBlank(str)) return '' return (str! @@ -49,7 +49,7 @@ export function processWords(str: string | null | undefined, joiner: (word: stri * * @param str 待处理字符串 */ -export function camelCase(str?: string) { +function camelCase(str?: string) { return processWords(str, (word, index) => index !== 0 ? capitalize(word) : word, ) @@ -60,7 +60,7 @@ export function camelCase(str?: string) { * * @param str 待处理字符串 */ -export function pascalCase(str?: string) { +function pascalCase(str?: string) { return capitalize(camelCase(str)) } @@ -69,7 +69,7 @@ export function pascalCase(str?: string) { * * @param str 待处理字符串 */ -export function snakeCase(str?: string) { +function snakeCase(str?: string) { return processWords(str, (word, index) => index !== 0 ? '_' + word : word, ) @@ -80,12 +80,20 @@ export function snakeCase(str?: string) { * * @param str 待处理字符串 */ -export function kebabCase(str?: string) { +function kebabCase(str?: string) { return processWords(str, (word, index) => index !== 0 ? '-' + word : word, ) } +function isNumStr(str: any) { + if (typeof str !== 'string') { + return false + } + const reg = /^[+-]?\d+(\.\d+)?$/ + return reg.test(str) +} + export default { isBlank, isEmpty, @@ -95,4 +103,5 @@ export default { kebabCase, pascalCase, processWords, + isNumStr, } diff --git a/src/pages/sys/task/TaskForm.vue b/src/pages/sys/task/TaskForm.vue index 5ba4460..64225de 100644 --- a/src/pages/sys/task/TaskForm.vue +++ b/src/pages/sys/task/TaskForm.vue @@ -17,22 +17,17 @@ - + - + - + + + + + @@ -76,7 +71,19 @@ function dialogCloseHandler() { taskFormData.value = {} } +function scheduleTypeChange(val: string) { + if (val === 'Manually') { + taskFormData.value.scheduleConf = '' + } else if (val === 'Fixed') { + taskFormData.value.scheduleConf = '1' + } else if (val === 'Cron') { + taskFormData.value.scheduleConf = '* * * * * ? ?' + } +} + function submitHandler() { + console.log(taskFormData.value.scheduleConf) + return if (status.value === 'view') return submiting.value = true if (taskFormData.value.id != null) { diff --git a/src/pages/sys/task/cron/Cron.vue b/src/pages/sys/task/cron/Cron.vue index 17dfc43..1c95547 100644 --- a/src/pages/sys/task/cron/Cron.vue +++ b/src/pages/sys/task/cron/Cron.vue @@ -1,21 +1,43 @@ diff --git a/src/pages/sys/task/cron/cron-expression.ts b/src/pages/sys/task/cron/cron-expression.ts new file mode 100644 index 0000000..3d210bc --- /dev/null +++ b/src/pages/sys/task/cron/cron-expression.ts @@ -0,0 +1,241 @@ +/** + * 提供类 Unix 风格 Cron 表达式的解析器和求值器。Cron 表达式能够指定复杂的时间组合, + * 例如“每周一至周五的上午 8:00”或“每月最后一个周五的凌晨 1:30”。 + * + *

+ * Cron 表达式由 6 个必填字段和 1 个可选字段组成,字段之间通过空格分隔。各字段的详细说明如下: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
字段名称 允许值 允许的特殊字符
秒(Seconds)  + * 0-59  + * , - * /
分(Minutes)  + * 0-59  + * , - * /
时(Hours)  + * 0-23  + * , - * /
日(Day-of-month)  + * 1-31  + * , - * ? / L W
月(Month)  + * 0-11 或 JAN-DEC(一月至十二月)  + * , - * /
周(Day-of-Week)  + * 1-7 或 SUN-SAT(周日至周六)  + * , - * ? / L #
年(Year,可选)  + * 空值、1970-2199  + * , - * /
+ *

+ * '*' 字符用于指定“所有可能的值”。例如,在“分钟”字段中使用 "*" 表示“每分钟”。 + *

+ * '?' 字符仅允许在“日(Day-of-month)”和“周(Day-of-Week)”字段中使用, + * 用于表示“不指定具体值”。当你需要在这两个字段中的一个指定条件,而另一个无需指定时,该字符非常有用。 + *

+ * '-' 字符用于指定时间范围。例如,在“小时”字段中使用 "10-12" 表示“上午 10 点、11 点和中午 12 点”。 + *

+ * ',' 字符用于指定多个额外的取值。例如,在“周”字段中使用 "MON,WED,FRI" 表示“周一、周三和周五”。 + *

+ * '/' 字符用于指定递增步长。例如,在“秒”字段中使用 "0/15" 表示“第 0 秒、15 秒、30 秒和 45 秒”; + * 而 "5/15" 在“秒”字段中表示“第 5 秒、20 秒、35 秒和 50 秒”。在 '/' 前指定 '*' 等价于以 0 作为起始值。 + * 本质上,表达式的每个字段都对应一组可启用或禁用的数字集合:秒和分的范围是 0-59,小时是 0-23, + * 日是 1-31,月是 0-11(对应 JAN 至 DEC)。'/' 字符的作用是在指定集合中启用“每隔 n 个”的取值。 + * 需要注意一个细节:在“月”字段中使用 "7/6" 仅会启用“7 月”,并不表示“每 6 个月”。 + *

+ * 'L' 字符仅允许在“日(Day-of-month)”和“周(Day-of-Week)”字段中使用,是“last(最后一个)”的缩写, + * 但在两个字段中的含义有所不同。例如,在“日”字段中使用 "L" 表示“当月的最后一天”——1 月对应 31 日, + * 非闰年的 2 月对应 28 日。若在“周”字段中单独使用 "L",仅表示“7”或“SAT(周六)”; + * 若在其他值后使用,则表示“当月最后一个指定星期几”,例如 "6L" 表示“当月最后一个周五”。 + * 你也可以指定相对于月末的偏移量,例如 "L-3" 表示“当月的倒数第三天”。 + * 注意:使用 'L' 选项时,请勿同时指定多个取值(列表)或范围,否则会得到混乱或不符合预期的结果。 + *

+ * 'W' 字符仅允许在“日(Day-of-month)”字段中使用,用于指定“距离给定日期最近的工作日(周一至周五)”。 + * 例如,若在“日”字段中指定 "15W",含义是“当月 15 日附近最近的工作日”:如果 15 日是周六, + * 则触发时间为 14 日(周五);如果 15 日是周日,则触发时间为 16 日(周一);如果 15 日是周二, + * 则触发时间为 15 日(周二)本身。若指定 "1W" 且 1 日是周六,触发时间会是 3 日(周一), + * 因为该字符不会“跨越”月份的边界。'W' 字符仅能在“日”字段指定单个日期时使用, + * 不能用于日期范围或多个日期列表。 + *

+ * 'L' 和 'W' 字符可在“日(Day-of-month)”字段中组合使用,形成 "LW",含义是“当月的最后一个工作日”。 + *

+ * '#' 字符仅允许在“周(Day-of-Week)”字段中使用,用于指定“当月第 n 个星期几”。 + * 例如,在“周”字段中使用 "6#3" 表示“当月第三个周五”(6 对应周五,"#3" 对应第三个)。 + * 其他示例:"2#1" 表示“当月第一个周一”,"4#5" 表示“当月第五个周三”。 + * 注意:若指定了 "#5" 但当月不存在对应的第 5 个星期几,则该月不会触发任务。 + * 此外,使用 '#' 字符时,“周”字段中只能包含一个表达式(例如 "3#1,6#3" 是无效的,因为它包含两个表达式)。 + *

+ * + *

+ * 所有合法字符以及月份、星期几的名称均不区分大小写。 + * + *

+ * 注意事项: + *

    + *
  • 同时指定“日(Day-of-month)”和“周(Day-of-Week)”字段的功能尚未完善, + * 你需要在其中一个字段中使用 '?' 字符。
  • + *
  • 支持“溢出范围”(即范围左侧的数字大于右侧):例如,你可以使用 22-2 表示“晚上 10 点至次日凌晨 2 点”, + * 或使用 NOV-FEB 表示“11 月至次年 2 月”。但需要特别注意,过度使用溢出范围会产生无意义的时间范围, + * 且 CronExpression 不会对这些无效范围进行特殊判断和处理(例如表达式 "0 0 14-6 ? * FRI-MON" 就是一个无意义的溢出范围示例)。
  • + *
+ *

+ * + * @author 沙拉达·詹布拉、詹姆斯·豪斯 + * @author 贡献者:麦兹·亨德森 + * @author 重构者:艾伦·克雷文(将功能从 CronTrigger 重构为 CronExpression) + *

+ * 源自 Quartz 框架 v2.3.1 版本 + */ + +export class CronExpression { + // 公共常量:最大年份(当前年份+100,与 Java 保持一致) + public static readonly MAX_YEAR: number = new Date().getFullYear() + 100 + + // 私有静态常量:字段索引(秒、分、时、日、月、周、年) + private static readonly SECOND: number = 0 + private static readonly MINUTE: number = 1 + private static readonly HOUR: number = 2 + private static readonly DAY_OF_MONTH: number = 3 + private static readonly MONTH: number = 4 + private static readonly DAY_OF_WEEK: number = 5 + private static readonly YEAR: number = 6 + + // 私有静态常量:特殊标记值(* 对应 99,? 对应 98) + private static readonly ALL_SPEC_INT: number = 99 // 对应 '*' + private static readonly NO_SPEC_INT: number = 98 // 对应 '?' + private static readonly ALL_SPEC: number = CronExpression.ALL_SPEC_INT + private static readonly NO_SPEC: number = CronExpression.NO_SPEC_INT + + // 私有静态映射:月份英文缩写 -> 数字(0-11,与 Java 一致) + private static readonly monthMap: Record = {JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11} + // 私有静态映射:星期英文缩写 -> 数字(1-7,与 Java 一致) + private static readonly dayMap: Record = {SUN: 1, MON: 2, TUE: 3, WED: 4, THU: 5, FRI: 6, SAT: 7} + + // 实例属性 + private readonly cronExpression: string + private seconds?: number[] // TS 中无原生TreeSet,后续可实现或用SortedSet替代 + private minutes?: number[] + private hours?: number[] + private daysOfMonth?: number[] + private months?: number[] + private daysOfWeek?: number[] + private years?: number[] + + private lastdayOfWeek: boolean = false + private nthdayOfWeek: number = 0 + private lastdayOfMonth: boolean = false + private nearestWeekday: boolean = false + private lastdayOffset: number = 0 + private expressionParsed: boolean = false + private timeZone?: TimeZone // 对应 Java TimeZone,TS 中可用 string 或原生 TimeZone 替代 + + + /** + * 根据指定的参数构造一个新的 CronExpression 实例。 + * + * @param cronExpression 字符串形式的 Cron 表达式,该表达式将由新建的对象进行承载 + * @throws Error 如果传入的字符串表达式无法被解析为一个有效的 CronExpression 实例,则抛出该异常 + */ + constructor(cronExpression: string) { + if (cronExpression == null) { + throw new Error('表达式不能为空') + } + + this.cronExpression = cronExpression.toUpperCase(Locale.US) + + buildExpression(this.cronExpression) + } + + /** + * 复制现有实例,创建新的 Cron 表达式 + */ + constructor(expression: CronExpression) { + this.cronExpression = expression.getCronExpression() + buildExpression(cronExpression) + if (expression.getTimeZone() != null) { + setTimeZone(expression.getTimeZone().clone()) + } + } + + /** + * 检查指定的 Cron 表达式是否能够被解析为一个有效的 Cron 表达式 + * @param cronExpression 待校验的 Cron 表达式字符串 + * @returns 布尔值:如果传入的表达式是有效的 Cron 表达式,返回 true;否则返回 false + */ + public static isValidExpression(cronExpression: string): boolean { + try { + new CronExpression(cronExpression) + } catch (e: Error) { + return false + } + return true + } + + /** + * 判断指定的日期是否满足当前 Cron 表达式的规则。 + * 注意:该方法会忽略日期的毫秒部分,因此在同一秒钟内不同毫秒数的两个日期,在此方法中会返回相同的结果。 + * @param date 待校验的日期对象 + * @returns 布尔值:如果指定日期满足 Cron 表达式规则,返回 true;否则返回 false + */ + public isSatisfiedBy(date: Date): boolean { + const testDateCal = new Date(date.getTime()) + testDateCal.setMilliseconds(0) + + const originalDate = new Date(testDateCal.getTime()) + + testDateCal.setSeconds(date.getSeconds() - 1) + + const timeAfter = this.getTimeAfter(testDateCal) + + return timeAfter !== null && timeAfter.getTime() === originalDate.getTime() + } +} + + +/** + * 时间区类型(简化版,对应 Java TimeZone) + * 可根据实际需求扩展,此处保持与原代码结构一致 + */ +type TimeZone = { + id: string; + offset: number; +} | null; diff --git a/src/pages/sys/task/cron/cron-panel/CronPanel.vue b/src/pages/sys/task/cron/cron-panel/CronPanel.vue index 5ea5b39..3ff9fb3 100644 --- a/src/pages/sys/task/cron/cron-panel/CronPanel.vue +++ b/src/pages/sys/task/cron/cron-panel/CronPanel.vue @@ -6,63 +6,42 @@ import DayPanel from '@/pages/sys/task/cron/cron-panel/DayPanel.vue' import MonthPanel from '@/pages/sys/task/cron/cron-panel/MonthPanel.vue' import WeekPanel from '@/pages/sys/task/cron/cron-panel/WeekPanel.vue' import YearPanel from '@/pages/sys/task/cron/cron-panel/YearPanel.vue' +import { useCronStore } from '@/pages/sys/task/cron/cron-store.ts' -const props = defineProps<{ - modelValue: string -}>() - -const emits = defineEmits([ 'update:modelValue' ]) - -const cronKernels = reactive({ - second: '*', - minute: '*', - hour: '*', - day: '*', - month: '*', - week: '?', - year: '?', -}) - -watch(cronKernels, newVal => { - console.log(`${newVal.second} ${newVal.minute} ${newVal.hour} ${newVal.day} ${newVal.month} ${newVal.week} ${newVal.year}`) - emits('update:modelValue', `${newVal.second} ${newVal.minute} ${newVal.hour} ${newVal.day} ${newVal.month} ${newVal.week} ${newVal.year}`) -}) -onMounted(() => { - let sections = props.modelValue.split(' ') - cronKernels.second = sections[0] - cronKernels.minute = sections[1] - cronKernels.hour = sections[2] - cronKernels.day = sections[3] - cronKernels.month = sections[4] - cronKernels.week = sections[5] - cronKernels.year = sections[6] -}) - +const { + secondVal, + minuteVal, + hourVal, + dayVal, + monthVal, + weekVal, + yearVal, +} = toRefs(useCronStore())