feat: support custom task recurrence
ci / docker (push) Successful in 5m54s

This commit is contained in:
2026-09-08 08:42:32 +08:00
parent 0a3b959cb8
commit 2eca978820
8 changed files with 110 additions and 15 deletions
+40
View File
@@ -104,6 +104,46 @@ export function moveItemWithinScope<T extends { id: string; parent_id?: string |
return items.map((item) => (item.parent_id ?? null) === (source.parent_id ?? null) ? reorderedIterator.next().value! : item)
}
export type TaskRepeatConfig = {
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly'
interval: number
weekdays?: string[]
monthDays?: number[]
endMode: 'never' | 'date' | 'count'
count?: number
until?: string
}
export function buildTaskRrule(config: TaskRepeatConfig) {
const interval = Math.floor(Number(config.interval))
if (!Number.isFinite(interval) || interval < 1) throw new Error('重复间隔至少为 1')
if (config.frequency === 'weekly' && !config.weekdays?.length) throw new Error('至少选择一个重复日期')
if (config.frequency === 'monthly' && (!config.monthDays?.length || config.monthDays.some((day) => day < 1 || day > 31))) throw new Error('请输入 1 到 31 的每月日期')
if (config.endMode === 'count' && (!config.count || config.count < 1)) throw new Error('重复次数至少为 1')
if (config.endMode === 'date' && !config.until) throw new Error('请选择结束日期')
const parts = [`FREQ=${config.frequency.toUpperCase()}`]
if (interval > 1) parts.push(`INTERVAL=${interval}`)
if (config.frequency === 'weekly' && config.weekdays?.length) parts.push(`BYDAY=${config.weekdays.join(',')}`)
if (config.frequency === 'monthly' && config.monthDays?.length) parts.push(`BYMONTHDAY=${config.monthDays.join(',')}`)
if (config.endMode === 'count') parts.push(`COUNT=${Math.max(1, Math.floor(config.count ?? 1))}`)
if (config.endMode === 'date' && config.until) parts.push(`UNTIL=${config.until}T23:59:59`)
return parts.join(';')
}
export function parseTaskRrule(rrule = ''): TaskRepeatConfig {
const parts = Object.fromEntries(rrule.split(';').filter(Boolean).map((part) => part.split('=', 2)))
const frequency = (parts.FREQ?.toLowerCase() || 'daily') as TaskRepeatConfig['frequency']
return {
frequency,
interval: Math.max(1, Number(parts.INTERVAL ?? 1)),
weekdays: parts.BYDAY?.split(',').filter(Boolean) ?? [],
monthDays: parts.BYMONTHDAY?.split(',').map(Number).filter(Number.isFinite) ?? [],
endMode: parts.COUNT ? 'count' : parts.UNTIL ? 'date' : 'never',
count: parts.COUNT ? Number(parts.COUNT) : 1,
until: parts.UNTIL?.slice(0, 10) ?? '',
}
}
export function toDateTimeLocal(value: string | null | undefined) {
if (!value) return ''
const date = new Date(value)