Refactor: Make page worker as a top module

This commit is contained in:
johnathan 2025-06-26 16:19:40 +08:00
parent ded0770aea
commit 95afadbec5
16 changed files with 172 additions and 55 deletions

View File

@ -5,9 +5,12 @@ defineProps<{ model: AmazonDetailItem }>();
<template> <template>
<div class="detail-description"> <div class="detail-description">
<n-descriptions label-placement="left" bordered :column="4" label-style="min-width: 100px"> <n-descriptions label-placement="left" bordered :column="4" label-style="min-width: 100px">
<n-descriptions-item label="ASIN" :span="2"> <n-descriptions-item label="ASIN">
{{ model.asin }} {{ model.asin }}
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="获取日期">
{{ model.timestamp }}
</n-descriptions-item>
<n-descriptions-item label="评价"> <n-descriptions-item label="评价">
{{ model.rating || '-' }} {{ model.rating || '-' }}
</n-descriptions-item> </n-descriptions-item>

View File

View File

@ -1,4 +0,0 @@
import amazon from './amazon';
import homedepot from './homedepot';
export { amazon, homedepot };

View File

@ -84,7 +84,7 @@ const columns: TableColumn[] = [
hidden: true, hidden: true,
}, },
{ {
title: '创建日期', title: '获取日期',
key: 'createTime', key: 'createTime',
minWidth: 160, minWidth: 160,
}, },
@ -146,6 +146,7 @@ const extraHeaders: Header<AmazonItem>[] = [
{ prop: 'category1.rank', label: '大类排行' }, { prop: 'category1.rank', label: '大类排行' },
{ prop: 'category2.name', label: '小类' }, { prop: 'category2.name', label: '小类' },
{ prop: 'category2.rank', label: '小类排行' }, { prop: 'category2.rank', label: '小类排行' },
{ prop: 'timestamp', label: '获取日期(详情页)' },
{ {
prop: 'imageUrls', prop: 'imageUrls',
label: '商品图片链接', label: '商品图片链接',

View File

@ -50,7 +50,7 @@ const columns: TableColumn[] = [
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'actions',
render(row: (typeof allItems.value)[0]) { render(row: (typeof allItems.value)[0]) {
return ( return (
<n-space> <n-space>

View File

@ -1,6 +1,6 @@
import type { AmazonPageWorker, AmazonPageWorkerEvents, LanchTaskBaseOptions } from './types'; import type { AmazonPageWorker, AmazonPageWorkerEvents, LanchTaskBaseOptions } from './types';
import type { Tabs } from 'webextension-polyfill'; import type { Tabs } from 'webextension-polyfill';
import { withErrorHandling } from '../error-handler'; import { withErrorHandling } from './error-handler';
import { import {
AmazonDetailPageInjector, AmazonDetailPageInjector,
AmazonReviewPageInjector, AmazonReviewPageInjector,

View File

@ -1,6 +1,6 @@
import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } from './types'; import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } from './types';
import { Tabs } from 'webextension-polyfill'; import { Tabs } from 'webextension-polyfill';
import { withErrorHandling } from '../error-handler'; import { withErrorHandling } from './error-handler';
import { HomedepotDetailPageInjector } from '~/logic/web-injectors/homedepot'; import { HomedepotDetailPageInjector } from '~/logic/web-injectors/homedepot';
import { BaseWorker } from './base'; import { BaseWorker } from './base';
@ -58,7 +58,7 @@ class HomedepotWorkerImpl
} }
export default { export default {
useHomedepotWorker() { getHomedepotWorker() {
return HomedepotWorkerImpl.getInstance(); return HomedepotWorkerImpl.getInstance();
}, },
}; };

View File

@ -1,6 +1,7 @@
import { amazon } from '~/logic/page-worker'; import amazon from './amazon';
import homedepot from './homedepot';
import { uploadImage } from '~/logic/upload'; import { uploadImage } from '~/logic/upload';
import { useLongTask } from './useLongTask'; import { useLongTask } from '~/composables/useLongTask';
export interface AmazonPageWorkerSettings { export interface AmazonPageWorkerSettings {
searchItems?: Ref<AmazonSearchItem[]>; searchItems?: Ref<AmazonSearchItem[]>;
@ -9,7 +10,7 @@ export interface AmazonPageWorkerSettings {
commitChangeIngerval?: number; commitChangeIngerval?: number;
} }
class PageWorkerFactory { class AmazonPageWorkerFactory {
public amazonWorker: ReturnType<typeof this.buildAmazonPageWorker> | null = null; public amazonWorker: ReturnType<typeof this.buildAmazonPageWorker> | null = null;
public amazonWorkerSettings: AmazonPageWorkerSettings = {}; public amazonWorkerSettings: AmazonPageWorkerSettings = {};
@ -160,11 +161,112 @@ class PageWorkerFactory {
} }
} }
const facotry = new PageWorkerFactory(); export interface HomedepotWorkerSettings {
detailItems?: Ref<Map<string, HomedepotDetailItem>>;
export function usePageWorker(type: 'amazon', settings?: AmazonPageWorkerSettings) { commitChangeIngerval?: number;
if (type === 'amazon') { }
return facotry.loadAmazonPageWorker(settings);
} class HomedepotWorkerFactory {
throw new Error(`Unsupported page worker type: ${type}`); public homedepotWorkerSettings: HomedepotWorkerSettings = {};
public homedepotWorker: ReturnType<typeof this.buildHomedepotWorker> | null = null;
public buildHomedepotWorker() {
const worker = homedepot.getHomedepotWorker();
const { isRunning, startTask } = useLongTask();
const detailCache = new Map<string, HomedepotDetailItem>();
const unsubscribeFuncs = [] as (() => void)[];
onMounted(() => {
unsubscribeFuncs.push(
...[
worker.on('error', () => {
worker.stop();
}),
worker.on('detail-item-collected', (ev) => {
const { item } = ev;
if (detailCache.has(item.OSMID)) {
const origin = detailCache.get(item.OSMID);
detailCache.set(item.OSMID, { ...origin, ...item });
} else {
detailCache.set(item.OSMID, item);
}
}),
],
);
});
onUnmounted(() => {
unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
});
const commitChange = () => {
const { detailItems } = this.homedepotWorkerSettings;
if (typeof detailItems !== 'undefined') {
for (const [k, v] of detailCache.entries()) {
detailItems.value.delete(k); // Trigger update
detailItems.value.set(k, v);
}
detailCache.clear();
}
};
const taskWrapper = <T extends (...params: any) => any>(func: T) => {
const { commitChangeIngerval = 1500 } = this.homedepotWorkerSettings;
return (...params: Parameters<T>) =>
startTask(async () => {
const interval = setInterval(() => commitChange(), commitChangeIngerval);
await func(...params);
clearInterval(interval);
commitChange();
});
};
const runDetailPageTask = taskWrapper(worker.runDetailPageTask.bind(worker));
return {
isRunning,
runDetailPageTask,
on: worker.on.bind(worker),
off: worker.off.bind(worker),
once: worker.once.bind(worker),
stop: worker.stop.bind(worker),
};
}
loadHomedepotWorker(settings?: HomedepotWorkerSettings) {
if (settings) {
this.homedepotWorkerSettings = { ...this.homedepotWorkerSettings, ...settings };
}
if (!this.homedepotWorker) {
this.homedepotWorker = this.buildHomedepotWorker();
}
return this.homedepotWorker;
}
}
const amazonfacotry = new AmazonPageWorkerFactory();
const homedepotfactory = new HomedepotWorkerFactory();
export function usePageWorker(
type: 'amazon',
settings?: AmazonPageWorkerSettings,
): ReturnType<typeof amazonfacotry.loadAmazonPageWorker>;
export function usePageWorker(
type: 'homedepot',
settings?: HomedepotWorkerSettings,
): ReturnType<typeof homedepotfactory.loadHomedepotWorker>;
export function usePageWorker(type: 'amazon' | 'homedepot', settings?: any) {
switch (type) {
case 'amazon':
return amazonfacotry.loadAmazonPageWorker(settings);
case 'homedepot':
return homedepotfactory.loadHomedepotWorker(settings);
default:
throw new Error(`Unsupported page worker type: ${type}`);
}
} }

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Timeline } from '~/components/ProgressReport.vue'; import type { Timeline } from '~/components/ProgressReport.vue';
import { usePageWorker } from '~/composables/usePageWorker'; import { usePageWorker } from '~/page-worker';
import { detailAsinInput, detailItems, detailWorkerSettings } from '~/logic/storages/amazon'; import { detailAsinInput, detailItems, detailWorkerSettings } from '~/logic/storages/amazon';
const message = useMessage(); const message = useMessage();

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Timeline } from '~/components/ProgressReport.vue'; import type { Timeline } from '~/components/ProgressReport.vue';
import { usePageWorker } from '~/composables/usePageWorker'; import { usePageWorker } from '~/page-worker';
import { reviewAsinInput, reviewItems } from '~/logic/storages/amazon'; import { reviewAsinInput, reviewItems } from '~/logic/storages/amazon';
const worker = usePageWorker('amazon', { reviewItems }); const worker = usePageWorker('amazon', { reviewItems });

View File

@ -2,7 +2,7 @@
import { keywordsList } from '~/logic/storages/amazon'; import { keywordsList } from '~/logic/storages/amazon';
import { searchItems } from '~/logic/storages/amazon'; import { searchItems } from '~/logic/storages/amazon';
import type { Timeline } from '~/components/ProgressReport.vue'; import type { Timeline } from '~/components/ProgressReport.vue';
import { usePageWorker } from '~/composables/usePageWorker'; import { usePageWorker } from '~/page-worker';
const message = useMessage(); const message = useMessage();

View File

@ -2,7 +2,7 @@
import DetailPageEntry from './AmazonEntries/DetailPageEntry.vue'; import DetailPageEntry from './AmazonEntries/DetailPageEntry.vue';
import SearchPageEntry from './AmazonEntries/SearchPageEntry.vue'; import SearchPageEntry from './AmazonEntries/SearchPageEntry.vue';
import ReviewPageEntry from './AmazonEntries/ReviewPageEntry.vue'; import ReviewPageEntry from './AmazonEntries/ReviewPageEntry.vue';
import { usePageWorker } from '~/composables/usePageWorker'; import { usePageWorker } from '~/page-worker';
const tabs = [ const tabs = [
{ {

View File

@ -1,48 +1,57 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Timeline } from '~/components/ProgressReport.vue'; import type { Timeline } from '~/components/ProgressReport.vue';
import { useLongTask } from '~/composables/useLongTask'; import { usePageWorker } from '~/page-worker';
import { homedepot } from '~/logic/page-worker';
import { detailItems } from '~/logic/storages/homedepot'; import { detailItems } from '~/logic/storages/homedepot';
const inputText = ref(''); const inputText = ref('');
const { isRunning, startTask } = useLongTask(); const idInputRef = useTemplateRef('id-input');
const worker = homedepot.useHomedepotWorker(); const worker = usePageWorker('homedepot', { detailItems });
worker.channel.on('detail-item-collected', ({ item }) => { worker.on('detail-item-collected', ({ item }) => {
timelines.value.push({ timelines.value.push({
type: 'success', type: 'success',
title: `成功`, title: `成功`,
content: `成功获取到${item.OSMID}的商品信息`, content: `成功获取到${item.OSMID}的商品信息`,
time: new Date().toLocaleString(), time: new Date().toLocaleString(),
}); });
detailItems.value.set(item.OSMID, item);
}); });
const timelines = ref<Timeline[]>([]); const timelines = ref<Timeline[]>([]);
const handleStart = () => const handleStart = async () => {
startTask(async () => { idInputRef.value?.validate().then(async (success) => {
timelines.value.push({ if (success) {
type: 'info', const ids = inputText.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
title: `开始`, timelines.value = [
content: '任务开始', {
time: new Date().toLocaleString(), type: 'info',
}); title: '开始',
await worker.runDetailPageTask( time: new Date().toLocaleString(),
inputText.value.split('\n').filter((id) => /\d+/.exec(id)), content: `开始采集OSMID: ${ids.join(', ')}`,
{
progress: (remains) => {
inputText.value = remains.join('\n');
}, },
}, ];
); await worker.runDetailPageTask(ids, {
timelines.value.push({ progress: (remains) => {
type: 'info', if (remains.length > 0) {
title: `结束`, timelines.value.push({
content: '任务完成', type: 'info',
time: new Date().toLocaleString(), title: '继续',
}); time: new Date().toLocaleString(),
content: `继续采集OSMID: ${remains.join(', ')}`,
});
inputText.value = remains.join('\n');
}
},
});
timelines.value.push({
type: 'info',
title: '结束',
time: new Date().toLocaleString(),
content: `数据采集完成`,
});
}
}); });
};
const handleInterrupt = () => { const handleInterrupt = () => {
worker.stop(); worker.stop();
@ -55,13 +64,19 @@ const handleInterrupt = () => {
<div class="interative-section"> <div class="interative-section">
<ids-input <ids-input
v-model="inputText" v-model="inputText"
:disabled="isRunning" :disabled="worker.isRunning.value"
ref="asin-input" ref="id-input"
:match-pattern="/^\d+(\n\d+)*\n?$/g" :match-pattern="/^\d+(\n\d+)*\n?$/g"
placeholder="输入OSMID" placeholder="输入OSMID"
validate-message="请输入格式正确的OSMID" validate-message="请输入格式正确的OSMID"
/> />
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart"> <n-button
v-if="!worker.isRunning.value"
round
size="large"
type="primary"
@click="handleStart"
>
<template #icon> <template #icon>
<ant-design-thunderbolt-outlined /> <ant-design-thunderbolt-outlined />
</template> </template>
@ -74,7 +89,7 @@ const handleInterrupt = () => {
停止 停止
</n-button> </n-button>
</div> </div>
<div v-if="isRunning" class="running-tip-section"> <div v-if="worker.isRunning.value" class="running-tip-section">
<n-alert title="Warning" type="warning"> 警告在插件运行期间请勿与浏览器交互 </n-alert> <n-alert title="Warning" type="warning"> 警告在插件运行期间请勿与浏览器交互 </n-alert>
</div> </div>
<progress-report class="progress-report" :timelines="timelines" /> <progress-report class="progress-report" :timelines="timelines" />