mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-08 08:13:15 +08:00
Update UI & Worker
This commit is contained in:
parent
3734c83d21
commit
a84f148743
@ -33,12 +33,14 @@ const props = defineProps<{ model: AmazonDetailItem }>();
|
|||||||
{{ link }}
|
{{ link }}
|
||||||
</div>
|
</div>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="评论" :span="2">
|
<n-descriptions-item v-if="props.model.topReviews" label="精选评论" :span="4">
|
||||||
<div v-for="review in props.model.topReviews" style="margin-bottom: 5px">
|
<div v-for="review in props.model.topReviews" style="margin-bottom: 5px">
|
||||||
<h5 style="margin: 0">{{ review.username }}:</h5>
|
<h3 style="margin: 0">{{ review.username }}: {{ review.title }}</h3>
|
||||||
|
<div style="color: gray; font-size: smaller">{{ review.rating }}</div>
|
||||||
<div v-for="paragraph in review.content.split('\n')">
|
<div v-for="paragraph in review.content.split('\n')">
|
||||||
{{ paragraph }}
|
{{ paragraph }}
|
||||||
</div>
|
</div>
|
||||||
|
<div style="color: gray; font-size: smaller">{{ review.dateInfo }}</div>
|
||||||
</div>
|
</div>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const filterFormItems = computed(() => {
|
|||||||
params: {
|
params: {
|
||||||
options: [
|
options: [
|
||||||
...records.reduce((o, c) => {
|
...records.reduce((o, c) => {
|
||||||
o.add(c.keywords);
|
c.keywords && o.add(c.keywords);
|
||||||
return o;
|
return o;
|
||||||
}, new Set<string>()),
|
}, new Set<string>()),
|
||||||
].map((opt) => ({
|
].map((opt) => ({
|
||||||
@ -67,6 +67,11 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
key: 'rank',
|
key: 'rank',
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'ASIN',
|
||||||
|
key: 'asin',
|
||||||
|
minWidth: 130,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '标题',
|
title: '标题',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
@ -75,9 +80,9 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ASIN',
|
title: '价格',
|
||||||
key: 'asin',
|
key: 'price',
|
||||||
minWidth: 130,
|
minWidth: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '封面图',
|
title: '封面图',
|
||||||
@ -141,6 +146,12 @@ const extraHeaders: Header[] = [
|
|||||||
formatOutputValue: (val?: string[]) => val?.join(';'),
|
formatOutputValue: (val?: string[]) => val?.join(';'),
|
||||||
parseImportValue: (val?: string) => val?.split(';'),
|
parseImportValue: (val?: string) => val?.split(';'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prop: 'topReviews',
|
||||||
|
label: '精选评论',
|
||||||
|
formatOutputValue: (val?: Record<string, any>[]) => JSON.stringify(val),
|
||||||
|
parseImportValue: (val?: string) => val && JSON.parse(val),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
|
const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
|
||||||
@ -148,7 +159,7 @@ const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
|
|||||||
if (search.trim() !== '') {
|
if (search.trim() !== '') {
|
||||||
data = data.filter((r) => {
|
data = data.filter((r) => {
|
||||||
return [r.title, r.asin, r.keywords].some((field) =>
|
return [r.title, r.asin, r.keywords].some((field) =>
|
||||||
field.toLowerCase().includes(search.toLowerCase()),
|
field?.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,11 @@ 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 { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
import { AmazonDetailPageInjector, AmazonSearchPageInjector } from '../web-injectors';
|
import {
|
||||||
|
AmazonDetailPageInjector,
|
||||||
|
AmazonReviewPageInjector,
|
||||||
|
AmazonSearchPageInjector,
|
||||||
|
} from '../web-injectors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AmazonPageWorkerImpl can run on background & sidepanel & popup,
|
* AmazonPageWorkerImpl can run on background & sidepanel & popup,
|
||||||
@ -75,7 +79,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
}
|
}
|
||||||
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() });
|
tab = await browser.tabs.update(tab.id, { url: url.toString(), active: true });
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
return url.toString();
|
return url.toString();
|
||||||
@ -127,10 +131,18 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
}
|
}
|
||||||
const injector = new AmazonDetailPageInjector(tab);
|
const injector = new AmazonDetailPageInjector(tab);
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Await Production Introduction Element Loaded and Determine Page Pattern
|
//#region Await Production Introduction Element Loaded
|
||||||
await injector.waitForPageLoaded();
|
await injector.waitForPageLoaded();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds.
|
await new Promise((resolve) => setTimeout(resolve, 3000)); // Wait 3 seconds.
|
||||||
//#endregion
|
//#endregion
|
||||||
|
//#region Fetch Base Info
|
||||||
|
const baseInfo = await injector.getBaseInfo();
|
||||||
|
this.channel.emit('item-base-info-collected', {
|
||||||
|
asin: params.asin,
|
||||||
|
title: baseInfo.title,
|
||||||
|
price: baseInfo.price,
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
//#region Fetch Rating Info
|
//#region Fetch Rating Info
|
||||||
const ratingInfo = await injector.getRatingInfo();
|
const ratingInfo = await injector.getRatingInfo();
|
||||||
if (ratingInfo && (ratingInfo.rating !== 0 || ratingInfo.ratingCount !== 0)) {
|
if (ratingInfo && (ratingInfo.rating !== 0 || ratingInfo.ratingCount !== 0)) {
|
||||||
@ -181,11 +193,28 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
reviews.length > 0 &&
|
reviews.length > 0 &&
|
||||||
this.channel.emit('item-top-reviews-collected', {
|
this.channel.emit('item-top-reviews-collected', {
|
||||||
asin: params.asin,
|
asin: params.asin,
|
||||||
topReviews: reviews.map((r) => ({ asin: params.asin, ...r })),
|
topReviews: reviews,
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@withErrorHandling
|
||||||
|
public async wanderReviewPage(asin: string) {
|
||||||
|
const baseUrl = `https://www.amazon.com/product-reviews/${asin}/ref=cm_cr_dp_d_show_all_btm?ie=UTF8&reviewerType=all_reviews`;
|
||||||
|
const tab = await this.createNewTab(baseUrl);
|
||||||
|
const injector = new AmazonReviewPageInjector(tab);
|
||||||
|
while (true) {
|
||||||
|
await injector.waitForPageLoad();
|
||||||
|
const reviews = await injector.getSinglePageReviews();
|
||||||
|
reviews.length > 0 && this.channel.emit('item-review-collected', { asin, reviews });
|
||||||
|
const hasNextPage = await injector.jumpToNextPageIfExist();
|
||||||
|
if (!hasNextPage) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(() => browser.tabs.remove(tab.id!), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
public async runSearchPageTask(
|
public async runSearchPageTask(
|
||||||
keywordsList: string[],
|
keywordsList: string[],
|
||||||
progress?: (remains: string[]) => Promise<void>,
|
progress?: (remains: string[]) => Promise<void>,
|
||||||
@ -221,17 +250,29 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async runReviewPageTask(
|
||||||
|
asins: string[],
|
||||||
|
progress?: (remains: string[]) => Promise<void>,
|
||||||
|
): Promise<void> {
|
||||||
|
let remains = [...asins];
|
||||||
|
while (remains.length > 0) {
|
||||||
|
const asin = remains.shift()!;
|
||||||
|
await this.wanderReviewPage(asin);
|
||||||
|
progress && progress(remains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
this._controlChannel.emit('interrupt');
|
this._controlChannel.emit('interrupt');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PageWorkerFactory {
|
class PageWorker {
|
||||||
public useAmazonPageWorker(): AmazonPageWorker {
|
public useAmazonPageWorker(): AmazonPageWorker {
|
||||||
return AmazonPageWorkerImpl.getInstance();
|
return AmazonPageWorkerImpl.getInstance();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageWorkerFactory = new PageWorkerFactory();
|
const pageWorker = new PageWorker();
|
||||||
|
|
||||||
export default pageWorkerFactory;
|
export default pageWorker;
|
||||||
|
|||||||
33
src/logic/page-worker/types.d.ts
vendored
33
src/logic/page-worker/types.d.ts
vendored
@ -8,12 +8,15 @@ type AmazonSearchItem = {
|
|||||||
title: string;
|
title: string;
|
||||||
asin: string;
|
asin: string;
|
||||||
rank: number;
|
rank: number;
|
||||||
|
price?: string;
|
||||||
imageSrc: string;
|
imageSrc: string;
|
||||||
createTime: string;
|
createTime: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AmazonDetailItem = {
|
type AmazonDetailItem = {
|
||||||
asin: string;
|
asin: string;
|
||||||
|
title: string;
|
||||||
|
price?: string;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
ratingCount?: number;
|
ratingCount?: number;
|
||||||
category1?: { name: string; rank: number };
|
category1?: { name: string; rank: number };
|
||||||
@ -23,7 +26,7 @@ type AmazonDetailItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type AmazonReview = {
|
type AmazonReview = {
|
||||||
asin: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
title: string;
|
title: string;
|
||||||
rating: string;
|
rating: string;
|
||||||
@ -31,7 +34,9 @@ type AmazonReview = {
|
|||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AmazonItem = AmazonSearchItem & Partial<AmazonDetailItem> & { hasDetail: boolean };
|
type AmazonItem = Pick<AmazonSearchItem, 'asin'> &
|
||||||
|
Partial<AmazonSearchItem> &
|
||||||
|
Partial<AmazonDetailItem> & { hasDetail: boolean };
|
||||||
|
|
||||||
interface AmazonPageWorkerEvents {
|
interface AmazonPageWorkerEvents {
|
||||||
/**
|
/**
|
||||||
@ -39,6 +44,11 @@ interface AmazonPageWorkerEvents {
|
|||||||
*/
|
*/
|
||||||
['item-links-collected']: { objs: AmazonSearchItem[] };
|
['item-links-collected']: { objs: AmazonSearchItem[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event is fired when worker collected goods' base info on the Amazon detail page.
|
||||||
|
*/
|
||||||
|
['item-base-info-collected']: Pick<AmazonDetailItem, 'asin' | 'title' | 'price'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
@ -55,12 +65,17 @@ interface AmazonPageWorkerEvents {
|
|||||||
['item-images-collected']: Pick<AmazonDetailItem, 'asin' | 'imageUrls'>;
|
['item-images-collected']: Pick<AmazonDetailItem, 'asin' | 'imageUrls'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The event is fired when top reviews collected
|
* The event is fired when top reviews collected in detail page
|
||||||
*/
|
*/
|
||||||
['item-top-reviews-collected']: Pick<AmazonDetailItem, 'asin' | 'topReviews'>;
|
['item-top-reviews-collected']: Pick<AmazonDetailItem, 'asin' | 'topReviews'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error event that occurs when there is an issue with the Amazon page worker.
|
* The event is fired when reviews collected in all review page
|
||||||
|
*/
|
||||||
|
['item-review-collected']: { asin: string; reviews: AmazonReview[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error event that occurs when there is an issue with the Amazon page worker
|
||||||
*/
|
*/
|
||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
}
|
}
|
||||||
@ -89,6 +104,16 @@ interface AmazonPageWorker {
|
|||||||
*/
|
*/
|
||||||
runDetaiPageTask(asins: string[], progress?: (remains: string[]) => Promise<void>): Promise<void>;
|
runDetaiPageTask(asins: string[], progress?: (remains: string[]) => Promise<void>): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsing goods review page and collect target information.
|
||||||
|
* @param asins Amazon Standard Identification Numbers.
|
||||||
|
* @param progress The callback that receive remaining asins as the parameter.
|
||||||
|
*/
|
||||||
|
runReviewPageTask(
|
||||||
|
asins: string[],
|
||||||
|
progress?: (remains: string[]) => Promise<void>,
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the worker.
|
* Stop the worker.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -7,27 +7,32 @@ export const asinInputText = useWebExtensionStorage<string>('asinInputText', '')
|
|||||||
|
|
||||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
||||||
|
|
||||||
export const detailItems = useWebExtensionStorage<AmazonDetailItem[]>('detailItems', []);
|
export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>(
|
||||||
|
'detailItems',
|
||||||
|
new Map(),
|
||||||
|
{
|
||||||
|
listenToStorageChanges: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const allItems = computed({
|
export const allItems = computed({
|
||||||
get() {
|
get() {
|
||||||
const sItems = searchItems.value;
|
const sItems = searchItems.value;
|
||||||
const dItems = detailItems.value.reduce<Map<string, AmazonDetailItem>>(
|
const dItems = detailItems.value;
|
||||||
(m, c) => (m.set(c.asin, c), m),
|
|
||||||
new Map(),
|
|
||||||
);
|
|
||||||
return sItems.map<AmazonItem>((si) => {
|
return sItems.map<AmazonItem>((si) => {
|
||||||
const asin = si.asin;
|
const asin = si.asin;
|
||||||
const dItem = dItems.get(asin);
|
const dItem = dItems.get(asin);
|
||||||
return dItem ? { ...si, ...dItem, hasDetail: true } : { ...si, hasDetail: false };
|
return dItems.has(asin) ? { ...si, ...dItem, hasDetail: true } : { ...si, hasDetail: false };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
set(newValue) {
|
set(newValue) {
|
||||||
const searchItemProps: (keyof AmazonSearchItem)[] = [
|
const searchItemProps: (keyof AmazonSearchItem)[] = [
|
||||||
'keywords',
|
'keywords',
|
||||||
'asin',
|
'asin',
|
||||||
|
'page',
|
||||||
'title',
|
'title',
|
||||||
'imageSrc',
|
'imageSrc',
|
||||||
|
'price',
|
||||||
'link',
|
'link',
|
||||||
'rank',
|
'rank',
|
||||||
'createTime',
|
'createTime',
|
||||||
@ -45,14 +50,17 @@ export const allItems = computed({
|
|||||||
'imageUrls',
|
'imageUrls',
|
||||||
'rating',
|
'rating',
|
||||||
'ratingCount',
|
'ratingCount',
|
||||||
|
'topReviews',
|
||||||
];
|
];
|
||||||
detailItems.value = newValue
|
detailItems.value = newValue
|
||||||
.filter((row) => row.hasDetail)
|
.filter((row) => row.hasDetail)
|
||||||
.map((row) => {
|
.reduce<Map<string, AmazonDetailItem>>((m, row) => {
|
||||||
const entries: [string, unknown][] = Object.entries(row).filter(([key]) =>
|
const entries = Object.entries(row).filter(([key]) =>
|
||||||
detailItemsProps.includes(key as keyof AmazonDetailItem),
|
detailItemsProps.includes(key as keyof AmazonDetailItem),
|
||||||
);
|
);
|
||||||
return Object.fromEntries(entries) as AmazonSearchItem;
|
const obj = Object.fromEntries(entries) as AmazonDetailItem;
|
||||||
});
|
m.set(obj.asin, obj);
|
||||||
|
return m;
|
||||||
|
}, new Map());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,16 +1,22 @@
|
|||||||
import { exec } from './execute-script';
|
import { exec } from './execute-script';
|
||||||
import type { Tabs } from 'webextension-polyfill';
|
import type { Tabs } from 'webextension-polyfill';
|
||||||
import type { AmazonReview } from './page-worker/types';
|
import type { AmazonReview, AmazonSearchItem } from './page-worker/types';
|
||||||
|
|
||||||
export class AmazonSearchPageInjector {
|
class BaseInjector {
|
||||||
readonly _tab: Tabs.Tab;
|
readonly _tab: Tabs.Tab;
|
||||||
|
|
||||||
constructor(tab: Tabs.Tab) {
|
constructor(tab: Tabs.Tab) {
|
||||||
this._tab = tab;
|
this._tab = tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitForPageLoaded() {
|
run<T>(func: (payload?: any) => Promise<T>, payload?: any): Promise<T> {
|
||||||
return exec(this._tab.id!, async () => {
|
return exec(this._tab.id!, func, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AmazonSearchPageInjector extends BaseInjector {
|
||||||
|
public waitForPageLoaded() {
|
||||||
|
return this.run(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 targetNode = document.querySelector('.s-pagination-next');
|
const targetNode = document.querySelector('.s-pagination-next');
|
||||||
@ -34,61 +40,81 @@ export class AmazonSearchPageInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getPagePattern() {
|
public async getPagePattern() {
|
||||||
return exec(this._tab.id!, async () => {
|
return this.run(async () => {
|
||||||
return [
|
return 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).length > 0
|
).filter((e) => e.getClientRects().length > 0).length > 0
|
||||||
? 'pattern-1'
|
? 'pattern-1'
|
||||||
: 'pattern-2';
|
: 'pattern-2';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPageData(pattern: 'pattern-1' | 'pattern-2') {
|
public async getPageData(pattern: 'pattern-1' | 'pattern-2') {
|
||||||
let data: { link: string; title: string; imageSrc: string }[] | null = null;
|
let data: Pick<AmazonSearchItem, 'link' | 'title' | 'imageSrc' | 'price'>[] | null = null;
|
||||||
switch (pattern) {
|
switch (pattern) {
|
||||||
// 处理商品以列表形式展示的情况
|
// 处理商品以列表形式展示的情况
|
||||||
case 'pattern-1':
|
case 'pattern-1':
|
||||||
data = await exec(this._tab.id!, async () => {
|
data = await this.run(async () => {
|
||||||
const items = Array.from(
|
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))',
|
||||||
),
|
),
|
||||||
).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<
|
||||||
(objs, el) => {
|
Pick<AmazonSearchItem, 'link' | 'title' | 'imageSrc' | 'price'>[]
|
||||||
const link = el.querySelector<HTMLAnchorElement>('a')?.href;
|
>((objs, el) => {
|
||||||
const title = el
|
const link = el.querySelector<HTMLAnchorElement>('a')?.href;
|
||||||
.querySelector<HTMLHeadingElement>('h2.a-color-base')!
|
const title = el
|
||||||
.getAttribute('aria-label')!;
|
.querySelector<HTMLHeadingElement>('h2.a-color-base')!
|
||||||
const imageSrc = el.querySelector<HTMLImageElement>('img.s-image')!.src!;
|
.getAttribute('aria-label')!;
|
||||||
link && objs.push({ link, title, imageSrc });
|
const imageSrc = el.querySelector<HTMLImageElement>('img.s-image')!.src!;
|
||||||
return objs;
|
const price =
|
||||||
},
|
el.querySelector<HTMLElement>('.a-price:not(.a-text-price) .a-offscreen')
|
||||||
[],
|
?.innerText ||
|
||||||
);
|
(
|
||||||
|
document.evaluate(
|
||||||
|
`.//div[@data-cy="secondary-offer-recipe"]//span[@class='a-color-base' and contains(., '$') and not(*)]`,
|
||||||
|
el,
|
||||||
|
null,
|
||||||
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||||
|
).singleNodeValue as HTMLSpanElement | null
|
||||||
|
)?.innerText;
|
||||||
|
link && objs.push({ link, title, imageSrc, price });
|
||||||
|
return objs;
|
||||||
|
}, []);
|
||||||
return linkObjs;
|
return linkObjs;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
// 处理商品以二维图片格展示的情况
|
// 处理商品以二维图片格展示的情况
|
||||||
case 'pattern-2':
|
case 'pattern-2':
|
||||||
data = await exec(this._tab.id!, async () => {
|
data = await this.run(async () => {
|
||||||
const items = Array.from(
|
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<
|
||||||
(objs, el) => {
|
Pick<AmazonSearchItem, 'link' | 'title' | 'imageSrc' | 'price'>[]
|
||||||
const link = el.querySelector<HTMLAnchorElement>('a.a-link-normal')?.href;
|
>((objs, el) => {
|
||||||
const title = el.querySelector<HTMLHeadingElement>('h2.a-color-base')!.innerText;
|
const link = el.querySelector<HTMLAnchorElement>('a.a-link-normal')?.href;
|
||||||
const imageSrc = el.querySelector<HTMLImageElement>('img.s-image')!.src!;
|
const title = el.querySelector<HTMLHeadingElement>('h2.a-color-base')!.innerText;
|
||||||
link && objs.push({ link, title, imageSrc });
|
const imageSrc = el.querySelector<HTMLImageElement>('img.s-image')!.src!;
|
||||||
return objs;
|
const price =
|
||||||
},
|
el.querySelector<HTMLElement>('.a-price:not(.a-text-price) .a-offscreen')
|
||||||
[],
|
?.innerText ||
|
||||||
);
|
(
|
||||||
|
document.evaluate(
|
||||||
|
`.//div[@data-cy="secondary-offer-recipe"]//span[@class='a-color-base' and contains(., '$') and not(*)]`,
|
||||||
|
el,
|
||||||
|
null,
|
||||||
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||||
|
).singleNodeValue as HTMLSpanElement | null
|
||||||
|
)?.innerText;
|
||||||
|
link && objs.push({ link, title, imageSrc, price });
|
||||||
|
return objs;
|
||||||
|
}, []);
|
||||||
return linkObjs;
|
return linkObjs;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -100,7 +126,7 @@ export class AmazonSearchPageInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getCurrentPage() {
|
public async getCurrentPage() {
|
||||||
return exec<number>(this._tab.id!, async () => {
|
return this.run(async () => {
|
||||||
const node = document.querySelector<HTMLDivElement>(
|
const node = document.querySelector<HTMLDivElement>(
|
||||||
'.s-pagination-item.s-pagination-selected',
|
'.s-pagination-item.s-pagination-selected',
|
||||||
);
|
);
|
||||||
@ -109,7 +135,7 @@ export class AmazonSearchPageInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async determineHasNextPage() {
|
public async determineHasNextPage() {
|
||||||
return exec(this._tab.id!, async () => {
|
return this.run(async () => {
|
||||||
const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
const nextButton = document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
||||||
if (nextButton) {
|
if (nextButton) {
|
||||||
if (!nextButton.classList.contains('s-pagination-disabled')) {
|
if (!nextButton.classList.contains('s-pagination-disabled')) {
|
||||||
@ -126,15 +152,9 @@ export class AmazonSearchPageInjector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AmazonDetailPageInjector {
|
export class AmazonDetailPageInjector extends BaseInjector {
|
||||||
readonly _tab: Tabs.Tab;
|
|
||||||
|
|
||||||
constructor(tab: Tabs.Tab) {
|
|
||||||
this._tab = tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async waitForPageLoaded() {
|
public async waitForPageLoaded() {
|
||||||
return exec(this._tab.id!, async () => {
|
return this.run(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() * 100) + 200));
|
await new Promise((resolve) => setTimeout(resolve, ~~(Math.random() * 100) + 200));
|
||||||
@ -156,8 +176,18 @@ export class AmazonDetailPageInjector {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getBaseInfo() {
|
||||||
|
return this.run(async () => {
|
||||||
|
const title = document.querySelector<HTMLElement>('#title')!.innerText;
|
||||||
|
const price = document.querySelector<HTMLElement>(
|
||||||
|
'.a-price:not(.a-text-price) .a-offscreen',
|
||||||
|
)?.innerText;
|
||||||
|
return { title, price };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async getRatingInfo() {
|
public async getRatingInfo() {
|
||||||
return await exec(this._tab.id!, async () => {
|
return this.run(async () => {
|
||||||
const review = document.querySelector('#averageCustomerReviews');
|
const review = document.querySelector('#averageCustomerReviews');
|
||||||
const rating = Number(
|
const rating = Number(
|
||||||
review?.querySelector('#acrPopover')?.getAttribute('title')?.split(' ')[0],
|
review?.querySelector('#acrPopover')?.getAttribute('title')?.split(' ')[0],
|
||||||
@ -177,9 +207,9 @@ export class AmazonDetailPageInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getRankText() {
|
public async getRankText() {
|
||||||
return exec(this._tab.id!, async () => {
|
return this.run(async () => {
|
||||||
const xpathExps = [
|
const xpathExps = [
|
||||||
`//div[@id='detailBulletsWrapper_feature_div']//ul[.//li[contains(., 'Best Sellers Rank')]]//span[@class='a-list-item']`,
|
`//div[@id='detailBulletsWrapper_feature_div']//ul[.//li[contains(., 'Best Sellers Rank')]]//span[@class='a-list-item' and contains(., 'Best Sellers Rank')]`,
|
||||||
`//div[@id='prodDetails']//table/tbody/tr[th[1][contains(text(), 'Best Sellers Rank')]]/td`,
|
`//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`,
|
`//div[@id='productDetails_db_sections']//table/tbody/tr[th[1][contains(text(), 'Best Sellers Rank')]]/td`,
|
||||||
];
|
];
|
||||||
@ -201,7 +231,7 @@ export class AmazonDetailPageInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getImageUrls() {
|
public async getImageUrls() {
|
||||||
return exec(this._tab.id!, async () => {
|
return this.run(async () => {
|
||||||
let urls = Array.from(document.querySelectorAll<HTMLImageElement>('.imageThumbnail img')).map(
|
let urls = Array.from(document.querySelectorAll<HTMLImageElement>('.imageThumbnail img')).map(
|
||||||
(e) => e.src,
|
(e) => e.src,
|
||||||
);
|
);
|
||||||
@ -212,11 +242,9 @@ export class AmazonDetailPageInjector {
|
|||||||
overlay.click();
|
overlay.click();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
urls = [
|
urls = Array.from(
|
||||||
...(document.querySelectorAll(
|
document.querySelectorAll<HTMLDivElement>('#ivThumbs .ivThumbImage[style]'),
|
||||||
'#ivThumbs .ivThumbImage[style]',
|
).map((e) => e.style.background);
|
||||||
) as unknown as HTMLDivElement[]),
|
|
||||||
].map((e) => e.style.background);
|
|
||||||
urls = urls.map((s) => {
|
urls = urls.map((s) => {
|
||||||
const [url] = /(?<=url\(").+(?=")/.exec(s)!;
|
const [url] = /(?<=url\(").+(?=")/.exec(s)!;
|
||||||
return url;
|
return url;
|
||||||
@ -243,7 +271,7 @@ export class AmazonDetailPageInjector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getTopReviews() {
|
public async getTopReviews() {
|
||||||
return exec<Omit<AmazonReview, 'asin'>[]>(this._tab.id!, async () => {
|
return this.run(async () => {
|
||||||
const targetNode = document.querySelector<HTMLDivElement>('.cr-widget-FocalReviews');
|
const targetNode = document.querySelector<HTMLDivElement>('.cr-widget-FocalReviews');
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
return [];
|
return [];
|
||||||
@ -258,18 +286,19 @@ export class AmazonDetailPageInjector {
|
|||||||
null,
|
null,
|
||||||
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
||||||
);
|
);
|
||||||
const items: Omit<AmazonReview, 'asin'>[] = [];
|
const items: AmazonReview[] = [];
|
||||||
for (let i = 0; i < xResult.snapshotLength; i++) {
|
for (let i = 0; i < xResult.snapshotLength; i++) {
|
||||||
const commentNode = xResult.snapshotItem(i) as HTMLDivElement | null;
|
const commentNode = xResult.snapshotItem(i) as HTMLDivElement | null;
|
||||||
if (!commentNode) {
|
if (!commentNode) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const id = commentNode.id.split('-')[0];
|
||||||
const username = commentNode.querySelector<HTMLDivElement>('.a-profile-name')!.innerText;
|
const username = commentNode.querySelector<HTMLDivElement>('.a-profile-name')!.innerText;
|
||||||
const title = commentNode.querySelector<HTMLDivElement>(
|
const title = commentNode.querySelector<HTMLDivElement>(
|
||||||
'[data-hook="review-title"] > span:not(.a-letter-space)',
|
'[data-hook="review-title"] > span:not(.a-letter-space)',
|
||||||
)!.innerText;
|
)!.innerText;
|
||||||
const rating = commentNode.querySelector<HTMLDivElement>(
|
const rating = commentNode.querySelector<HTMLDivElement>(
|
||||||
'[data-hook="review-star-rating"]',
|
'[data-hook*="review-star-rating"]',
|
||||||
)!.innerText;
|
)!.innerText;
|
||||||
const dateInfo = commentNode.querySelector<HTMLDivElement>(
|
const dateInfo = commentNode.querySelector<HTMLDivElement>(
|
||||||
'[data-hook="review-date"]',
|
'[data-hook="review-date"]',
|
||||||
@ -277,9 +306,101 @@ export class AmazonDetailPageInjector {
|
|||||||
const content = commentNode.querySelector<HTMLDivElement>(
|
const content = commentNode.querySelector<HTMLDivElement>(
|
||||||
'[data-hook="review-body"]',
|
'[data-hook="review-body"]',
|
||||||
)!.innerText;
|
)!.innerText;
|
||||||
items.push({ username, title, rating, dateInfo, content });
|
items.push({ id, username, title, rating, dateInfo, content });
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AmazonReviewPageInjector extends BaseInjector {
|
||||||
|
public async waitForPageLoad() {
|
||||||
|
return this.run(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
while (true) {
|
||||||
|
const targetNode = document.querySelector(
|
||||||
|
'#cm_cr-review_list .reviews-content,ul[role="list"]:not(.histogram)',
|
||||||
|
);
|
||||||
|
targetNode?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
if (
|
||||||
|
targetNode &&
|
||||||
|
targetNode.getClientRects().length > 0 &&
|
||||||
|
document.readyState !== 'loading'
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
const loadingNode = document.querySelector('.reviews-loading');
|
||||||
|
if (loadingNode && loadingNode.getClientRects().length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSinglePageReviews() {
|
||||||
|
return this.run(async () => {
|
||||||
|
const targetNode = document.querySelector('#cm_cr-review_list');
|
||||||
|
if (!targetNode) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// targetNode.scrollIntoView({ behavior: "smooth", block: "end" })
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
const xResult = document.evaluate(
|
||||||
|
`.//div[contains(@id, 'review-card')]`,
|
||||||
|
targetNode,
|
||||||
|
null,
|
||||||
|
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
||||||
|
);
|
||||||
|
const items: AmazonReview[] = [];
|
||||||
|
for (let i = 0; i < xResult.snapshotLength; i++) {
|
||||||
|
console.log('handling', i);
|
||||||
|
|
||||||
|
const commentNode = xResult.snapshotItem(i) as HTMLDivElement;
|
||||||
|
if (!commentNode) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const id = commentNode.id.split('-')[0];
|
||||||
|
const username = commentNode.querySelector<HTMLDivElement>('.a-profile-name')!.innerText;
|
||||||
|
const title = commentNode.querySelector<HTMLDivElement>(
|
||||||
|
'[data-hook="review-title"] > span:not(.a-letter-space)',
|
||||||
|
)!.innerText;
|
||||||
|
const rating = commentNode.querySelector<HTMLDivElement>(
|
||||||
|
'[data-hook*="review-star-rating"]',
|
||||||
|
)!.innerText;
|
||||||
|
const dateInfo = commentNode.querySelector<HTMLDivElement>(
|
||||||
|
'[data-hook="review-date"]',
|
||||||
|
)!.innerText;
|
||||||
|
const content = commentNode.querySelector<HTMLDivElement>(
|
||||||
|
'[data-hook="review-body"]',
|
||||||
|
)!.innerText;
|
||||||
|
items.push({ id, username, title, rating, dateInfo, content });
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public jumpToNextPageIfExist() {
|
||||||
|
return this.run(async () => {
|
||||||
|
const latestReview = document.evaluate(
|
||||||
|
`//*[@id='cm_cr-review_list']//li[@data-hook='review'][last()]`,
|
||||||
|
document.body,
|
||||||
|
null,
|
||||||
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||||
|
).singleNodeValue as HTMLElement | null;
|
||||||
|
latestReview?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
const nextPageNode = document.querySelector<HTMLDivElement>(
|
||||||
|
'[data-hook="pagination-bar"] .a-pagination > *:nth-of-type(2)',
|
||||||
|
);
|
||||||
|
nextPageNode?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
const ret = nextPageNode && !nextPageNode.classList.contains('a-disabled');
|
||||||
|
ret && nextPageNode?.querySelector('a')?.click();
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ main {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.result-table {
|
.result-table {
|
||||||
width: 90%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormItemRule, UploadOnChange } from 'naive-ui';
|
import type { FormItemRule, UploadOnChange } from 'naive-ui';
|
||||||
import pageWorkerFactory from '~/logic/page-worker';
|
import pageWorker from '~/logic/page-worker';
|
||||||
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
||||||
import { asinInputText, detailItems } from '~/logic/storage';
|
import { asinInputText, detailItems } from '~/logic/storage';
|
||||||
|
|
||||||
@ -27,7 +27,8 @@ const timelines = ref<
|
|||||||
|
|
||||||
const running = ref(false);
|
const running = ref(false);
|
||||||
|
|
||||||
const worker = pageWorkerFactory.useAmazonPageWorker(); // 获取Page Worker单例
|
//#region Page Worker 初始化Code
|
||||||
|
const worker = pageWorker.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',
|
||||||
@ -38,6 +39,15 @@ worker.channel.on('error', ({ message: msg }) => {
|
|||||||
message.error(msg);
|
message.error(msg);
|
||||||
running.value = false;
|
running.value = false;
|
||||||
});
|
});
|
||||||
|
worker.channel.on('item-base-info-collected', (ev) => {
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'success',
|
||||||
|
title: `商品${ev.asin}基本信息`,
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: `标题: ${ev.title};价格:${ev.price}`,
|
||||||
|
});
|
||||||
|
updateDetailItems(ev);
|
||||||
|
});
|
||||||
worker.channel.on('item-rating-collected', (ev) => {
|
worker.channel.on('item-rating-collected', (ev) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@ -77,6 +87,16 @@ worker.channel.on('item-top-reviews-collected', (ev) => {
|
|||||||
});
|
});
|
||||||
updateDetailItems(ev);
|
updateDetailItems(ev);
|
||||||
});
|
});
|
||||||
|
const updateDetailItems = (row: { asin: string } & Partial<AmazonDetailItem>) => {
|
||||||
|
const asin = row.asin;
|
||||||
|
if (detailItems.value.has(row.asin)) {
|
||||||
|
const origin = detailItems.value.get(row.asin);
|
||||||
|
detailItems.value.set(asin, { ...origin, ...row } as AmazonDetailItem);
|
||||||
|
} else {
|
||||||
|
detailItems.value.set(asin, row as AmazonDetailItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
const handleImportAsin: UploadOnChange = ({ fileList }) => {
|
const handleImportAsin: UploadOnChange = ({ fileList }) => {
|
||||||
if (fileList.length > 0) {
|
if (fileList.length > 0) {
|
||||||
@ -147,17 +167,6 @@ const handleInterrupt = () => {
|
|||||||
worker.stop();
|
worker.stop();
|
||||||
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateDetailItems = (info: AmazonDetailItem) => {
|
|
||||||
const targetIndex = detailItems.value.findLastIndex((item) => info.asin === item.asin);
|
|
||||||
if (targetIndex > -1) {
|
|
||||||
const origin = detailItems.value[targetIndex];
|
|
||||||
const updatedItem = { ...origin, ...info };
|
|
||||||
detailItems.value.splice(targetIndex, 1, updatedItem);
|
|
||||||
} else {
|
|
||||||
detailItems.value.push(info);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -1,8 +1,25 @@
|
|||||||
<script lang="ts" setup></script>
|
<script lang="ts" setup>
|
||||||
|
import pageWorker from '~/logic/page-worker';
|
||||||
|
|
||||||
|
const worker = pageWorker.useAmazonPageWorker();
|
||||||
|
worker.channel.on('item-review-collected', (ev) => {
|
||||||
|
output.value = ev;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputText = ref('');
|
||||||
|
const output = ref<any>(null);
|
||||||
|
|
||||||
|
const handleStart = () => {
|
||||||
|
worker.runReviewPageTask(inputText.value.split('\n').filter((t) => /^[A-Z0-9]{10}/.exec(t)));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="review-page-entry">
|
<div class="review-page-entry">
|
||||||
<header-title>Review Page</header-title>
|
<header-title>Review Page</header-title>
|
||||||
|
<n-input type="textarea" v-model:value="inputText" />
|
||||||
|
<n-button @click="handleStart">测试</n-button>
|
||||||
|
<div>{{ output }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user