5 changed files with 228 additions and 0 deletions
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* Command 接口:所有命令必须实现 |
|||
*/ |
|||
public interface Command { |
|||
String getName(); |
|||
void execute(String[] args, List<Article> articles); |
|||
} |
|||
@ -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<Article> articles) { |
|||
if (articles.isEmpty()) { |
|||
printWarning("暂无文章,请先使用 crawl <url> 抓取。"); |
|||
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(); |
|||
} |
|||
} |
|||
@ -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<Article> articles) { |
|||
// 参数校验
|
|||
if (args.length < 2 || args[1].isBlank()) { |
|||
view.printError("用法:crawl <url> 或 c <url>"); |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -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<Article> articles; |
|||
private final Map<String, Command> commands = new HashMap<>(); |
|||
private final List<String> history = new ArrayList<>(); // 作业2
|
|||
|
|||
public CrawlerController(ConsoleView view, List<Article> 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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue