mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-01 20:33:40 +08:00
Update
This commit is contained in:
parent
2d29e5c95a
commit
4a12fc7ed3
@ -1,6 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AmazonDetailItem } from '~/logic/page-worker/types';
|
|
||||||
|
|
||||||
defineProps<{ model: AmazonDetailItem }>();
|
defineProps<{ model: AmazonDetailItem }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -31,6 +29,9 @@ defineProps<{ model: AmazonDetailItem }>();
|
|||||||
<n-descriptions-item label="图片链接" :span="4">
|
<n-descriptions-item label="图片链接" :span="4">
|
||||||
<image-link v-for="link in model.imageUrls" :url="link" />
|
<image-link v-for="link in model.imageUrls" :url="link" />
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item v-if="model.aplus" label="A+" :span="4">
|
||||||
|
<image-link :url="model.aplus" />
|
||||||
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -82,20 +82,38 @@ defineExpose({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="ids-input">
|
<div class="ids-input">
|
||||||
<n-space>
|
<div class="header">
|
||||||
<n-button @click="fileDialog.open()" :disabled="disabled" round size="small">
|
<span>
|
||||||
<template #icon>
|
<n-button @click="fileDialog.open()" :disabled="disabled" round size="small">
|
||||||
<gg-import />
|
<template #icon>
|
||||||
</template>
|
<n-icon>
|
||||||
导入
|
<gg-import />
|
||||||
</n-button>
|
</n-icon>
|
||||||
<n-button :disabled="disabled" @click="handleExportIds" round size="small">
|
</template>
|
||||||
<template #icon>
|
导入
|
||||||
<ion-arrow-up-right-box-outline />
|
</n-button>
|
||||||
</template>
|
<n-button :disabled="disabled" @click="handleExportIds" round size="small">
|
||||||
导出
|
<template #icon>
|
||||||
</n-button>
|
<n-icon>
|
||||||
</n-space>
|
<ion-arrow-up-right-box-outline />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
导出
|
||||||
|
</n-button>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<n-popover v-if="$slots['extra-settings']" placement="bottom-end" trigger="click">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button :disabled="disabled" circle size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="18px"><solar-settings-linear /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
<slot name="extra-settings" />
|
||||||
|
</n-popover>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div style="height: 7px" />
|
<div style="height: 7px" />
|
||||||
<n-form-item ref="detail-form-item" label-placement="left" :rule="formItemRule">
|
<n-form-item ref="detail-form-item" label-placement="left" :rule="formItemRule">
|
||||||
<n-input
|
<n-input
|
||||||
@ -109,10 +127,22 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style scoped lang="scss">
|
||||||
.asin-input {
|
.asin-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
> *:first-of-type {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ defineProps<{
|
|||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style scoped lang="scss">
|
||||||
.progress-report {
|
.progress-report {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AmazonReview } from '~/logic/page-worker/types';
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
model: AmazonReview;
|
model: AmazonReview;
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
import { exportToXLSX, Header, importFromXLSX } from '~/logic/excel';
|
||||||
import type { AmazonReview } from '~/logic/page-worker/types';
|
|
||||||
import { reviewItems } from '~/logic/storages/amazon';
|
import { reviewItems } from '~/logic/storages/amazon';
|
||||||
|
|
||||||
const props = defineProps<{ asin: string }>();
|
const props = defineProps<{ asin: string }>();
|
||||||
|
|||||||
64
src/global.d.ts
vendored
64
src/global.d.ts
vendored
@ -8,3 +8,67 @@ declare module '*.vue' {
|
|||||||
const component: any;
|
const component: any;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type AmazonSearchItem = {
|
||||||
|
keywords: string;
|
||||||
|
page: number;
|
||||||
|
link: string;
|
||||||
|
title: string;
|
||||||
|
asin: string;
|
||||||
|
rank: number;
|
||||||
|
price?: string;
|
||||||
|
imageSrc: string;
|
||||||
|
createTime: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type AmazonDetailItem = {
|
||||||
|
asin: string;
|
||||||
|
title: string;
|
||||||
|
price?: string;
|
||||||
|
rating?: number;
|
||||||
|
ratingCount?: number;
|
||||||
|
category1?: { name: string; rank: number };
|
||||||
|
category2?: { name: string; rank: number };
|
||||||
|
imageUrls?: string[];
|
||||||
|
aplus?: string;
|
||||||
|
topReviews?: AmazonReview[];
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type AmazonReview = {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
title: string;
|
||||||
|
rating: string;
|
||||||
|
dateInfo: string;
|
||||||
|
content: string;
|
||||||
|
imageSrc: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type AmazonItem = Pick<AmazonSearchItem, 'asin'> &
|
||||||
|
Partial<AmazonSearchItem> &
|
||||||
|
Partial<AmazonDetailItem> & { hasDetail: boolean };
|
||||||
|
|
||||||
|
declare type HomedepotDetailItem = {
|
||||||
|
OSMID: string;
|
||||||
|
link: string;
|
||||||
|
brandName?: string;
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
rate?: string;
|
||||||
|
reviewCount?: number;
|
||||||
|
mainImageUrl: string;
|
||||||
|
modelInfo?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type LowesDetailItem = {
|
||||||
|
OSMID: string;
|
||||||
|
link: string;
|
||||||
|
brandName?: string;
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
rate?: string;
|
||||||
|
innerText: string;
|
||||||
|
reviewCount?: number;
|
||||||
|
mainImageUrl: string;
|
||||||
|
modelInfo?: string;
|
||||||
|
};
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class Worksheet {
|
|||||||
this.workbook = wb;
|
this.workbook = wb;
|
||||||
}
|
}
|
||||||
|
|
||||||
async readJson(data: Record<string, unknown>[], options: { headers?: Header[] } = {}) {
|
async readJson(data: Record<string, unknown>[], options: { headers?: Header<any>[] } = {}) {
|
||||||
const {
|
const {
|
||||||
headers = data.length > 0
|
headers = data.length > 0
|
||||||
? Object.keys(data[0]).map((k) => ({ label: k, prop: k }) as Header)
|
? Object.keys(data[0]).map((k) => ({ label: k, prop: k }) as Header)
|
||||||
@ -22,9 +22,9 @@ class Worksheet {
|
|||||||
const cols = headers.filter((h) => h.ignore?.out !== true);
|
const cols = headers.filter((h) => h.ignore?.out !== true);
|
||||||
for (let j = 0; j < cols.length; j++) {
|
for (let j = 0; j < cols.length; j++) {
|
||||||
const header = cols[j];
|
const header = cols[j];
|
||||||
const value = getAttribute(item, header.prop);
|
const value = getAttribute(item, header.prop as string);
|
||||||
if (header.formatOutputValue) {
|
if (header.formatOutputValue) {
|
||||||
record[header.label] = await header.formatOutputValue(value, i);
|
record[header.label] = await header.formatOutputValue(value, i, item);
|
||||||
} else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
|
} else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
|
||||||
record[header.label] = value;
|
record[header.label] = value;
|
||||||
} else {
|
} else {
|
||||||
@ -76,7 +76,7 @@ class Worksheet {
|
|||||||
const value = header.parseImportValue
|
const value = header.parseImportValue
|
||||||
? await header.parseImportValue(item[header.label], i)
|
? await header.parseImportValue(item[header.label], i)
|
||||||
: item[header.label];
|
: item[header.label];
|
||||||
setAttribute(mappedItem, header.prop, value);
|
setAttribute(mappedItem, header.prop as string, value);
|
||||||
}
|
}
|
||||||
return mappedItem;
|
return mappedItem;
|
||||||
}),
|
}),
|
||||||
@ -172,11 +172,11 @@ function setAttribute(obj: Record<string, unknown>, path: string, value: unknown
|
|||||||
current[finalKey] = value;
|
current[finalKey] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Header = {
|
export type Header<T = any> = {
|
||||||
label: string;
|
label: string;
|
||||||
prop: string;
|
prop: keyof T | string;
|
||||||
parseImportValue?: (val: any, index: number) => any;
|
parseImportValue?: (val: any, index: number) => any;
|
||||||
formatOutputValue?: (val: any, index: number) => any;
|
formatOutputValue?: (val: any, index: number, rowData: T) => any;
|
||||||
ignore?: {
|
ignore?: {
|
||||||
in?: boolean;
|
in?: boolean;
|
||||||
out?: boolean;
|
out?: boolean;
|
||||||
@ -193,26 +193,26 @@ export type ImportBaseOptions = {
|
|||||||
headers?: Header[];
|
headers?: Header[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function castRecordsByHeaders<T = Record<string, unknown>>(
|
export function castRecordsByHeaders(
|
||||||
jsonData: Record<string, unknown>[],
|
jsonData: Record<string, unknown>[],
|
||||||
headers: Omit<Header, 'parseImportValue'>[],
|
headers: Omit<Header, 'parseImportValue'>[],
|
||||||
): Promise<T[]> {
|
): Promise<Record<string, unknown>[]> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
jsonData.map(async (item, i) => {
|
jsonData.map(async (item, i) => {
|
||||||
const record: Record<string, unknown> = {};
|
const record: Record<string, unknown> = {};
|
||||||
const cols = headers.filter((h) => h.ignore?.out !== true);
|
const cols = headers.filter((h) => h.ignore?.out !== true);
|
||||||
for (let j = 0; j < cols.length; j++) {
|
for (let j = 0; j < cols.length; j++) {
|
||||||
const header = cols[j];
|
const header = cols[j];
|
||||||
const value = getAttribute(item, header.prop);
|
const value = getAttribute(item, header.prop as string);
|
||||||
if (header.formatOutputValue) {
|
if (header.formatOutputValue) {
|
||||||
record[header.label] = await header.formatOutputValue(value, i);
|
record[header.label] = await header.formatOutputValue(value, i, item);
|
||||||
} else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
|
} else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
|
||||||
record[header.label] = value;
|
record[header.label] = value;
|
||||||
} else {
|
} else {
|
||||||
record[header.label] = JSON.stringify(value);
|
record[header.label] = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return record as T;
|
return record;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import Emittery from 'emittery';
|
import Emittery from 'emittery';
|
||||||
import type { AmazonDetailItem, AmazonPageWorker, AmazonPageWorkerEvents } from './types';
|
import type { AmazonPageWorker, AmazonPageWorkerEvents, LanchTaskBaseOptions } from './types';
|
||||||
import type { Tabs } from 'webextension-polyfill';
|
import type { Tabs } from 'webextension-polyfill';
|
||||||
import { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
import {
|
import {
|
||||||
@ -22,11 +22,11 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
}
|
}
|
||||||
return this._instance;
|
return this._instance;
|
||||||
}
|
}
|
||||||
|
private constructor() {}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
private readonly _controlChannel = new Emittery<{ interrupt: undefined }>();
|
private readonly _controlChannel = new Emittery<{ interrupt: undefined }>();
|
||||||
|
|
||||||
public readonly channel = new Emittery<AmazonPageWorkerEvents>();
|
public readonly channel = new Emittery<AmazonPageWorkerEvents>();
|
||||||
|
|
||||||
private async getCurrentTab(): Promise<Tabs.Tab> {
|
private async getCurrentTab(): Promise<Tabs.Tab> {
|
||||||
@ -103,7 +103,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
public async wanderDetailPage(entry: string) {
|
public async wanderDetailPage(entry: string, aplus: boolean = false) {
|
||||||
//#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}/)) {
|
||||||
@ -182,7 +182,10 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
// });
|
// });
|
||||||
//#endregion
|
//#endregion
|
||||||
// #region Get APlus Sreen shot
|
// #region Get APlus Sreen shot
|
||||||
await injector.scanAPlus();
|
if (aplus && (await injector.scanAPlus())) {
|
||||||
|
const { b64: base64data } = await injector.captureAPlus();
|
||||||
|
this.channel.emit('item-aplus-screenshot-collect', { asin: params.asin, base64data });
|
||||||
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,8 +215,9 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
|
|
||||||
public async runSearchPageTask(
|
public async runSearchPageTask(
|
||||||
keywordsList: string[],
|
keywordsList: string[],
|
||||||
progress?: (remains: string[]) => Promise<void>,
|
options: LanchTaskBaseOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { progress } = options;
|
||||||
let remains = [...keywordsList];
|
let remains = [...keywordsList];
|
||||||
let interrupt = false;
|
let interrupt = false;
|
||||||
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
||||||
@ -230,8 +234,9 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
|
|
||||||
public async runDetaiPageTask(
|
public async runDetaiPageTask(
|
||||||
asins: string[],
|
asins: string[],
|
||||||
progress?: (remains: string[]) => Promise<void>,
|
options: LanchTaskBaseOptions & { aplus?: boolean } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { progress, aplus = false } = options;
|
||||||
const remains = [...asins];
|
const remains = [...asins];
|
||||||
let interrupt = false;
|
let interrupt = false;
|
||||||
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
||||||
@ -239,7 +244,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
});
|
});
|
||||||
while (remains.length > 0 && !interrupt) {
|
while (remains.length > 0 && !interrupt) {
|
||||||
const asin = remains.shift()!;
|
const asin = remains.shift()!;
|
||||||
await this.wanderDetailPage(asin);
|
await this.wanderDetailPage(asin, aplus);
|
||||||
progress && progress(remains);
|
progress && progress(remains);
|
||||||
}
|
}
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
@ -247,8 +252,9 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
|
|
||||||
public async runReviewPageTask(
|
public async runReviewPageTask(
|
||||||
asins: string[],
|
asins: string[],
|
||||||
progress?: (remains: string[]) => Promise<void>,
|
options: LanchTaskBaseOptions = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const { progress } = options;
|
||||||
const remains = [...asins];
|
const remains = [...asins];
|
||||||
let interrupt = false;
|
let interrupt = false;
|
||||||
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
||||||
@ -268,7 +274,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
useAmazonPageWorker(): AmazonPageWorker {
|
getAmazonPageWorker(): AmazonPageWorker {
|
||||||
return AmazonPageWorkerImpl.getInstance();
|
return AmazonPageWorkerImpl.getInstance();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import Emittery from 'emittery';
|
import Emittery from 'emittery';
|
||||||
import { HomedepotEvents, HomedepotWorker } from './types';
|
import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } from './types';
|
||||||
import { Tabs } from 'webextension-polyfill';
|
import { Tabs } from 'webextension-polyfill';
|
||||||
import { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
import { HomedepotDetailPageInjector } from '~/logic/web-injectors/homedepot';
|
import { HomedepotDetailPageInjector } from '~/logic/web-injectors/homedepot';
|
||||||
@ -12,7 +12,7 @@ class HomedepotWorkerImpl implements HomedepotWorker {
|
|||||||
}
|
}
|
||||||
return HomedepotWorkerImpl._instance as HomedepotWorker;
|
return HomedepotWorkerImpl._instance as HomedepotWorker;
|
||||||
}
|
}
|
||||||
private constructor() {}
|
protected constructor() {}
|
||||||
|
|
||||||
readonly channel: Emittery<HomedepotEvents> = new Emittery();
|
readonly channel: Emittery<HomedepotEvents> = new Emittery();
|
||||||
|
|
||||||
@ -36,10 +36,8 @@ class HomedepotWorkerImpl implements HomedepotWorker {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runDetailPageTask(
|
async runDetailPageTask(OSMIDs: string[], options: LanchTaskBaseOptions = {}): Promise<void> {
|
||||||
OSMIDs: string[],
|
const { progress } = options;
|
||||||
progress?: (remains: string[]) => Promise<void> | void,
|
|
||||||
): Promise<void> {
|
|
||||||
const remains = [...OSMIDs];
|
const remains = [...OSMIDs];
|
||||||
let interrupt = false;
|
let interrupt = false;
|
||||||
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
const unsubscribe = this._controlChannel.on('interrupt', () => {
|
||||||
|
|||||||
@ -1,45 +1,8 @@
|
|||||||
import type Emittery from 'emittery';
|
import type Emittery from 'emittery';
|
||||||
import { TaskQueue } from '../task-queue';
|
|
||||||
|
|
||||||
type AmazonSearchItem = {
|
export type LanchTaskBaseOptions = { progress?: (remains: string[]) => Promise<void> | void };
|
||||||
keywords: string;
|
|
||||||
page: number;
|
|
||||||
link: string;
|
|
||||||
title: string;
|
|
||||||
asin: string;
|
|
||||||
rank: number;
|
|
||||||
price?: string;
|
|
||||||
imageSrc: string;
|
|
||||||
createTime: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AmazonDetailItem = {
|
export interface AmazonPageWorkerEvents {
|
||||||
asin: string;
|
|
||||||
title: string;
|
|
||||||
price?: string;
|
|
||||||
rating?: number;
|
|
||||||
ratingCount?: number;
|
|
||||||
category1?: { name: string; rank: number };
|
|
||||||
category2?: { name: string; rank: number };
|
|
||||||
imageUrls?: string[];
|
|
||||||
topReviews?: AmazonReview[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type AmazonReview = {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
title: string;
|
|
||||||
rating: string;
|
|
||||||
dateInfo: string;
|
|
||||||
content: string;
|
|
||||||
imageSrc: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type AmazonItem = Pick<AmazonSearchItem, 'asin'> &
|
|
||||||
Partial<AmazonSearchItem> &
|
|
||||||
Partial<AmazonDetailItem> & { hasDetail: boolean };
|
|
||||||
|
|
||||||
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.
|
||||||
*/
|
*/
|
||||||
@ -63,6 +26,10 @@ interface AmazonPageWorkerEvents {
|
|||||||
* The event is fired when top reviews collected in detail page
|
* 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'>;
|
||||||
|
/**
|
||||||
|
* The event is fired when aplus screenshot-collect
|
||||||
|
*/
|
||||||
|
['item-aplus-screenshot-collect']: { asin: string; base64data: string };
|
||||||
/**
|
/**
|
||||||
* The event is fired when reviews collected in all review page
|
* The event is fired when reviews collected in all review page
|
||||||
*/
|
*/
|
||||||
@ -73,7 +40,7 @@ interface AmazonPageWorkerEvents {
|
|||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AmazonPageWorker {
|
export interface AmazonPageWorker {
|
||||||
/**
|
/**
|
||||||
* The channel for communication with the Amazon page worker.
|
* The channel for communication with the Amazon page worker.
|
||||||
* This is an instance of Emittery, which allows for event-based communication.
|
* This is an instance of Emittery, which allows for event-based communication.
|
||||||
@ -83,29 +50,26 @@ interface AmazonPageWorker {
|
|||||||
/**
|
/**
|
||||||
* 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 keywordsList - The keywords list to search for on Amazon.
|
||||||
* @param progress The callback that receive remaining keywords as the parameter.
|
* @param options The Options Specify Behaviors.
|
||||||
*/
|
*/
|
||||||
runSearchPageTask(
|
runSearchPageTask(keywordsList: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
||||||
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 asins Amazon Standard Identification Numbers.
|
* @param asins Amazon Standard Identification Numbers.
|
||||||
* @param progress The callback that receive remaining asins as the parameter.
|
* @param options The Options Specify Behaviors.
|
||||||
*/
|
*/
|
||||||
runDetaiPageTask(asins: string[], progress?: (remains: string[]) => Promise<void>): Promise<void>;
|
runDetaiPageTask(
|
||||||
|
asins: string[],
|
||||||
|
options?: LanchTaskBaseOptions & { aplus?: boolean },
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browsing goods review page and collect target information.
|
* Browsing goods review page and collect target information.
|
||||||
* @param asins Amazon Standard Identification Numbers.
|
* @param asins Amazon Standard Identification Numbers.
|
||||||
* @param progress The callback that receive remaining asins as the parameter.
|
* @param options The Options Specify Behaviors.
|
||||||
*/
|
*/
|
||||||
runReviewPageTask(
|
runReviewPageTask(asins: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
||||||
asins: string[],
|
|
||||||
progress?: (remains: string[]) => Promise<void>,
|
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the worker.
|
* Stop the worker.
|
||||||
@ -113,19 +77,7 @@ interface AmazonPageWorker {
|
|||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type HomedepotDetailItem = {
|
export interface HomedepotEvents {
|
||||||
OSMID: string;
|
|
||||||
link: string;
|
|
||||||
brandName?: string;
|
|
||||||
title: string;
|
|
||||||
price: string;
|
|
||||||
rate?: string;
|
|
||||||
reviewCount?: number;
|
|
||||||
mainImageUrl: string;
|
|
||||||
modelInfo?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface HomedepotEvents {
|
|
||||||
/**
|
/**
|
||||||
* The event is fired when detail items collect
|
* The event is fired when detail items collect
|
||||||
*/
|
*/
|
||||||
@ -136,7 +88,7 @@ interface HomedepotEvents {
|
|||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HomedepotWorker {
|
export interface HomedepotWorker {
|
||||||
/**
|
/**
|
||||||
* The channel for communication with the Homedepot page worker.
|
* The channel for communication with the Homedepot page worker.
|
||||||
*/
|
*/
|
||||||
@ -145,10 +97,7 @@ interface HomedepotWorker {
|
|||||||
/**
|
/**
|
||||||
* Browsing goods detail page and collect target information
|
* Browsing goods detail page and collect target information
|
||||||
*/
|
*/
|
||||||
runDetailPageTask(
|
runDetailPageTask(OSMIDs: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
||||||
OSMIDs: string[],
|
|
||||||
progress?: (remains: string[]) => Promise<void> | void,
|
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the worker.
|
* Stop the worker.
|
||||||
@ -156,20 +105,7 @@ interface HomedepotWorker {
|
|||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type LowesDetailItem = {
|
export interface LowesEvents {
|
||||||
OSMID: string;
|
|
||||||
link: string;
|
|
||||||
brandName?: string;
|
|
||||||
title: string;
|
|
||||||
price: string;
|
|
||||||
rate?: string;
|
|
||||||
innerText: string;
|
|
||||||
reviewCount?: number;
|
|
||||||
mainImageUrl: string;
|
|
||||||
modelInfo?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface LowesEvents {
|
|
||||||
/**
|
/**
|
||||||
* The event is fired when detail items collect
|
* The event is fired when detail items collect
|
||||||
*/
|
*/
|
||||||
@ -180,7 +116,7 @@ interface LowesEvents {
|
|||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LowesWorker {
|
export interface LowesWorker {
|
||||||
/**
|
/**
|
||||||
* The channel for communication with the Lowes page worker.
|
* The channel for communication with the Lowes page worker.
|
||||||
*/
|
*/
|
||||||
@ -189,10 +125,7 @@ interface LowesWorker {
|
|||||||
/**
|
/**
|
||||||
* Browsing goods detail page and collect target information
|
* Browsing goods detail page and collect target information
|
||||||
*/
|
*/
|
||||||
runDetailPageTask(
|
runDetailPageTask(urls: string[], options?: LanchTaskBaseOptions): Promise<void>;
|
||||||
urls: string[],
|
|
||||||
progress?: (remains: string[]) => Promise<void> | void,
|
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the worker.
|
* Stop the worker.
|
||||||
@ -1,10 +1,4 @@
|
|||||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||||
import type {
|
|
||||||
AmazonDetailItem,
|
|
||||||
AmazonItem,
|
|
||||||
AmazonReview,
|
|
||||||
AmazonSearchItem,
|
|
||||||
} from '~/logic/page-worker/types';
|
|
||||||
|
|
||||||
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
||||||
|
|
||||||
@ -14,6 +8,11 @@ export const reviewAsinInput = useWebExtensionStorage<string>('reviewAsinInputTe
|
|||||||
|
|
||||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
||||||
|
|
||||||
|
export const detailWorkerSettings = useWebExtensionStorage<{ aplus: boolean }>(
|
||||||
|
'amazon-detail-worker-settings',
|
||||||
|
{ aplus: false },
|
||||||
|
);
|
||||||
|
|
||||||
export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>(
|
export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>(
|
||||||
'detailItems',
|
'detailItems',
|
||||||
new Map(),
|
new Map(),
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||||
import { HomedepotDetailItem } from '../page-worker/types';
|
|
||||||
|
|
||||||
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
|
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
|
||||||
'homedepot-details',
|
'homedepot-details',
|
||||||
new Map(),
|
new Map(),
|
||||||
|
|||||||
31
src/logic/upload.ts
Normal file
31
src/logic/upload.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { remoteHost } from '~/env';
|
||||||
|
|
||||||
|
export async function uploadImage(
|
||||||
|
base64String: string,
|
||||||
|
filename: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
// Remove the data URL prefix if present
|
||||||
|
const base64Data = base64String.replace(/^data:image\/png;base64,/, '');
|
||||||
|
// Convert base64 to binary
|
||||||
|
const byteCharacters = atob(base64Data);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
// Create a Blob from the byte array
|
||||||
|
const blob = new Blob([byteArray], { type: 'image/png' });
|
||||||
|
// Create FormData and append the file
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', blob, filename);
|
||||||
|
|
||||||
|
const url = `http://${remoteHost}/upload/image/${encodeURIComponent(filename)}`;
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
return data.file ? `http://${remoteHost}${data.file}` : undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import { BaseInjector } from './base';
|
import { BaseInjector } from './base';
|
||||||
import { AmazonReview, AmazonSearchItem } from '../page-worker/types';
|
|
||||||
|
|
||||||
export class AmazonSearchPageInjector extends BaseInjector {
|
export class AmazonSearchPageInjector extends BaseInjector {
|
||||||
public waitForPageLoaded() {
|
public waitForPageLoaded() {
|
||||||
@ -242,6 +241,7 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
/(?<="large":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
|
/(?<="large":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
document.querySelector<HTMLElement>('header > [data-action="a-popover-close"]')?.click();
|
||||||
return urls;
|
return urls;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -302,7 +302,10 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
|
|
||||||
public async scanAPlus() {
|
public async scanAPlus() {
|
||||||
return this.run(async () => {
|
return this.run(async () => {
|
||||||
const aplusEl = document.querySelector<HTMLElement>('#aplus')!;
|
const aplusEl = document.querySelector<HTMLElement>('#aplus_feature_div');
|
||||||
|
if (!aplusEl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
while (aplusEl.getClientRects().length === 0) {
|
while (aplusEl.getClientRects().length === 0) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
}
|
}
|
||||||
@ -315,8 +318,13 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
window.scrollBy({ top: 100, behavior: 'smooth' });
|
window.scrollBy({ top: 100, behavior: 'smooth' });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100 + ~~(100 * Math.random())));
|
await new Promise((resolve) => setTimeout(resolve, 100 + ~~(100 * Math.random())));
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async captureAPlus() {
|
||||||
|
return this.screenshot({ type: 'CSS', selector: '#aplus_feature_div' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AmazonReviewPageInjector extends BaseInjector {
|
export class AmazonReviewPageInjector extends BaseInjector {
|
||||||
|
|||||||
@ -1,17 +1,42 @@
|
|||||||
import { Tabs } from 'webextension-polyfill';
|
import { Tabs } from 'webextension-polyfill';
|
||||||
import { exec } from '~/logic/execute-script';
|
import { exec } from '~/logic/execute-script';
|
||||||
|
import type { ProtocolMap } from 'webext-bridge';
|
||||||
|
|
||||||
export class BaseInjector {
|
export class BaseInjector {
|
||||||
readonly _tab: Tabs.Tab;
|
readonly _tab: Tabs.Tab;
|
||||||
|
|
||||||
readonly _timeout: number;
|
readonly _timeout: number;
|
||||||
|
readonly _appContext: AppContext;
|
||||||
|
|
||||||
constructor(tab: Tabs.Tab, timeout: number = 30000) {
|
constructor(tab: Tabs.Tab, options: { timeout?: number; appContext?: AppContext } = {}) {
|
||||||
|
const { timeout = 30000, appContext = 'sidepanel' } = options;
|
||||||
this._tab = tab;
|
this._tab = tab;
|
||||||
this._timeout = timeout;
|
this._timeout = timeout;
|
||||||
|
this._appContext = appContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
run<T, P extends Record<string, unknown>>(
|
protected async getMessageSender() {
|
||||||
|
let sender = null;
|
||||||
|
switch (this._appContext) {
|
||||||
|
case 'sidepanel':
|
||||||
|
sender = await import('webext-bridge/sidepanel');
|
||||||
|
return { sendMessage: sender.sendMessage };
|
||||||
|
case 'options':
|
||||||
|
sender = await import('webext-bridge/options');
|
||||||
|
return { sendMessage: sender.sendMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async screenshot(params: ProtocolMap['html-to-image']['data']) {
|
||||||
|
const sender = await this.getMessageSender();
|
||||||
|
return Promise.resolve<ProtocolMap['html-to-image']['return']>(
|
||||||
|
sender.sendMessage('html-to-image', params, {
|
||||||
|
context: 'content-script',
|
||||||
|
tabId: this._tab.id!,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected run<T, P extends Record<string, unknown>>(
|
||||||
func: (payload: P) => Promise<T>,
|
func: (payload: P) => Promise<T>,
|
||||||
payload?: P,
|
payload?: P,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { Tabs } from 'webextension-polyfill';
|
import { Tabs } from 'webextension-polyfill';
|
||||||
import { BaseInjector } from './base';
|
import { BaseInjector } from './base';
|
||||||
import { HomedepotDetailItem } from '../page-worker/types';
|
|
||||||
|
|
||||||
export class HomedepotDetailPageInjector extends BaseInjector {
|
export class HomedepotDetailPageInjector extends BaseInjector {
|
||||||
constructor(tab: Tabs.Tab) {
|
constructor(tab: Tabs.Tab) {
|
||||||
super(tab, 60000);
|
super(tab, { timeout: 60000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
public waitForPageLoad() {
|
public waitForPageLoad() {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { BaseInjector } from './base';
|
|||||||
|
|
||||||
export class LowesDetailPageInjector extends BaseInjector {
|
export class LowesDetailPageInjector extends BaseInjector {
|
||||||
constructor(tab: Tabs.Tab) {
|
constructor(tab: Tabs.Tab) {
|
||||||
super(tab, 60000);
|
super(tab, { timeout: 60000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
public waitForPageLoad() {
|
public waitForPageLoad() {
|
||||||
|
|||||||
@ -2,8 +2,7 @@
|
|||||||
import { NButton, NSpace } from 'naive-ui';
|
import { NButton, NSpace } from 'naive-ui';
|
||||||
import type { TableColumn } from '~/components/ResultTable.vue';
|
import type { TableColumn } from '~/components/ResultTable.vue';
|
||||||
import { useCloudExporter } from '~/composables/useCloudExporter';
|
import { useCloudExporter } from '~/composables/useCloudExporter';
|
||||||
import { castRecordsByHeaders, createWorkbook, Header, importFromXLSX } from '~/logic/data-io';
|
import { castRecordsByHeaders, createWorkbook, Header, importFromXLSX } from '~/logic/excel';
|
||||||
import type { AmazonItem, AmazonReview } from '~/logic/page-worker/types';
|
|
||||||
import { allItems, reviewItems } from '~/logic/storages/amazon';
|
import { allItems, reviewItems } from '~/logic/storages/amazon';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
@ -47,7 +46,7 @@ const columns: TableColumn[] = [
|
|||||||
type: 'expand',
|
type: 'expand',
|
||||||
expandable: (row) => row.hasDetail,
|
expandable: (row) => row.hasDetail,
|
||||||
renderExpand(row) {
|
renderExpand(row) {
|
||||||
return <detail-description model={row} />;
|
return <amazon-detail-description model={row} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -133,7 +132,7 @@ const columns: TableColumn[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const extraHeaders: Header[] = [
|
const extraHeaders: Header<AmazonItem>[] = [
|
||||||
{ prop: 'link', label: '商品链接' },
|
{ prop: 'link', label: '商品链接' },
|
||||||
{
|
{
|
||||||
prop: 'hasDetail',
|
prop: 'hasDetail',
|
||||||
@ -150,12 +149,15 @@ const extraHeaders: Header[] = [
|
|||||||
{
|
{
|
||||||
prop: 'imageUrls',
|
prop: 'imageUrls',
|
||||||
label: '商品图片链接',
|
label: '商品图片链接',
|
||||||
formatOutputValue: (val?: string[]) => val?.join(';'),
|
formatOutputValue: (val: string[] | undefined, _i, rowData) => {
|
||||||
|
if (!val) return undefined;
|
||||||
|
return rowData.aplus ? val.concat([rowData.aplus]).join(';') : val.join(';');
|
||||||
|
},
|
||||||
parseImportValue: (val?: string) => val?.split(';'),
|
parseImportValue: (val?: string) => val?.split(';'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const reviewHeaders: Header[] = [
|
const reviewHeaders: Header<AmazonReview>[] = [
|
||||||
{ prop: 'asin', label: 'ASIN' },
|
{ prop: 'asin', label: 'ASIN' },
|
||||||
{ prop: 'username', label: '用户名' },
|
{ prop: 'username', label: '用户名' },
|
||||||
{ prop: 'title', label: '标题' },
|
{ prop: 'title', label: '标题' },
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import type { TableColumn } from '~/components/ResultTable.vue';
|
import type { TableColumn } from '~/components/ResultTable.vue';
|
||||||
import { useCloudExporter } from '~/composables/useCloudExporter';
|
import { useCloudExporter } from '~/composables/useCloudExporter';
|
||||||
import { castRecordsByHeaders, exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
import { castRecordsByHeaders, exportToXLSX, Header, importFromXLSX } from '~/logic/excel';
|
||||||
import { allItems } from '~/logic/storages/homedepot';
|
import { allItems } from '~/logic/storages/homedepot';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|||||||
@ -29,7 +29,6 @@ watch(currentUrl, (newVal) => {
|
|||||||
site.value = 'homedepot';
|
site.value = 'homedepot';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
router.push(`/${site.value}`);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
import type { Timeline } from '~/components/ProgressReport.vue';
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { useLongTask } from '~/composables/useLongTask';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import { amazon as pageWorker } from '~/logic/page-worker';
|
import { amazon as pageWorker } from '~/logic/page-worker';
|
||||||
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
import { detailAsinInput, detailItems, detailWorkerSettings } from '~/logic/storages/amazon';
|
||||||
import { detailAsinInput, detailItems } from '~/logic/storages/amazon';
|
import { uploadImage } from '~/logic/upload';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ watch(isRunning, (newVal) => {
|
|||||||
const asinInputRef = useTemplateRef('asin-input');
|
const asinInputRef = useTemplateRef('asin-input');
|
||||||
|
|
||||||
//#region Page Worker 初始化Code
|
//#region Page Worker 初始化Code
|
||||||
const worker = pageWorker.useAmazonPageWorker(); // 获取Page Worker单例
|
const worker = pageWorker.getAmazonPageWorker(); // 获取Page Worker单例
|
||||||
worker.channel.on('error', ({ message: msg }) => {
|
worker.channel.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -73,6 +73,18 @@ worker.channel.on('item-top-reviews-collected', (ev) => {
|
|||||||
});
|
});
|
||||||
updateDetailItems(ev);
|
updateDetailItems(ev);
|
||||||
});
|
});
|
||||||
|
worker.channel.on('item-aplus-screenshot-collect', (ev) => {
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'success',
|
||||||
|
title: `商品${ev.asin}A+`,
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: `获取到A+`,
|
||||||
|
});
|
||||||
|
uploadImage(ev.base64data, `${ev.asin}.png`).then((url) => {
|
||||||
|
url && updateDetailItems({ asin: ev.asin, aplus: url });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const updateDetailItems = (row: { asin: string } & Partial<AmazonDetailItem>) => {
|
const updateDetailItems = (row: { asin: string } & Partial<AmazonDetailItem>) => {
|
||||||
const asin = row.asin;
|
const asin = row.asin;
|
||||||
if (detailItems.value.has(row.asin)) {
|
if (detailItems.value.has(row.asin)) {
|
||||||
@ -94,8 +106,11 @@ const task = async () => {
|
|||||||
content: '开始数据采集',
|
content: '开始数据采集',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await worker.runDetaiPageTask(asinList, async (remains) => {
|
await worker.runDetaiPageTask(asinList, {
|
||||||
detailAsinInput.value = remains.join('\n');
|
progress: (remains) => {
|
||||||
|
detailAsinInput.value = remains.join('\n');
|
||||||
|
},
|
||||||
|
aplus: detailWorkerSettings.value.aplus,
|
||||||
});
|
});
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -120,7 +135,17 @@ const handleInterrupt = () => {
|
|||||||
<div class="detail-page-entry">
|
<div class="detail-page-entry">
|
||||||
<header-title>Amazon Detail</header-title>
|
<header-title>Amazon Detail</header-title>
|
||||||
<div class="interative-section">
|
<div class="interative-section">
|
||||||
<ids-input v-model="detailAsinInput" :disabled="isRunning" ref="asin-input" />
|
<ids-input v-model="detailAsinInput" :disabled="isRunning" ref="asin-input">
|
||||||
|
<template #extra-settings>
|
||||||
|
<div class="setting-panel">
|
||||||
|
<n-form label-placement="left">
|
||||||
|
<n-form-item label="Aplus: " :feedback-style="{ display: 'none' }">
|
||||||
|
<n-switch v-model:value="detailWorkerSettings.aplus" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ids-input>
|
||||||
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
|
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
@ -173,4 +198,8 @@ const handleInterrupt = () => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-panel {
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
import type { Timeline } from '~/components/ProgressReport.vue';
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { useLongTask } from '~/composables/useLongTask';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import { amazon as pageWorker } from '~/logic/page-worker';
|
import { amazon as pageWorker } from '~/logic/page-worker';
|
||||||
import type { AmazonReview } from '~/logic/page-worker/types';
|
|
||||||
import { reviewAsinInput, reviewItems } from '~/logic/storages/amazon';
|
import { reviewAsinInput, reviewItems } from '~/logic/storages/amazon';
|
||||||
|
|
||||||
const { isRunning, startTask } = useLongTask();
|
const { isRunning, startTask } = useLongTask();
|
||||||
@ -16,7 +15,7 @@ watch(isRunning, (newVal) => {
|
|||||||
newVal ? emit('start') : emit('stop');
|
newVal ? emit('start') : emit('stop');
|
||||||
});
|
});
|
||||||
|
|
||||||
const worker = pageWorker.useAmazonPageWorker();
|
const worker = pageWorker.getAmazonPageWorker();
|
||||||
worker.channel.on('error', ({ message: msg }) => {
|
worker.channel.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -52,8 +51,10 @@ const task = async () => {
|
|||||||
content: '开始数据采集',
|
content: '开始数据采集',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
await worker.runReviewPageTask(asinList, async (remains) => {
|
await worker.runReviewPageTask(asinList, {
|
||||||
reviewAsinInput.value = remains.join('\n');
|
progress: (remains) => {
|
||||||
|
reviewAsinInput.value = remains.join('\n');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
|
|||||||
@ -19,7 +19,7 @@ watch(isRunning, (newVal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//#region Initial Page Worker
|
//#region Initial Page Worker
|
||||||
const worker = pageWorker.useAmazonPageWorker();
|
const worker = pageWorker.getAmazonPageWorker();
|
||||||
worker.channel.on('error', ({ message: msg }) => {
|
worker.channel.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -54,16 +54,18 @@ const task = async () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
timelines.value.push();
|
timelines.value.push();
|
||||||
await worker.runSearchPageTask(kws, async (remains) => {
|
await worker.runSearchPageTask(kws, {
|
||||||
if (remains.length > 0) {
|
progress: (remains) => {
|
||||||
timelines.value.push({
|
if (remains.length > 0) {
|
||||||
type: 'info',
|
timelines.value.push({
|
||||||
title: '开始',
|
type: 'info',
|
||||||
time: new Date().toLocaleString(),
|
title: '开始',
|
||||||
content: `关键词: ${remains[0]} 数据采集开始`,
|
time: new Date().toLocaleString(),
|
||||||
});
|
content: `关键词: ${remains[0]} 数据采集开始`,
|
||||||
keywordsList.value = remains;
|
});
|
||||||
}
|
keywordsList.value = remains;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
|
|||||||
@ -28,7 +28,14 @@ const handleStart = () =>
|
|||||||
content: '任务开始',
|
content: '任务开始',
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
});
|
});
|
||||||
await worker.runDetailPageTask(inputText.value.split('\n').filter((id) => /\d+/.exec(id)));
|
await worker.runDetailPageTask(
|
||||||
|
inputText.value.split('\n').filter((id) => /\d+/.exec(id)),
|
||||||
|
{
|
||||||
|
progress: (remains) => {
|
||||||
|
inputText.value = remains.join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: `结束`,
|
title: `结束`,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user