2026-04-13 17:36:44 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 工作周报管理工具
|
|
|
|
|
|
* 用于管理个人工作周报,包括制定计划、记录工作、查询周报等
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
|
|
|
|
// 数据文件路径
|
|
|
|
|
|
const DATA_DIR = path.join(__dirname, '..', 'data');
|
|
|
|
|
|
const DATA_FILE = path.join(DATA_DIR, 'weekly-reports.json');
|
|
|
|
|
|
|
|
|
|
|
|
// 确保数据目录存在
|
|
|
|
|
|
if (!fs.existsSync(DATA_DIR)) {
|
|
|
|
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成 UUID
|
|
|
|
|
|
*/
|
|
|
|
|
|
function generateUUID() {
|
|
|
|
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
|
|
|
|
const r = Math.random() * 16 | 0;
|
|
|
|
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
|
|
|
|
return v.toString(16);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取本地日期字符串 YYYY-MM-DD
|
|
|
|
|
|
*/
|
|
|
|
|
|
function toLocalDateString(date) {
|
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前日期字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getCurrentDateStr() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
return now.toISOString().split('T')[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前时间 ISO 字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getCurrentISO() {
|
|
|
|
|
|
return new Date().toISOString();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 判断是否为法定工作日(周一至周五)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function isWorkday(dateStr) {
|
|
|
|
|
|
const date = new Date(dateStr);
|
|
|
|
|
|
const day = date.getDay();
|
|
|
|
|
|
return day >= 1 && day <= 5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取日期所在周的第一天(周一)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getWeekStartDate(dateStr) {
|
|
|
|
|
|
const date = new Date(dateStr);
|
|
|
|
|
|
const day = date.getDay();
|
|
|
|
|
|
// 计算到周一的天数
|
|
|
|
|
|
const diff = day === 0 ? -6 : 1 - day;
|
|
|
|
|
|
const monday = new Date(date);
|
|
|
|
|
|
monday.setDate(date.getDate() + diff);
|
|
|
|
|
|
return toLocalDateString(monday);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取日期所在周的周五
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getWeekEndDate(weekStartDate) {
|
|
|
|
|
|
const monday = new Date(weekStartDate);
|
|
|
|
|
|
const friday = new Date(monday);
|
|
|
|
|
|
friday.setDate(monday.getDate() + 4);
|
|
|
|
|
|
return toLocalDateString(friday);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取 ISO 周编号 (YYYY-Www)
|
2026-04-15 17:21:12 +08:00
|
|
|
|
* 遵循 ISO 8601 标准:每周从周一开始,第一个包含1月4日的周是W1
|
2026-04-13 17:36:44 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function getWeekNumber(dateStr) {
|
|
|
|
|
|
const date = new Date(dateStr);
|
2026-04-15 17:21:12 +08:00
|
|
|
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
// 找到该日期所在周的周四(ISO 8601 以包含1月4日的周为W1)
|
2026-04-13 17:36:44 +08:00
|
|
|
|
const thursday = new Date(date);
|
|
|
|
|
|
thursday.setDate(date.getDate() + (4 - (date.getDay() === 0 ? 7 : date.getDay())));
|
2026-04-15 17:21:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算该年的1月1日是周几
|
2026-04-13 17:36:44 +08:00
|
|
|
|
const yearStart = new Date(thursday.getFullYear(), 0, 1);
|
2026-04-15 17:21:12 +08:00
|
|
|
|
const jan1Day = yearStart.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
|
|
|
|
|
|
|
|
|
|
|
|
// 计算这是该年的第几天(1月1日=1)
|
|
|
|
|
|
const dayOfYear = Math.floor((thursday - yearStart) / MS_PER_DAY) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
// ISO 周号:(dayOfYear + 该年1月1日的星期几 - 1) / 7 向上取整
|
|
|
|
|
|
// 如果1月1日是周四(day=4),则 Jan1 就在 W01 中
|
|
|
|
|
|
// 如果1月1日是周五/周六/周日,则需要调整(这些天属于上一年最后一周)
|
|
|
|
|
|
const weekNum = Math.ceil((dayOfYear + (jan1Day === 0 ? 6 : jan1Day) - 1) / 7);
|
|
|
|
|
|
|
|
|
|
|
|
// 处理年末/年初边界情况:
|
|
|
|
|
|
// 如果计算的周号是0,说明属于上一年最后一周
|
|
|
|
|
|
// 如果周号超过该年最大周数,说明属于下一年第一周
|
|
|
|
|
|
let year = thursday.getFullYear();
|
|
|
|
|
|
let finalWeek = weekNum;
|
|
|
|
|
|
|
|
|
|
|
|
if (finalWeek === 0) {
|
|
|
|
|
|
// 上一年最后一周
|
|
|
|
|
|
year = year - 1;
|
|
|
|
|
|
const prevJan1Day = new Date(year, 0, 1).getDay();
|
|
|
|
|
|
finalWeek = prevJan1Day === 4 || (prevJan1Day === 3 && isLeapYear(year)) ? 53 : 52;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const maxWeeks = (jan1Day === 4 || (jan1Day === 3 && isLeapYear(thursday.getFullYear()))) ? 53 : 52;
|
|
|
|
|
|
if (finalWeek > maxWeeks) {
|
|
|
|
|
|
// 下一年第一周
|
|
|
|
|
|
year = year + 1;
|
|
|
|
|
|
finalWeek = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `${year}-W${String(finalWeek).padStart(2, '0')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isLeapYear(year) {
|
|
|
|
|
|
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
|
2026-04-13 17:36:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前工作周信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getCurrentWeek() {
|
|
|
|
|
|
const today = getCurrentDateStr();
|
|
|
|
|
|
return getWeekInfo(today);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取指定日期所在的工作周信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getWeekInfo(dateStr) {
|
|
|
|
|
|
const weekStart = getWeekStartDate(dateStr);
|
|
|
|
|
|
const weekEnd = getWeekEndDate(weekStart);
|
|
|
|
|
|
const weekNumber = getWeekNumber(dateStr);
|
|
|
|
|
|
return {
|
|
|
|
|
|
weekNumber,
|
|
|
|
|
|
startDate: weekStart,
|
|
|
|
|
|
endDate: weekEnd
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析周标识为周编号
|
|
|
|
|
|
*/
|
|
|
|
|
|
function parseWeekIdentifier(identifier) {
|
|
|
|
|
|
if (!identifier || identifier === '本周' || identifier === '当前周') {
|
|
|
|
|
|
return getCurrentWeek().weekNumber;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (identifier === '上周') {
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
today.setDate(today.getDate() - 7);
|
|
|
|
|
|
return getWeekNumber(today.toISOString().split('T')[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (identifier === '上上周') {
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
today.setDate(today.getDate() - 14);
|
|
|
|
|
|
return getWeekNumber(today.toISOString().split('T')[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (identifier === '下周') {
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
today.setDate(today.getDate() + 7);
|
|
|
|
|
|
return getWeekNumber(today.toISOString().split('T')[0]);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 假设是 YYYY-Www 格式
|
|
|
|
|
|
if (/^\d{4}-W\d{2}$/.test(identifier)) {
|
|
|
|
|
|
return identifier;
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(`无法解析周标识: ${identifier}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载数据文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
function loadData() {
|
|
|
|
|
|
if (!fs.existsSync(DATA_FILE)) {
|
|
|
|
|
|
const initial = {
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
|
createdAt: getCurrentISO(),
|
|
|
|
|
|
updatedAt: getCurrentISO()
|
|
|
|
|
|
},
|
|
|
|
|
|
weeklyReports: {}
|
|
|
|
|
|
};
|
|
|
|
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify(initial, null, 2), 'utf8');
|
|
|
|
|
|
return initial;
|
|
|
|
|
|
}
|
|
|
|
|
|
const content = fs.readFileSync(DATA_FILE, 'utf8');
|
|
|
|
|
|
return JSON.parse(content);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 保存数据文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
function saveData(data) {
|
|
|
|
|
|
data.meta.updatedAt = getCurrentISO();
|
|
|
|
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从周编号反推该周的周一日期
|
|
|
|
|
|
* 使用 ISO 8601 标准:每年第一个周四所在的周为第1周(W1从该周的周一开始)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getWeekStartFromWeekNumber(year, weekNum) {
|
|
|
|
|
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
|
|
// 找到该年1月4日(ISO 8601 定义:包含该年第一个周四的那一周是W1)
|
|
|
|
|
|
const jan4 = new Date(year, 0, 4);
|
|
|
|
|
|
const jan4Day = jan4.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
|
|
|
|
|
|
|
2026-04-15 17:21:12 +08:00
|
|
|
|
// 找到W1的周一(ISO 8601:每年第一个包含1月4日的周是W1,该周的周一是W1起始)
|
|
|
|
|
|
// jan4Day: 0=Sun, 1=Mon, ..., 6=Sat
|
|
|
|
|
|
// 如果1月4日是周日,W1周一是Dec 29(上周一)
|
|
|
|
|
|
// 如果1月4日是周一,W1周一是1月4日
|
|
|
|
|
|
// 否则,W1周一是1月4日往前到周一的天数
|
|
|
|
|
|
const daysToSubtract = (jan4Day === 0 ? 7 : jan4Day) - 1;
|
|
|
|
|
|
const firstMonday = new Date(jan4.getTime() - daysToSubtract * MS_PER_DAY);
|
2026-04-13 17:36:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算目标周的周一
|
|
|
|
|
|
const targetMonday = new Date(firstMonday.getTime() + (weekNum - 1) * 7 * MS_PER_DAY);
|
|
|
|
|
|
return targetMonday;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取或创建指定周的周报对象
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getOrCreateWeeklyReport(weekNumber) {
|
|
|
|
|
|
const data = loadData();
|
|
|
|
|
|
if (!data.weeklyReports[weekNumber]) {
|
|
|
|
|
|
const [yearStr, weekPart] = weekNumber.split('-W');
|
|
|
|
|
|
const year = parseInt(yearStr, 10);
|
|
|
|
|
|
const weekNum = parseInt(weekPart, 10);
|
|
|
|
|
|
|
|
|
|
|
|
const targetMonday = getWeekStartFromWeekNumber(year, weekNum);
|
|
|
|
|
|
const targetFriday = new Date(targetMonday);
|
|
|
|
|
|
targetFriday.setDate(targetMonday.getDate() + 4);
|
|
|
|
|
|
|
|
|
|
|
|
data.weeklyReports[weekNumber] = {
|
|
|
|
|
|
weekNumber,
|
|
|
|
|
|
weekStartDate: toLocalDateString(targetMonday),
|
|
|
|
|
|
weekEndDate: toLocalDateString(targetFriday),
|
|
|
|
|
|
plan: [],
|
|
|
|
|
|
records: {},
|
|
|
|
|
|
summary: ''
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return { data, report: data.weeklyReports[weekNumber] };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 制定/更新周计划
|
|
|
|
|
|
*/
|
2026-04-15 09:50:26 +08:00
|
|
|
|
function planWeek(weekNumber, tasks, priority = 'medium', expectedDate = null) {
|
2026-04-13 17:36:44 +08:00
|
|
|
|
const { data, report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
const now = getCurrentISO();
|
|
|
|
|
|
|
|
|
|
|
|
for (const taskDesc of tasks) {
|
|
|
|
|
|
const planItem = {
|
|
|
|
|
|
id: generateUUID(),
|
|
|
|
|
|
description: taskDesc,
|
2026-04-15 09:50:26 +08:00
|
|
|
|
expectedDate: expectedDate,
|
2026-04-13 17:36:44 +08:00
|
|
|
|
priority: priority,
|
|
|
|
|
|
status: 'pending',
|
|
|
|
|
|
createdAt: now,
|
|
|
|
|
|
updatedAt: now
|
|
|
|
|
|
};
|
|
|
|
|
|
report.plan.push(planItem);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
saveData(data);
|
|
|
|
|
|
return report;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 添加工作记录
|
|
|
|
|
|
*/
|
|
|
|
|
|
function addRecord(date, content, status, options = {}) {
|
|
|
|
|
|
const { hours, planItemId } = options;
|
|
|
|
|
|
|
|
|
|
|
|
if (!isWorkday(date)) {
|
|
|
|
|
|
throw new Error(`${date} 不是法定工作日(周一至周五)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const weekInfo = getWeekInfo(date);
|
|
|
|
|
|
const { data, report } = getOrCreateWeeklyReport(weekInfo.weekNumber);
|
|
|
|
|
|
|
|
|
|
|
|
if (!report.records[date]) {
|
|
|
|
|
|
report.records[date] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const record = {
|
|
|
|
|
|
id: generateUUID(),
|
|
|
|
|
|
content,
|
|
|
|
|
|
date,
|
|
|
|
|
|
status,
|
|
|
|
|
|
hours: hours || null,
|
|
|
|
|
|
planItemId: planItemId || null,
|
|
|
|
|
|
createdAt: getCurrentISO(),
|
|
|
|
|
|
updatedAt: getCurrentISO()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
report.records[date].push(record);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有关联的计划项,自动更新其状态
|
|
|
|
|
|
if (planItemId) {
|
|
|
|
|
|
const planItem = report.plan.find(p => p.id === planItemId);
|
|
|
|
|
|
if (planItem) {
|
|
|
|
|
|
planItem.status = status === 'completed' ? 'completed' :
|
|
|
|
|
|
status === 'in-progress' ? 'in-progress' : planItem.status;
|
|
|
|
|
|
planItem.updatedAt = getCurrentISO();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
saveData(data);
|
|
|
|
|
|
return report;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取未完成的工作项
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getPendingItems(weekIdentifier) {
|
|
|
|
|
|
const weekNumber = parseWeekIdentifier(weekIdentifier);
|
|
|
|
|
|
const { data, report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
|
|
|
|
|
|
const pending = report.plan.filter(p => p.status === 'pending' || p.status === 'in-progress');
|
|
|
|
|
|
|
|
|
|
|
|
// 按优先级排序
|
|
|
|
|
|
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
|
|
|
|
pending.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
|
|
|
|
|
|
|
|
|
|
return pending;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询周报
|
|
|
|
|
|
*/
|
|
|
|
|
|
function queryWeek(weekIdentifier, format = 'detail') {
|
|
|
|
|
|
const weekNumber = parseWeekIdentifier(weekIdentifier);
|
|
|
|
|
|
const { report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
|
|
|
|
|
|
if (format === 'summary') {
|
|
|
|
|
|
return formatWeekSummary(report);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return report;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 格式化周报摘要
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatWeekSummary(report) {
|
|
|
|
|
|
const total = report.plan.length;
|
|
|
|
|
|
const completed = report.plan.filter(p => p.status === 'completed').length;
|
|
|
|
|
|
const pending = report.plan.filter(p => p.status === 'pending' || p.status === 'in-progress');
|
|
|
|
|
|
|
|
|
|
|
|
let output = `周报摘要: ${report.weekNumber} (${report.weekStartDate} 至 ${report.weekEndDate})\n`;
|
|
|
|
|
|
output += '================================================================\n\n';
|
|
|
|
|
|
|
|
|
|
|
|
output += `计划完成率: ${completed}/${total}`;
|
|
|
|
|
|
if (total > 0) {
|
|
|
|
|
|
output += ` (${Math.round(completed / total * 100)}%)`;
|
|
|
|
|
|
}
|
|
|
|
|
|
output += '\n\n';
|
|
|
|
|
|
|
|
|
|
|
|
if (report.plan.length > 0) {
|
|
|
|
|
|
output += '计划项:\n';
|
|
|
|
|
|
for (const item of report.plan) {
|
|
|
|
|
|
const statusIcon = item.status === 'completed' ? '[√]' : '[ ]';
|
|
|
|
|
|
const priorityTag = item.priority === 'high' ? '[高]' : item.priority === 'low' ? '[低]' : '[中]';
|
|
|
|
|
|
output += ` ${statusIcon} ${priorityTag} ${item.description}`;
|
|
|
|
|
|
if (item.expectedDate) {
|
|
|
|
|
|
output += ` (预计: ${item.expectedDate})`;
|
|
|
|
|
|
}
|
|
|
|
|
|
output += '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
output += '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Object.keys(report.records).length > 0) {
|
|
|
|
|
|
output += '每日记录:\n';
|
|
|
|
|
|
const sortedDates = Object.keys(report.records).sort();
|
|
|
|
|
|
for (const date of sortedDates) {
|
|
|
|
|
|
const records = report.records[date];
|
|
|
|
|
|
const hours = records.reduce((sum, r) => sum + (r.hours || 0), 0);
|
|
|
|
|
|
const content = records.map(r => r.content).join('; ');
|
|
|
|
|
|
output += ` - ${date}: ${content}`;
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
|
output += ` (${hours}h)`;
|
|
|
|
|
|
}
|
|
|
|
|
|
output += '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
output += '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pending.length > 0) {
|
|
|
|
|
|
output += '未完成工作:\n';
|
|
|
|
|
|
for (const item of pending) {
|
|
|
|
|
|
output += ` - ${item.description}\n`;
|
|
|
|
|
|
}
|
|
|
|
|
|
output += '\n';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (report.summary) {
|
|
|
|
|
|
output += `总结: ${report.summary}\n`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新计划项状态
|
|
|
|
|
|
*/
|
|
|
|
|
|
function updatePlanItemStatus(weekNumber, planItemId, status) {
|
|
|
|
|
|
const { data, report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
|
|
|
|
|
|
const planItem = report.plan.find(p => p.id === planItemId);
|
|
|
|
|
|
if (!planItem) {
|
|
|
|
|
|
throw new Error(`未找到计划项: ${planItemId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
planItem.status = status;
|
|
|
|
|
|
planItem.updatedAt = getCurrentISO();
|
|
|
|
|
|
|
|
|
|
|
|
saveData(data);
|
|
|
|
|
|
return report;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 09:50:26 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 延期计划项
|
|
|
|
|
|
* 将计划项的预计完成日期延长到指定日期
|
|
|
|
|
|
*/
|
|
|
|
|
|
function extendPlanItem(weekNumber, planItemId, newExpectedDate, reason = '') {
|
|
|
|
|
|
const { data, report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
|
|
|
|
|
|
const planItem = report.plan.find(p => p.id === planItemId);
|
|
|
|
|
|
if (!planItem) {
|
|
|
|
|
|
throw new Error(`未找到计划项: ${planItemId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const oldDate = planItem.expectedDate;
|
|
|
|
|
|
planItem.expectedDate = newExpectedDate;
|
|
|
|
|
|
planItem.updatedAt = getCurrentISO();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果传入了延期原因,记录在 extendedReason 字段
|
|
|
|
|
|
if (reason) {
|
|
|
|
|
|
planItem.extendedReason = reason;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 增加延期次数统计
|
|
|
|
|
|
planItem.extendCount = (planItem.extendCount || 0) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
saveData(data);
|
|
|
|
|
|
return { report, planItem, oldDate, newDate: newExpectedDate };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 17:36:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 设置周报总结
|
|
|
|
|
|
*/
|
|
|
|
|
|
function setSummary(weekNumber, summary) {
|
|
|
|
|
|
const { data, report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
report.summary = summary;
|
|
|
|
|
|
saveData(data);
|
|
|
|
|
|
return report;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除计划项
|
|
|
|
|
|
*/
|
|
|
|
|
|
function removePlanItem(weekNumber, planItemId) {
|
|
|
|
|
|
const { data, report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
const index = report.plan.findIndex(p => p.id === planItemId);
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
throw new Error(`未找到计划项: ${planItemId}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
report.plan.splice(index, 1);
|
|
|
|
|
|
saveData(data);
|
|
|
|
|
|
return report;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取工作周的所有工作日
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getWorkdays(weekNumber) {
|
|
|
|
|
|
const { report } = getOrCreateWeeklyReport(weekNumber);
|
|
|
|
|
|
const workdays = [];
|
|
|
|
|
|
const current = new Date(report.weekStartDate);
|
|
|
|
|
|
const end = new Date(report.weekEndDate);
|
|
|
|
|
|
|
|
|
|
|
|
while (current <= end) {
|
|
|
|
|
|
workdays.push(current.toISOString().split('T')[0]);
|
|
|
|
|
|
current.setDate(current.getDate() + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return workdays;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === CLI 模式 ===
|
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
const command = args[0];
|
|
|
|
|
|
|
|
|
|
|
|
// 已知的标志列表
|
2026-04-15 09:50:26 +08:00
|
|
|
|
const FLAGS = ['--week', '--tasks', '--priority', '--date', '--content', '--status', '--hours', '--plan-item', '--format', '--expected-date'];
|
2026-04-13 17:36:44 +08:00
|
|
|
|
|
|
|
|
|
|
function getArgValue(flag) {
|
|
|
|
|
|
const index = args.indexOf(flag);
|
|
|
|
|
|
if (index !== -1 && index + 1 < args.length) {
|
|
|
|
|
|
const nextArg = args[index + 1];
|
|
|
|
|
|
// 如果下一个参数也是标志,返回null
|
|
|
|
|
|
if (FLAGS.includes(nextArg)) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return nextArg;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 支持 --flag=value 格式
|
|
|
|
|
|
const combined = args.find(arg => arg.startsWith(`${flag}=`));
|
|
|
|
|
|
if (combined) {
|
|
|
|
|
|
return combined.split('=')[1];
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getRemainingArgs(flag) {
|
|
|
|
|
|
const index = args.indexOf(flag);
|
|
|
|
|
|
if (index === -1) return [];
|
|
|
|
|
|
|
|
|
|
|
|
const result = [];
|
|
|
|
|
|
let i = index + 1;
|
|
|
|
|
|
|
|
|
|
|
|
while (i < args.length) {
|
|
|
|
|
|
const arg = args[i];
|
|
|
|
|
|
// 如果遇到另一个标志,停止
|
|
|
|
|
|
if (FLAGS.includes(arg)) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
result.push(arg);
|
|
|
|
|
|
i++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
switch (command) {
|
|
|
|
|
|
case 'plan': {
|
|
|
|
|
|
const week = getArgValue('--week') || getCurrentWeek().weekNumber;
|
|
|
|
|
|
const tasks = getRemainingArgs('--tasks');
|
|
|
|
|
|
if (tasks.length === 0) {
|
|
|
|
|
|
console.error('错误: 请提供任务列表 (--tasks "任务1" "任务2" ...)');
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const priority = getArgValue('--priority') || 'medium';
|
2026-04-15 09:50:26 +08:00
|
|
|
|
const expectedDate = getArgValue('--expected-date');
|
|
|
|
|
|
const report = planWeek(week, tasks, priority, expectedDate);
|
2026-04-13 17:36:44 +08:00
|
|
|
|
console.log(`已在 ${report.weekNumber} 创建 ${tasks.length} 个计划项`);
|
|
|
|
|
|
console.log('\n计划项:');
|
|
|
|
|
|
report.plan.slice(-tasks.length).forEach((item, i) => {
|
2026-04-15 09:50:26 +08:00
|
|
|
|
const dateStr = item.expectedDate ? ` [预计: ${item.expectedDate}]` : '';
|
|
|
|
|
|
console.log(` ${i + 1}. [${item.priority}] ${item.description}${dateStr} (${item.id})`);
|
2026-04-13 17:36:44 +08:00
|
|
|
|
});
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'record': {
|
|
|
|
|
|
const date = getArgValue('--date') || getCurrentDateStr();
|
|
|
|
|
|
const content = getArgValue('--content');
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
|
console.error('错误: 请提供工作内容 (--content "内容")');
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const status = getArgValue('--status') || 'in-progress';
|
|
|
|
|
|
const hours = getArgValue('--hours') ? parseFloat(getArgValue('--hours')) : null;
|
|
|
|
|
|
const planItemId = getArgValue('--plan-item');
|
|
|
|
|
|
const report = addRecord(date, content, status, { hours, planItemId });
|
|
|
|
|
|
console.log(`已在 ${date} 添加工作记录`);
|
|
|
|
|
|
console.log(`周 ${report.weekNumber} 当前共有 ${report.plan.length} 个计划项`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'pending': {
|
|
|
|
|
|
const week = getArgValue('--week') || getCurrentWeek().weekNumber;
|
|
|
|
|
|
const pending = getPendingItems(week);
|
|
|
|
|
|
const currentWeek = getCurrentWeek();
|
|
|
|
|
|
console.log(`本周未完成的工作 (${week})\n`);
|
|
|
|
|
|
console.log('========================================\n');
|
|
|
|
|
|
|
|
|
|
|
|
if (pending.length === 0) {
|
|
|
|
|
|
console.log('所有任务已完成!');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const high = pending.filter(p => p.priority === 'high');
|
|
|
|
|
|
const medium = pending.filter(p => p.priority === 'medium');
|
|
|
|
|
|
const low = pending.filter(p => p.priority === 'low');
|
|
|
|
|
|
|
|
|
|
|
|
if (high.length > 0) {
|
|
|
|
|
|
console.log('[高优先级]');
|
|
|
|
|
|
high.forEach(p => console.log(` - ${p.description}`));
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (medium.length > 0) {
|
|
|
|
|
|
console.log('[中优先级]');
|
|
|
|
|
|
medium.forEach(p => console.log(` - ${p.description}`));
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (low.length > 0) {
|
|
|
|
|
|
console.log('[低优先级]');
|
|
|
|
|
|
low.forEach(p => console.log(` - ${p.description}`));
|
|
|
|
|
|
console.log();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log(`共 ${pending.length} 项未完成任务`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'query': {
|
|
|
|
|
|
const week = getArgValue('--week') || '本周';
|
|
|
|
|
|
const format = getArgValue('--format') || 'detail';
|
|
|
|
|
|
const weekNumber = parseWeekIdentifier(week);
|
|
|
|
|
|
const result = queryWeek(weekNumber, format);
|
|
|
|
|
|
console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'update-status': {
|
|
|
|
|
|
const weekIdentifier = getArgValue('--week') || '本周';
|
|
|
|
|
|
const planItemId = getArgValue('--plan-item');
|
|
|
|
|
|
const status = getArgValue('--status');
|
|
|
|
|
|
if (!planItemId || !status) {
|
|
|
|
|
|
console.error('错误: 请提供计划项ID (--plan-item) 和状态 (--status)');
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const weekNumber = parseWeekIdentifier(weekIdentifier);
|
|
|
|
|
|
const report = updatePlanItemStatus(weekNumber, planItemId, status);
|
|
|
|
|
|
console.log(`已更新计划项 ${planItemId} 状态为 ${status}`);
|
|
|
|
|
|
console.log(`周 ${report.weekNumber} 计划完成率: ${report.plan.filter(p => p.status === 'completed').length}/${report.plan.length}`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'summary': {
|
|
|
|
|
|
const weekIdentifier = getArgValue('--week') || '本周';
|
|
|
|
|
|
const content = getArgValue('--content');
|
|
|
|
|
|
if (!content) {
|
|
|
|
|
|
console.error('错误: 请提供总结内容 (--content "内容")');
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const weekNumber = parseWeekIdentifier(weekIdentifier);
|
|
|
|
|
|
const report = setSummary(weekNumber, content);
|
|
|
|
|
|
console.log(`已在 ${weekNumber} 设置周报总结`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case 'remove-plan': {
|
|
|
|
|
|
const weekIdentifier = getArgValue('--week') || '本周';
|
|
|
|
|
|
const planItemId = getArgValue('--plan-item');
|
|
|
|
|
|
if (!planItemId) {
|
|
|
|
|
|
console.error('错误: 请提供计划项ID (--plan-item)');
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const weekNumber = parseWeekIdentifier(weekIdentifier);
|
|
|
|
|
|
const report = removePlanItem(weekNumber, planItemId);
|
|
|
|
|
|
console.log(`已删除计划项 ${planItemId}`);
|
|
|
|
|
|
console.log(`周 ${report.weekNumber} 剩余 ${report.plan.length} 个计划项`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 09:50:26 +08:00
|
|
|
|
case 'extend-plan': {
|
|
|
|
|
|
const weekIdentifier = getArgValue('--week') || '本周';
|
|
|
|
|
|
const planItemId = getArgValue('--plan-item');
|
|
|
|
|
|
const newDate = getArgValue('--new-date');
|
|
|
|
|
|
const reason = getArgValue('--reason') || '';
|
|
|
|
|
|
if (!planItemId || !newDate) {
|
|
|
|
|
|
console.error('错误: 请提供计划项ID (--plan-item) 和新日期 (--new-date YYYY-MM-DD)');
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
const weekNumber = parseWeekIdentifier(weekIdentifier);
|
|
|
|
|
|
const result = extendPlanItem(weekNumber, planItemId, newDate, reason);
|
|
|
|
|
|
const item = result.planItem;
|
|
|
|
|
|
console.log(`已将计划项延期:`);
|
|
|
|
|
|
console.log(` 任务: ${item.description}`);
|
|
|
|
|
|
console.log(` 原定日期: ${result.oldDate || '未设置'}`);
|
|
|
|
|
|
console.log(` 新日期: ${result.newDate}`);
|
|
|
|
|
|
if (reason) console.log(` 延期原因: ${reason}`);
|
|
|
|
|
|
console.log(` 延期次数: ${item.extendCount} 次`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 17:36:44 +08:00
|
|
|
|
case 'week-info': {
|
|
|
|
|
|
const date = getArgValue('--date') || getCurrentDateStr();
|
|
|
|
|
|
const info = getWeekInfo(date);
|
|
|
|
|
|
console.log(`日期 ${date} 所在工作周:`);
|
|
|
|
|
|
console.log(` 周编号: ${info.weekNumber}`);
|
|
|
|
|
|
console.log(` 周一: ${info.startDate}`);
|
|
|
|
|
|
console.log(` 周五: ${info.endDate}`);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.log('用法:');
|
|
|
|
|
|
console.log(' node work-weekly-report.js plan --tasks "任务1" "任务2" [--week YYYY-Www] [--priority high|medium|low]');
|
|
|
|
|
|
console.log(' node work-weekly-report.js record --content "内容" [--date YYYY-MM-DD] [--status completed|in-progress|blocked] [--hours N] [--plan-item UUID]');
|
|
|
|
|
|
console.log(' node work-weekly-report.js pending [--week YYYY-Www]');
|
|
|
|
|
|
console.log(' node work-weekly-report.js query [--week YYYY-Www] [--format detail|summary]');
|
|
|
|
|
|
console.log(' node work-weekly-report.js update-status --plan-item UUID --status STATUS [--week YYYY-Www]');
|
|
|
|
|
|
console.log(' node work-weekly-report.js summary --content "总结" [--week YYYY-Www]');
|
|
|
|
|
|
console.log(' node work-weekly-report.js remove-plan --plan-item UUID [--week YYYY-Www]');
|
2026-04-15 09:50:26 +08:00
|
|
|
|
console.log(' node work-weekly-report.js extend-plan --plan-item UUID --new-date YYYY-MM-DD [--week YYYY-Www] [--reason "原因"]');
|
2026-04-13 17:36:44 +08:00
|
|
|
|
console.log(' node work-weekly-report.js week-info [--date YYYY-MM-DD]');
|
|
|
|
|
|
console.log('\n周标识: YYYY-Www, 本周, 上周, 上上周, 下周');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('错误:', error.message);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 导出模块
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
getCurrentWeek,
|
|
|
|
|
|
getWeekInfo,
|
|
|
|
|
|
planWeek,
|
|
|
|
|
|
addRecord,
|
|
|
|
|
|
getPendingItems,
|
|
|
|
|
|
queryWeek,
|
|
|
|
|
|
updatePlanItemStatus,
|
|
|
|
|
|
setSummary,
|
|
|
|
|
|
removePlanItem,
|
2026-04-15 09:50:26 +08:00
|
|
|
|
extendPlanItem,
|
2026-04-13 17:36:44 +08:00
|
|
|
|
getWorkdays,
|
|
|
|
|
|
parseWeekIdentifier,
|
|
|
|
|
|
isWorkday
|
|
|
|
|
|
};
|