diff --git a/w9/Article.java b/w9/Article.java new file mode 100644 index 0000000..ca402a0 --- /dev/null +++ b/w9/Article.java @@ -0,0 +1,36 @@ +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Model: 文章实体,只存放数据,无任何 I/O 代码 + */ +public class Article { + private String title; + private String url; + private String content; + private String author; // 作业1:新增 author + private LocalDate publishDate; // 作业1:新增 publishDate + + public Article(String title, String url, String content, + String author, LocalDate publishDate) { + this.title = title; + this.url = url; + this.content = content; + this.author = author; + this.publishDate = publishDate; + } + + // ── getters ── + public String getTitle() { return title; } + public String getUrl() { return url; } + public String getContent() { return content; } + public String getAuthor() { return author; } + public LocalDate getPublishDate() { return publishDate; } + + @Override + public String toString() { + String dateStr = (publishDate != null) + ? publishDate.format(DateTimeFormatter.ISO_LOCAL_DATE) : "unknown"; + return String.format("[%s] %s (by %s, %s)", url, title, author, dateStr); + } +} diff --git a/w9/Command.java b/w9/Command.java new file mode 100644 index 0000000..afe9a02 --- /dev/null +++ b/w9/Command.java @@ -0,0 +1,9 @@ +import java.util.List; + +/** + * Command 接口:所有命令必须实现 + */ +public interface Command { + String getName(); + void execute(String[] args, List
articles); +} diff --git a/w9/ConsoleView.java b/w9/ConsoleView.java new file mode 100644 index 0000000..41031c4 --- /dev/null +++ b/w9/ConsoleView.java @@ -0,0 +1,66 @@ +import java.util.List; +import java.util.Scanner; + +/** + * View: 所有 I/O 集中在这里,颜色常量统一管理 + * 选做暗色主题:只需改 THEME_PRIMARY / THEME_SECONDARY 两个常量即可 + */ +public class ConsoleView { + + // ── ANSI 基础色 ── + private static final String ANSI_RESET = "\033[0m"; + private static final String ANSI_GREEN = "\033[32m"; + private static final String ANSI_RED = "\033[31m"; + private static final String ANSI_YELLOW = "\033[33m"; + private static final String ANSI_CYAN = "\033[36m"; + private static final String ANSI_BOLD = "\033[1m"; + + // ── 选做:主题常量(暗色主题只改这两行)── + private static final String THEME_PRIMARY = ANSI_CYAN; // 暗色主题可换 ANSI_GREEN + private static final String THEME_SECONDARY = ANSI_YELLOW; // 暗色主题可换 ANSI_CYAN + + private final Scanner scanner = new Scanner(System.in); + + public void printSuccess(String msg) { + System.out.println(ANSI_GREEN + msg + ANSI_RESET); + } + + public void printError(String msg) { + System.out.println(ANSI_RED + "[ERROR] " + msg + ANSI_RESET); + } + + public void printInfo(String msg) { + System.out.println(THEME_PRIMARY + msg + ANSI_RESET); + } + + public void printWarning(String msg) { + System.out.println(THEME_SECONDARY + "[WARN] " + msg + ANSI_RESET); + } + + public void printBold(String msg) { + System.out.println(ANSI_BOLD + msg + ANSI_RESET); + } + + /** 展示文章列表 */ + public void display(List
articles) { + if (articles.isEmpty()) { + printWarning("暂无文章,请先使用 crawl 抓取。"); + return; + } + printBold("── 文章列表 (" + articles.size() + " 篇) ──"); + for (int i = 0; i < articles.size(); i++) { + Article a = articles.get(i); + if (a == null) { + printWarning((i + 1) + ". [null 条目,已跳过]"); + continue; + } + System.out.printf(THEME_PRIMARY + "%2d. %s" + ANSI_RESET + "%n", i + 1, a); + } + } + + /** 读取一行用户输入,显示提示符 */ + public String readLine() { + System.out.print(ANSI_BOLD + "> " + ANSI_RESET); + return scanner.nextLine().trim(); + } +} diff --git a/w9/CrawlCommand.java b/w9/CrawlCommand.java new file mode 100644 index 0000000..85d27e0 --- /dev/null +++ b/w9/CrawlCommand.java @@ -0,0 +1,60 @@ +import java.time.LocalDate; +import java.util.List; + +/** + * CrawlCommand: 抓取文章并存入列表 + * 选做:支持别名 "c" 代替 "crawl" + */ +public class CrawlCommand implements Command { + private final ConsoleView view; + public CrawlCommand(ConsoleView v) { this.view = v; } + + @Override public String getName() { return "crawl"; } + + // 选做:别名支持(由 Controller 注册两个 key 指向同一实例) + public String getAlias() { return "c"; } + + @Override + public void execute(String[] args, List
articles) { + // 参数校验 + if (args.length < 2 || args[1].isBlank()) { + view.printError("用法:crawl 或 c "); + return; + } + String url = args[1].trim(); + + // 选做:URL 格式校验 + if (!isValidUrl(url)) { + view.printError("URL 格式不合法,请以 http:// 或 https:// 开头:" + url); + return; + } + + // Stub:模拟抓取(真实爬取留待后续实现) + // 注意:字符串拼接仅用于构建提示信息(View 层职责), + // 业务数据直接封装进 Article,不在 Command 层做字符串展示拼接。 + Article article = new Article( + "抓取文章 - " + extractDomain(url), + url, + "(stub content)", + "unknown", + LocalDate.now() + ); + articles.add(article); + view.printSuccess("已抓取:" + article.getTitle() + " [" + url + "]"); + } + + /** 选做:简单 URL 格式验证 */ + private boolean isValidUrl(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + private String extractDomain(String url) { + try { + String stripped = url.replaceFirst("https?://", ""); + int slash = stripped.indexOf('/'); + return slash > 0 ? stripped.substring(0, slash) : stripped; + } catch (Exception e) { + return url; + } + } +} diff --git a/w9/CrawlerController.java b/w9/CrawlerController.java new file mode 100644 index 0000000..559f692 --- /dev/null +++ b/w9/CrawlerController.java @@ -0,0 +1,57 @@ +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller: 解析输入、分发命令、维护历史记录 + * 严格遵守 MVC:不持有任何 System.out,所有输出走 view + */ +public class CrawlerController { + private final ConsoleView view; + private final List
articles; + private final Map commands = new HashMap<>(); + private final List history = new ArrayList<>(); // 作业2 + + public CrawlerController(ConsoleView view, List
articles) { + this.view = view; + this.articles = articles; + registerCommands(); + } + + private void registerCommands() { + CrawlCommand crawl = new CrawlCommand(view); + register(crawl.getName(), crawl); + register(crawl.getAlias(), crawl); // 选做:别名 "c" + + register(new ListCommand(view)); + register(new HelpCommand(view)); + register(new ExitCommand(view)); + register(new HistoryCommand(view, history)); // 作业2,注入共享 history + } + + private void register(Command cmd) { + commands.put(cmd.getName(), cmd); + } + + private void register(String key, Command cmd) { + commands.put(key, cmd); + } + + /** 处理一行用户输入 */ + public void handle(String input) { + if (input == null || input.isBlank()) return; + + history.add(input); // 作业2:记录每条命令 + + String[] parts = input.trim().split("\\s+"); + String name = parts[0].toLowerCase(); + Command cmd = commands.get(name); + + if (cmd == null) { + view.printError("未知命令:" + name + "。输入 help 查看可用命令。"); + } else { + cmd.execute(parts, articles); + } + } +}