#!/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) * 遵循 ISO 8601 标准:每周从周一开始,第一个包含1月4日的周是W1 */ function getWeekNumber(dateStr) { const date = new Date(dateStr); const MS_PER_DAY = 24 * 60 * 60 * 1000; // 找到该日期所在周的周四(ISO 8601 以包含1月4日的周为W1) const thursday = new Date(date); thursday.setDate(date.getDate() + (4 - (date.getDay() === 0 ? 7 : date.getDay()))); // 计算该年的1月1日是周几 const yearStart = new Date(thursday.getFullYear(), 0, 1); 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); } /** * 获取当前工作周信息 */ 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 // 找到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); // 计算目标周的周一 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] }; } /** * 制定/更新周计划 */ function planWeek(weekNumber, tasks, priority = 'medium', expectedDate = null) { const { data, report } = getOrCreateWeeklyReport(weekNumber); const now = getCurrentISO(); for (const taskDesc of tasks) { const planItem = { id: generateUUID(), description: taskDesc, expectedDate: expectedDate, 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; } /** * 延期计划项 * 将计划项的预计完成日期延长到指定日期 */ 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 }; } /** * 设置周报总结 */ 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]; // 已知的标志列表 const FLAGS = ['--week', '--tasks', '--priority', '--date', '--content', '--status', '--hours', '--plan-item', '--format', '--expected-date']; 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'; const expectedDate = getArgValue('--expected-date'); const report = planWeek(week, tasks, priority, expectedDate); console.log(`已在 ${report.weekNumber} 创建 ${tasks.length} 个计划项`); console.log('\n计划项:'); report.plan.slice(-tasks.length).forEach((item, i) => { const dateStr = item.expectedDate ? ` [预计: ${item.expectedDate}]` : ''; console.log(` ${i + 1}. [${item.priority}] ${item.description}${dateStr} (${item.id})`); }); 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; } 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; } 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]'); console.log(' node work-weekly-report.js extend-plan --plan-item UUID --new-date YYYY-MM-DD [--week YYYY-Www] [--reason "原因"]'); 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, extendPlanItem, getWorkdays, parseWeekIdentifier, isWorkday };