mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-01-19 13:13:22 +08:00
Update
This commit is contained in:
parent
e444777758
commit
296f6687df
@ -139,7 +139,7 @@ export function useWebExtensionStorage<T>(
|
||||
});
|
||||
}
|
||||
|
||||
pullFromStorage(); // Init
|
||||
void pullFromStorage(); // Init
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ watch(opt, (val) => {
|
||||
site.value = 'amazon';
|
||||
break;
|
||||
case '/homedepot':
|
||||
case '/homedepot-reviews':
|
||||
site.value = 'homedepot';
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -142,9 +142,7 @@ function buildAmazonPageWorker(): WorkerComposable<AmazonPageWorker, AmazonPageW
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
}
|
||||
|
||||
// Register event handlers on mount
|
||||
|
||||
@ -37,9 +37,7 @@ function buildHomedepotWorker(): WorkerComposable<HomedepotWorker, HomedepotWork
|
||||
reviewCache.set(OSMID, (reviewCache.get(OSMID) || []).concat(reviews));
|
||||
}),
|
||||
];
|
||||
return () => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
};
|
||||
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
}
|
||||
|
||||
const unsubscribe = registerWorkerEvents();
|
||||
|
||||
94
src/page-worker/composables/lowes.ts
Normal file
94
src/page-worker/composables/lowes.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { createGlobalState } from '@vueuse/core';
|
||||
import lowes from '../impls/lowes';
|
||||
import { useLongTask } from '~/composables/useLongTask';
|
||||
import { detailItems } from '~/storages/lowes';
|
||||
import { taskOptionBase, WorkerComposable } from '../interfaces/common';
|
||||
import { LowesWorker } from '../interfaces/lowes';
|
||||
|
||||
export interface LowesWorkerSettings {
|
||||
objects?: 'detail'[];
|
||||
commitChangeIngerval?: number;
|
||||
}
|
||||
|
||||
function buildLowesWorkerComposable(): WorkerComposable<LowesWorker, LowesWorkerSettings> {
|
||||
const settings = shallowRef<LowesWorkerSettings>({});
|
||||
const worker = lowes.getLowesWorker();
|
||||
const { isRunning, startTask } = useLongTask();
|
||||
|
||||
const detailCache = new Map<string, LowesDetailItem>();
|
||||
|
||||
function registerWorkerEvent() {
|
||||
const unsubscribes = [
|
||||
worker.on('error', () => worker.stop()),
|
||||
worker.on('detail-item-collected', (ev) => {
|
||||
const { item } = ev;
|
||||
if (detailCache.has(item.link)) {
|
||||
const origin = detailCache.get(item.link);
|
||||
detailCache.set(item.link, { ...origin, ...item });
|
||||
} else {
|
||||
detailCache.set(item.link, item);
|
||||
}
|
||||
}),
|
||||
];
|
||||
return () => unsubscribes.forEach((unsbuscribe) => unsbuscribe());
|
||||
}
|
||||
|
||||
const unsbuscribe = registerWorkerEvent();
|
||||
|
||||
onUnmounted(() => {
|
||||
unsbuscribe();
|
||||
});
|
||||
|
||||
const commitChange = async () => {
|
||||
const { objects } = settings.value;
|
||||
if (objects?.includes('detail')) {
|
||||
for (const [k, v] of detailCache.entries()) {
|
||||
if (detailItems.value.has(k)) {
|
||||
const origin = detailItems.value.get(k)!;
|
||||
detailItems.value.set(k, { ...origin, ...v });
|
||||
} else {
|
||||
detailItems.value.set(k, v);
|
||||
}
|
||||
}
|
||||
detailCache.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const taskWrapper2 = <T extends (input: any, options?: taskOptionBase) => Promise<void>>(
|
||||
func: T,
|
||||
) => {
|
||||
return (...params: Parameters<T>) =>
|
||||
startTask(async () => {
|
||||
if (!params?.[1]) {
|
||||
params[1] = {};
|
||||
}
|
||||
const progressReporter = params[1].progress;
|
||||
if (progressReporter) {
|
||||
params[1].progress = async (...p: Parameters<typeof progressReporter>) => {
|
||||
await commitChange();
|
||||
return progressReporter(...p);
|
||||
};
|
||||
} else {
|
||||
params[1].progress = async () => {
|
||||
await commitChange();
|
||||
};
|
||||
}
|
||||
await func(params[0], params[1]);
|
||||
await commitChange();
|
||||
});
|
||||
};
|
||||
|
||||
const runDetailPageTask = taskWrapper2(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 useLowesWorker = createGlobalState(buildLowesWorkerComposable);
|
||||
@ -1,8 +1,12 @@
|
||||
import { taskOptionBase } from '../interfaces/common';
|
||||
import { BaseWorker } from './base';
|
||||
import { LowesEvents, LowesWorker } from '../interfaces/lowes';
|
||||
import { LowesDetailPageInjector } from '../web-injectors/lowes';
|
||||
|
||||
class LowesWorkerImpl extends BaseWorker<LowesEvents> implements LowesWorker {
|
||||
class LowesWorkerImpl
|
||||
extends BaseWorker<LowesEvents & { interrupt: undefined }>
|
||||
implements LowesWorker
|
||||
{
|
||||
private static instance: LowesWorker | null = null;
|
||||
public static getInstance() {
|
||||
if (!this.instance) {
|
||||
@ -14,12 +18,27 @@ class LowesWorkerImpl extends BaseWorker<LowesEvents> implements LowesWorker {
|
||||
super();
|
||||
}
|
||||
|
||||
runDetailPageTask(urls: string[], options?: taskOptionBase): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
async runDetailPageTask(urls: string[], options: taskOptionBase = {}): Promise<void> {
|
||||
const { progress } = options;
|
||||
let interrupt = false;
|
||||
const remains = [...urls];
|
||||
this.on('interrupt', () => {
|
||||
interrupt = true;
|
||||
});
|
||||
while (remains.length > 0 && !interrupt) {
|
||||
const url = remains.shift()!;
|
||||
const tab = await browser.tabs.create({ url });
|
||||
const injector = new LowesDetailPageInjector(tab);
|
||||
await injector.waitForPageLoad();
|
||||
const baseInfo = await injector.getBaseInfo();
|
||||
await this.emit('detail-item-collected', { item: { ...baseInfo, link: url } });
|
||||
progress && progress(remains);
|
||||
setTimeout(() => browser.tabs.remove(tab.id!), 1500);
|
||||
}
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
return this.emit('interrupt');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { AmazonPageWorkerSettings, useAmazonWorker } from './composables/amazon';
|
||||
import { HomedepotWorkerSettings, useHomedepotWorker } from './composables/homedepot';
|
||||
import { LowesWorkerSettings, useLowesWorker } from './composables/lowes';
|
||||
|
||||
export function usePageWorker(
|
||||
type: 'amazon',
|
||||
@ -9,6 +10,10 @@ export function usePageWorker(
|
||||
type: 'homedepot',
|
||||
settings?: HomedepotWorkerSettings,
|
||||
): ReturnType<typeof useHomedepotWorker>;
|
||||
export function usePageWorker(
|
||||
type: 'homedepot',
|
||||
settings?: LowesWorkerSettings,
|
||||
): ReturnType<typeof useLowesWorker>;
|
||||
export function usePageWorker(type: Website, settings: any) {
|
||||
let worker = null;
|
||||
switch (type) {
|
||||
@ -18,6 +23,9 @@ export function usePageWorker(type: Website, settings: any) {
|
||||
case 'homedepot':
|
||||
worker = useHomedepotWorker();
|
||||
break;
|
||||
case 'lowes':
|
||||
worker = useLowesWorker();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported page worker type: ${type}`);
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ export interface LowesEvents {
|
||||
['error']: { message: string; url?: string };
|
||||
}
|
||||
|
||||
export interface LowesWorker {
|
||||
export interface LowesWorker extends Listener<LowesEvents> {
|
||||
/**
|
||||
* Browsing item detail page and collect target information
|
||||
*/
|
||||
|
||||
@ -114,7 +114,7 @@ export class HomedepotDetailPageInjector extends BaseInjector {
|
||||
'script#thd-helmet__script--productStructureData',
|
||||
)!.innerText;
|
||||
const obj = JSON.parse(text);
|
||||
return (obj['image'] as string[]).map((url) => url.slice(1, -1));
|
||||
return obj['image'] as string[];
|
||||
});
|
||||
}
|
||||
|
||||
@ -157,7 +157,9 @@ export class HomedepotDetailPageInjector extends BaseInjector {
|
||||
.filter((t) => t.length !== 0);
|
||||
const imageUrls = Array.from(
|
||||
root.querySelectorAll<HTMLElement>('.media-carousel__media > button'),
|
||||
).map((el) => el.style.backgroundImage.split(/[\(\)]/, 3)[1]);
|
||||
)
|
||||
.map((el) => el.style.backgroundImage.split(/[\(\)]/, 3)[1])
|
||||
.map((url) => url.slice(1, -1));
|
||||
return { title, content, username, dateInfo, rating, badges, imageUrls } as HomedepotReview;
|
||||
});
|
||||
});
|
||||
|
||||
@ -49,12 +49,12 @@ export class LowesDetailPageInjector extends BaseInjector {
|
||||
// 获取标题
|
||||
const title = document.querySelector<HTMLDivElement>(
|
||||
`h1.product-brand-description`,
|
||||
)?.innerText;
|
||||
)!.innerText;
|
||||
|
||||
// 获取价格
|
||||
const price = document
|
||||
.querySelector<HTMLDivElement>(`.screen-reader`)
|
||||
?.innerText.replaceAll('\n', '');
|
||||
.querySelector<HTMLDivElement>(`.screen-reader`)!
|
||||
.innerText.replaceAll('\n', '');
|
||||
|
||||
// 获取评分
|
||||
const rate = document.querySelector<HTMLDivElement>(`.avgrating`)?.innerText;
|
||||
@ -68,7 +68,7 @@ export class LowesDetailPageInjector extends BaseInjector {
|
||||
// 获取图片URL
|
||||
const mainImageUrl = document.querySelector<HTMLImageElement>(
|
||||
`#mfe-gallery .productImage.tile-img`,
|
||||
)?.src;
|
||||
)!.src;
|
||||
|
||||
return {
|
||||
brandName,
|
||||
|
||||
140
src/sidepanel/views/LowesSidepanel.vue
Normal file
140
src/sidepanel/views/LowesSidepanel.vue
Normal file
@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||
import { usePageWorker } from '~/page-worker';
|
||||
import { detailInputText } from '~/storages/homedepot';
|
||||
import { detailWorkerSettings } from '~/storages/homedepot';
|
||||
|
||||
const idInputRef = useTemplateRef('id-input');
|
||||
|
||||
const worker = usePageWorker('homedepot', { objects: ['detail'] });
|
||||
worker.on('detail-item-collected', ({ item }) => {
|
||||
timelines.value.push({
|
||||
type: 'success',
|
||||
title: `成功`,
|
||||
content: `成功获取到${item.OSMID}的商品信息`,
|
||||
time: new Date().toLocaleString(),
|
||||
});
|
||||
});
|
||||
worker.on('review-collected', ({ OSMID, reviews }) => {
|
||||
timelines.value.push({
|
||||
type: 'success',
|
||||
title: `成功`,
|
||||
content: `成功获取到${OSMID}的${reviews.length}条评论`,
|
||||
time: new Date().toLocaleString(),
|
||||
});
|
||||
});
|
||||
|
||||
const timelines = ref<Timeline[]>([]);
|
||||
|
||||
const handleStart = async () => {
|
||||
idInputRef.value?.validate().then(async (success) => {
|
||||
if (success) {
|
||||
const ids = detailInputText.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
||||
timelines.value = [
|
||||
{
|
||||
type: 'info',
|
||||
title: '开始',
|
||||
time: new Date().toLocaleString(),
|
||||
content: `开始采集OSMID: ${ids.join(', ')}`,
|
||||
},
|
||||
];
|
||||
await worker.runDetailPageTask(ids, {
|
||||
progress: (remains) => {
|
||||
if (remains.length > 0) {
|
||||
timelines.value.push({
|
||||
type: 'info',
|
||||
title: '继续',
|
||||
time: new Date().toLocaleString(),
|
||||
content: `剩余: ${remains.length}`,
|
||||
});
|
||||
}
|
||||
detailInputText.value = remains.join('\n');
|
||||
},
|
||||
review: detailWorkerSettings.value.review,
|
||||
});
|
||||
timelines.value.push({
|
||||
type: 'info',
|
||||
title: '结束',
|
||||
time: new Date().toLocaleString(),
|
||||
content: `数据采集完成`,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleInterrupt = () => {
|
||||
worker.stop();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="homedepot-sidepanel">
|
||||
<header-title>Lowes</header-title>
|
||||
<div class="interative-section">
|
||||
<id-input
|
||||
v-model="detailInputText"
|
||||
:disabled="worker.isRunning.value"
|
||||
ref="id-input"
|
||||
:match-pattern="
|
||||
/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?(\n(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?)*\n?$/g
|
||||
"
|
||||
placeholder="输入URL"
|
||||
validate-message="请输入格式正确的URL"
|
||||
/>
|
||||
<optional-button v-if="!worker.isRunning.value" round size="large" @click="handleStart">
|
||||
<template #popover>
|
||||
<div class="setting-panel">
|
||||
<n-form :label-width="50" label-placement="left" :show-feedback="false">
|
||||
<n-form-item label="评论:">
|
||||
<n-switch v-model:value="detailWorkerSettings.review" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
<n-icon :size="20"><ant-design-thunderbolt-outlined /></n-icon>
|
||||
开始
|
||||
</optional-button>
|
||||
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
||||
<n-icon :size="20"><ant-design-thunderbolt-outlined /></n-icon>
|
||||
停止
|
||||
</n-button>
|
||||
</div>
|
||||
<div v-if="worker.isRunning.value" class="running-tip-section">
|
||||
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
|
||||
</div>
|
||||
<progress-report class="progress-report" :timelines="timelines" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.homedepot-sidepanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.interative-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
width: 85%;
|
||||
border-radius: 10px;
|
||||
border: 1px #00000020 dashed;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.running-tip-section {
|
||||
margin: 10px 0 0 0;
|
||||
height: 100px;
|
||||
border-radius: 10px;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.progress-report {
|
||||
margin-top: 10px;
|
||||
width: 95%;
|
||||
}
|
||||
</style>
|
||||
6
src/storages/lowes.ts
Normal file
6
src/storages/lowes.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||
|
||||
export const detailItems = useWebExtensionStorage<Map<string, LowesDetailItem>>(
|
||||
'lowes-details',
|
||||
new Map(),
|
||||
);
|
||||
2
src/types/lowes.d.ts
vendored
2
src/types/lowes.d.ts
vendored
@ -1,11 +1,9 @@
|
||||
declare type LowesDetailItem = {
|
||||
OSMID: string;
|
||||
link: string;
|
||||
brandName?: string;
|
||||
title: string;
|
||||
price: string;
|
||||
rate?: string;
|
||||
innerText: string;
|
||||
reviewCount?: number;
|
||||
mainImageUrl: string;
|
||||
modelInfo?: string;
|
||||
|
||||
@ -19,7 +19,7 @@ declare module '*.md' {
|
||||
|
||||
declare type AppContext = 'options' | 'sidepanel' | 'background' | 'content script';
|
||||
|
||||
declare type Website = 'amazon' | 'homedepot';
|
||||
declare type Website = 'amazon' | 'homedepot' | 'lowes';
|
||||
|
||||
declare const appContext: AppContext;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user