mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-05 06:50:06 +08:00
Adjust page worker struture
This commit is contained in:
parent
d66c40b9e7
commit
3aef704fa5
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "azon-seeker",
|
"name": "azon-seeker",
|
||||||
"displayName": "Azon Seeker",
|
"displayName": "Azon Seeker",
|
||||||
"version": "0.4.1",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Starter modify by honestfox101",
|
"description": "Starter modify by honestfox101",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
88
src/components/ExportPanel.vue
Normal file
88
src/components/ExportPanel.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useExcelHelper } from '~/composables/useExcelHelper';
|
||||||
|
|
||||||
|
const excelHelper = useExcelHelper();
|
||||||
|
|
||||||
|
const emit = defineEmits<{ ['export']: [opt: 'local' | 'cloud'] }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul v-if="!excelHelper.isRunning.value" class="exporter-menu">
|
||||||
|
<li @click="emit('export', 'local')">
|
||||||
|
<n-tooltip :delay="1000" placement="right">
|
||||||
|
<template #trigger>
|
||||||
|
<div class="menu-item">
|
||||||
|
<n-icon><lucide-sheet /></n-icon>
|
||||||
|
<span>本地导出</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
不包含图片
|
||||||
|
</n-tooltip>
|
||||||
|
</li>
|
||||||
|
<li @click="emit('export', 'cloud')">
|
||||||
|
<n-tooltip :delay="1000" placement="right">
|
||||||
|
<template #trigger>
|
||||||
|
<div class="menu-item">
|
||||||
|
<n-icon><ic-outline-cloud /></n-icon>
|
||||||
|
<span>云端导出</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
包含图片
|
||||||
|
</n-tooltip>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="expoter-progress-panel">
|
||||||
|
<n-progress
|
||||||
|
type="circle"
|
||||||
|
:percentage="(excelHelper.progress.current * 100) / excelHelper.progress.total"
|
||||||
|
>
|
||||||
|
<span> {{ excelHelper.progress.current }}/{{ excelHelper.progress.total }} </span>
|
||||||
|
</n-progress>
|
||||||
|
<n-button @click="excelHelper.stop()">停止</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.exporter-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
font-size: 15px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
color: #222;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f0f6fa;
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expoter-progress-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 15px;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
import { exportToXLSX, Header, importFromXLSX } from '~/logic/excel';
|
import { exportToXLSX, Header, importFromXLSX } from '~/logic/excel';
|
||||||
import { reviewItems } from '~/logic/storages/amazon';
|
import { reviewItems } from '~/storages/amazon';
|
||||||
|
|
||||||
const props = defineProps<{ asin: string }>();
|
const props = defineProps<{ asin: string }>();
|
||||||
|
|
||||||
|
|||||||
82
src/composables/useExcelHelper.ts
Normal file
82
src/composables/useExcelHelper.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { createGlobalState } from '@vueuse/core';
|
||||||
|
import {
|
||||||
|
useCloudExporter,
|
||||||
|
type DataFragment as CloudDataFragment,
|
||||||
|
} from '~/composables/useCloudExporter';
|
||||||
|
import { formatRecords, createWorkbook, importFromXLSX, type Header } from '~/logic/excel';
|
||||||
|
|
||||||
|
export type InputDataFragment = {
|
||||||
|
headers: Header[];
|
||||||
|
} & CloudDataFragment;
|
||||||
|
|
||||||
|
export type OutputDataFragment<T = Record<string, unknown>> = {
|
||||||
|
headers: Header[];
|
||||||
|
data: T[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildExcelHelper() {
|
||||||
|
const cloudExporter = useCloudExporter();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
const exportFile = async (
|
||||||
|
dataFragments: InputDataFragment[],
|
||||||
|
options: { cloud?: boolean } = {},
|
||||||
|
) => {
|
||||||
|
dataFragments = toRaw(dataFragments);
|
||||||
|
const { cloud = false } = options;
|
||||||
|
if (cloud) {
|
||||||
|
message.warning('正在导出,请勿关闭当前页面!', { duration: 2000 });
|
||||||
|
const fragments = await Promise.all(
|
||||||
|
dataFragments.map(async (fragment) => {
|
||||||
|
const { data, headers } = fragment;
|
||||||
|
fragment.data = await formatRecords(data, headers);
|
||||||
|
return fragment;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const filename = await cloudExporter.doExport(fragments);
|
||||||
|
filename && message.info(`导出完成`);
|
||||||
|
} else {
|
||||||
|
const wb = createWorkbook();
|
||||||
|
for (const fragment of dataFragments) {
|
||||||
|
const { data, headers, name } = fragment;
|
||||||
|
const sheet = wb.addSheet(name);
|
||||||
|
await sheet.readJson(data, { headers });
|
||||||
|
}
|
||||||
|
await wb.exportFile(`${dayjs().format('YYYY-MM-DD')}.xlsx`);
|
||||||
|
message.info('导出完成');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importFile = async (
|
||||||
|
file: File,
|
||||||
|
options: { [sheetname: string]: Header[] } | Header[][],
|
||||||
|
) => {
|
||||||
|
const wb = await importFromXLSX(file, { asWorkBook: true });
|
||||||
|
const output: OutputDataFragment[] = [];
|
||||||
|
if (Array.isArray(options)) {
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
const sheet = wb.getSheet(i)!;
|
||||||
|
const headers = options[i];
|
||||||
|
const data = await sheet.toJson({ headers });
|
||||||
|
output.push({ data, headers });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [sheetname, headers] of Object.entries(options)) {
|
||||||
|
const sheet = wb.getSheet(sheetname)!;
|
||||||
|
const data = await sheet.toJson({ headers });
|
||||||
|
output.push({ data, headers });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
exportFile,
|
||||||
|
importFile,
|
||||||
|
isRunning: cloudExporter.isRunning,
|
||||||
|
progress: cloudExporter.progress,
|
||||||
|
stop: cloudExporter.stop.bind(cloudExporter),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useExcelHelper = createGlobalState(buildExcelHelper);
|
||||||
@ -52,7 +52,7 @@ class Worksheet {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async toJson<T>(options: { headers?: Header[] } = {}) {
|
async toJson<T = Record<string, unknown>>(options: { headers?: Header[] } = {}) {
|
||||||
const { headers } = options;
|
const { headers } = options;
|
||||||
|
|
||||||
let jsonData: Record<string, unknown>[] = [];
|
let jsonData: Record<string, unknown>[] = [];
|
||||||
@ -113,8 +113,11 @@ class Workbook {
|
|||||||
this._wb = await this._wb.xlsx.load(bf);
|
this._wb = await this._wb.xlsx.load(bf);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSheet(index: number): Worksheet | undefined {
|
getSheet(indexOrName: number | string): Worksheet | undefined {
|
||||||
const ws = this._wb.getWorksheet(index + 1); // Align the index
|
if (typeof indexOrName === 'number') {
|
||||||
|
indexOrName += 1; // Align the index
|
||||||
|
}
|
||||||
|
const ws = this._wb.getWorksheet(indexOrName);
|
||||||
return ws ? new Worksheet(ws, this) : undefined;
|
return ws ? new Worksheet(ws, this) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +196,7 @@ export type ImportBaseOptions = {
|
|||||||
headers?: Header[];
|
headers?: Header[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function castRecordsByHeaders(
|
export function formatRecords(
|
||||||
jsonData: Record<string, unknown>[],
|
jsonData: Record<string, unknown>[],
|
||||||
headers: Omit<Header, 'parseImportValue'>[],
|
headers: Omit<Header, 'parseImportValue'>[],
|
||||||
): Promise<Record<string, unknown>[]> {
|
): Promise<Record<string, unknown>[]> {
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { isFirefox } from '~/env';
|
type ExecOptions = {
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a provided asynchronous function in the context of a specific browser tab.
|
* Executes a provided asynchronous function in the context of a specific browser tab.
|
||||||
@ -6,6 +8,7 @@ import { isFirefox } from '~/env';
|
|||||||
* @param func - The asynchronous function to execute in the tab's context. This function
|
* @param func - The asynchronous function to execute in the tab's context. This function
|
||||||
* should be serializable and must not rely on external closures.
|
* should be serializable and must not rely on external closures.
|
||||||
* @param payload - An optional payload object to pass as an argument to the executed function.
|
* @param payload - An optional payload object to pass as an argument to the executed function.
|
||||||
|
* @param options - The options apply to execute
|
||||||
*
|
*
|
||||||
* @returns A promise that resolves to the result of the executed function, or `null` if an error occurs.
|
* @returns A promise that resolves to the result of the executed function, or `null` if an error occurs.
|
||||||
*
|
*
|
||||||
@ -23,10 +26,6 @@ import { isFirefox } from '~/env';
|
|||||||
* console.log(result); // Outputs: 42
|
* console.log(result); // Outputs: 42
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
type ExecOptions = {
|
|
||||||
timeout?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function exec<T>(
|
export async function exec<T>(
|
||||||
tabId: number,
|
tabId: number,
|
||||||
func: () => Promise<T>,
|
func: () => Promise<T>,
|
||||||
|
|||||||
@ -1,17 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import { site } from '~/logic/storages/global';
|
import { site } from '~/storages/global';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const options: { label: string; value: 'amazon' | 'homedepot' }[] = [
|
const options: { label: string; value: string }[] = [
|
||||||
{ label: 'Amazon Search&Detail', value: 'amazon' },
|
{ label: 'Amazon', value: 'amazon' },
|
||||||
{ label: 'Homedepot Detail', value: 'homedepot' },
|
{ label: 'Amazon Review', value: 'amazon-reviews' },
|
||||||
|
{ label: 'Homedepot', value: 'homedepot' },
|
||||||
];
|
];
|
||||||
|
|
||||||
watch(site, (newVal) => {
|
watch(site, (val) => {
|
||||||
router.push(`/${newVal}`);
|
router.push(val);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
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/excel';
|
import { formatRecords, createWorkbook, Header, importFromXLSX } from '~/logic/excel';
|
||||||
import { allItems, itemColumnSettings, reviewItems } from '~/logic/storages/amazon';
|
import { allItems, itemColumnSettings, reviewItems } from '~/storages/amazon';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
@ -138,7 +138,6 @@ const extraHeaders: Header<AmazonItem>[] = [
|
|||||||
{ prop: 'category1.rank', label: '大类排行' },
|
{ prop: 'category1.rank', label: '大类排行' },
|
||||||
{ prop: 'category2.name', label: '小类' },
|
{ prop: 'category2.name', label: '小类' },
|
||||||
{ prop: 'category2.rank', label: '小类排行' },
|
{ prop: 'category2.rank', label: '小类排行' },
|
||||||
{ prop: 'timestamp', label: '获取日期(详情页)' },
|
|
||||||
{
|
{
|
||||||
prop: 'imageUrls',
|
prop: 'imageUrls',
|
||||||
label: '商品图片链接',
|
label: '商品图片链接',
|
||||||
@ -249,8 +248,8 @@ const handleCloudExport = async () => {
|
|||||||
a.push(...reviews.map((r) => ({ asin, ...r })));
|
a.push(...reviews.map((r) => ({ asin, ...r })));
|
||||||
return a;
|
return a;
|
||||||
}, []);
|
}, []);
|
||||||
const mappedData1 = await castRecordsByHeaders(items, itemHeaders);
|
const mappedData1 = await formatRecords(items, itemHeaders);
|
||||||
const mappedData2 = await castRecordsByHeaders(reviews, reviewHeaders);
|
const mappedData2 = await formatRecords(reviews, reviewHeaders);
|
||||||
const fragments = [
|
const fragments = [
|
||||||
{ data: mappedData1, imageColumn: ['A+截图', '商品图片链接'], name: 'items' },
|
{ data: mappedData1, imageColumn: ['A+截图', '商品图片链接'], name: 'items' },
|
||||||
{ data: mappedData2, imageColumn: '图片链接', name: 'reviews' },
|
{ data: mappedData2, imageColumn: '图片链接', name: 'reviews' },
|
||||||
@ -393,10 +392,10 @@ const handleClearData = async () => {
|
|||||||
v-model:value="filter.detailDateRange"
|
v-model:value="filter.detailDateRange"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="列筛选">
|
<n-form-item label="列展示">
|
||||||
<n-checkbox-group
|
<n-checkbox-group
|
||||||
:value="Array.from(itemColumnSettings)"
|
:value="Array.from(itemColumnSettings)"
|
||||||
@update:value="(val) => (itemColumnSettings = new Set(val) as any)"
|
@update:value="(val: any) => (itemColumnSettings = new Set(val) as any)"
|
||||||
>
|
>
|
||||||
<n-space item-style="display: flex;">
|
<n-space item-style="display: flex;">
|
||||||
<n-checkbox value="keywords" label="关键词" />
|
<n-checkbox value="keywords" label="关键词" />
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { useCloudExporter } from '~/composables/useCloudExporter';
|
||||||
|
import type { TableColumn } from '~/components/ResultTable.vue';
|
||||||
|
import { allReviews } from '~/storages/amazon';
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
const cloudExporter = useCloudExporter();
|
||||||
|
|
||||||
|
const filter = ref<{ keywords?: string; rating?: string }>({});
|
||||||
|
|
||||||
|
const columns = computed<TableColumn[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: 'ASIN',
|
||||||
|
key: 'asin',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredData = computed(() => {
|
||||||
|
return allReviews.value;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="result-table">
|
||||||
|
<result-table :columns="columns" :records="filteredData"> </result-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.result-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
<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/excel';
|
import { formatRecords, exportToXLSX, Header, importFromXLSX } from '~/logic/excel';
|
||||||
import { allItems } from '~/logic/storages/homedepot';
|
import { allItems } from '~/storages/homedepot';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const cloudExporter = useCloudExporter();
|
const cloudExporter = useCloudExporter();
|
||||||
@ -11,10 +11,12 @@ const columns: TableColumn[] = [
|
|||||||
{
|
{
|
||||||
title: 'OSMID',
|
title: 'OSMID',
|
||||||
key: 'OSMID',
|
key: 'OSMID',
|
||||||
|
minWidth: 100,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '品牌名称',
|
title: '品牌名称',
|
||||||
key: 'brandName',
|
key: 'brandName',
|
||||||
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '型号信息',
|
title: '型号信息',
|
||||||
@ -27,6 +29,7 @@ const columns: TableColumn[] = [
|
|||||||
{
|
{
|
||||||
title: '价格',
|
title: '价格',
|
||||||
key: 'price',
|
key: 'price',
|
||||||
|
minWidth: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '评分',
|
title: '评分',
|
||||||
@ -110,7 +113,7 @@ const handleLocalExport = async () => {
|
|||||||
const handleCloudExport = async () => {
|
const handleCloudExport = async () => {
|
||||||
message.warning('正在导出,请勿关闭当前页面!', { duration: 2000 });
|
message.warning('正在导出,请勿关闭当前页面!', { duration: 2000 });
|
||||||
const itemHeaders = getItemHeaders();
|
const itemHeaders = getItemHeaders();
|
||||||
const mappedData = await castRecordsByHeaders(filteredData.value, itemHeaders);
|
const mappedData = await formatRecords(filteredData.value, itemHeaders);
|
||||||
const fragments = [{ data: mappedData, imageColumn: '主图链接' }];
|
const fragments = [{ data: mappedData, imageColumn: '主图链接' }];
|
||||||
const filename = await cloudExporter.doExport(fragments);
|
const filename = await cloudExporter.doExport(fragments);
|
||||||
filename && message.info(`导出完成`);
|
filename && message.info(`导出完成`);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
AmazonDetailPageInjector,
|
AmazonDetailPageInjector,
|
||||||
AmazonReviewPageInjector,
|
AmazonReviewPageInjector,
|
||||||
AmazonSearchPageInjector,
|
AmazonSearchPageInjector,
|
||||||
} from '~/logic/web-injectors/amazon';
|
} from './web-injectors/amazon';
|
||||||
import { isForbiddenUrl } from '~/env';
|
import { isForbiddenUrl } from '~/env';
|
||||||
import { BaseWorker } from './base';
|
import { BaseWorker } from './base';
|
||||||
|
|
||||||
|
|||||||
157
src/page-worker/composables/amazon.ts
Normal file
157
src/page-worker/composables/amazon.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
|
import amazon from '../amazon';
|
||||||
|
import { uploadImage } from '~/logic/upload';
|
||||||
|
import {
|
||||||
|
detailItems as amazonDetailItems,
|
||||||
|
reviewItems as amazonReviewItems,
|
||||||
|
searchItems as amazonSearchItems,
|
||||||
|
} from '~/storages/amazon';
|
||||||
|
import { createGlobalState } from '@vueuse/core';
|
||||||
|
|
||||||
|
export interface AmazonPageWorkerSettings {
|
||||||
|
objects?: ('search' | 'detail' | 'review')[];
|
||||||
|
commitChangeIngerval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAmazonPageWorker() {
|
||||||
|
const settings = shallowRef<AmazonPageWorkerSettings>({});
|
||||||
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
|
const worker = amazon.getAmazonPageWorker();
|
||||||
|
|
||||||
|
const searchCache = [] as AmazonSearchItem[];
|
||||||
|
const detailCache = new Map<string, AmazonDetailItem>();
|
||||||
|
const reviewCache = new Map<string, AmazonReview[]>();
|
||||||
|
|
||||||
|
const updateSearchCache = (data: AmazonSearchItem[]) => {
|
||||||
|
searchCache.push(...data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDetailCache = (data: { asin: string } & Partial<AmazonDetailItem>) => {
|
||||||
|
const asin = data.asin;
|
||||||
|
if (detailCache.has(data.asin)) {
|
||||||
|
const origin = detailCache.get(data.asin);
|
||||||
|
detailCache.set(asin, { ...origin, ...data } as AmazonDetailItem);
|
||||||
|
} else {
|
||||||
|
detailCache.set(asin, data as AmazonDetailItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateReviews = (data: { asin: string; reviews: AmazonReview[] }) => {
|
||||||
|
const { asin, reviews } = data;
|
||||||
|
const values = reviewCache.get(asin) || [];
|
||||||
|
const ids = new Set(values.map((item) => item.id));
|
||||||
|
for (const review of reviews) {
|
||||||
|
ids.has(review.id) || values.push(review);
|
||||||
|
}
|
||||||
|
reviewCache.set(asin, values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unsubscribeFuncs = [] as (() => void)[];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
unsubscribeFuncs.push(
|
||||||
|
...[
|
||||||
|
worker.on('error', () => {
|
||||||
|
worker.stop();
|
||||||
|
}),
|
||||||
|
worker.on('item-links-collected', ({ objs }) => {
|
||||||
|
updateSearchCache(objs);
|
||||||
|
}),
|
||||||
|
worker.on('item-base-info-collected', (ev) => {
|
||||||
|
updateDetailCache(ev);
|
||||||
|
}),
|
||||||
|
worker.on('item-category-rank-collected', (ev) => {
|
||||||
|
updateDetailCache(ev);
|
||||||
|
}),
|
||||||
|
worker.on('item-images-collected', (ev) => {
|
||||||
|
updateDetailCache(ev);
|
||||||
|
}),
|
||||||
|
worker.on('item-top-reviews-collected', (ev) => {
|
||||||
|
updateDetailCache(ev);
|
||||||
|
}),
|
||||||
|
worker.on('item-aplus-screenshot-collect', async (ev) => {
|
||||||
|
const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
|
||||||
|
url && updateDetailCache({ asin: ev.asin, aplus: url });
|
||||||
|
}),
|
||||||
|
worker.on('item-review-collected', (ev) => {
|
||||||
|
updateReviews(ev);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
|
||||||
|
unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const commitChange = () => {
|
||||||
|
const { objects } = settings.value;
|
||||||
|
if (objects?.includes('search')) {
|
||||||
|
amazonSearchItems.value = amazonSearchItems.value.concat(searchCache);
|
||||||
|
searchCache.splice(0, searchCache.length);
|
||||||
|
}
|
||||||
|
if (objects?.includes('detail')) {
|
||||||
|
const detailItems = toRaw(amazonDetailItems.value);
|
||||||
|
for (const [k, v] of detailCache.entries()) {
|
||||||
|
if (detailItems.has(k)) {
|
||||||
|
const item = detailItems.get(k)!;
|
||||||
|
detailItems.set(k, { ...item, ...v });
|
||||||
|
} else {
|
||||||
|
detailItems.set(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
amazonDetailItems.value = detailItems;
|
||||||
|
detailCache.clear();
|
||||||
|
}
|
||||||
|
if (objects?.includes('review')) {
|
||||||
|
const reviewItems = toRaw(amazonReviewItems.value);
|
||||||
|
for (const [asin, reviews] of reviewCache.entries()) {
|
||||||
|
if (reviewItems.has(asin)) {
|
||||||
|
const addIds = new Set(reviews.map((x) => x.id));
|
||||||
|
const origin = reviewItems.get(asin)!;
|
||||||
|
const newReviews = origin.filter((x) => !addIds.has(x.id)).concat(reviews);
|
||||||
|
newReviews.sort((a, b) => dayjs(b.dateInfo).diff(dayjs(a.dateInfo)));
|
||||||
|
reviewItems.set(asin, newReviews);
|
||||||
|
} else {
|
||||||
|
reviewItems.set(asin, reviews);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
amazonReviewItems.value = reviewItems;
|
||||||
|
reviewCache.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskWrapper = <T extends (...params: any) => any>(func: T) => {
|
||||||
|
const { commitChangeIngerval = 10000 } = settings.value;
|
||||||
|
searchCache.splice(0, searchCache.length);
|
||||||
|
detailCache.clear();
|
||||||
|
reviewCache.clear();
|
||||||
|
return (...params: Parameters<T>) =>
|
||||||
|
startTask(async () => {
|
||||||
|
const interval = setInterval(() => commitChange(), commitChangeIngerval);
|
||||||
|
await func(...params);
|
||||||
|
clearInterval(interval);
|
||||||
|
commitChange();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runDetailPageTask = taskWrapper(worker.runDetailPageTask.bind(worker));
|
||||||
|
const runSearchPageTask = taskWrapper(worker.runSearchPageTask.bind(worker));
|
||||||
|
const runReviewPageTask = taskWrapper(worker.runReviewPageTask.bind(worker));
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
isRunning,
|
||||||
|
runSearchPageTask,
|
||||||
|
runDetailPageTask,
|
||||||
|
runReviewPageTask,
|
||||||
|
on: worker.on.bind(worker),
|
||||||
|
off: worker.off.bind(worker),
|
||||||
|
once: worker.once.bind(worker),
|
||||||
|
stop: worker.stop.bind(worker),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAmazonWorker = createGlobalState(buildAmazonPageWorker);
|
||||||
85
src/page-worker/composables/homedepot.ts
Normal file
85
src/page-worker/composables/homedepot.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
|
import { detailItems as homedepotDetailItems } from '~/storages/homedepot';
|
||||||
|
import homedepot from '../homedepot';
|
||||||
|
import { createGlobalState } from '@vueuse/core';
|
||||||
|
|
||||||
|
export interface HomedepotWorkerSettings {
|
||||||
|
objects?: 'detail'[];
|
||||||
|
commitChangeIngerval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHomedepotWorker() {
|
||||||
|
const settings = shallowRef<HomedepotWorkerSettings>({});
|
||||||
|
const worker = homedepot.getHomedepotWorker();
|
||||||
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
|
const detailCache = new Map<string, HomedepotDetailItem>();
|
||||||
|
|
||||||
|
const unsubscribeFuncs = [] as (() => void)[];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
unsubscribeFuncs.push(
|
||||||
|
...[
|
||||||
|
worker.on('error', () => {
|
||||||
|
worker.stop();
|
||||||
|
}),
|
||||||
|
worker.on('detail-item-collected', (ev) => {
|
||||||
|
const { item } = ev;
|
||||||
|
if (detailCache.has(item.OSMID)) {
|
||||||
|
const origin = detailCache.get(item.OSMID);
|
||||||
|
detailCache.set(item.OSMID, { ...origin, ...item });
|
||||||
|
} else {
|
||||||
|
detailCache.set(item.OSMID, item);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
|
||||||
|
unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const commitChange = () => {
|
||||||
|
const { objects } = settings.value;
|
||||||
|
if (objects?.includes('detail')) {
|
||||||
|
const detailItems = toRaw(homedepotDetailItems.value);
|
||||||
|
for (const [k, v] of detailCache.entries()) {
|
||||||
|
if (detailItems.has(k)) {
|
||||||
|
const origin = detailItems.get(k)!;
|
||||||
|
detailItems.set(k, { ...origin, ...v });
|
||||||
|
} else {
|
||||||
|
detailItems.set(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
homedepotDetailItems.value = detailItems;
|
||||||
|
detailCache.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const taskWrapper = <T extends (...params: any) => any>(func: T) => {
|
||||||
|
const { commitChangeIngerval = 10000 } = settings.value;
|
||||||
|
return (...params: Parameters<T>) =>
|
||||||
|
startTask(async () => {
|
||||||
|
const interval = setInterval(() => commitChange(), commitChangeIngerval);
|
||||||
|
await func(...params);
|
||||||
|
clearInterval(interval);
|
||||||
|
commitChange();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runDetailPageTask = taskWrapper(worker.runDetailPageTask.bind(worker));
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
isRunning,
|
||||||
|
runDetailPageTask,
|
||||||
|
on: worker.on.bind(worker),
|
||||||
|
off: worker.off.bind(worker),
|
||||||
|
once: worker.once.bind(worker),
|
||||||
|
stop: worker.stop.bind(worker),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useHomedepotWorker = createGlobalState(buildHomedepotWorker);
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } 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 './web-injectors/homedepot';
|
||||||
import { BaseWorker } from './base';
|
import { BaseWorker } from './base';
|
||||||
|
|
||||||
class HomedepotWorkerImpl
|
class HomedepotWorkerImpl
|
||||||
|
|||||||
@ -1,267 +1,28 @@
|
|||||||
import amazon from './amazon';
|
import { AmazonPageWorkerSettings, useAmazonWorker } from './composables/amazon';
|
||||||
import homedepot from './homedepot';
|
import { HomedepotWorkerSettings, useHomedepotWorker } from './composables/homedepot';
|
||||||
import { uploadImage } from '~/logic/upload';
|
|
||||||
import { useLongTask } from '~/composables/useLongTask';
|
|
||||||
|
|
||||||
export interface AmazonPageWorkerSettings {
|
|
||||||
searchItems?: Ref<AmazonSearchItem[]>;
|
|
||||||
detailItems?: Ref<Map<string, AmazonDetailItem>>;
|
|
||||||
reviewItems?: Ref<Map<string, AmazonReview[]>>;
|
|
||||||
commitChangeIngerval?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AmazonPageWorkerFactory {
|
|
||||||
public amazonWorker: ReturnType<typeof this.buildAmazonPageWorker> | null = null;
|
|
||||||
|
|
||||||
public amazonWorkerSettings: AmazonPageWorkerSettings = {};
|
|
||||||
|
|
||||||
public buildAmazonPageWorker() {
|
|
||||||
const { isRunning, startTask } = useLongTask();
|
|
||||||
|
|
||||||
const worker = amazon.getAmazonPageWorker();
|
|
||||||
|
|
||||||
const searchCache = [] as AmazonSearchItem[];
|
|
||||||
const detailCache = new Map<string, AmazonDetailItem>();
|
|
||||||
const reviewCache = new Map<string, AmazonReview[]>();
|
|
||||||
|
|
||||||
const unsubscribeFuncs = [] as (() => void)[];
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
unsubscribeFuncs.push(
|
|
||||||
...[
|
|
||||||
worker.on('error', () => {
|
|
||||||
worker.stop();
|
|
||||||
}),
|
|
||||||
worker.on('item-links-collected', ({ objs }) => {
|
|
||||||
updateSearchCache(objs);
|
|
||||||
}),
|
|
||||||
worker.on('item-base-info-collected', (ev) => {
|
|
||||||
updateDetailCache(ev);
|
|
||||||
}),
|
|
||||||
worker.on('item-category-rank-collected', (ev) => {
|
|
||||||
updateDetailCache(ev);
|
|
||||||
}),
|
|
||||||
worker.on('item-images-collected', (ev) => {
|
|
||||||
updateDetailCache(ev);
|
|
||||||
}),
|
|
||||||
worker.on('item-top-reviews-collected', (ev) => {
|
|
||||||
updateDetailCache(ev);
|
|
||||||
}),
|
|
||||||
worker.on('item-aplus-screenshot-collect', async (ev) => {
|
|
||||||
const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
|
|
||||||
url && updateDetailCache({ asin: ev.asin, aplus: url });
|
|
||||||
}),
|
|
||||||
worker.on('item-review-collected', (ev) => {
|
|
||||||
updateReviews(ev);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
|
|
||||||
unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateSearchCache = (data: AmazonSearchItem[]) => {
|
|
||||||
searchCache.push(...data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDetailCache = (data: { asin: string } & Partial<AmazonDetailItem>) => {
|
|
||||||
const asin = data.asin;
|
|
||||||
if (detailCache.has(data.asin)) {
|
|
||||||
const origin = detailCache.get(data.asin);
|
|
||||||
detailCache.set(asin, { ...origin, ...data } as AmazonDetailItem);
|
|
||||||
} else {
|
|
||||||
detailCache.set(asin, data as AmazonDetailItem);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateReviews = (data: { asin: string; reviews: AmazonReview[] }) => {
|
|
||||||
const { asin, reviews } = data;
|
|
||||||
const values = reviewCache.get(asin) || [];
|
|
||||||
const ids = new Set(values.map((item) => item.id));
|
|
||||||
for (const review of reviews) {
|
|
||||||
ids.has(review.id) || values.push(review);
|
|
||||||
}
|
|
||||||
reviewCache.set(asin, values);
|
|
||||||
};
|
|
||||||
|
|
||||||
const commitChange = () => {
|
|
||||||
const { searchItems, detailItems, reviewItems } = this.amazonWorkerSettings;
|
|
||||||
if (typeof searchItems !== 'undefined') {
|
|
||||||
searchItems.value = searchItems.value.concat(searchCache);
|
|
||||||
}
|
|
||||||
if (typeof detailItems !== 'undefined') {
|
|
||||||
for (const [k, v] of detailCache.entries()) {
|
|
||||||
detailItems.value.delete(k); // Trigger update
|
|
||||||
detailItems.value.set(k, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof reviewItems !== 'undefined') {
|
|
||||||
for (const [asin, reviews] of reviewCache.entries()) {
|
|
||||||
if (reviewItems.value.has(asin)) {
|
|
||||||
const adds = new Set(reviews.map((x) => x.id));
|
|
||||||
const newReviews = reviewItems.value
|
|
||||||
.get(asin)!
|
|
||||||
.filter((x) => !adds.has(x.id))
|
|
||||||
.concat(reviews);
|
|
||||||
newReviews.sort((a, b) => dayjs(b.dateInfo).diff(dayjs(a.dateInfo)));
|
|
||||||
reviewItems.value.delete(asin); // Trigger update
|
|
||||||
reviewItems.value.set(asin, newReviews);
|
|
||||||
} else {
|
|
||||||
reviewItems.value.set(asin, reviews);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const taskWrapper = <T extends (...params: any) => any>(func: T) => {
|
|
||||||
const { commitChangeIngerval = 10000 } = this.amazonWorkerSettings;
|
|
||||||
searchCache.splice(0, searchCache.length);
|
|
||||||
detailCache.clear();
|
|
||||||
reviewCache.clear();
|
|
||||||
return (...params: Parameters<T>) =>
|
|
||||||
startTask(async () => {
|
|
||||||
const interval = setInterval(() => commitChange(), commitChangeIngerval);
|
|
||||||
await func(...params);
|
|
||||||
clearInterval(interval);
|
|
||||||
commitChange();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const runDetailPageTask = taskWrapper(worker.runDetailPageTask.bind(worker));
|
|
||||||
const runSearchPageTask = taskWrapper(worker.runSearchPageTask.bind(worker));
|
|
||||||
const runReviewPageTask = taskWrapper(worker.runReviewPageTask.bind(worker));
|
|
||||||
|
|
||||||
return {
|
|
||||||
isRunning,
|
|
||||||
runSearchPageTask,
|
|
||||||
runDetailPageTask,
|
|
||||||
runReviewPageTask,
|
|
||||||
on: worker.on.bind(worker),
|
|
||||||
off: worker.off.bind(worker),
|
|
||||||
once: worker.once.bind(worker),
|
|
||||||
stop: worker.stop.bind(worker),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
loadAmazonPageWorker(settings?: AmazonPageWorkerSettings) {
|
|
||||||
if (settings) {
|
|
||||||
this.amazonWorkerSettings = { ...this.amazonWorkerSettings, ...settings };
|
|
||||||
}
|
|
||||||
if (!this.amazonWorker) {
|
|
||||||
this.amazonWorker = this.buildAmazonPageWorker();
|
|
||||||
}
|
|
||||||
return this.amazonWorker;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HomedepotWorkerSettings {
|
|
||||||
detailItems?: Ref<Map<string, HomedepotDetailItem>>;
|
|
||||||
commitChangeIngerval?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class HomedepotWorkerFactory {
|
|
||||||
public homedepotWorkerSettings: HomedepotWorkerSettings = {};
|
|
||||||
|
|
||||||
public homedepotWorker: ReturnType<typeof this.buildHomedepotWorker> | null = null;
|
|
||||||
|
|
||||||
public buildHomedepotWorker() {
|
|
||||||
const worker = homedepot.getHomedepotWorker();
|
|
||||||
const { isRunning, startTask } = useLongTask();
|
|
||||||
|
|
||||||
const detailCache = new Map<string, HomedepotDetailItem>();
|
|
||||||
|
|
||||||
const unsubscribeFuncs = [] as (() => void)[];
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
unsubscribeFuncs.push(
|
|
||||||
...[
|
|
||||||
worker.on('error', () => {
|
|
||||||
worker.stop();
|
|
||||||
}),
|
|
||||||
worker.on('detail-item-collected', (ev) => {
|
|
||||||
const { item } = ev;
|
|
||||||
if (detailCache.has(item.OSMID)) {
|
|
||||||
const origin = detailCache.get(item.OSMID);
|
|
||||||
detailCache.set(item.OSMID, { ...origin, ...item });
|
|
||||||
} else {
|
|
||||||
detailCache.set(item.OSMID, item);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
|
|
||||||
unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
const commitChange = () => {
|
|
||||||
const { detailItems } = this.homedepotWorkerSettings;
|
|
||||||
if (typeof detailItems !== 'undefined') {
|
|
||||||
for (const [k, v] of detailCache.entries()) {
|
|
||||||
detailItems.value.delete(k); // Trigger update
|
|
||||||
detailItems.value.set(k, v);
|
|
||||||
}
|
|
||||||
detailCache.clear();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const taskWrapper = <T extends (...params: any) => any>(func: T) => {
|
|
||||||
const { commitChangeIngerval = 10000 } = this.homedepotWorkerSettings;
|
|
||||||
return (...params: Parameters<T>) =>
|
|
||||||
startTask(async () => {
|
|
||||||
const interval = setInterval(() => commitChange(), commitChangeIngerval);
|
|
||||||
await func(...params);
|
|
||||||
clearInterval(interval);
|
|
||||||
commitChange();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const runDetailPageTask = taskWrapper(worker.runDetailPageTask.bind(worker));
|
|
||||||
|
|
||||||
return {
|
|
||||||
isRunning,
|
|
||||||
runDetailPageTask,
|
|
||||||
on: worker.on.bind(worker),
|
|
||||||
off: worker.off.bind(worker),
|
|
||||||
once: worker.once.bind(worker),
|
|
||||||
stop: worker.stop.bind(worker),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
loadHomedepotWorker(settings?: HomedepotWorkerSettings) {
|
|
||||||
if (settings) {
|
|
||||||
this.homedepotWorkerSettings = { ...this.homedepotWorkerSettings, ...settings };
|
|
||||||
}
|
|
||||||
if (!this.homedepotWorker) {
|
|
||||||
this.homedepotWorker = this.buildHomedepotWorker();
|
|
||||||
}
|
|
||||||
return this.homedepotWorker;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const amazonfacotry = new AmazonPageWorkerFactory();
|
|
||||||
const homedepotfactory = new HomedepotWorkerFactory();
|
|
||||||
|
|
||||||
export function usePageWorker(
|
export function usePageWorker(
|
||||||
type: 'amazon',
|
type: 'amazon',
|
||||||
settings?: AmazonPageWorkerSettings,
|
settings?: AmazonPageWorkerSettings,
|
||||||
): ReturnType<typeof amazonfacotry.loadAmazonPageWorker>;
|
): ReturnType<typeof useAmazonWorker>;
|
||||||
export function usePageWorker(
|
export function usePageWorker(
|
||||||
type: 'homedepot',
|
type: 'homedepot',
|
||||||
settings?: HomedepotWorkerSettings,
|
settings?: HomedepotWorkerSettings,
|
||||||
): ReturnType<typeof homedepotfactory.loadHomedepotWorker>;
|
): ReturnType<typeof useHomedepotWorker>;
|
||||||
export function usePageWorker(type: 'amazon' | 'homedepot', settings?: any) {
|
export function usePageWorker(type: 'amazon' | 'homedepot', settings: any) {
|
||||||
|
let worker = null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'amazon':
|
case 'amazon':
|
||||||
return amazonfacotry.loadAmazonPageWorker(settings);
|
worker = useAmazonWorker();
|
||||||
|
break;
|
||||||
case 'homedepot':
|
case 'homedepot':
|
||||||
return homedepotfactory.loadHomedepotWorker(settings);
|
worker = useHomedepotWorker();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported page worker type: ${type}`);
|
throw new Error(`Unsupported page worker type: ${type}`);
|
||||||
}
|
}
|
||||||
|
if (!!settings && !!worker.settings) {
|
||||||
|
worker.settings.value = settings;
|
||||||
|
}
|
||||||
|
return worker;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,22 @@ export class HomedepotDetailPageInjector extends BaseInjector {
|
|||||||
let timeout = false;
|
let timeout = false;
|
||||||
setTimeout(() => (timeout = true), 15000);
|
setTimeout(() => (timeout = true), 15000);
|
||||||
const isLoaded = () => {
|
const isLoaded = () => {
|
||||||
const reviewPlaceholderEl = document.querySelector(
|
const reviewSuffix = document.evaluate(
|
||||||
`#product-section-rr div[role='button'][aria-expanded='true']`,
|
`//*[@id='product-section-rr']//p/text()[starts-with(., ' out of 5')]`,
|
||||||
|
document,
|
||||||
|
null,
|
||||||
|
XPathResult.STRING_TYPE,
|
||||||
|
);
|
||||||
|
const writeFirstReviewButton = document.evaluate(
|
||||||
|
`//section[@id='product-section-ratings-reviews']//span[starts-with(text(), 'Write the First Review')]`,
|
||||||
|
document,
|
||||||
|
null,
|
||||||
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
(!!writeFirstReviewButton.singleNodeValue || !!reviewSuffix.stringValue) &&
|
||||||
|
(document.readyState == 'complete' || timeout)
|
||||||
);
|
);
|
||||||
return reviewPlaceholderEl && (document.readyState == 'complete' || timeout);
|
|
||||||
};
|
};
|
||||||
while (true) {
|
while (true) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(Math.random() * 500)));
|
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(Math.random() * 500)));
|
||||||
@ -5,12 +5,13 @@ import {
|
|||||||
createMemoryHistory,
|
createMemoryHistory,
|
||||||
RouteRecordRaw,
|
RouteRecordRaw,
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
import { site } from '~/logic/storages/global';
|
import { site } from '~/storages/global';
|
||||||
|
|
||||||
const routeObj: Record<'sidepanel' | 'options', RouteRecordRaw[]> = {
|
const routeObj: Record<'sidepanel' | 'options', RouteRecordRaw[]> = {
|
||||||
options: [
|
options: [
|
||||||
{ path: '/', redirect: `/${site.value}` },
|
{ path: '/', redirect: `/${site.value}` },
|
||||||
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
|
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
|
||||||
|
{ path: '/amazon-reviews', component: () => import('~/options/views/AmazonReviews.vue') },
|
||||||
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.vue') },
|
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.vue') },
|
||||||
],
|
],
|
||||||
sidepanel: [
|
sidepanel: [
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useCurrentUrl } from '~/composables/useCurrentUrl';
|
import { useCurrentUrl } from '~/composables/useCurrentUrl';
|
||||||
import { site } from '~/logic/storages/global';
|
import { site } from '~/storages/global';
|
||||||
import { zhCN, dateZhCN } from 'naive-ui';
|
import { zhCN, dateZhCN } from 'naive-ui';
|
||||||
|
|
||||||
const theme: GlobalThemeOverrides = {
|
const theme: GlobalThemeOverrides = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Timeline } from '~/components/ProgressReport.vue';
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { usePageWorker } from '~/page-worker';
|
import { usePageWorker } from '~/page-worker';
|
||||||
import { detailAsinInput, detailItems, detailWorkerSettings } from '~/logic/storages/amazon';
|
import { detailAsinInput, detailWorkerSettings } from '~/storages/amazon';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ const timelines = ref<Timeline[]>([]);
|
|||||||
const asinInputRef = useTemplateRef('asin-input');
|
const asinInputRef = useTemplateRef('asin-input');
|
||||||
|
|
||||||
//#region Page Worker 初始化Code
|
//#region Page Worker 初始化Code
|
||||||
const worker = usePageWorker('amazon', { detailItems });
|
const worker = usePageWorker('amazon', { objects: ['detail'] });
|
||||||
worker.on('error', ({ message: msg }) => {
|
worker.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Timeline } from '~/components/ProgressReport.vue';
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { usePageWorker } from '~/page-worker';
|
import { usePageWorker } from '~/page-worker';
|
||||||
import { reviewAsinInput, reviewItems } from '~/logic/storages/amazon';
|
import { reviewAsinInput } from '~/storages/amazon';
|
||||||
|
|
||||||
const worker = usePageWorker('amazon', { reviewItems });
|
const worker = usePageWorker('amazon', { objects: ['review'] });
|
||||||
worker.on('error', ({ message: msg }) => {
|
worker.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { keywordsList } from '~/logic/storages/amazon';
|
import { keywordsList } from '~/storages/amazon';
|
||||||
import { searchItems } from '~/logic/storages/amazon';
|
|
||||||
import type { Timeline } from '~/components/ProgressReport.vue';
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { usePageWorker } from '~/page-worker';
|
import { usePageWorker } from '~/page-worker';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
//#region Initial Page Worker
|
//#region Initial Page Worker
|
||||||
const worker = usePageWorker('amazon', { searchItems });
|
const worker = usePageWorker('amazon', { objects: ['search'] });
|
||||||
worker.on('error', ({ message: msg }) => {
|
worker.on('error', ({ message: msg }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Timeline } from '~/components/ProgressReport.vue';
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { usePageWorker } from '~/page-worker';
|
import { usePageWorker } from '~/page-worker';
|
||||||
import { detailItems } from '~/logic/storages/homedepot';
|
|
||||||
|
|
||||||
const inputText = ref('');
|
const inputText = ref('');
|
||||||
const idInputRef = useTemplateRef('id-input');
|
const idInputRef = useTemplateRef('id-input');
|
||||||
|
|
||||||
const worker = usePageWorker('homedepot', { detailItems });
|
const worker = usePageWorker('homedepot', { objects: ['detail'] });
|
||||||
worker.on('detail-item-collected', ({ item }) => {
|
worker.on('detail-item-collected', ({ item }) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
|||||||
@ -33,6 +33,32 @@ export const reviewItems = useWebExtensionStorage<Map<string, AmazonReview[]>>(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const allReviews = computed({
|
||||||
|
get() {
|
||||||
|
const reviews: ({ asin: string } & AmazonReview)[] = [];
|
||||||
|
for (const [asin, values] of reviewItems.value) {
|
||||||
|
for (const review of values) {
|
||||||
|
reviews.push({ asin, ...review });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reviews;
|
||||||
|
},
|
||||||
|
set(newVal) {
|
||||||
|
const reviews = newVal.reduce<Map<string, AmazonReview[]>>((m, r) => {
|
||||||
|
if (!m.has(r.asin)) {
|
||||||
|
m.set(r.asin, []);
|
||||||
|
}
|
||||||
|
const reviews = m.get(r.asin)!;
|
||||||
|
reviews.push(r);
|
||||||
|
return m;
|
||||||
|
}, new Map());
|
||||||
|
for (const [_, values] of reviews) {
|
||||||
|
values.sort((a, b) => dayjs(b.dateInfo).diff(a.dateInfo));
|
||||||
|
}
|
||||||
|
reviewItems.value = reviews;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const allItems = computed({
|
export const allItems = computed({
|
||||||
get() {
|
get() {
|
||||||
const sItems = unref(searchItems);
|
const sItems = unref(searchItems);
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||||
|
|
||||||
|
export const detailInputText = useWebExtensionStorage('homedepot-detail-input-text', '');
|
||||||
|
|
||||||
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
|
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
|
||||||
'homedepot-details',
|
'homedepot-details',
|
||||||
new Map(),
|
new Map(),
|
||||||
Loading…
x
Reference in New Issue
Block a user