From 7ca9dabaf91080773830431a86c2802b755c03cb Mon Sep 17 00:00:00 2001 From: zhu <1812073942@qq.com> Date: Thu, 30 Apr 2026 11:17:16 +0800 Subject: [PATCH] 1 --- src/background/domScraper.ts | 0 src/background/service.ts | 24 ++++ src/background/types.ts | 14 ++ src/content/App.vue | 256 +++++++++++++++++++---------------- src/content/main.ts | 24 ++-- step.md | 6 +- 6 files changed, 192 insertions(+), 132 deletions(-) create mode 100644 src/background/domScraper.ts diff --git a/src/background/domScraper.ts b/src/background/domScraper.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/background/service.ts b/src/background/service.ts index 0ff8140..a587a7f 100644 --- a/src/background/service.ts +++ b/src/background/service.ts @@ -2,19 +2,24 @@ import { getPlatformById } from '@/config/platforms'; import type { CrawlProgressStep, CrawlTaskState } from '@/types'; import type { BackgroundCommand, BackgroundResponse, CrawlStateResponse } from './types'; +/** chrome.storage.local 中保存当前爬取任务状态的键名。 */ const CRAWL_TASK_STORAGE_KEY = 'crawlTaskState'; +/** 扩展安装完成时的初始化入口,当前仅保留日志方便调试生命周期。 */ export async function handleInstalled(): Promise { console.log('[background] installed'); } +/** 浏览器启动并加载扩展时的初始化入口,当前仅保留日志方便调试生命周期。 */ export async function handleStartup(): Promise { console.log('[background] startup'); } +/** 监听窗口关闭事件;如果关闭的是爬取窗口,就把当前任务标记为取消。 */ export async function handleWindowRemoved(windowId: number): Promise { console.log('[background] window removed', windowId); + /** 当前保存的爬取任务状态。 */ const state = await getCrawlTaskState(); if (state?.windowId === windowId && state.status === 'running') { @@ -28,6 +33,7 @@ export async function handleWindowRemoved(windowId: number): Promise { } } +/** 根据 popup/content 发来的 action 分发到对应的后台处理函数。 */ export async function handleBackgroundCommand( message: BackgroundCommand, ): Promise { @@ -43,14 +49,18 @@ export async function handleBackgroundCommand( } } +/** 创建新的爬取任务,打开目标平台窗口,并把初始时间轴状态写入 storage。 */ async function startCrawl(platformId: string): Promise { + /** 根据平台 ID 找到对应的平台爬取配置。 */ const platform = getPlatformById(platformId); if (!platform) { return { ok: false, error: '平台配置不存在' }; } + /** 当前任务的开始时间戳,用于计算正计时。 */ const startedAt = Date.now(); + /** 窗口创建前的初始任务状态,先写入 storage 让所有页面能立即感知爬取开始。 */ const nextState: CrawlTaskState = { id: `${platform.id}-${startedAt}`, platformId: platform.id, @@ -68,11 +78,14 @@ async function startCrawl(platformId: string): Promise { await setCrawlTaskState(nextState); try { + /** background 创建出来的目标平台窗口信息。 */ const windowInfo = await createCrawlWindow(platform.baseUrl); + /** 补充 windowId 后的任务状态,后续可用于取消或监听窗口关闭。 */ const stateWithWindow = { ...nextState, windowId: windowInfo.id }; await setCrawlTaskState(stateWithWindow); return { ok: true, data: stateWithWindow }; } catch (error: unknown) { + /** 窗口创建失败时写入的失败状态,供 popup/content 显示错误进度。 */ const failedState: CrawlTaskState = { ...nextState, status: 'failed', @@ -85,13 +98,16 @@ async function startCrawl(platformId: string): Promise { } } +/** 取消当前爬取任务,并尝试关闭正在爬取的平台窗口。 */ async function cancelCrawl(): Promise { + /** 当前保存的爬取任务状态。 */ const state = await getCrawlTaskState(); if (!state) { return { ok: true, data: null }; } + /** 用户取消后的任务状态,当前执行步骤会显示为失败并附带取消原因。 */ const canceledState: CrawlTaskState = { ...state, status: 'canceled', @@ -109,16 +125,21 @@ async function cancelCrawl(): Promise { return { ok: true, data: canceledState }; } +/** 从 chrome.storage.local 读取当前爬取任务状态。 */ async function getCrawlTaskState(): Promise { + /** chrome.storage.local 返回的原始键值对象。 */ const result = await chrome.storage.local.get(CRAWL_TASK_STORAGE_KEY); + /** 取出的任务状态候选值,需要经过结构校验后才能使用。 */ const state = result[CRAWL_TASK_STORAGE_KEY]; return isCrawlTaskState(state) ? state : null; } +/** 将最新爬取任务状态写入 chrome.storage.local,供 popup 和 content script 同步读取。 */ async function setCrawlTaskState(state: CrawlTaskState): Promise { await chrome.storage.local.set({ [CRAWL_TASK_STORAGE_KEY]: state }); } +/** 打开一个普通浏览器窗口承载目标平台页面。 */ function createCrawlWindow(url: string): Promise { return new Promise((resolve, reject) => { chrome.windows.create( @@ -130,6 +151,7 @@ function createCrawlWindow(url: string): Promise { height: 900, }, (windowInfo) => { + /** Chrome 扩展 API 回调中的运行时错误。 */ const runtimeError = chrome.runtime.lastError; if (runtimeError) { @@ -148,6 +170,7 @@ function createCrawlWindow(url: string): Promise { }); } +/** 根据窗口 ID 关闭爬取窗口;关闭失败时不阻塞取消状态写入。 */ function removeWindow(windowId: number): Promise { return new Promise((resolve) => { chrome.windows.remove(windowId, () => { @@ -156,6 +179,7 @@ function removeWindow(windowId: number): Promise { }); } +/** 粗略判断 storage 中读取到的值是否像一个爬取任务状态对象。 */ function isCrawlTaskState(value: unknown): value is CrawlTaskState { return typeof value === 'object' && value !== null && 'id' in value && 'steps' in value; } diff --git a/src/background/types.ts b/src/background/types.ts index a782421..9e9e196 100644 --- a/src/background/types.ts +++ b/src/background/types.ts @@ -1,26 +1,40 @@ import type { CrawlTaskState } from '@/types'; +/** 启动爬取任务的后台消息。 */ export interface StartCrawlCommand { + /** 消息动作类型:请求 background 创建爬取窗口并初始化任务状态。 */ action: 'START_CRAWL'; + /** 启动爬取所需参数。 */ payload: { + /** 当前要爬取的平台 ID,对应 config/platforms.ts 中的平台配置。 */ platformId: string; }; } +/** 获取当前爬取任务状态的后台消息。 */ export interface GetCrawlStateCommand { + /** 消息动作类型:请求 background 返回当前任务快照。 */ action: 'GET_CRAWL_STATE'; } +/** 取消当前爬取任务的后台消息。 */ export interface CancelCrawlCommand { + /** 消息动作类型:请求 background 标记任务取消并关闭爬取窗口。 */ action: 'CANCEL_CRAWL'; } +/** popup/content script 能发送给 background 的全部消息类型。 */ export type BackgroundCommand = StartCrawlCommand | GetCrawlStateCommand | CancelCrawlCommand; +/** background 统一响应结构。 */ export interface BackgroundResponse { + /** 当前请求是否处理成功。 */ ok: boolean; + /** 成功或部分失败时返回的业务数据。 */ data?: T; + /** 请求失败时返回的错误文案。 */ error?: string; } +/** 获取或变更爬取任务后返回的响应结构。 */ export type CrawlStateResponse = BackgroundResponse; diff --git a/src/content/App.vue b/src/content/App.vue index 428dbb7..b5ac8f9 100644 --- a/src/content/App.vue +++ b/src/content/App.vue @@ -2,218 +2,234 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'; import type { CrawlTaskState } from '@/types'; +/** 当前后台保存的爬取任务快照,用于决定是否展示右下角浮窗。 */ const crawlState = ref(null); +/** 当前爬取任务已经运行的秒数,页面上会格式化为 mm:ss。 */ const elapsedSeconds = ref(0); +/** 控制右下角时间轴面板是否展开。 */ const isPanelOpen = ref(false); +/** 轮询后台爬取状态和刷新计时器的定时器 ID。 */ let timer: number | undefined; +/** 只有任务处于运行中时,才在网页右下角展示计时按钮。 */ const isVisible = computed(() => crawlState.value?.status === 'running'); +/** 内容脚本挂载后立即同步一次状态,并开始每秒刷新计时和任务进度。 */ onMounted(() => { - void refreshCrawlState(); - timer = window.setInterval(() => { - updateElapsedSeconds(); void refreshCrawlState(); - }, 1000); + timer = window.setInterval(() => { + updateElapsedSeconds(); + void refreshCrawlState(); + }, 1000); }); +/** 内容脚本卸载时清理定时器,避免页面残留轮询。 */ onUnmounted(() => { - if (timer) { - window.clearInterval(timer); - } + if (timer) { + window.clearInterval(timer); + } }); +/** 从 background 获取最新爬取任务状态,并在任务结束时自动收起面板。 */ async function refreshCrawlState() { - const response = await sendBackgroundMessage({ action: 'GET_CRAWL_STATE' }); + /** background 返回的当前爬取任务状态响应。 */ + const response = await sendBackgroundMessage({ action: 'GET_CRAWL_STATE' }); - if (response.ok) { - crawlState.value = response.data ?? null; - updateElapsedSeconds(); + if (response.ok) { + crawlState.value = response.data ?? null; + updateElapsedSeconds(); - if (!isVisible.value) { - isPanelOpen.value = false; + if (!isVisible.value) { + isPanelOpen.value = false; + } } - } } +/** 根据任务开始时间实时计算已经运行的秒数。 */ function updateElapsedSeconds() { - if (!crawlState.value) { - elapsedSeconds.value = 0; - return; - } + if (!crawlState.value) { + elapsedSeconds.value = 0; + return; + } - elapsedSeconds.value = Math.max(0, Math.floor((Date.now() - crawlState.value.startedAt) / 1000)); + elapsedSeconds.value = Math.max(0, Math.floor((Date.now() - crawlState.value.startedAt) / 1000)); } +/** 将秒数格式化为 mm:ss,展示在圆形计时按钮和面板标题里。 */ function formatElapsed(totalSeconds: number): string { - const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0'); - const seconds = (totalSeconds % 60).toString().padStart(2, '0'); - return `${minutes}:${seconds}`; + /** 运行时长中的分钟部分。 */ + const minutes = Math.floor(totalSeconds / 60).toString().padStart(2, '0'); + /** 运行时长中的秒数部分。 */ + const seconds = (totalSeconds % 60).toString().padStart(2, '0'); + return `${minutes}:${seconds}`; } +/** 将步骤状态枚举转换成中文展示文案。 */ function getStepText(status: string): string { - const textMap: Record = { - pending: '等待中', - running: '爬取中', - success: '已完成', - failed: '爬取失败', - }; + /** 步骤状态到展示文案的映射表。 */ + const textMap: Record = { + pending: '等待中', + running: '爬取中', + success: '已完成', + failed: '爬取失败', + }; - return textMap[status] ?? status; + return textMap[status] ?? status; } +/** 发送消息到 background;非扩展环境下返回空成功响应,方便本地页面不报错。 */ function sendBackgroundMessage(message: unknown): Promise<{ ok: boolean; data?: T; error?: string }> { - if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) { - return Promise.resolve({ ok: true, data: null as T }); - } + if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) { + return Promise.resolve({ ok: true, data: null as T }); + } - return chrome.runtime.sendMessage(message); + return chrome.runtime.sendMessage(message); } diff --git a/src/content/main.ts b/src/content/main.ts index f63fc1e..46c9ffa 100644 --- a/src/content/main.ts +++ b/src/content/main.ts @@ -3,22 +3,24 @@ import App from './App.vue'; /** 将内容脚本应用挂载到页面中。 */ function mountApp() { - if (document.getElementById('dianshan-crx-root')) { - return; - } + if (document.getElementById('dianshan-crx-root')) { + return; + } - const container = document.createElement('div'); - container.id = 'dianshan-crx-root'; - const appRoot = document.createElement('div'); + /** 内容脚本在宿主页面中的根容器,用于避免污染业务页面结构。 */ + const container = document.createElement('div'); + container.id = 'dianshan-crx-root'; + /** Vue 应用实际挂载的节点。 */ + const appRoot = document.createElement('div'); - container.appendChild(appRoot); - document.body.appendChild(container); + container.appendChild(appRoot); + document.body.appendChild(container); - createApp(App).mount(appRoot); + createApp(App).mount(appRoot); } if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', mountApp, { once: true }); + document.addEventListener('DOMContentLoaded', mountApp, { once: true }); } else { - mountApp(); + mountApp(); } diff --git a/step.md b/step.md index 0ff3a2a..44e3b8b 100644 --- a/step.md +++ b/step.md @@ -45,4 +45,8 @@ src:. - 在所有网页(包括新打开的窗口和所有网页)的右下角都放一个圆形正计时(表示正在爬取中) - 点击圆形正计时时,出现一个popup,内容如下 - 以时间轴的形式,表示当前爬取进度,即:根据platforms.ts中的steps - - 同时点击扩展的popup里的内容,也变得和上面的时间轴内容一致,显示爬取进度,隐藏立即爬取等按钮, \ No newline at end of file + - 同时点击扩展的popup里的内容,也变得和上面的时间轴内容一致,显示爬取进度,隐藏立即爬取等按钮, + +3.前提:1和2都已完成,ui和交互操作上ok + - 开始爬取网页中的数据,查看message.js内容,吧里面的爬取方法都提取出来放到background/domScraper.ts中去, + - 基于2,每次根据steps打开一个新网页后,根据它的fields数组字段,调用domScraper中的方法,来提取数据,并打印到控制台即可 \ No newline at end of file