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
f68b72fbb6
commit
804c2f60c6
@ -19,7 +19,7 @@ defineProps<{
|
|||||||
:title="item.title"
|
:title="item.title"
|
||||||
:time="item.time"
|
:time="item.time"
|
||||||
>
|
>
|
||||||
{{ item.content }}
|
<div v-for="line in item.content.split('\n')">{{ line }}</div>
|
||||||
</n-timeline-item>
|
</n-timeline-item>
|
||||||
</n-timeline>
|
</n-timeline>
|
||||||
<n-empty v-else size="large">
|
<n-empty v-else size="large">
|
||||||
|
|||||||
@ -35,6 +35,7 @@ const filterFormItems = computed(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFilterReset = () => {
|
const onFilterReset = () => {
|
||||||
Object.assign(filter, {
|
Object.assign(filter, {
|
||||||
keywords: null as string | null,
|
keywords: null as string | null,
|
||||||
@ -74,7 +75,7 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
minWidth: 130,
|
minWidth: 130,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '图片',
|
title: '封面图',
|
||||||
key: 'imageSrc',
|
key: 'imageSrc',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
@ -84,7 +85,7 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
minWidth: 160,
|
minWidth: 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '链接',
|
||||||
key: 'link',
|
key: 'link',
|
||||||
render(row) {
|
render(row) {
|
||||||
return h(
|
return h(
|
||||||
@ -106,14 +107,15 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const itemView = computed<{ records: AmazonItem[]; pageCount: number }>(() => {
|
const itemView = computed<{ records: AmazonItem[]; pageCount: number; origin: AmazonItem[] }>(
|
||||||
const { current, size } = page;
|
() => {
|
||||||
let data = filterItemData(allItems.value); // Filter Data
|
const { current, size } = page;
|
||||||
let pageCount = ~~(data.length / size);
|
let data = filterItemData(allItems.value); // Filter Data
|
||||||
pageCount += data.length % size > 0 ? 1 : 0;
|
let pageCount = ~~(data.length / size);
|
||||||
data = data.slice((current - 1) * size, current * size);
|
pageCount += data.length % size > 0 ? 1 : 0;
|
||||||
return { records: data, pageCount };
|
return { records: data.slice((current - 1) * size, current * size), pageCount, origin: data };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const extraHeaders: Header[] = [
|
const extraHeaders: Header[] = [
|
||||||
{
|
{
|
||||||
@ -166,7 +168,7 @@ const handleExport = async () => {
|
|||||||
[] as { label: string; prop: string }[],
|
[] as { label: string; prop: string }[],
|
||||||
)
|
)
|
||||||
.concat(extraHeaders);
|
.concat(extraHeaders);
|
||||||
const data = filterItemData(allItems.value);
|
const { origin: data } = itemView.value;
|
||||||
exportToXLSX(data, { headers });
|
exportToXLSX(data, { headers });
|
||||||
message.info('导出完成');
|
message.info('导出完成');
|
||||||
};
|
};
|
||||||
@ -202,7 +204,7 @@ const handleClearData = async () => {
|
|||||||
<n-card class="result-content-container">
|
<n-card class="result-content-container">
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-space>
|
<n-space>
|
||||||
<div style="padding-right: 10px">结果框</div>
|
<div style="padding-right: 10px">结果数据表</div>
|
||||||
<n-switch size="small" class="filter-switch" v-model:value="filter.detailOnly">
|
<n-switch size="small" class="filter-switch" v-model:value="filter.detailOnly">
|
||||||
<template #checked> 详情 </template>
|
<template #checked> 详情 </template>
|
||||||
<template #unchecked> 全部</template>
|
<template #unchecked> 全部</template>
|
||||||
|
|||||||
@ -76,6 +76,9 @@ export function exportToXLSX(
|
|||||||
const worksheet = utils.json_to_sheet(rows, {
|
const worksheet = utils.json_to_sheet(rows, {
|
||||||
header: headers.map((h) => h.label),
|
header: headers.map((h) => h.label),
|
||||||
});
|
});
|
||||||
|
worksheet['!autofilter'] = {
|
||||||
|
ref: utils.encode_range({ c: 0, r: 0 }, { c: headers.length - 1, r: rows.length }),
|
||||||
|
}; // Use Auto Filter: https://github.com/SheetJS/sheetjs/issues/472#issuecomment-292852308
|
||||||
const workbook = utils.book_new();
|
const workbook = utils.book_new();
|
||||||
utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||||
const fileName = options.fileName || `export_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
const fileName = options.fileName || `export_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export async function exec<T, P extends Record<string, unknown>>(
|
|||||||
const ret = injectResults.pop();
|
const ret = injectResults.pop();
|
||||||
if (ret?.error) {
|
if (ret?.error) {
|
||||||
console.error('注入脚本时发生错误', ret.error);
|
console.error('注入脚本时发生错误', ret.error);
|
||||||
|
throw new Error('注入脚本时发生错误');
|
||||||
}
|
}
|
||||||
return ret?.result as T | null;
|
return ret?.result as T | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,30 @@
|
|||||||
import Emittery from 'emittery';
|
import Emittery from 'emittery';
|
||||||
import type { 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process unknown errors.
|
||||||
|
*/
|
||||||
|
function withErrorHandling(
|
||||||
|
target: (this: AmazonPageWorker, ...args: any[]) => Promise<any>,
|
||||||
|
_context: ClassMethodDecoratorContext,
|
||||||
|
): (this: AmazonPageWorker, ...args: any[]) => Promise<any> {
|
||||||
|
// target 就是当前被装饰的 class 方法
|
||||||
|
const originalMethod = target;
|
||||||
|
// 定义一个新方法
|
||||||
|
const decoratedMethod = async function (this: AmazonPageWorker, ...args: any[]) {
|
||||||
|
try {
|
||||||
|
return await originalMethod.call(this, ...args); // 调用原有方法
|
||||||
|
} catch (error) {
|
||||||
|
this.channel.emit('error', { message: `发生未知错误:${error}` });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 返回装饰后的方法
|
||||||
|
return decoratedMethod;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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!
|
||||||
@ -27,7 +49,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
/**
|
/**
|
||||||
* The signal to interrupt the current operation.
|
* The signal to interrupt the current operation.
|
||||||
*/
|
*/
|
||||||
private _interruptSignal = false;
|
private _isCancel = false;
|
||||||
|
|
||||||
private async getCurrentTab(): Promise<Tabs.Tab> {
|
private async getCurrentTab(): Promise<Tabs.Tab> {
|
||||||
const tab = await browser.tabs
|
const tab = await browser.tabs
|
||||||
@ -49,10 +71,14 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
// #region Wait for the Next button to appear, indicating that the product items have finished loading
|
// #region Wait for the Next button to appear, indicating that the product items have finished loading
|
||||||
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())));
|
||||||
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
while (true) {
|
||||||
while (!document.querySelector('.s-pagination-strip')) {
|
const target = document.querySelector('.s-pagination-strip');
|
||||||
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() * 50) + 500));
|
||||||
|
if (target || document.readyState === 'complete') {
|
||||||
|
target?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// #endregion
|
// #endregion
|
||||||
@ -77,11 +103,11 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
// 处理商品以列表形式展示的情况
|
// 处理商品以列表形式展示的情况
|
||||||
case 'pattern-1':
|
case 'pattern-1':
|
||||||
data = await exec(tabId, async () => {
|
data = await exec(tabId, async () => {
|
||||||
const items = [
|
const items = Array.from(
|
||||||
...(document.querySelectorAll<HTMLDivElement>(
|
document.querySelectorAll<HTMLDivElement>(
|
||||||
'.puisg-row:has(.a-section.a-spacing-small.a-spacing-top-small:not(.a-text-right))',
|
'.puisg-row:has(.a-section.a-spacing-small.a-spacing-top-small:not(.a-text-right))',
|
||||||
) as unknown as HTMLDivElement[]),
|
),
|
||||||
].filter((e) => e.getClientRects().length > 0);
|
).filter((e) => e.getClientRects().length > 0);
|
||||||
const linkObjs = items.reduce<{ link: string; title: string; imageSrc: string }[]>(
|
const linkObjs = items.reduce<{ link: string; title: string; imageSrc: string }[]>(
|
||||||
(objs, el) => {
|
(objs, el) => {
|
||||||
const link = el.querySelector<HTMLAnchorElement>('a')?.href;
|
const link = el.querySelector<HTMLAnchorElement>('a')?.href;
|
||||||
@ -100,11 +126,11 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
// 处理商品以二维图片格展示的情况
|
// 处理商品以二维图片格展示的情况
|
||||||
case 'pattern-2':
|
case 'pattern-2':
|
||||||
data = await exec(tabId, async () => {
|
data = await exec(tabId, async () => {
|
||||||
const items = [
|
const items = Array.from(
|
||||||
...(document.querySelectorAll<HTMLDivElement>(
|
document.querySelectorAll<HTMLDivElement>(
|
||||||
'.puis-card-container:has(.a-section.a-spacing-small.puis-padding-left-small)',
|
'.puis-card-container:has(.a-section.a-spacing-small.puis-padding-left-small)',
|
||||||
) as unknown as HTMLDivElement[]),
|
) as unknown as HTMLDivElement[],
|
||||||
].filter((e) => e.getClientRects().length > 0);
|
).filter((e) => e.getClientRects().length > 0);
|
||||||
const linkObjs = items.reduce<{ link: string; title: string; imageSrc: string }[]>(
|
const linkObjs = items.reduce<{ link: string; title: string; imageSrc: string }[]>(
|
||||||
(objs, el) => {
|
(objs, el) => {
|
||||||
const link = el.querySelector<HTMLAnchorElement>('a.a-link-normal')?.href;
|
const link = el.querySelector<HTMLAnchorElement>('a.a-link-normal')?.href;
|
||||||
@ -134,7 +160,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Error: next page button not found');
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// #endregion
|
// #endregion
|
||||||
@ -146,6 +172,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
return { data, hasNextPage };
|
return { data, hasNextPage };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@withErrorHandling
|
||||||
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);
|
||||||
@ -161,18 +188,20 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@withErrorHandling
|
||||||
public async wanderSearchPage(): Promise<void> {
|
public async wanderSearchPage(): Promise<void> {
|
||||||
const tab = await this.getCurrentTab();
|
const tab = await this.getCurrentTab();
|
||||||
this._interruptSignal = false;
|
this._isCancel = false;
|
||||||
const stop = async (_: unknown): Promise<void> => {
|
const stop = this.channel.on('error', async (_: unknown): Promise<void> => {
|
||||||
this._interruptSignal = true;
|
this._isCancel = true;
|
||||||
};
|
});
|
||||||
this.channel.on('error', stop);
|
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
while (!this._interruptSignal) {
|
while (!this._isCancel) {
|
||||||
const { hasNextPage, data } = await this.wanderSearchSinglePage(tab);
|
const { hasNextPage, data } = await this.wanderSearchSinglePage(tab);
|
||||||
|
const keywords = new URL(tab.url!).searchParams.get('k')!;
|
||||||
const objs = data.map((r, i) => ({
|
const objs = data.map((r, i) => ({
|
||||||
...r,
|
...r,
|
||||||
|
keywords,
|
||||||
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],
|
||||||
@ -183,12 +212,14 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._interruptSignal = false;
|
this._isCancel = false;
|
||||||
this.channel.off('error', stop);
|
this.channel.off('error', stop);
|
||||||
return new Promise((resolve) => setTimeout(resolve, 1000));
|
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@withErrorHandling
|
||||||
public async wanderDetailPage(entry: string): Promise<void> {
|
public async wanderDetailPage(entry: string): Promise<void> {
|
||||||
|
//#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}/)) {
|
||||||
const [asin] = /\/\/dp\/[A-Z0-9]{10}/.exec(entry)!;
|
const [asin] = /\/\/dp\/[A-Z0-9]{10}/.exec(entry)!;
|
||||||
@ -198,18 +229,30 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
params.asin = entry;
|
params.asin = entry;
|
||||||
params.url = `https://www.amazon.com/dp/${entry}`;
|
params.url = `https://www.amazon.com/dp/${entry}`;
|
||||||
}
|
}
|
||||||
const tab = await this.createNewTab(params.url);
|
let tab = await this.getCurrentTab();
|
||||||
|
if (!tab.url || !tab.url.startsWith('http')) {
|
||||||
|
tab = await this.createNewTab(params.url);
|
||||||
|
} else {
|
||||||
|
tab = await browser.tabs.update(tab.id, {
|
||||||
|
url: params.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
//#region Await Production Introduction Element Loaded and Determine Page Pattern
|
//#region Await Production Introduction Element Loaded and Determine Page Pattern
|
||||||
const pattern = await exec(tab.id!, async () => {
|
await exec(tab.id!, async () => {
|
||||||
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
while (true) {
|
||||||
let targetNode = document.querySelector('#prodDetails, #detailBulletsWrapper_feature_div');
|
|
||||||
while (!targetNode || document.readyState === 'loading') {
|
|
||||||
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() * 50) + 50));
|
||||||
targetNode = document.querySelector('#prodDetails, #detailBulletsWrapper_feature_div');
|
const targetNode = document.querySelector(
|
||||||
|
'#prodDetails:has(td), #detailBulletsWrapper_feature_div:has(li)',
|
||||||
|
);
|
||||||
|
if (targetNode && document.readyState !== 'loading') {
|
||||||
|
targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
return targetNode.getAttribute('id') === 'prodDetails' ? 'pattern-1' : 'pattern-2';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return targetNode.getAttribute('id') === 'prodDetails' ? 'pattern-1' : 'pattern-2';
|
|
||||||
});
|
});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds.
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Fetch Rating Info
|
//#region Fetch Rating Info
|
||||||
const ratingInfo = await exec(tab.id!, async () => {
|
const ratingInfo = await exec(tab.id!, async () => {
|
||||||
@ -237,55 +280,49 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Fetch Category Rank Info
|
//#region Fetch Category Rank Info
|
||||||
let rawRankingText: string | null = null;
|
let rawRankingText: string | null = await exec(tab.id!, async () => {
|
||||||
switch (pattern) {
|
const xpathExps = [
|
||||||
case 'pattern-1':
|
`//div[@id='detailBulletsWrapper_feature_div']//ul[.//li[contains(., 'Best Sellers Rank')]]//span[@class='a-list-item']`,
|
||||||
rawRankingText = await exec(tab.id!, async () => {
|
`//div[@id='prodDetails']//table/tbody/tr[th[1][contains(text(), 'Best Sellers Rank')]]/td`,
|
||||||
const xpathExp = `//div[@id='prodDetails']//table/tbody/tr[th[1][contains(text(), 'Best Sellers Rank')]]/td`;
|
`//div[@id='productDetails_db_sections']//table/tbody/tr[th[1][contains(text(), 'Best Sellers Rank')]]/td`,
|
||||||
const targetNode = document.evaluate(
|
];
|
||||||
xpathExp,
|
for (const xpathExp of xpathExps) {
|
||||||
document,
|
const targetNode = document.evaluate(
|
||||||
null,
|
xpathExp,
|
||||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
document,
|
||||||
null,
|
null,
|
||||||
).singleNodeValue as HTMLDivElement | null;
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||||
return targetNode?.innerText || null;
|
null,
|
||||||
});
|
).singleNodeValue as HTMLDivElement | null;
|
||||||
break;
|
if (targetNode) {
|
||||||
case 'pattern-2':
|
targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
rawRankingText = await exec(tab.id!, async () => {
|
return targetNode.innerText;
|
||||||
const xpathExp = `//div[@id='detailBulletsWrapper_feature_div']//ul[.//li[contains(., 'Best Sellers Rank')]]//span[@class='a-list-item']`;
|
}
|
||||||
const targetNode = document.evaluate(
|
}
|
||||||
xpathExp,
|
return null;
|
||||||
document,
|
});
|
||||||
null,
|
|
||||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
||||||
null,
|
|
||||||
).singleNodeValue as HTMLDivElement | null;
|
|
||||||
return targetNode?.innerText || null;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (rawRankingText) {
|
if (rawRankingText) {
|
||||||
let statement = /#[0-9,]+\sin\s\S[\s\w&\(\)]+/.exec(rawRankingText)![0]!;
|
const info: Pick<AmazonDetailItem, 'category1' | 'category2'> = {};
|
||||||
const category1Name = /(?<=in\s).+(?=\s\(See)/.exec(statement)?.[0] || null;
|
let statement = /#[0-9,]+\sin\s\S[\s\w&\(\)]+/.exec(rawRankingText)?.[0];
|
||||||
const category1Ranking =
|
if (statement) {
|
||||||
Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replace(',', '')) || null;
|
const name = /(?<=in\s).+(?=\s\(See)/.exec(statement)?.[0] || null;
|
||||||
|
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replace(',', '')) || null;
|
||||||
rawRankingText = rawRankingText.replace(statement, '');
|
if (name && rank) {
|
||||||
statement = /#[0-9,]+\sin\s\S[\s\w&\(\)]+/.exec(rawRankingText)![0]!;
|
info['category1'] = { name, rank };
|
||||||
const category2Name = /(?<=in\s).+/.exec(statement)?.[0].replace(/[\s]+$/, '') || null;
|
}
|
||||||
const category2Ranking =
|
rawRankingText = rawRankingText.replace(statement, '');
|
||||||
Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replace(',', '')) || null;
|
}
|
||||||
|
statement = /#[0-9,]+\sin\s\S[\s\w&\(\)]+/.exec(rawRankingText)?.[0];
|
||||||
|
if (statement) {
|
||||||
|
const name = /(?<=in\s).+/.exec(statement)?.[0].replace(/[\s]+$/, '') || null;
|
||||||
|
const rank = Number(/(?<=#)[0-9,]+/.exec(statement)?.[0].replace(',', '')) || null;
|
||||||
|
if (name && rank) {
|
||||||
|
info['category2'] = { name, rank };
|
||||||
|
}
|
||||||
|
}
|
||||||
this.channel.emit('item-category-rank-collected', {
|
this.channel.emit('item-category-rank-collected', {
|
||||||
asin: params.asin,
|
asin: params.asin,
|
||||||
category1: ![category1Name, category1Ranking].includes(null)
|
...info,
|
||||||
? { name: category1Name!, rank: category1Ranking! }
|
|
||||||
: undefined,
|
|
||||||
category2: ![category2Name, category2Ranking].includes(null)
|
|
||||||
? { name: category2Name!, rank: category2Ranking! }
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -295,8 +332,8 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
...(document.querySelectorAll('.imageThumbnail img') as unknown as HTMLImageElement[]),
|
...(document.querySelectorAll('.imageThumbnail img') as unknown as HTMLImageElement[]),
|
||||||
].map((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
|
||||||
if (document.querySelector('.overlayRestOfImages')) {
|
const overlay = document.querySelector<HTMLDivElement>('.overlayRestOfImages');
|
||||||
const overlay = document.querySelector<HTMLDivElement>('.overlayRestOfImages')!;
|
if (overlay) {
|
||||||
if (document.querySelector<HTMLDivElement>('#ivThumbs')!.getClientRects().length === 0) {
|
if (document.querySelector<HTMLDivElement>('#ivThumbs')!.getClientRects().length === 0) {
|
||||||
overlay.click();
|
overlay.click();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
@ -310,6 +347,10 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
const [url] = /(?<=url\(").+(?=")/.exec(s)!;
|
const [url] = /(?<=url\(").+(?=")/.exec(s)!;
|
||||||
return url;
|
return url;
|
||||||
});
|
});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
document
|
||||||
|
.querySelector<HTMLButtonElement>(".a-popover button[data-action='a-popover-close']")
|
||||||
|
?.click();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region post-process image urls
|
//#region post-process image urls
|
||||||
@ -331,11 +372,11 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
imageUrls,
|
imageUrls,
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
await browser.tabs.remove(tab.id!);
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2 seconds.
|
||||||
}
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
this._interruptSignal = true;
|
this._isCancel = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
src/logic/page-worker/types.d.ts
vendored
2
src/logic/page-worker/types.d.ts
vendored
@ -25,7 +25,7 @@ interface AmazonPageWorkerEvents {
|
|||||||
/**
|
/**
|
||||||
* The event is fired when worker collected links to items on the Amazon search page.
|
* The event is fired when worker collected links to items on the Amazon search page.
|
||||||
*/
|
*/
|
||||||
['item-links-collected']: { objs: Omit<AmazonSearchItem, 'keywords'>[] };
|
['item-links-collected']: { objs: AmazonSearchItem[] };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The event is fired when worker collected goods' rating on the Amazon detail page.
|
* The event is fired when worker collected goods' rating on the Amazon detail page.
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||||
import type { AmazonDetailItem, AmazonItem, AmazonSearchItem } from './page-worker/types';
|
import type { AmazonDetailItem, AmazonItem, AmazonSearchItem } from './page-worker/types';
|
||||||
|
|
||||||
export const keywords = useWebExtensionStorage<string>('keywords', '');
|
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
||||||
|
|
||||||
|
export const asinInputText = useWebExtensionStorage<string>('asinInputText', '');
|
||||||
|
|
||||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('itemList', []);
|
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('itemList', []);
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
import Options from './Options.vue';
|
import Options from './Options.vue';
|
||||||
|
|
||||||
|
const theme: GlobalThemeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#007bff',
|
||||||
|
primaryColorHover: '#0056b3',
|
||||||
|
primaryColorPressed: '#004085',
|
||||||
|
primaryColorSuppl: '#003366',
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Naive UI Wrapper-->
|
<!-- Naive UI Wrapper-->
|
||||||
<n-config-provider
|
<n-config-provider :theme-overrides="theme">
|
||||||
:theme-overrides="{
|
|
||||||
common: {
|
|
||||||
primaryColor: '#007bff',
|
|
||||||
primaryColorHover: '#0056b3',
|
|
||||||
primaryColorPressed: '#004085',
|
|
||||||
primaryColorSuppl: '#003366',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<n-modal-provider>
|
<n-modal-provider>
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
|
|||||||
@ -1,19 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
import SidePanel from './SidePanel.vue';
|
import SidePanel from './SidePanel.vue';
|
||||||
|
|
||||||
|
const theme: GlobalThemeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#007bff',
|
||||||
|
primaryColorHover: '#0056b3',
|
||||||
|
primaryColorPressed: '#004085',
|
||||||
|
primaryColorSuppl: '#003366',
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Naive UI Wrapper-->
|
<!-- Naive UI Wrapper-->
|
||||||
<n-config-provider
|
<n-config-provider :theme-overrides="theme">
|
||||||
:theme-overrides="{
|
|
||||||
common: {
|
|
||||||
primaryColor: '#007bff',
|
|
||||||
primaryColorHover: '#0056b3',
|
|
||||||
primaryColorPressed: '#004085',
|
|
||||||
primaryColorSuppl: '#003366',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<n-modal-provider>
|
<n-modal-provider>
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
|
|||||||
@ -1,26 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormRules, UploadOnChange } from 'naive-ui';
|
import type { FormItemRule, UploadOnChange } from 'naive-ui';
|
||||||
import pageWorkerFactory from '~/logic/page-worker';
|
import pageWorkerFactory from '~/logic/page-worker';
|
||||||
import { detailItems } from '~/logic/storage';
|
import { asinInputText, detailItems } from '~/logic/storage';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const formItem = reactive({ asin: '' });
|
const formItemRef = useTemplateRef('detail-form-item');
|
||||||
const formRef = useTemplateRef('detail-form');
|
const formItemRule: FormItemRule = {
|
||||||
const formRules: FormRules = {
|
required: true,
|
||||||
asin: [
|
trigger: ['submit', 'blur'],
|
||||||
{
|
message: '请输入格式正确的ASIN',
|
||||||
required: true,
|
validator: () => {
|
||||||
trigger: ['submit', 'blur'],
|
return asinInputText.value.match(/^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g) !== null;
|
||||||
message: '请输入格式正确的ASIN',
|
},
|
||||||
validator: (_rule, val: string) => {
|
|
||||||
return (
|
|
||||||
typeof val === 'string' &&
|
|
||||||
val.match(/^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g) !== null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const timelines = ref<
|
const timelines = ref<
|
||||||
@ -32,20 +24,23 @@ const timelines = ref<
|
|||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const running = ref(false);
|
||||||
|
|
||||||
const worker = pageWorkerFactory.useAmazonPageWorker(); // 获取Page Worker单例
|
const worker = pageWorkerFactory.useAmazonPageWorker(); // 获取Page Worker单例
|
||||||
worker.channel.on('error', ({ message: msg }) => {
|
worker.channel.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: '错误',
|
title: '错误发生',
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: msg,
|
content: msg,
|
||||||
});
|
});
|
||||||
message.error(msg);
|
message.error(msg);
|
||||||
|
running.value = false;
|
||||||
});
|
});
|
||||||
worker.channel.on('item-rating-collected', (ev) => {
|
worker.channel.on('item-rating-collected', (ev) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: `商品: ${ev.asin}`,
|
title: `商品${ev.asin}评价信息`,
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: `评分: ${ev.rating};评价数:${ev.ratingCount}`,
|
content: `评分: ${ev.rating};评价数:${ev.ratingCount}`,
|
||||||
});
|
});
|
||||||
@ -54,7 +49,7 @@ worker.channel.on('item-rating-collected', (ev) => {
|
|||||||
worker.channel.on('item-category-rank-collected', (ev) => {
|
worker.channel.on('item-category-rank-collected', (ev) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: `商品: ${ev.asin}`,
|
title: `商品${ev.asin}分类排名`,
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: [
|
content: [
|
||||||
ev.category1 ? `#${ev.category1.rank} in ${ev.category1.name}` : '',
|
ev.category1 ? `#${ev.category1.rank} in ${ev.category1.name}` : '',
|
||||||
@ -66,7 +61,7 @@ worker.channel.on('item-category-rank-collected', (ev) => {
|
|||||||
worker.channel.on('item-images-collected', (ev) => {
|
worker.channel.on('item-images-collected', (ev) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: `商品: ${ev.asin}`,
|
title: `商品${ev.asin}图像`,
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: `图片数: ${ev.imageUrls.length}`,
|
content: `图片数: ${ev.imageUrls.length}`,
|
||||||
});
|
});
|
||||||
@ -81,7 +76,7 @@ const handleImportAsin: UploadOnChange = ({ fileList }) => {
|
|||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const content = e.target?.result;
|
const content = e.target?.result;
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
formItem.asin = content;
|
asinInputText.value = content;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(file.file, 'utf-8');
|
reader.readAsText(file.file, 'utf-8');
|
||||||
@ -90,7 +85,7 @@ const handleImportAsin: UploadOnChange = ({ fileList }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleExportAsin = () => {
|
const handleExportAsin = () => {
|
||||||
const blob = new Blob([formItem.asin], { type: 'text/plain' });
|
const blob = new Blob([asinInputText.value], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const filename = `asin-${new Date().toISOString()}.txt`;
|
const filename = `asin-${new Date().toISOString()}.txt`;
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@ -103,33 +98,39 @@ const handleExportAsin = () => {
|
|||||||
message.info('导出完成');
|
message.info('导出完成');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGetInfo = () => {
|
const handleFetchInfoFromPage = () => {
|
||||||
formRef.value?.validate(async (errors) => {
|
const runTask = async () => {
|
||||||
if (errors) {
|
const asinList = asinInputText.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
||||||
message.error('格式错误,请检查输入');
|
running.value = true;
|
||||||
return;
|
timelines.value = [
|
||||||
}
|
{
|
||||||
const asinList = formItem.asin.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
|
||||||
if (asinList.length > 0) {
|
|
||||||
timelines.value = [
|
|
||||||
{
|
|
||||||
type: 'info',
|
|
||||||
title: '开始',
|
|
||||||
time: new Date().toLocaleString(),
|
|
||||||
content: '开始数据采集',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
for (const asin of asinList) {
|
|
||||||
await worker.wanderDetailPage(asin);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
||||||
}
|
|
||||||
timelines.value.push({
|
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '结束',
|
title: '开始',
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: '数据采集完成',
|
content: '开始数据采集',
|
||||||
});
|
},
|
||||||
|
];
|
||||||
|
while (asinList.length > 0) {
|
||||||
|
const asin = asinList.shift()!;
|
||||||
|
await worker.wanderDetailPage(asin);
|
||||||
|
asinInputText.value = asinList.join('\n'); // Update Input Text
|
||||||
}
|
}
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'info',
|
||||||
|
title: '结束',
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: '数据采集完成',
|
||||||
|
});
|
||||||
|
running.value = false;
|
||||||
|
};
|
||||||
|
formItemRef.value?.validate({
|
||||||
|
callback: (errors) => {
|
||||||
|
if (errors) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
runTask();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -138,10 +139,10 @@ const handleGetInfo = () => {
|
|||||||
<div class="detail-page-worker">
|
<div class="detail-page-worker">
|
||||||
<header-menu />
|
<header-menu />
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<n-icon :size="60"> <mdi-cat style="color: black" /> </n-icon>
|
<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 class="interative-section">
|
<div v-if="!running" 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 round size="small">
|
||||||
@ -158,23 +159,29 @@ const handleGetInfo = () => {
|
|||||||
导出
|
导出
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-form :rules="formRules" :model="formItem" ref="detail-form" label-placement="left">
|
<n-form-item
|
||||||
<n-form-item style="padding-top: 0px" path="asin">
|
ref="detail-form-item"
|
||||||
<n-input
|
label-placement="left"
|
||||||
v-model:value="formItem.asin"
|
:rule="formItemRule"
|
||||||
placeholder="输入ASINs"
|
style="padding-top: 0px"
|
||||||
type="textarea"
|
>
|
||||||
size="large"
|
<n-input
|
||||||
/>
|
v-model:value="asinInputText"
|
||||||
</n-form-item>
|
placeholder="输入ASINs"
|
||||||
</n-form>
|
type="textarea"
|
||||||
<n-button class="start-button" round size="large" type="primary" @click="handleGetInfo">
|
size="large"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-button 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>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="running-tip-section">
|
||||||
|
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
||||||
|
</div>
|
||||||
<progress-report class="progress-report" :timelines="timelines" />
|
<progress-report class="progress-report" :timelines="timelines" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -189,6 +196,7 @@ const handleGetInfo = () => {
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin: 20px 0 30px 0;
|
margin: 20px 0 30px 0;
|
||||||
|
font-size: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -200,13 +208,22 @@ const handleGetInfo = () => {
|
|||||||
.interative-section {
|
.interative-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 15px;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
width: 85%;
|
width: 85%;
|
||||||
padding: 15px 15px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px #00000020 dashed;
|
border: 1px #00000020 dashed;
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.running-tip-section {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
height: 100px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-report {
|
.progress-report {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { keywords } from '~/logic/storage';
|
import { keywordsList } from '~/logic/storage';
|
||||||
import pageWorker from '~/logic/page-worker';
|
import pageWorker from '~/logic/page-worker';
|
||||||
import type { AmazonSearchItem } from '~/logic/page-worker/types';
|
|
||||||
import { NButton } from 'naive-ui';
|
import { NButton } from 'naive-ui';
|
||||||
import { searchItems } from '~/logic/storage';
|
import { searchItems } from '~/logic/storage';
|
||||||
|
|
||||||
@ -11,12 +10,13 @@ const worker = pageWorker.useAmazonPageWorker();
|
|||||||
worker.channel.on('error', ({ message: msg }) => {
|
worker.channel.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: '错误',
|
title: '错误发生',
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: msg,
|
content: msg,
|
||||||
});
|
});
|
||||||
message.error(msg);
|
message.error(msg);
|
||||||
worker.stop();
|
worker.stop();
|
||||||
|
running.value = false;
|
||||||
});
|
});
|
||||||
worker.channel.on('item-links-collected', ({ objs }) => {
|
worker.channel.on('item-links-collected', ({ objs }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
@ -25,16 +25,10 @@ worker.channel.on('item-links-collected', ({ objs }) => {
|
|||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: `成功采集到 ${objs.length} 条数据`,
|
content: `成功采集到 ${objs.length} 条数据`,
|
||||||
});
|
});
|
||||||
const addedRows = objs.map<AmazonSearchItem>((v) => {
|
searchItems.value = searchItems.value.concat(objs); // Add records
|
||||||
return {
|
|
||||||
...v,
|
|
||||||
keywords: keywords.value,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
searchItems.value = searchItems.value.concat(addedRows);
|
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
const workerRunning = ref(false);
|
const running = ref(false);
|
||||||
|
|
||||||
const timelines = ref<
|
const timelines = ref<
|
||||||
{
|
{
|
||||||
@ -45,36 +39,28 @@ const timelines = ref<
|
|||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const onCollectStart = async () => {
|
const handleFetchInfoFromPage = async () => {
|
||||||
workerRunning.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: '开始数据采集',
|
content: `开始关键词:${keywords} 数据采集`,
|
||||||
},
|
});
|
||||||
];
|
//#region start page worker
|
||||||
if (keywords.value.trim() === '') {
|
await worker.doSearch(keywords);
|
||||||
return;
|
await worker.wanderSearchPage();
|
||||||
|
//#endregion
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'info',
|
||||||
|
title: '结束',
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: `关键词: ${keywords} 数据采集完成`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//#region start page worker
|
running.value = false;
|
||||||
await worker.doSearch(keywords.value);
|
|
||||||
await worker.wanderSearchPage();
|
|
||||||
//#endregion
|
|
||||||
timelines.value.push({
|
|
||||||
type: 'info',
|
|
||||||
title: '结束',
|
|
||||||
time: new Date().toLocaleString(),
|
|
||||||
content: '数据采集完成',
|
|
||||||
});
|
|
||||||
workerRunning.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCollectStop = async () => {
|
|
||||||
workerRunning.value = false;
|
|
||||||
worker.stop();
|
|
||||||
message.info('停止收集');
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -85,39 +71,28 @@ const onCollectStop = 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 class="interactive-section">
|
<div v-if="!running" class="interactive-section">
|
||||||
<n-space>
|
<n-dynamic-input
|
||||||
<n-input
|
v-model:value="keywordsList"
|
||||||
:disabled="workerRunning"
|
:min="1"
|
||||||
v-model:value="keywords"
|
:max="10"
|
||||||
class="search-input-box"
|
class="search-input-box"
|
||||||
autosize
|
autosize
|
||||||
size="large"
|
size="large"
|
||||||
round
|
round
|
||||||
placeholder="请输入关键词采集信息"
|
placeholder="请输入关键词采集信息"
|
||||||
>
|
/>
|
||||||
<template #prefix>
|
<n-button type="primary" round size="large" @click="handleFetchInfoFromPage()">
|
||||||
<n-icon size="20">
|
<template #icon>
|
||||||
<ion-search />
|
<n-icon>
|
||||||
</n-icon>
|
<ant-design-thunderbolt-outlined />
|
||||||
</template>
|
</n-icon>
|
||||||
</n-input>
|
</template>
|
||||||
<n-button
|
开始
|
||||||
type="primary"
|
</n-button>
|
||||||
round
|
</div>
|
||||||
size="large"
|
<div v-else class="running-tip-section">
|
||||||
@click="!workerRunning ? onCollectStart() : onCollectStop()"
|
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon v-if="!workerRunning" size="20">
|
|
||||||
<ant-design-thunderbolt-outlined />
|
|
||||||
</n-icon>
|
|
||||||
<n-icon v-else size="20">
|
|
||||||
<ion-stop-outline />
|
|
||||||
</n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 10px"></div>
|
<div style="height: 10px"></div>
|
||||||
<progress-report class="progress-report" :timelines="timelines" />
|
<progress-report class="progress-report" :timelines="timelines" />
|
||||||
@ -134,19 +109,30 @@ const onCollectStop = async () => {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
.app-title {
|
.app-title {
|
||||||
margin-top: 60px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.interactive-section {
|
.interactive-section {
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px #00000020 dashed;
|
width: 80%;
|
||||||
|
outline: 1px #00000020 dashed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px 25px;
|
||||||
|
|
||||||
.search-input-box {
|
.search-input-box {
|
||||||
min-width: 240px;
|
min-width: 240px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.running-tip-section {
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
.progress-report {
|
.progress-report {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const currentComponent = computed(() => {
|
|||||||
const tab = tabs.find((tab) => tab.name === selectedTab.value);
|
const tab = tabs.find((tab) => tab.name === selectedTab.value);
|
||||||
return tab ? tab.component : null;
|
return tab ? tab.component : null;
|
||||||
});
|
});
|
||||||
const showHeader = ref(false);
|
const showHeader = ref(true);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user