todo代办任务模块的实现
Todo代办任务模块的实现
#tech / dev / frame
#type / howto
#status / growing
准备
目标是添加待办事项,高大上一点,任务管理系统!!
重点在于实现复杂的任务创建、管理:
- 任务的信息
- 任务的开始时间、结束时间、时间点或时间段任务(有些任务可能只关注开始时间,如8点开始抢票;有的则关注开始时间到结束时间,具体活动)
- 重复任务(开始时间、结束时间、重复类型(每日、每周))
- 任务提醒(提前多少分钟/具体时间)
模板-实例模式(Template-Instance Pattern) 是一种将可复用的配置(模板)与具体执行(实例)分离的设计模式。在TODO应用中,这种模式特别适用于处理重复性任务。 要实现重复任务,可以采用 模板-实例 模式
- 模板负责定义任务的蓝图
- 实例负责生成具体的任务
我们需要的数据有很多
先思考任务的数据结构
时间结构
/**
* 时间点类型 - 更精确的时间表示
*/
export type TimePoint = {
/** 小时 (0-23) */
hour: number;
/** 分钟 (0-59) */
minute: number;
/** 时区 (可选,默认本地时区) */
timezone?: string;
};
/**
* 时间段类型
*/
export type TimeRange = {
/** 开始时间 */
start: TimePoint;
/** 结束时间 */
end: TimePoint;
/** 是否跨天 */
crossDay?: boolean;
};
/**
* 日期类型 - 使用更标准的格式
*/
export type DateInfo = {
/** 年份 */
year: number;
/** 月份 (1-12) */
month: number;
/** 日期 (1-31) */
day: number;
};
/**
* 完整的日期时间类型
*/
export type DateTime = {
/** 日期信息 */
date: DateInfo;
/** 时间信息 */
time?: TimePoint;
/** UTC 时间戳 (毫秒) - 用于比较和排序 */
timestamp: number;
/** ISO 字符串 - 用于存储和传输 */
isoString: string;
};
/**
* 重复规则 - 更灵活的重复模式
*/
export type RecurrenceRule = {
/** 重复类型 */
type: "none" | "daily" | "weekly" | "monthly" | "yearly" | "custom";
/** 重复间隔 (例如:每2天、每3周) */
interval?: number;
/** 结束条件 */
endCondition: {
/** 结束类型 */
type: "never" | "date" | "count";
/** 结束日期 (当 type 为 'date') */
endDate?: DateTime;
/** 重复次数 (当 type 为 'count') */
count?: number;
};
/** 重复的具体配置 */
config?: {
/** 周重复:星期几 (0=周日, 1=周一, ..., 6=周六) */
weekdays?: number[];
/** 月重复:每月的第几天 */
monthDays?: number[];
/** 月重复:每月的第几个星期几 (如第二个周一) */
monthWeekdays?: Array<{
week: number; // 第几周 (1-5, -1表示最后一周)
weekday: number; // 星期几 (0-6)
}>;
/** 年重复:月份 */
months?: number[];
};
};
/**
* 提醒规则 - 更灵活的提醒设置
*/
export type ReminderRule = {
/** 是否启用提醒 */
enabled: boolean;
/** 多个提醒时间点 */
alerts: Array<{
/** 提醒ID */
id: string;
/** 提前时间 (分钟) */
minutesBefore: number;
/** 提醒类型 */
type: "notification" | "email" | "sound";
/** 自定义消息 */
message?: string;
/** 是否已触发 */
triggered?: boolean;
}>;
/** 提醒重复设置 */
snooze?: {
/** 是否允许稍后提醒 */
enabled: boolean;
/** 稍后提醒间隔 (分钟) */
interval: number;
/** 最大重复次数 */
maxCount: number;
};
};
/**
* 任务时间配置 - 整合所有时间相关信息
*/
export type TaskTimeConfig = {
/** 任务类型 */
type: "allDay" | "timed" | "timeRange";
/** 基础时间信息 */
baseTime: {
/** 开始时间 */
start: DateTime;
/** 结束时间 (可选) */
end?: DateTime;
/** 持续时间 (分钟,用于验证) */
duration?: number;
};
/** 重复规则 */
recurrence: RecurrenceRule;
/** 提醒规则 */
reminder: ReminderRule;
/** 时区信息 */
timezone: string;
/** 夏令时处理 */
dstHandling?: "auto" | "ignore";
};
任务的数据结构
import type { TaskTimeConfig, DateTime } from "./timeStructure";
/**
* 关键结果关联
*/
export type KeyResultLink = {
/** 目标ID */
goalId: string;
/** 关键结果ID */
keyResultId: string;
/** 完成任务时增加的值 */
incrementValue: number;
};
export type ITaskTemplate = {
/** 任务模板ID */
id: string;
/** 任务标题 */
title: string;
/** 任务描述 */
description?: string;
/** 时间配置 - 使用新的时间结构 */
timeConfig: TaskTimeConfig;
/** 调度策略 */
schedulingPolicy?: {
allowReschedule: boolean;
maxDelayDays: number;
skipWeekends: boolean;
skipHolidays: boolean;
workingHoursOnly: boolean;
};
/** 元素据 */
metadata: {
category: string;
tags: string[];
estimatedDuration?: number; // 分钟
difficulty: 1 | 2 | 3 | 4 | 5;
location?: string;
};
/** 生命周期 */
lifecycle: {
status: 'draft' | 'active' | 'paused' | 'archived';
createdAt: DateTime;
updatedAt: DateTime;
activatedAt?: DateTime;
pausedAt?: DateTime;
};
/** 统计数据 */
analytics: {
totalInstances: number;
completedInstances: number;
averageCompletionTime?: number;
successRate: number;
lastInstanceDate?: DateTime;
};
/** 关联的关键结果 */
keyResultLinks?: KeyResultLink[];
/** 任务优先级 */
priority?: 1 | 2 | 3 | 4;
/** 创建时间 */
createdAt: DateTime;
/** 更新时间 */
updatedAt: DateTime;
/** 版本号 - 用于数据迁移 */
version: number;
};
export type ITaskInstance = {
/** 任务实例ID */
id: string;
/** 关联的任务模板ID */
templateId: string;
/** 任务标题 */
title: string;
/** 任务描述 */
description?: string;
/** 计划执行时间 */
scheduledTime: DateTime;
/** 实际开始时间 */
actualStartTime?: DateTime;
/** 实际结束时间 */
actualEndTime?: DateTime;
/** 任务关联的关键结果 */
keyResultLinks?: KeyResultLink[];
/** 任务优先级 */
priority: 1 | 2 | 3 | 4;
/** 任务状态 */
status: "pending" | "inProgress" | "completed" | "cancelled" | "overdue";
/** 完成时间 */
completedAt?: DateTime;
/** 提醒状态 */
reminderStatus: {
/** 已触发的提醒 */
triggeredAlerts: string[];
/** 下一个提醒时间 */
nextReminderTime?: DateTime;
/** 稍后提醒次数 */
snoozeCount: number;
};
};
/**
* 临时任务模板(用于表单)- 兼容旧格式
*/
export type ITempTaskTemplate = {
id: string;
title: string;
description?: string;
startTime?: string;
endTime?: string;
repeatPattern: {
type: "none" | "daily" | "weekly" | "monthly";
days?: number[];
startDate: string;
endDate?: string;
};
reminderPattern: {
isReminder: boolean;
timeBefore?: "5" | "10" | "15" | "30" | "60";
};
keyResultLinks?: KeyResultLink[];
priority: 1 | 2 | 3 | 4;
createdAt: string;
updatedAt: string;
};
/**
* 任务统计
*/
export interface ITaskStats {
/** 总任务数 */
total: number;
/** 已完成数 */
completed: number;
/** 今日完成数 */
todayCompleted: number;
/** 逾期数 */
overdue: number;
}
任务的数据结构
代码实现
配置数据很多,如果每次都逐个配置的话,十分影响用户体验
可以创建一个 模板工厂工具类 来生成常用模板(即模板的模板)
可以是 给定常见的任务,自己配置其他, 或者 给定常见的时间设置,配置任务内容
- 点击添加按钮,弹出模板选择
- 选择模板,根据模板返回预配置数据的任务模板编辑对话框
- 进入编辑框,编辑数据
- 点击保存按钮,调用添加服务
1. 根据数据结构创建 任务模板工厂
作为工具类来生成模板
即任务模板的模板,初始就先一个选项「自定义」,后续可根据用户需求调研来添加例如工作安排模板
模板工厂代码
// src/modules/Task/utils/taskTemplateFactory.ts
import { TimeUtils } from './timeUtils';
import type { TaskTemplate, CreateTaskTemplateOptions } from '../types/task';
import { v4 as uuidv4 } from 'uuid';
export class TaskTemplateFactory {
/**
* 创建新的空白任务模板
*/
static createEmpty(options: CreateTaskTemplateOptions = {}): TaskTemplate {
const now = TimeUtils.now();
const today = TimeUtils.createDateTime(
now.date.year,
now.date.month,
now.date.day,
{
hour: 0,
minute: 0
}
);
return {
id: uuidv4(),
title: options.title || '',
description: options.description || '',
timeConfig: {
type: 'timed', // 默认指定时间
baseTime: {
start: today,
end: TimeUtils.addMinutes(today, 60) // 默认1小时
},
recurrence: {
type: 'none', // 默认不重复
interval: 1,
endCondition: {
type: 'never'
}
},
reminder: {
enabled: false,
alerts: [],
snooze: {
enabled: false,
interval: 5, // 默认5分钟
maxCount: 1 // 默认最多1次
}
},
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
},
schedulingPolicy: {
allowReschedule: true,
maxDelayDays: 3,
skipWeekends: false,
skipHolidays: false,
workingHoursOnly: false
},
metadata: {
category: options.category || 'general',
tags: [],
estimatedDuration: 60, // 默认60分钟
difficulty: options.priority || 2,
location: ''
},
lifecycle: {
status: 'draft',
createdAt: now,
updatedAt: now
},
analytics: {
totalInstances: 0,
completedInstances: 0,
successRate: 0
},
keyResultLinks: options.keyResultLinks || [],
priority: options.priority || 2,
createdAt: now,
updatedAt: now,
version: 1
};
}
/**
* 从现有模板创建副本
*/
static createFromTemplate(template: TaskTemplate): TaskTemplate {
const now = TimeUtils.now();
return {
...template,
id: uuidv4(),
title: `${template.title} - 副本`,
lifecycle: {
...template.lifecycle,
status: 'draft',
createdAt: now,
updatedAt: now,
activatedAt: undefined,
pausedAt: undefined
},
analytics: {
totalInstances: 0,
completedInstances: 0,
averageCompletionTime: undefined,
successRate: 0,
lastInstanceDate: undefined
},
createdAt: now,
updatedAt: now
};
}
/**
* 根据模板类型创建预配置的任务模板
*/
static createByType(templateType: string, options: CreateTaskTemplateOptions = {}): TaskTemplate {
switch (templateType) {
case 'empty':
return this.createEmpty(options);
case 'habit':
return this.createHabitTemplate(options);
default:
return this.createEmpty(options);
}
}
/**
* 习惯养成模板
*/
static createHabitTemplate(options: CreateTaskTemplateOptions = {}): TaskTemplate {
const template = this.createEmpty(options);
return {
...template,
title: options.title || '新习惯',
timeConfig: {
...template.timeConfig,
type: 'timed',
recurrence: {
type: 'daily',
interval: 1,
endCondition: {
type: 'count',
count: 21 // 21天习惯养成
}
},
reminder: {
enabled: true,
alerts: [{
id: uuidv4(),
timing: {
type: 'relative',
minutesBefore: 15
},
type: 'notification',
message: '是时候培养好习惯了!'
}],
snooze: {
enabled: true,
interval: 5,
maxCount: 3
}
}
},
metadata: {
...template.metadata,
category: 'habit',
tags: ['习惯', '自我提升'],
estimatedDuration: 30
}
};
}
/**
* 验证模板数据
*/
static validate(template: TaskTemplate): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!template.title.trim()) {
errors.push('任务标题不能为空');
}
if (template.timeConfig.type === 'timed' || template.timeConfig.type === 'timeRange') {
if (!template.timeConfig.baseTime.start) {
errors.push('必须设置开始时间');
}
if (template.timeConfig.type === 'timeRange' && !template.timeConfig.baseTime.end) {
errors.push('时间段模式下必须设置结束时间');
}
if (template.timeConfig.baseTime.start && template.timeConfig.baseTime.end) {
if (template.timeConfig.baseTime.end.timestamp <= template.timeConfig.baseTime.start.timestamp) {
errors.push('结束时间必须晚于开始时间');
}
}
}
if (template.timeConfig.recurrence.type !== 'none') {
if (template.timeConfig.recurrence.endCondition.type === 'date' &&
!template.timeConfig.recurrence.endCondition.endDate) {
errors.push('重复任务必须设置结束日期');
}
if (template.timeConfig.recurrence.endCondition.type === 'count' &&
(!template.timeConfig.recurrence.endCondition.count ||
template.timeConfig.recurrence.endCondition.count <= 0)) {
errors.push('重复次数必须大于0');
}
}
// 验证关键结果链接
if (template.keyResultLinks) {
template.keyResultLinks.forEach((link, index) => {
if (!link.goalId) {
errors.push(`第${index + 1}个关联缺少目标ID`);
}
if (!link.keyResultId) {
errors.push(`第${index + 1}个关联缺少关键结果ID`);
}
if (link.incrementValue <= 0) {
errors.push(`第${index + 1}个关联的增加值必须大于0`);
}
});
}
return {
valid: errors.length === 0,
errors
};
}
}
2. 创建 时间工具类
任务数据中,时间毫无疑问是最重要的数据之一,上面的数据结构中也给出了一个独特的时间类型
export type DateTime = {
/** 日期信息 */
date: DateInfo;
/** 时间信息 */
time?: TimePoint;
/** UTC 时间戳 (毫秒) - 用于比较和排序 */
timestamp: number;
/** ISO 字符串 - 用于存储和传输 */
isoString: string;
};
对于这中自定义的时间类型,需要创建一个时间工具类来提供相关的帮助,比如将 iso 时间转为该类型等
自定义的时间的工具类
// src/modules/Task/utils/timeUtils.ts
import type { TimePoint, DateTime, DateInfo, TaskTimeConfig, ReminderRule, RecurrenceRule } from '../types/timeStructure';
export class TimeUtils {
/**
* 创建时间点
*/
static createTimePoint(hour: number, minute: number, timezone?: string): TimePoint {
return {
hour: Math.max(0, Math.min(23, hour)),
minute: Math.max(0, Math.min(59, minute)),
timezone
};
}
/**
* 创建日期时间
*/
static createDateTime(year: number, month: number, day: number, time?: TimePoint): DateTime {
const date: DateInfo = { year, month, day };
const jsDate = new Date(year, month - 1, day, time?.hour || 0, time?.minute || 0);
return {
date,
time,
timestamp: jsDate.getTime(),
isoString: jsDate.toISOString()
};
}
/**
* 检查是否为 DateTime 对象
*/
static isDateTime(obj: any): obj is DateTime {
return obj &&
typeof obj === 'object' &&
obj.timestamp &&
obj.isoString &&
obj.date;
}
/**
* 从ISO字符串创建DateTime
*/
static fromISOString(isoString: string): DateTime {
const jsDate = new Date(isoString);
return {
date: {
year: jsDate.getFullYear(),
month: jsDate.getMonth() + 1,
day: jsDate.getDate()
},
time: {
hour: jsDate.getHours(),
minute: jsDate.getMinutes()
},
timestamp: jsDate.getTime(),
isoString
};
}
/**
* 获取当前时间
*/
static now(): DateTime {
return this.fromTimestamp(Date.now());
}
/**
* 从时间戳创建DateTime
*/
static fromTimestamp(timestamp: number): DateTime {
const jsDate = new Date(timestamp);
return {
date: {
year: jsDate.getFullYear(),
month: jsDate.getMonth() + 1,
day: jsDate.getDate()
},
time: {
hour: jsDate.getHours(),
minute: jsDate.getMinutes()
},
timestamp,
isoString: jsDate.toISOString()
};
}
/**
* 从各种格式创建 DateTime
*/
static toDateTime(input: Date | string | number | DateTime): DateTime {
if (this.isDateTime(input)) {
return input as DateTime;
}
if (typeof input === 'number') {
return this.fromTimestamp(input);
}
if (typeof input === 'string') {
return this.fromISOString(input);
}
if (input instanceof Date) {
return this.fromTimestamp(input.getTime());
}
throw new Error('Invalid input type for DateTime conversion');
}
/**
* 比较两个DateTime
*/
static compare(dt1: DateTime, dt2: DateTime): number {
return dt1.timestamp - dt2.timestamp;
}
/**
* 批量比较多个时间
*/
static compareMultiple(times: DateTime[]): DateTime[] {
return [...times].sort((a, b) => this.compare(a, b));
}
/**
* 时间比较的便捷方法
*/
static isAfter(dt1: DateTime, dt2: DateTime): boolean {
return this.compare(dt1, dt2) > 0;
}
static isBefore(dt1: DateTime, dt2: DateTime): boolean {
return this.compare(dt1, dt2) < 0;
}
static isEqual(dt1: DateTime, dt2: DateTime): boolean {
return this.compare(dt1, dt2) === 0;
}
/**
* 检查时间是否在指定范围内
*/
static isInRange(target: DateTime, start: DateTime, end: DateTime): boolean {
return target.timestamp >= start.timestamp && target.timestamp <= end.timestamp;
}
/**
* 计算下一个重复时间
*/
static getNextOccurrence(config: TaskTimeConfig, from?: DateTime): DateTime | null {
const fromTime = from || this.now();
const { recurrence, baseTime } = config;
if (recurrence.type === 'none') {
return baseTime.start.timestamp > fromTime.timestamp ? baseTime.start : null;
}
// 根据重复规则计算下一次时间
return this.calculateNextByRule(baseTime.start, recurrence, fromTime);
}
/**
* 计算所有提醒时间
*/
static calculateReminderTimes(taskTime: DateTime, reminderRule: ReminderRule): DateTime[] {
if (!reminderRule.enabled) return [];
return reminderRule.alerts
.filter(alert => !alert.triggered)
.map(alert => {
if (alert.timing.type === 'absolute' && alert.timing.absoluteTime) {
return alert.timing.absoluteTime;
} else if (alert.timing.type === 'relative' && alert.timing.minutesBefore) {
return this.addMinutes(taskTime, -alert.timing.minutesBefore);
} else {
throw new Error('Invalid reminder timing configuration');
}
})
.filter(reminderTime => reminderTime.timestamp > Date.now())
.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* 添加一段时间(以分钟为单位)
*/
static addMinutes(dateTime: DateTime, minutes: number): DateTime {
const newTimestamp = dateTime.timestamp + (minutes * 60 * 1000);
return this.fromTimestamp(newTimestamp);
}
/**
* 根据重复规则计算下一次时间
*/
private static calculateNextByRule(
baseTime: DateTime,
rule: RecurrenceRule,
fromTime: DateTime
): DateTime | null {
const { type, interval = 1, endCondition, config } = rule;
// 检查是否已达到结束条件
if (endCondition.type === 'date' && endCondition.endDate &&
fromTime.timestamp > endCondition.endDate.timestamp) {
return null;
}
let nextTime: DateTime;
const baseDate = new Date(baseTime.timestamp);
const fromDate = new Date(fromTime.timestamp);
switch (type) {
case 'daily':
nextTime = this.getNextDaily(baseDate, fromDate, interval);
break;
case 'weekly':
nextTime = this.getNextWeekly(baseDate, fromDate, interval, config?.weekdays);
break;
case 'monthly':
nextTime = this.getNextMonthly(baseDate, fromDate, interval, config);
break;
default:
return null;
}
return nextTime;
}
private static getNextDaily(baseDate: Date, fromDate: Date, interval: number): DateTime {
const nextDate = new Date(Math.max(baseDate.getTime(), fromDate.getTime()));
if (nextDate.getTime() === fromDate.getTime()) {
nextDate.setDate(nextDate.getDate() + interval);
}
return this.fromTimestamp(nextDate.getTime());
}
private static getNextWeekly(
baseDate: Date,
fromDate: Date,
interval: number,
weekdays?: number[]
): DateTime {
if (!weekdays || weekdays.length === 0) {
const nextDate = new Date(Math.max(baseDate.getTime(), fromDate.getTime()));
nextDate.setDate(nextDate.getDate() + (7 * interval));
return this.fromTimestamp(nextDate.getTime());
}
// 找到下一个匹配的星期几
const currentWeekday = fromDate.getDay();
const sortedWeekdays = [...weekdays].sort((a, b) => a - b);
for (const weekday of sortedWeekdays) {
if (weekday > currentWeekday) {
const daysToAdd = weekday - currentWeekday;
const nextDate = new Date(fromDate.getTime());
nextDate.setDate(nextDate.getDate() + daysToAdd);
nextDate.setHours(baseDate.getHours(), baseDate.getMinutes(), 0, 0);
return this.fromTimestamp(nextDate.getTime());
}
}
// 如果没有找到本周的,找下周的第一个
const daysToAdd = (7 * interval) + sortedWeekdays[0] - currentWeekday;
const nextDate = new Date(fromDate.getTime());
nextDate.setDate(nextDate.getDate() + daysToAdd);
nextDate.setHours(baseDate.getHours(), baseDate.getMinutes(), 0, 0);
return this.fromTimestamp(nextDate.getTime());
}
private static getNextMonthly(
baseDate: Date,
fromDate: Date,
interval: number,
config?: any
): DateTime {
const nextDate = new Date(Math.max(baseDate.getTime(), fromDate.getTime()));
nextDate.setMonth(nextDate.getMonth() + interval);
// 处理月末日期 (如31号在2月不存在)
if (nextDate.getDate() !== baseDate.getDate()) {
nextDate.setDate(0); // 设置为上个月的最后一天
}
return this.fromTimestamp(nextDate.getTime());
}
}
3. 生成实例
任务实例的服务
import { v4 as uuidv4 } from 'uuid';
import type { TaskTemplate, ITaskInstance } from '../types/task';
import type { DateTime } from '../types/timeStructure';
import { TimeUtils } from '../utils/timeUtils';
import { useTaskStore } from '../stores/taskStore';
export class TaskInstanceService {
private taskStore = useTaskStore();
/**
* 根据任务模板生成任务实例
*/
generateInstancesFromTemplate(template: TaskTemplate, maxInstances: number = 100): ITaskInstance[] {
const instances: ITaskInstance[] = [];
const { timeConfig } = template;
if (timeConfig.recurrence.type === 'none') {
return [this.createTaskInstance(template, timeConfig.baseTime.start)];
}
// 重复任务生成逻辑
let currentTime = timeConfig.baseTime.start;
let count = 0;
const now = TimeUtils.now();
while (count < maxInstances) {
const nextTime = TimeUtils.getNextOccurrence(timeConfig, currentTime);
if (!nextTime) break;
if (this.shouldStopGeneration(timeConfig, nextTime, count)) {
break;
}
if (nextTime.timestamp >= now.timestamp) {
instances.push(this.createTaskInstance(template, nextTime));
}
currentTime = nextTime;
count++;
if (count > 1000) {
console.warn('任务实例生成达到最大限制');
break;
}
}
return instances;
}
/**
* 生成指定时间范围内的任务实例
*/
generateInstancesInRange(
template: TaskTemplate,
startDate: DateTime,
endDate: DateTime
): ITaskInstance[] {
const instances: ITaskInstance[] = [];
const { timeConfig } = template;
if (timeConfig.recurrence.type === 'none') {
if (TimeUtils.isInRange(timeConfig.baseTime.start, startDate, endDate)) {
instances.push(this.createTaskInstance(template, timeConfig.baseTime.start));
}
return instances;
}
let currentTime = timeConfig.baseTime.start;
if (currentTime.timestamp < startDate.timestamp) {
currentTime = startDate;
}
let count = 0;
const maxInstances = 1000;
while (currentTime.timestamp <= endDate.timestamp && count < maxInstances) {
const nextTime = TimeUtils.getNextOccurrence(timeConfig, currentTime);
if (!nextTime || nextTime.timestamp > endDate.timestamp) break;
if (this.shouldStopGeneration(timeConfig, nextTime, count)) {
break;
}
instances.push(this.createTaskInstance(template, nextTime));
currentTime = nextTime;
count++;
}
return instances;
}
/**
* 创建单个任务实例
*/
private createTaskInstance(template: TaskTemplate, scheduledTime: DateTime): ITaskInstance {
return {
id: uuidv4(),
templateId: template.id,
title: template.title,
description: template.description,
scheduledTime,
keyResultLinks: template.keyResultLinks ? [...template.keyResultLinks] : undefined,
priority: template.priority || 3,
status: 'pending',
reminderStatus: {
triggeredAlerts: [],
snoozeCount: 0,
nextReminderTime: template.timeConfig.reminder.enabled ?
TimeUtils.calculateReminderTimes(scheduledTime, template.timeConfig.reminder)[0] : undefined
}
};
}
/**
* 批量添加任务实例到存储
*/
async addInstancesToStore(instances: ITaskInstance[]): Promise<void> {
instances.forEach(instance => {
this.taskStore.taskInstances.push(instance);
});
await this.taskStore.saveTaskInstances();
}
/**
* 为模板生成并保存未来的任务实例
*/
async generateAndSaveInstances(template: TaskTemplate, daysAhead: number = 30): Promise<ITaskInstance[]> {
const now = TimeUtils.now();
const endDate = TimeUtils.addDays(now, daysAhead);
const instances = this.generateInstancesInRange(template, now, endDate);
await this.addInstancesToStore(instances);
return instances;
}
/**
* 检查是否应该停止生成
*/
private shouldStopGeneration(timeConfig: any, nextTime: DateTime, count: number): boolean {
if (timeConfig.recurrence.endCondition.type === 'date' &&
timeConfig.recurrence.endCondition.endDate &&
nextTime.timestamp > timeConfig.recurrence.endCondition.endDate.timestamp) {
return true;
}
if (timeConfig.recurrence.endCondition.type === 'count' &&
timeConfig.recurrence.endCondition.count &&
count >= timeConfig.recurrence.endCondition.count) {
return true;
}
return false;
}
}
4. 初始化任务实例的提醒服务
- 当任务新添加时,创建实例的同时,如果开启提醒,调用任务队列服务将实例添加到队列中
- 重新启动应用时,需要重新将开启提醒的任务实例添加到队列中
5. 制作页面组件
TaskManagementView.vue 页面框架,切换任务管理 和 实例
TaskTemplateManagement.vue 任务模板的管理页面
TaskTemplateCard.vue 展示单个任务模板
TemplateSelectionDialog.vue 任务模板的模板选择
TaskTemplateDialog.vue 创建任务模板
TaskTemplateForm.vue 任务模板编辑框
TaskInstanceManagement.vue 任务实例的管理页面