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>
<div class="detail-description">
<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 }}
</n-descriptions-item>
<n-descriptions-item label="获取日期">
{{ model.timestamp }}
</n-descriptions-item>
<n-descriptions-item label="评价">
{{ model.rating || '-' }}
</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,
},
{
title: '创建日期',
title: '获取日期',
key: 'createTime',
minWidth: 160,
},
@ -146,6 +146,7 @@ const extraHeaders: Header<AmazonItem>[] = [
{ prop: 'category1.rank', label: '大类排行' },
{ prop: 'category2.name', label: '小类' },
{ prop: 'category2.rank', label: '小类排行' },
{ prop: 'timestamp', label: '获取日期(详情页)' },
{
prop: 'imageUrls',
label: '商品图片链接',

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } from './types';
import { Tabs } from 'webextension-polyfill';
import { withErrorHandling } from '../error-handler';
import { withErrorHandling } from './error-handler';
import { HomedepotDetailPageInjector } from '~/logic/web-injectors/homedepot';
import { BaseWorker } from './base';
@ -58,7 +58,7 @@ class HomedepotWorkerImpl
}
export default {
useHomedepotWorker() {
getHomedepotWorker() {
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 { useLongTask } from './useLongTask';
import { useLongTask } from '~/composables/useLongTask';
export interface AmazonPageWorkerSettings {
searchItems?: Ref<AmazonSearchItem[]>;
@ -9,7 +10,7 @@ export interface AmazonPageWorkerSettings {
commitChangeIngerval?: number;
}
class PageWorkerFactory {
class AmazonPageWorkerFactory {
public amazonWorker: ReturnType<typeof this.buildAmazonPageWorker> | null = null;
public amazonWorkerSettings: AmazonPageWorkerSettings = {};
@ -160,11 +161,112 @@ class PageWorkerFactory {
}
}
const facotry = new PageWorkerFactory();
export function usePageWorker(type: 'amazon', settings?: AmazonPageWorkerSettings) {
if (type === 'amazon') {
return facotry.loadAmazonPageWorker(settings);
}
throw new Error(`Unsupported page worker type: ${type}`);
export interface HomedepotWorkerSettings {
detailItems?: Ref<Map<string, HomedepotDetailItem>>;
commitChangeIngerval?: number;
}
class HomedepotWorkerFactory {
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">
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';
const message = useMessage();

View File

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

View File

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

View File

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

View File

@ -1,48 +1,57 @@
<script setup lang="ts">
import type { Timeline } from '~/components/ProgressReport.vue';
import { useLongTask } from '~/composables/useLongTask';
import { homedepot } from '~/logic/page-worker';
import { usePageWorker } from '~/page-worker';
import { detailItems } from '~/logic/storages/homedepot';
const inputText = ref('');
const { isRunning, startTask } = useLongTask();
const idInputRef = useTemplateRef('id-input');
const worker = homedepot.useHomedepotWorker();
worker.channel.on('detail-item-collected', ({ item }) => {
const worker = usePageWorker('homedepot', { detailItems });
worker.on('detail-item-collected', ({ item }) => {
timelines.value.push({
type: 'success',
title: `成功`,
content: `成功获取到${item.OSMID}的商品信息`,
time: new Date().toLocaleString(),
});
detailItems.value.set(item.OSMID, item);
});
const timelines = ref<Timeline[]>([]);
const handleStart = () =>
startTask(async () => {
timelines.value.push({
type: 'info',
title: `开始`,
content: '任务开始',
time: new Date().toLocaleString(),
});
await worker.runDetailPageTask(
inputText.value.split('\n').filter((id) => /\d+/.exec(id)),
const handleStart = async () => {
idInputRef.value?.validate().then(async (success) => {
if (success) {
const ids = inputText.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
timelines.value = [
{
type: 'info',
title: '开始',
time: new Date().toLocaleString(),
content: `开始采集OSMID: ${ids.join(', ')}`,
},
];
await worker.runDetailPageTask(ids, {
progress: (remains) => {
inputText.value = remains.join('\n');
},
},
);
if (remains.length > 0) {
timelines.value.push({
type: 'info',
title: `结束`,
content: '任务完成',
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 = () => {
worker.stop();
@ -55,13 +64,19 @@ const handleInterrupt = () => {
<div class="interative-section">
<ids-input
v-model="inputText"
:disabled="isRunning"
ref="asin-input"
:disabled="worker.isRunning.value"
ref="id-input"
:match-pattern="/^\d+(\n\d+)*\n?$/g"
placeholder="输入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>
<ant-design-thunderbolt-outlined />
</template>
@ -74,7 +89,7 @@ const handleInterrupt = () => {
停止
</n-button>
</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>
</div>
<progress-report class="progress-report" :timelines="timelines" />