mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-07 07:43:12 +08:00
Update UI & Worker
This commit is contained in:
parent
74c4f8bb28
commit
520a1c765f
@ -13,7 +13,7 @@ defineProps<{
|
|||||||
<n-card class="progress-report" title="数据获取情况">
|
<n-card class="progress-report" title="数据获取情况">
|
||||||
<n-timeline v-if="timelines.length > 0">
|
<n-timeline v-if="timelines.length > 0">
|
||||||
<n-timeline-item
|
<n-timeline-item
|
||||||
v-for="(item, index) in timelines"
|
v-for="(item, index) in timelines.toReversed()"
|
||||||
:key="index"
|
:key="index"
|
||||||
:type="item.type"
|
:type="item.type"
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
|
|||||||
@ -57,6 +57,11 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
key: 'keywords',
|
key: 'keywords',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '页码',
|
||||||
|
key: 'page',
|
||||||
|
minWidth: 60,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '排位',
|
title: '排位',
|
||||||
key: 'rank',
|
key: 'rank',
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export function withErrorHandling(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Object.defineProperty(decoratedMethod, 'name', { value: originalMethod.name });
|
||||||
// 返回装饰后的方法
|
// 返回装饰后的方法
|
||||||
return decoratedMethod;
|
return decoratedMethod;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,26 +21,31 @@
|
|||||||
* console.log(result); // Outputs: 42
|
* console.log(result); // Outputs: 42
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function exec<T>(tabId: number, func: () => Promise<T>): Promise<T | null>;
|
export async function exec<T>(tabId: number, func: () => Promise<T>): Promise<T>;
|
||||||
export async function exec<T, P extends Record<string, unknown>>(
|
export async function exec<T, P extends Record<string, unknown>>(
|
||||||
tabId: number,
|
tabId: number,
|
||||||
func: (payload: P) => Promise<T>,
|
func: (payload: P) => Promise<T>,
|
||||||
payload: P,
|
payload: P,
|
||||||
): Promise<T | null>;
|
): Promise<T>;
|
||||||
export async function exec<T, P extends Record<string, unknown>>(
|
export async function exec<T, P extends Record<string, unknown>>(
|
||||||
tabId: number,
|
tabId: number,
|
||||||
func: (payload?: P) => Promise<T>,
|
func: (payload?: P) => Promise<T>,
|
||||||
payload?: P,
|
payload?: P,
|
||||||
): Promise<T | null> {
|
): Promise<T> {
|
||||||
const injectResults = await browser.scripting.executeScript({
|
const { timeout } = {
|
||||||
target: { tabId },
|
timeout: 30000,
|
||||||
func,
|
};
|
||||||
args: payload ? [payload] : undefined,
|
return new Promise<T>(async (resolve, reject) => {
|
||||||
|
setTimeout(() => reject('脚本运行超时'), timeout);
|
||||||
|
const injectResults = await browser.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
func,
|
||||||
|
args: payload ? [payload] : undefined,
|
||||||
|
});
|
||||||
|
const ret = injectResults.pop();
|
||||||
|
if (ret?.error) {
|
||||||
|
reject(`注入脚本时发生错误: ${ret.error}`);
|
||||||
|
}
|
||||||
|
resolve(ret!.result as T);
|
||||||
});
|
});
|
||||||
const ret = injectResults.pop();
|
|
||||||
if (ret?.error) {
|
|
||||||
console.error('注入脚本时发生错误', ret.error);
|
|
||||||
throw new Error('注入脚本时发生错误');
|
|
||||||
}
|
|
||||||
return ret?.result as T | null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,13 @@ import Emittery from 'emittery';
|
|||||||
import type { AmazonDetailItem, AmazonPageWorker, AmazonPageWorkerEvents } from './types';
|
import type { AmazonDetailItem, AmazonPageWorker, AmazonPageWorkerEvents } from './types';
|
||||||
import type { Tabs } from 'webextension-polyfill';
|
import type { Tabs } from 'webextension-polyfill';
|
||||||
import { exec } from '../execute-script';
|
import { exec } from '../execute-script';
|
||||||
import { TaskController, TaskQueue, taskUnit } from '../task-queue';
|
|
||||||
import { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AmazonPageWorkerImpl can run on background & sidepanel & popup,
|
* AmazonPageWorkerImpl can run on background & sidepanel & popup,
|
||||||
* **can't** run on content script!
|
* **can't** run on content script!
|
||||||
*/
|
*/
|
||||||
class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||||
//#region Singleton
|
//#region Singleton
|
||||||
private static _instance: AmazonPageWorker | null = null;
|
private static _instance: AmazonPageWorker | null = null;
|
||||||
public static getInstance() {
|
public static getInstance() {
|
||||||
@ -18,18 +17,12 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
}
|
}
|
||||||
return this._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
private constructor() {}
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
/**
|
private constructor() {}
|
||||||
* The channel for communication with the Amazon page worker.
|
|
||||||
*/
|
|
||||||
readonly channel = new Emittery<AmazonPageWorkerEvents>();
|
|
||||||
|
|
||||||
/**
|
private _controlChannel = new Emittery<{ interrupt: undefined }>();
|
||||||
* The Task queue
|
public readonly channel = new Emittery<AmazonPageWorkerEvents>();
|
||||||
*/
|
|
||||||
readonly taskQueue = new TaskQueue();
|
|
||||||
|
|
||||||
private async getCurrentTab(): Promise<Tabs.Tab> {
|
private async getCurrentTab(): Promise<Tabs.Tab> {
|
||||||
const tab = await browser.tabs
|
const tab = await browser.tabs
|
||||||
@ -52,11 +45,20 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
await exec(tabId, async () => {
|
await exec(tabId, async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||||
while (true) {
|
while (true) {
|
||||||
const target = document.querySelector('.s-pagination-strip');
|
const targetNode = document.querySelector('.s-pagination-next');
|
||||||
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
||||||
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 500));
|
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 500));
|
||||||
if (target || document.readyState === 'complete') {
|
if (targetNode || document.readyState === 'complete') {
|
||||||
target?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
targetNode?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
|
||||||
|
const spins = Array.from(document.querySelectorAll<HTMLDivElement>('.a-spinner')).filter(
|
||||||
|
(e) => e.getClientRects().length > 0,
|
||||||
|
);
|
||||||
|
if (spins.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,6 +130,14 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
|
// #region get current page
|
||||||
|
const page = (await exec<number>(tab.id!, async () => {
|
||||||
|
const node = document.querySelector<HTMLDivElement>(
|
||||||
|
'.s-pagination-item.s-pagination-selected',
|
||||||
|
);
|
||||||
|
return node ? Number(node.innerText) : 1;
|
||||||
|
}))!;
|
||||||
|
// #endregion
|
||||||
// #region Determine if it is the last page, otherwise navigate to the next page
|
// #region Determine if it is the last page, otherwise navigate to the next page
|
||||||
const hasNextPage = await exec(tabId, async () => {
|
const hasNextPage = await exec(tabId, async () => {
|
||||||
const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
||||||
@ -149,18 +159,17 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
this.channel.emit('error', { message: '爬取单页信息失败', url: tab.url });
|
this.channel.emit('error', { message: '爬取单页信息失败', url: tab.url });
|
||||||
throw new Error('爬取单页信息失败');
|
throw new Error('爬取单页信息失败');
|
||||||
}
|
}
|
||||||
return { data, hasNextPage };
|
return { data, hasNextPage, page };
|
||||||
}
|
}
|
||||||
|
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
@taskUnit
|
|
||||||
public async doSearch(keywords: string): Promise<string> {
|
public async doSearch(keywords: string): Promise<string> {
|
||||||
const url = new URL('https://www.amazon.com/s');
|
const url = new URL('https://www.amazon.com/s');
|
||||||
url.searchParams.append('k', keywords);
|
url.searchParams.append('k', keywords);
|
||||||
|
let tab = await this.getCurrentTab();
|
||||||
const tab = await browser.tabs
|
if (!tab.url?.startsWith('http')) {
|
||||||
.query({ active: true, currentWindow: true })
|
tab = await this.createNewTab('https://www.amazon.com/');
|
||||||
.then((tabs) => tabs[0]);
|
}
|
||||||
const currentUrl = new URL(tab.url!);
|
const currentUrl = new URL(tab.url!);
|
||||||
if (currentUrl.hostname !== url.hostname || currentUrl.searchParams.get('k') !== keywords) {
|
if (currentUrl.hostname !== url.hostname || currentUrl.searchParams.get('k') !== keywords) {
|
||||||
await browser.tabs.update(tab.id, { url: url.toString() });
|
await browser.tabs.update(tab.id, { url: url.toString() });
|
||||||
@ -170,16 +179,16 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
@taskUnit
|
|
||||||
public async wanderSearchPage(): Promise<void> {
|
public async wanderSearchPage(): Promise<void> {
|
||||||
const tab = await this.getCurrentTab();
|
let tab = await this.getCurrentTab();
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
const { hasNextPage, data } = await this.wanderSearchSinglePage(tab);
|
const { hasNextPage, data, page } = await this.wanderSearchSinglePage(tab);
|
||||||
const keywords = new URL(tab.url!).searchParams.get('k')!;
|
const keywords = new URL(tab.url!).searchParams.get('k')!;
|
||||||
const objs = data.map((r, i) => ({
|
const objs = data.map((r, i) => ({
|
||||||
...r,
|
...r,
|
||||||
keywords,
|
keywords,
|
||||||
|
page,
|
||||||
rank: offset + 1 + i,
|
rank: offset + 1 + i,
|
||||||
createTime: new Date().toLocaleString(),
|
createTime: new Date().toLocaleString(),
|
||||||
asin: /(?<=\/dp\/)[A-Z0-9]{10}/.exec(r.link as string)![0],
|
asin: /(?<=\/dp\/)[A-Z0-9]{10}/.exec(r.link as string)![0],
|
||||||
@ -190,13 +199,11 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.channel.off('error', stop);
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, 1000));
|
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
@taskUnit
|
public async wanderDetailPage(entry: string) {
|
||||||
public async wanderDetailPage(entry: string): Promise<void> {
|
|
||||||
//#region Initial Meta Info
|
//#region Initial Meta Info
|
||||||
const params = { asin: '', url: '' };
|
const params = { asin: '', url: '' };
|
||||||
if (entry.match(/^https?:\/\/www\.amazon\.com.*\/dp\/[A-Z0-9]{10}/)) {
|
if (entry.match(/^https?:\/\/www\.amazon\.com.*\/dp\/[A-Z0-9]{10}/)) {
|
||||||
@ -220,15 +227,22 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
await exec(tab.id!, async () => {
|
await exec(tab.id!, async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
||||||
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 50) + 50));
|
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 100) + 200));
|
||||||
const targetNode = document.querySelector(
|
const targetNode = document.querySelector(
|
||||||
'#prodDetails:has(td), #detailBulletsWrapper_feature_div:has(li), .av-page-desktop',
|
'#prodDetails:has(td), #detailBulletsWrapper_feature_div:has(li), .av-page-desktop',
|
||||||
);
|
);
|
||||||
|
const exceptionalNodeSelectors = ['music-detail-header', '.avu-retail-page'];
|
||||||
|
for (const selector of exceptionalNodeSelectors) {
|
||||||
|
if (document.querySelector(selector)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (targetNode && document.readyState !== 'loading') {
|
if (targetNode && document.readyState !== 'loading') {
|
||||||
targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
return targetNode.getAttribute('id') === 'prodDetails' ? 'pattern-1' : 'pattern-2';
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds.
|
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds.
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -306,9 +320,9 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
//#endregion
|
//#endregion
|
||||||
//#region Fetch Goods' Images
|
//#region Fetch Goods' Images
|
||||||
const imageUrls = await exec(tab.id!, async () => {
|
const imageUrls = await exec(tab.id!, async () => {
|
||||||
let urls = [
|
let urls = Array.from(document.querySelectorAll<HTMLImageElement>('.imageThumbnail img')).map(
|
||||||
...(document.querySelectorAll('.imageThumbnail img') as unknown as HTMLImageElement[]),
|
(e) => e.src,
|
||||||
].map((e) => e.src);
|
);
|
||||||
//#region process more images https://github.com/primedigitaltech/azon_seeker/issues/4
|
//#region process more images https://github.com/primedigitaltech/azon_seeker/issues/4
|
||||||
const overlay = document.querySelector<HTMLDivElement>('.overlayRestOfImages');
|
const overlay = document.querySelector<HTMLDivElement>('.overlayRestOfImages');
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
@ -344,17 +358,52 @@ class AmazonPageWorkerImpl implements AmazonPageWorker, TaskController {
|
|||||||
//#endregion
|
//#endregion
|
||||||
return urls;
|
return urls;
|
||||||
});
|
});
|
||||||
imageUrls &&
|
imageUrls.length > 0 &&
|
||||||
this.channel.emit('item-images-collected', {
|
this.channel.emit('item-images-collected', {
|
||||||
asin: params.asin,
|
asin: params.asin,
|
||||||
imageUrls,
|
imageUrls: Array.from(new Set(imageUrls)),
|
||||||
});
|
});
|
||||||
//#endregion
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2 seconds.
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2 seconds.
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runSearchPageTask(
|
||||||
|
keywordsList: string[],
|
||||||
|
progress?: (remains: string[]) => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
let remains = [...keywordsList];
|
||||||
|
let interrupt = false;
|
||||||
|
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
||||||
|
interrupt = true;
|
||||||
|
});
|
||||||
|
while (remains.length > 0 && !interrupt) {
|
||||||
|
const kw = remains.shift()!;
|
||||||
|
await this.doSearch(kw);
|
||||||
|
await this.wanderSearchPage();
|
||||||
|
progress && progress(remains);
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async runDetaiPageTask(
|
||||||
|
asins: string[],
|
||||||
|
progress?: (remains: string[]) => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
let remains = [...asins];
|
||||||
|
let interrupt = false;
|
||||||
|
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
||||||
|
interrupt = true;
|
||||||
|
});
|
||||||
|
while (remains.length > 0 && !interrupt) {
|
||||||
|
const asin = remains.shift()!;
|
||||||
|
await this.wanderDetailPage(asin);
|
||||||
|
progress && progress(remains);
|
||||||
|
}
|
||||||
|
unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
this.taskQueue.clear();
|
this._controlChannel.emit('interrupt');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/logic/page-worker/types.d.ts
vendored
20
src/logic/page-worker/types.d.ts
vendored
@ -3,6 +3,7 @@ import { TaskQueue } from '../task-queue';
|
|||||||
|
|
||||||
type AmazonSearchItem = {
|
type AmazonSearchItem = {
|
||||||
keywords: string;
|
keywords: string;
|
||||||
|
page: number;
|
||||||
link: string;
|
link: string;
|
||||||
title: string;
|
title: string;
|
||||||
asin: string;
|
asin: string;
|
||||||
@ -56,23 +57,22 @@ interface AmazonPageWorker {
|
|||||||
*/
|
*/
|
||||||
readonly channel: Emittery<AmazonPageWorkerEvents>;
|
readonly channel: Emittery<AmazonPageWorkerEvents>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for a list of goods on Amazon
|
|
||||||
* @param keywords - The keywords to search for on Amazon.
|
|
||||||
* @returns A promise that resolves to a string representing the search URL.
|
|
||||||
*/
|
|
||||||
doSearch(keywords: string): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browsing goods search page and collect links to those goods.
|
* Browsing goods search page and collect links to those goods.
|
||||||
|
* @param keywordsList - The keywords list to search for on Amazon.
|
||||||
|
* @param progress The callback that receive remaining keywords as the parameter.
|
||||||
*/
|
*/
|
||||||
wanderSearchPage(): Promise<void>;
|
runSearchPageTask(
|
||||||
|
keywordsList: string[],
|
||||||
|
progress?: (remains: string[]) => Promise<void>,
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browsing goods detail page and collect target information.
|
* Browsing goods detail page and collect target information.
|
||||||
* @param entry Product link or Amazon Standard Identification Number.
|
* @param asins Amazon Standard Identification Numbers.
|
||||||
|
* @param progress The callback that receive remaining asins as the parameter.
|
||||||
*/
|
*/
|
||||||
wanderDetailPage(entry: string | string[]): Promise<void>;
|
runDetaiPageTask(asins: string[], progress?: (remains: string[]) => Promise<void>): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the worker.
|
* Stop the worker.
|
||||||
|
|||||||
@ -12,22 +12,16 @@ export type TaskExecutionResult<T = undefined> =
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TaskInit<
|
export interface TaskInit<T = undefined, P extends any[] = unknown[]> {
|
||||||
T = undefined,
|
func: (...args: P) => Promise<T>;
|
||||||
F extends (...args: unknown[]) => Promise<T> = (...args: unknown[]) => Promise<T>,
|
args?: P;
|
||||||
> {
|
|
||||||
func: F;
|
|
||||||
args?: Parameters<F>;
|
|
||||||
callback?: (result: TaskExecutionResult<T>) => Promise<void> | void;
|
callback?: (result: TaskExecutionResult<T>) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Task<
|
export class Task<T = undefined, P extends any[] = any[]> {
|
||||||
T = undefined,
|
|
||||||
F extends (...args: unknown[]) => Promise<T> = (...args: unknown[]) => Promise<T>,
|
|
||||||
> {
|
|
||||||
private _name: string;
|
private _name: string;
|
||||||
private _func: F;
|
private _func: (...args: P) => Promise<T>;
|
||||||
private _args: Parameters<F>;
|
private _args: P;
|
||||||
private _status: 'initialization' | 'running' | 'success' | 'failure' = 'initialization';
|
private _status: 'initialization' | 'running' | 'success' | 'failure' = 'initialization';
|
||||||
private _result: TaskExecutionResult<T> | null = null;
|
private _result: TaskExecutionResult<T> | null = null;
|
||||||
private _callback: ((result: TaskExecutionResult<T>) => Promise<void> | void) | undefined;
|
private _callback: ((result: TaskExecutionResult<T>) => Promise<void> | void) | undefined;
|
||||||
@ -44,10 +38,10 @@ export class Task<
|
|||||||
return this._result;
|
return this._result;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(name: string, init: TaskInit<T, F>) {
|
constructor(name: string, init: TaskInit<T, P>) {
|
||||||
this._name = name;
|
this._name = name;
|
||||||
this._func = init.func;
|
this._func = init.func;
|
||||||
this._args = init.args ?? ([] as unknown as Parameters<F>);
|
this._args = init.args ?? ([] as unknown as P);
|
||||||
this._callback = init.callback;
|
this._callback = init.callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +75,7 @@ export class Task<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TaskQueue {
|
export class TaskQueue {
|
||||||
private _queue: Task<any>[] = [];
|
private _queue: Task<any, any[]>[] = [];
|
||||||
private _running = false;
|
private _running = false;
|
||||||
private _channel: Emittery<{ interrupt: undefined; start: undefined; stop: undefined }> =
|
private _channel: Emittery<{ interrupt: undefined; start: undefined; stop: undefined }> =
|
||||||
new Emittery();
|
new Emittery();
|
||||||
@ -99,7 +93,11 @@ export class TaskQueue {
|
|||||||
return this._running;
|
return this._running;
|
||||||
}
|
}
|
||||||
|
|
||||||
public add<T>(task: Task<T>) {
|
public get channel() {
|
||||||
|
return this._channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(task: Task<any, any>) {
|
||||||
this._queue.push(task);
|
this._queue.push(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,41 +139,3 @@ export interface TaskController {
|
|||||||
*/
|
*/
|
||||||
readonly taskQueue: TaskQueue;
|
readonly taskQueue: TaskQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A decorator function that wraps a method to manage its execution as a task in a task queue.
|
|
||||||
*
|
|
||||||
* This function takes a method and returns a new method that, when called, will create a
|
|
||||||
* `Task` and add it to the `taskQueue` of the `TaskController`. The original method will be
|
|
||||||
* executed asynchronously, and the result will be resolved or rejected based on the task's
|
|
||||||
* outcome.
|
|
||||||
*/
|
|
||||||
export function taskUnit<T>(
|
|
||||||
target: (this: TaskController, ...args: any[]) => Promise<T>,
|
|
||||||
context: ClassMethodDecoratorContext,
|
|
||||||
): (this: TaskController, ...args: any[]) => Promise<T> {
|
|
||||||
// target 就是当前被装饰的 class 方法
|
|
||||||
const originalMethod = target;
|
|
||||||
// 定义一个新方法
|
|
||||||
const decoratedMethod = async function (this: TaskController, ...args: any[]) {
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
const task = new Task<T, typeof originalMethod>(context.name.toString(), {
|
|
||||||
func: (o, ...a) => originalMethod.call(o, ...a),
|
|
||||||
args: [this, ...args],
|
|
||||||
callback: (r) => {
|
|
||||||
if (r.status === 'success') {
|
|
||||||
resolve(r.result);
|
|
||||||
} else if (r.status === 'failure') {
|
|
||||||
reject(r.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.taskQueue.add(task);
|
|
||||||
if (!this.taskQueue.running) {
|
|
||||||
this.taskQueue.start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
// 返回装饰后的方法
|
|
||||||
return decoratedMethod;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<base target="_blank" />
|
<base target="_blank" />
|
||||||
<title>Options</title>
|
<title>结果页</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@ -111,11 +111,9 @@ const handleFetchInfoFromPage = () => {
|
|||||||
content: '开始数据采集',
|
content: '开始数据采集',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
while (asinList.length > 0) {
|
await worker.runDetaiPageTask(asinList, async (remains) => {
|
||||||
const asin = asinList.shift()!;
|
asinInputText.value = remains.join('\n');
|
||||||
await worker.wanderDetailPage(asin);
|
});
|
||||||
asinInputText.value = asinList.join('\n'); // Update Input Text
|
|
||||||
}
|
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '结束',
|
title: '结束',
|
||||||
@ -135,6 +133,12 @@ const handleFetchInfoFromPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInterrupt = () => {
|
||||||
|
if (!running.value) return;
|
||||||
|
worker.stop();
|
||||||
|
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
||||||
|
};
|
||||||
|
|
||||||
const createOrUpdateDetailItem = (info: AmazonDetailItem) => {
|
const createOrUpdateDetailItem = (info: AmazonDetailItem) => {
|
||||||
const targetIndex = detailItems.value.findLastIndex((item) => info.asin === item.asin);
|
const targetIndex = detailItems.value.findLastIndex((item) => info.asin === item.asin);
|
||||||
if (targetIndex > -1) {
|
if (targetIndex > -1) {
|
||||||
@ -154,17 +158,17 @@ const createOrUpdateDetailItem = (info: AmazonDetailItem) => {
|
|||||||
<mdi-cat style="color: black; font-size: 60px" />
|
<mdi-cat style="color: black; font-size: 60px" />
|
||||||
<h1 style="font-size: 30px; color: black">Detail Page</h1>
|
<h1 style="font-size: 30px; color: black">Detail Page</h1>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!running" class="interative-section">
|
<div class="interative-section">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-upload @change="handleImportAsin" accept=".txt" :max="1">
|
<n-upload @change="handleImportAsin" accept=".txt" :max="1">
|
||||||
<n-button round size="small">
|
<n-button :disabled="running" round size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<gg-import />
|
<gg-import />
|
||||||
</template>
|
</template>
|
||||||
导入
|
导入
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-upload>
|
</n-upload>
|
||||||
<n-button @click="handleExportAsin" round size="small">
|
<n-button :disabled="running" @click="handleExportAsin" round size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ion-arrow-up-right-box-outline />
|
<ion-arrow-up-right-box-outline />
|
||||||
</template>
|
</template>
|
||||||
@ -178,20 +182,27 @@ const createOrUpdateDetailItem = (info: AmazonDetailItem) => {
|
|||||||
style="padding-top: 0px"
|
style="padding-top: 0px"
|
||||||
>
|
>
|
||||||
<n-input
|
<n-input
|
||||||
|
:disabled="running"
|
||||||
v-model:value="asinInputText"
|
v-model:value="asinInputText"
|
||||||
placeholder="输入ASINs"
|
placeholder="输入ASINs"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-button round size="large" type="primary" @click="handleFetchInfoFromPage">
|
<n-button v-if="!running" round size="large" type="primary" @click="handleFetchInfoFromPage">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
</template>
|
</template>
|
||||||
开始
|
开始
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
||||||
|
<template #icon>
|
||||||
|
<ant-design-thunderbolt-outlined />
|
||||||
|
</template>
|
||||||
|
停止
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="running-tip-section">
|
<div v-if="running" 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" />
|
||||||
@ -230,14 +241,14 @@ const createOrUpdateDetailItem = (info: AmazonDetailItem) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.running-tip-section {
|
.running-tip-section {
|
||||||
margin: 0 0 10px 0;
|
margin: 10px 0 0 0;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-report {
|
.progress-report {
|
||||||
margin-top: 20px;
|
margin-top: 10px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,28 +40,45 @@ const timelines = ref<
|
|||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const handleFetchInfoFromPage = async () => {
|
const handleFetchInfoFromPage = async () => {
|
||||||
|
if (keywordsList.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const kws = unref(keywordsList);
|
||||||
running.value = true;
|
running.value = true;
|
||||||
timelines.value = [];
|
timelines.value = [
|
||||||
for (const keywords of keywordsList.value.filter((k) => k.trim() !== '')) {
|
{
|
||||||
timelines.value.push({
|
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '开始',
|
title: '开始',
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: `开始关键词:${keywords} 数据采集`,
|
content: `关键词: ${kws[0]} 数据采集开始`,
|
||||||
});
|
},
|
||||||
//#region start page worker
|
];
|
||||||
await worker.doSearch(keywords);
|
timelines.value.push();
|
||||||
await worker.wanderSearchPage();
|
await worker.runSearchPageTask(kws, async (remains) => {
|
||||||
//#endregion
|
if (remains.length > 0) {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '结束',
|
title: '开始',
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: `关键词: ${keywords} 数据采集完成`,
|
content: `关键词: ${remains[0]} 数据采集开始`,
|
||||||
});
|
});
|
||||||
}
|
keywordsList.value = remains;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'info',
|
||||||
|
title: '结束',
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: `搜索任务结束`,
|
||||||
|
});
|
||||||
running.value = false;
|
running.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInterrupt = () => {
|
||||||
|
if (!running.value) return;
|
||||||
|
worker.stop();
|
||||||
|
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -71,8 +88,9 @@ const handleFetchInfoFromPage = async () => {
|
|||||||
<mdi-cat style="font-size: 60px; color: black" />
|
<mdi-cat style="font-size: 60px; color: black" />
|
||||||
<h1>Search Page</h1>
|
<h1>Search Page</h1>
|
||||||
</n-space>
|
</n-space>
|
||||||
<div v-if="!running" class="interactive-section">
|
<div class="interactive-section">
|
||||||
<n-dynamic-input
|
<n-dynamic-input
|
||||||
|
:disabled="running"
|
||||||
v-model:value="keywordsList"
|
v-model:value="keywordsList"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="10"
|
:max="10"
|
||||||
@ -82,7 +100,7 @@ const handleFetchInfoFromPage = async () => {
|
|||||||
round
|
round
|
||||||
placeholder="请输入关键词采集信息"
|
placeholder="请输入关键词采集信息"
|
||||||
/>
|
/>
|
||||||
<n-button type="primary" round size="large" @click="handleFetchInfoFromPage()">
|
<n-button v-if="!running" type="primary" round size="large" @click="handleFetchInfoFromPage">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon>
|
<n-icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
@ -90,11 +108,18 @@ const handleFetchInfoFromPage = async () => {
|
|||||||
</template>
|
</template>
|
||||||
开始
|
开始
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button v-else type="primary" round size="large" @click="handleInterrupt">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ant-design-thunderbolt-outlined />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
中断
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="running-tip-section">
|
<div v-if="running" class="running-tip-section">
|
||||||
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 10px"></div>
|
|
||||||
<progress-report class="progress-report" :timelines="timelines" />
|
<progress-report class="progress-report" :timelines="timelines" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<base target="_blank" />
|
<base target="_blank" />
|
||||||
<title>Sidepanel</title>
|
<title>Sidepanel</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="min-width: 300px">
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="./main.ts"></script>
|
<script type="module" src="./main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -95,7 +95,9 @@ export default defineConfig(({ command }) => ({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
sidepanel: r('src/sidepanel/index.html'),
|
sidepanel: r('src/sidepanel/index.html'),
|
||||||
|
options: r('src/options/index.html'),
|
||||||
},
|
},
|
||||||
|
output: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user