chinaweal-claude-code/skills/work-weekly-report/scripts/work-weekly-report.js

748 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
};