This commit is contained in:
zhu
2026-05-06 10:22:38 +08:00
parent 350d4fc2e2
commit 53e4f0b2f4
10 changed files with 256 additions and 183 deletions

View File

@@ -1,20 +1,20 @@
<script setup lang="ts">
<script setup lang="ts">
import {computed, onMounted, onUnmounted, ref} from 'vue';
import type {CrawlTaskState} from '@/types';
/** 当前后台保存的爬取任务快照,用于决定是否展示右下角浮窗。 */
// 当前后台保存的爬取任务快照,用于决定是否展示右下角浮窗。
const crawlState = ref<CrawlTaskState | null>(null);
/** 当前爬取任务已经运行的秒数,页面上会格式化为 mm:ss。 */
// 当前爬取任务已经运行的秒数,页面上会格式化为 mm:ss。
const elapsedSeconds = ref(0);
/** 控制右下角时间轴面板是否展开。 */
// 控制右下角时间轴面板是否展开。
const isPanelOpen = ref(false);
/** 轮询后台爬取状态和刷新计时器的定时器 ID。 */
// 轮询后台爬取状态和刷新计时器的定时器 ID。
let timer: number | undefined;
/** 只有任务处于运行中时,才在网页右下角展示计时按钮。 */
// 只有任务处于运行中时,才在网页右下角展示计时按钮。
const isVisible = computed(() => crawlState.value?.status === 'running');
/** 内容脚本挂载后立即同步一次状态,并开始每秒刷新计时和任务进度。 */
// 内容脚本挂载后立即同步一次状态,并开始每秒刷新计时和任务进度。
onMounted(() => {
void refreshCrawlState();
timer = window.setInterval(() => {
@@ -23,16 +23,18 @@ onMounted(() => {
}, 1000);
});
/** 内容脚本卸载时清理定时器,避免页面残留轮询。 */
// 内容脚本卸载时清理定时器,避免页面残留轮询。
onUnmounted(() => {
if (timer) {
window.clearInterval(timer);
}
});
/** 从 background 获取最新爬取任务状态,并在任务结束时自动收起面板。 */
/**
* 从 background 获取最新爬取任务状态,并在任务结束时自动收起面板。
*/
async function refreshCrawlState() {
/** background 返回的当前爬取任务状态响应。 */
// background 返回的当前爬取任务状态响应。
const response = await sendBackgroundMessage<CrawlTaskState | null>({action: 'GET_CRAWL_STATE'});
if (response.ok) {
@@ -45,7 +47,9 @@ async function refreshCrawlState() {
}
}
/** 根据任务开始时间实时计算已经运行的秒数。 */
/**
* 根据任务开始时间实时计算已经运行的秒数。
*/
function updateElapsedSeconds() {
if (!crawlState.value) {
elapsedSeconds.value = 0;
@@ -55,18 +59,22 @@ function updateElapsedSeconds() {
elapsedSeconds.value = Math.max(0, Math.floor((Date.now() - crawlState.value.startedAt) / 1000));
}
/** 将秒数格式化为 mm:ss展示在圆形计时按钮和面板标题里。 */
/**
* 将秒数格式化为 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}`;
}
/** 将步骤状态枚举转换成中文展示文案。 */
/**
* 将步骤状态枚举转换成中文展示文案。
*/
function getStepText(status: string): string {
/** 步骤状态到展示文案的映射表。 */
// 步骤状态到展示文案的映射表。
const textMap: Record<string, string> = {
pending: '等待中',
running: '爬取中',
@@ -77,7 +85,9 @@ function getStepText(status: string): string {
return textMap[status] ?? status;
}
/** 发送消息到 background非扩展环境下返回空成功响应方便本地页面不报错。 */
/**
* 发送消息到 background非扩展环境下返回空成功响应方便本地页面不报错。
*/
function sendBackgroundMessage<T>(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});

View File

@@ -1,16 +1,19 @@
import { createApp } from 'vue';
import { createApp } from 'vue';
import App from './App.vue';
/** 将内容脚本应用挂载到页面中。 */
/**
* 将内容脚本应用挂载到页面中。
*/
function mountApp() {
if (document.getElementById('dianshan-crx-root')) {
return;
}
/** 内容脚本在宿主页面中的根容器,用于避免污染业务页面结构。 */
// 内容脚本在宿主页面中的根容器
// 用于避免污染业务页面结构。
const container = document.createElement('div');
container.id = 'dianshan-crx-root';
/** Vue 应用实际挂载的节点。 */
// Vue 应用实际挂载的节点。
const appRoot = document.createElement('div');
container.appendChild(appRoot);