折腾侠
项目实战

从零打造命令行待办事项管理器:一个完整的 Node.js 实战项目

折腾侠
2026/03/25 发布
13约 10 分钟1375 字 / 1388 词00

从零打造命令行待办事项管理器:一个完整的 Node.js 实战项目

项目概述

在现代快节奏的工作和生活中,高效的任务管理变得愈发重要。虽然市面上有众多图形化的待办事项应用,但命令行工具以其轻量、快速、可脚本化的特点,依然拥有大量忠实用户。本项目将带你从零开始,使用 Node.js 构建一个功能完整的命令行待办事项管理器(CLI Todo Manager)。\n 这个项目不仅适合 Node.js 初学者练手,也适合有一定经验的开发者深入理解 CLI 工具的设计模式、数据持久化方案以及模块化架构。完成这个项目后,你将掌握构建实用 CLI 工具的核心技能。

项目功能说明

核心功能

  1. 任务添加:支持快速添加待办事项,可设置优先级和截止日期
  2. 任务列表:以清晰的格式展示所有任务,支持按状态、优先级筛选
  3. 任务完成:标记任务为已完成状态
  4. 任务删除:删除指定任务或清空已完成任务
  5. 任务编辑:修改已有任务的内容、优先级或截止日期
  6. 数据统计:展示任务完成情况的统计信息
  7. 数据持久化:任务数据自动保存到本地 JSON 文件

扩展功能

  • 支持任务分类/标签
  • 支持任务搜索
  • 支持任务优先级排序
  • 支持自定义数据文件路径
  • 支持彩色输出和格式化显示

技术栈选择

核心技术

  • 运行时:Node.js 18+
  • 语言:JavaScript (ES6+)
  • 包管理:npm 或 yarn

第三方依赖

依赖包版本用途
commander^11.0.0CLI 命令解析
chalk^5.3.0终端彩色输出
ora^7.0.0加载动画效果
lowdb^7.0.0轻量级 JSON 数据库
dayjs^1.11.0日期处理
inquirer^9.2.0交互式命令行输入

技术选型理由

  1. commander:业界最成熟的 CLI 框架之一,API 简洁,功能完善
  2. chalk:轻量级的终端样式库,让输出更加直观
  3. lowdb:基于 JSON 文件的微型数据库,无需额外配置
  4. dayjs:比 moment.js 更轻量的日期库,API 友好
  5. 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

项目扩展建议

完成基础版本后,可以考虑以下扩展方向:

  1. 任务提醒:集成系统通知,在任务截止前提醒
  2. 云同步:使用 GitHub Gist 或自建 API 实现多设备同步
  3. 自然语言处理:支持 "todo add 明天下午开会" 这样的自然输入
  4. 时间追踪:记录每个任务的实际耗时
  5. 插件系统:允许用户编写自定义命令插件
  6. GUI 界面:使用 Electron 或 Tauri 添加图形界面
  7. 团队协作:支持任务分配和协作功能

总结

通过这个项目,我们完成了一个功能完整的命令行待办事项管理器。项目涵盖了:

  • ✅ CLI 工具的基本架构设计
  • ✅ 命令解析与参数处理
  • ✅ 数据持久化方案
  • ✅ 模块化代码组织
  • ✅ 终端美化输出
  • ✅ 输入验证与错误处理

这个项目可以作为学习 Node.js CLI 开发的起点,也可以作为个人效率工具长期使用。更重要的是,通过亲手实现每一个功能模块,你对 Node.js 生态系统、模块化编程和 CLI 工具设计有了更深入的理解。

现在,开始你的命令行效率之旅吧!🚀


项目源码https://github.com/yourusername/todo-cli
作者:折腾虾
发布日期:2024 年

分享到:

如果这篇文章对你有帮助,欢迎请作者喝杯咖啡 ☕

加载评论中...