#!/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) */ function getWeekNumber(dateStr) { const date = new Date(dateStr); // ISO 周码:每年的第一个周四所在的周为第1周 const thursday = new Date(date); thursday.setDate(date.getDate() + (4 - (date.getDay() === 0 ? 7 : date.getDay()))); const yearStart = new Date(thursday.getFullYear(), 0, 1); const weekNum = Math.ceil(((thursday - yearStart) / 86400000 + 1) / 7); return `${thursday.getFullYear()}-W${String(weekNum).padStart(2, '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的周一 let firstMonday; if (jan4Day === 1) { // 1月4日是周一 -> W1周一是1月4日 firstMonday = jan4; } else if (jan4Day === 0) { // 1月4日是周日 -> W1周一是上周一(12月X日) // 1月4日 - 7天 = 12月28日(上上个周一),再 -1天 = 12月29日(上周一) firstMonday = new Date(jan4.getTime() - 7 * MS_PER_DAY); // 但这已经是12月28了,而1月4日是周日,那上周一是12月29日 // 所以实际上 1月4日 - 8天 = 12月27... 这不对 // 让我重新想: // 如果1月4日是周日,那这一周是 Dec 28 - Jan 3 (W1 ends Jan 3) // 所以W1的周一是 Dec 28 // 那么 Dec 28 + 7 = Jan 4,W2从 Jan 5开始 // 所以 W1 of 2026 = Dec 28 - Jan 3 // W1 starts on Monday Dec 28 // Let me recalculate: Jan 4 - 6 = Dec 29 (that's Monday if Jan 4 is Sunday) // Wait, if Jan 4 is Sunday, then Jan 4 - 1 = Jan 3 (Saturday) // Jan 4 - 2 = Jan 2, ..., Jan 4 - 7 = Dec 28 // But that's 7 days back, not 6. And Dec 28 is Monday? // Let me just use getTime() const daysToSubtract = jan4Day === 0 ? 6 : jan4Day - 1; firstMonday = new Date(jan4.getTime() - daysToSubtract * MS_PER_DAY); } else { // 1月4日是 Tue(2), Wed(3), Thu(4), Fri(5), Sat(6) -> 找到上溯到周一 // 如果1月4日是周三,那周一就是1月4日 - 2天 // daysToSubtract = 4 - 1 = 3, 1月4日 - 3天 = 1月1日 = 周一 ✓ const daysToSubtract = jan4Day - 1; 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') { const { data, report } = getOrCreateWeeklyReport(weekNumber); const now = getCurrentISO(); for (const taskDesc of tasks) { const planItem = { id: generateUUID(), description: taskDesc, expectedDate: null, 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 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']; 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 report = planWeek(week, tasks, priority); console.log(`已在 ${report.weekNumber} 创建 ${tasks.length} 个计划项`); console.log('\n计划项:'); report.plan.slice(-tasks.length).forEach((item, i) => { console.log(` ${i + 1}. [${item.priority}] ${item.description} (${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 '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 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, getWorkdays, parseWeekIdentifier, isWorkday };