从零打造命令行待办事项管理器:一个完整的 Node.js 实战项目
折
折腾侠
2026/03/25 发布
13约 10 分钟1375 字 / 1388 词00
从零打造命令行待办事项管理器:一个完整的 Node.js 实战项目
项目概述
在现代快节奏的工作和生活中,高效的任务管理变得愈发重要。虽然市面上有众多图形化的待办事项应用,但命令行工具以其轻量、快速、可脚本化的特点,依然拥有大量忠实用户。本项目将带你从零开始,使用 Node.js 构建一个功能完整的命令行待办事项管理器(CLI Todo Manager)。\n 这个项目不仅适合 Node.js 初学者练手,也适合有一定经验的开发者深入理解 CLI 工具的设计模式、数据持久化方案以及模块化架构。完成这个项目后,你将掌握构建实用 CLI 工具的核心技能。
项目功能说明
核心功能
- 任务添加:支持快速添加待办事项,可设置优先级和截止日期
- 任务列表:以清晰的格式展示所有任务,支持按状态、优先级筛选
- 任务完成:标记任务为已完成状态
- 任务删除:删除指定任务或清空已完成任务
- 任务编辑:修改已有任务的内容、优先级或截止日期
- 数据统计:展示任务完成情况的统计信息
- 数据持久化:任务数据自动保存到本地 JSON 文件
扩展功能
- 支持任务分类/标签
- 支持任务搜索
- 支持任务优先级排序
- 支持自定义数据文件路径
- 支持彩色输出和格式化显示
技术栈选择
核心技术
- 运行时:Node.js 18+
- 语言:JavaScript (ES6+)
- 包管理:npm 或 yarn
第三方依赖
| 依赖包 | 版本 | 用途 |
|---|---|---|
| commander | ^11.0.0 | CLI 命令解析 |
| chalk | ^5.3.0 | 终端彩色输出 |
| ora | ^7.0.0 | 加载动画效果 |
| lowdb | ^7.0.0 | 轻量级 JSON 数据库 |
| dayjs | ^1.11.0 | 日期处理 |
| inquirer | ^9.2.0 | 交互式命令行输入 |
技术选型理由
- commander:业界最成熟的 CLI 框架之一,API 简洁,功能完善
- chalk:轻量级的终端样式库,让输出更加直观
- lowdb:基于 JSON 文件的微型数据库,无需额外配置
- dayjs:比 moment.js 更轻量的日期库,API 友好
- inquirer:提供丰富的交互式输入组件
项目结构设计
todo-cli/
├── bin/
│ └── todo.js # CLI 入口文件
├── src/
│ ├── index.js # 模块导出
│ ├── commands/
│ │ ├── add.js # 添加任务命令
│ │ ├── list.js # 列表命令
│ │ ├── complete.js # 完成任务命令
│ │ ├── delete.js # 删除任务命令
│ │ ├── edit.js # 编辑任务命令
│ │ └── stats.js # 统计命令
│ ├── utils/
│ │ ├── db.js # 数据库操作
│ │ ├── formatter.js # 输出格式化
│ │ └── validator.js # 输入验证
│ └── config/
│ └── index.js # 配置文件
├── data/
│ └── todos.json # 任务数据存储
├── package.json
├── README.md
└── .gitignore
核心代码实现
1. 项目初始化
Bash
mkdir todo-cli && cd todo-cli
npm init -y
npm install commander chalk ora lowdb dayjs inquirer
npm install -D @types/node
2. 入口文件 (bin/todo.js)
JavaScript
#!/usr/bin/env node
const { Command } = require('commander');
const chalk = require('chalk');
const program = new Command();
// 导入命令模块
const addCmd = require('../src/commands/add');
const listCmd = require('../src/commands/list');
const completeCmd = require('../src/commands/complete');
const deleteCmd = require('../src/commands/delete');
const editCmd = require('../src/commands/edit');
const statsCmd = require('../src/commands/stats');
program
.name('todo')
.description('命令行待办事项管理器')
.version('1.0.0');
// 添加任务
program
.command('add <description>')
.description('添加新任务')
.option('-p, --priority <level>', '优先级 (high/medium/low)', 'medium')
.option('-d, --deadline <date>', '截止日期 (YYYY-MM-DD)')
.option('-t, --tags <tags>', '标签,逗号分隔')
.action(addCmd);
// 列出任务
program
.command('list')
.description('列出所有任务')
.option('-s, --status <status>', '按状态筛选 (pending/completed)')
.option('-p, --priority <level>', '按优先级筛选')
.action(listCmd);
// 完成任务
program
.command('complete <id>')
.description('标记任务为已完成')
.action(completeCmd);
// 删除任务
program
.command('delete <id>')
.description('删除指定任务')
.option('-a, --all', '删除所有已完成任务')
.action(deleteCmd);
// 编辑任务
program
.command('edit <id>')
.description('编辑任务内容')
.option('-d, --description <text>', '新描述')
.option('-p, --priority <level>', '新优先级')
.action(editCmd);
// 统计信息
program
.command('stats')
.description('显示任务统计信息')
.action(statsCmd);
program.parse(process.argv);
3. 数据库模块 (src/utils/db.js)
JavaScript
const { JSONFilePreset } = require('lowdb/node');
const path = require('path');
const fs = require('fs');
const dataDir = path.join(process.env.HOME || process.env.USERPROFILE, '.todo-cli');
const dbPath = path.join(dataDir, 'db.json');
// 确保数据目录存在
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const defaultData = { todos: [], nextId: 1 };
let db = null;
async function getDB() {
if (!db) {
db = await JSONFilePreset(dbPath, defaultData);
}
return db;
}
async function addTodo(todo) {
const database = await getDB();
const id = database.data.nextId++;
const newTodo = {
id,
description: todo.description,
priority: todo.priority || 'medium',
deadline: todo.deadline || null,
tags: todo.tags || [],
completed: false,
createdAt: new Date().toISOString(),
completedAt: null
};
database.data.todos.push(newTodo);
await database.write();
return newTodo;
}
async function getTodos(filter = {}) {
const database = await getDB();
let todos = database.data.todos;
if (filter.status === 'pending') {
todos = todos.filter(t => !t.completed);
} else if (filter.status === 'completed') {
todos = todos.filter(t => t.completed);
}
if (filter.priority) {
todos = todos.filter(t => t.priority === filter.priority);
}
return todos;
}
async function completeTodo(id) {
const database = await getDB();
const todo = database.data.todos.find(t => t.id === id);
if (todo) {
todo.completed = true;
todo.completedAt = new Date().toISOString();
await database.write();
}
return todo;
}
async function deleteTodo(id) {
const database = await getDB();
const index = database.data.todos.findIndex(t => t.id === id);
if (index !== -1) {
database.data.todos.splice(index, 1);
await database.write();
return true;
}
return false;
}
async function deleteCompleted() {
const database = await getDB();
const before = database.data.todos.length;
database.data.todos = database.data.todos.filter(t => !t.completed);
const deleted = before - database.data.todos.length;
await database.write();
return deleted;
}
async function updateTodo(id, updates) {
const database = await getDB();
const todo = database.data.todos.find(t => t.id === id);
if (todo) {
Object.assign(todo, updates);
await database.write();
}
return todo;
}
async function getStats() {
const database = await getDB();
const todos = database.data.todos;
const total = todos.length;
const completed = todos.filter(t => t.completed).length;
const pending = total - completed;
const highPriority = todos.filter(t => t.priority === 'high' && !t.completed).length;
return { total, completed, pending, highPriority };
}
module.exports = {
getDB,
addTodo,
getTodos,
completeTodo,
deleteTodo,
deleteCompleted,
updateTodo,
getStats
};
4. 添加任务命令 (src/commands/add.js)
JavaScript
const chalk = require('chalk');
const ora = require('ora');
const db = require('../utils/db');
const { validatePriority, validateDate } = require('../utils/validator');
async function addTask(description, options) {
const spinner = ora('添加任务中...').start();
try {
// 验证优先级
if (!validatePriority(options.priority)) {
spinner.fail(chalk.red('无效的优先级,请使用 high/medium/low'));
process.exit(1);
}
// 验证日期格式
if (options.deadline && !validateDate(options.deadline)) {
spinner.fail(chalk.red('无效的日期格式,请使用 YYYY-MM-DD'));
process.exit(1);
}
// 处理标签
let tags = [];
if (options.tags) {
tags = options.tags.split(',').map(t => t.trim()).filter(t => t);
}
// 添加任务
const todo = await db.addTodo({
description,
priority: options.priority,
deadline: options.deadline,
tags
});
spinner.succeed(chalk.green('任务添加成功!'));
console.log(chalk.blue('\n任务详情:'));
console.log(` ID: ${chalk.yellow(todo.id)}`);
console.log(` 描述:${todo.description}`);
console.log(` 优先级:${getPriorityColor(todo.priority)(todo.priority)}`);
if (todo.deadline) {
console.log(` 截止日期:${chalk.cyan(todo.deadline)}`);
}
if (tags.length > 0) {
console.log(` 标签:${tags.map(t => chalk.magenta(`#${t}`)).join(' ')}`);
}
} catch (error) {
spinner.fail(chalk.red('添加任务失败: ' + error.message));
process.exit(1);
}
}
function getPriorityColor(priority) {
switch (priority) {
case 'high': return chalk.red;
case 'medium': return chalk.yellow;
case 'low': return chalk.green;
default: return chalk.white;
}
}
module.exports = addTask;
5. 列表命令 (src/commands/list.js)
JavaScript
const chalk = require('chalk');
const db = require('../utils/db');
const dayjs = require('dayjs');
async function listTasks(options) {
try {
const todos = await db.getTodos({
status: options.status,
priority: options.priority
});
if (todos.length === 0) {
console.log(chalk.gray('暂无任务'));
return;
}
console.log(chalk.blue('\n待办事项列表\n'));
console.log(chalk.gray('─'.repeat(60)));
todos.forEach(todo => {
const status = todo.completed
? chalk.green('✓')
: chalk.red('○');
const priority = getPriorityBadge(todo.priority);
const description = todo.completed
? chalk.gray.strikethrough(todo.description)
: todo.description;
console.log(`${status} [${todo.id}] ${priority} ${description}`);
if (todo.deadline) {
const isOverdue = !todo.completed && dayjs(todo.deadline).isBefore(dayjs(), 'day');
const deadlineColor = isOverdue ? chalk.red : chalk.cyan;
console.log(` 截止:${deadlineColor(todo.deadline)}`);
}
if (todo.tags && todo.tags.length > 0) {
console.log(` 标签:${todo.tags.map(t => chalk.magenta(`#${t}`)).join(' ')}`);
}
console.log(chalk.gray('─'.repeat(60)));
});
console.log(chalk.gray(`\n共 ${todos.length} 个任务`));
} catch (error) {
console.error(chalk.red('获取任务列表失败: ' + error.message));
process.exit(1);
}
}
function getPriorityBadge(priority) {
const badges = {
high: chalk.bgRed.white(' 高 '),
medium: chalk.bgYellowBright.black(' 中 '),
low: chalk.bgGreen.white(' 低 ')
};
return badges[priority] || badges.medium;
}
module.exports = listTasks;
6. 统计命令 (src/commands/stats.js)
JavaScript
const chalk = require('chalk');
const db = require('../utils/db');
async function showStats() {
try {
const stats = await db.getStats();
const completionRate = stats.total > 0
? ((stats.completed / stats.total) * 100).toFixed(1)
: 0;
console.log(chalk.blue('\n📊 任务统计\n'));
console.log(chalk.gray('─'.repeat(40)));
console.log(`${chalk.cyan('总任务数')}:${chalk.white(stats.total)}`);
console.log(`${chalk.green('已完成')}:${chalk.white(stats.completed)}`);
console.log(`${chalk.yellow('待完成')}:${chalk.white(stats.pending)}`);
console.log(`${chalk.red('高优先级待办')}:${chalk.white(stats.highPriority)}`);
console.log(chalk.gray('─'.repeat(40)));
// 进度条
const barLength = 30;
const filledLength = Math.round((barLength * stats.completed) / (stats.total || 1));
const emptyLength = barLength - filledLength;
const progressBar = chalk.green('█'.repeat(filledLength)) +
chalk.gray('░'.repeat(emptyLength));
console.log(`\n完成率:${progressBar} ${completionRate}%`);
} catch (error) {
console.error(chalk.red('获取统计信息失败: ' + error.message));
process.exit(1);
}
}
module.exports = showStats;
7. 验证工具 (src/utils/validator.js)
JavaScript
const dayjs = require('dayjs');
function validatePriority(priority) {
return ['high', 'medium', 'low'].includes(priority);
}
function validateDate(dateString) {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(dateString)) {
return false;
}
return dayjs(dateString, 'YYYY-MM-DD', true).isValid();
}
module.exports = {
validatePriority,
validateDate
};
8. 配置文件 (package.json)
JSON
{
"name": "todo-cli",
"version": "1.0.0",
"description": "命令行待办事项管理器",
"main": "src/index.js",
"bin": {
"todo": "./bin/todo.js"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"link": "npm link"
},
"keywords": [
"todo",
"cli",
"task-manager",
"productivity"
],
"author": "",
"license": "MIT",
"dependencies": {
"chalk": "^5.3.0",
"commander": "^11.0.0",
"dayjs": "^1.11.10",
"inquirer": "^9.2.12",
"lowdb": "^7.0.1",
"ora": "^7.0.1"
}
}
运行步骤
第一步:克隆与安装
Bash
# 克隆项目(或创建新项目)
git clone https://github.com/yourusername/todo-cli.git
cd todo-cli
# 安装依赖
npm install
# 全局链接(可选,方便使用)
npm link
第二步:基本使用
Bash
# 查看帮助
todo --help
# 添加任务
todo add "完成项目文档" -p high -d 2024-12-31 -t work,urgent
# 列出所有任务
todo list
# 只列出待完成任务
todo list -s pending
# 按优先级筛选
todo list -p high
# 完成任务
todo complete 1
# 编辑任务
todo edit 1 -d "完成项目文档和测试"
# 删除任务
todo delete 1
# 删除所有已完成任务
todo delete -a
# 查看统计
todo stats
第三步:进阶使用技巧
Bash
# 组合使用:添加高优先级任务并设置标签
todo add "准备技术分享" -p high -t work,speaking
# 查看本周到期任务(配合 grep)
todo list | grep $(date +%Y-%m-%d)
# 快速完成多个任务
todo complete 1 && todo complete 2 && todo complete 3
# 查看高优先级待办
todo list -s pending -p high
项目扩展建议
完成基础版本后,可以考虑以下扩展方向:
- 任务提醒:集成系统通知,在任务截止前提醒
- 云同步:使用 GitHub Gist 或自建 API 实现多设备同步
- 自然语言处理:支持 "todo add 明天下午开会" 这样的自然输入
- 时间追踪:记录每个任务的实际耗时
- 插件系统:允许用户编写自定义命令插件
- GUI 界面:使用 Electron 或 Tauri 添加图形界面
- 团队协作:支持任务分配和协作功能
总结
通过这个项目,我们完成了一个功能完整的命令行待办事项管理器。项目涵盖了:
- ✅ CLI 工具的基本架构设计
- ✅ 命令解析与参数处理
- ✅ 数据持久化方案
- ✅ 模块化代码组织
- ✅ 终端美化输出
- ✅ 输入验证与错误处理
这个项目可以作为学习 Node.js CLI 开发的起点,也可以作为个人效率工具长期使用。更重要的是,通过亲手实现每一个功能模块,你对 Node.js 生态系统、模块化编程和 CLI 工具设计有了更深入的理解。
现在,开始你的命令行效率之旅吧!🚀
项目源码:https://github.com/yourusername/todo-cli
作者:折腾虾
发布日期:2024 年