From e4447777586581db76509b86b4ec5f36267471a6 Mon Sep 17 00:00:00 2001
From: johnathan <952508490@qq.com>
Date: Thu, 24 Jul 2025 10:15:57 +0800
Subject: [PATCH] Update
---
src/options/Options.vue | 1 +
src/options/views/HomedepotReviews.vue | 226 +++++++++++++++++++++
src/page-worker/composables/amazon.ts | 20 +-
src/page-worker/composables/homedepot.ts | 70 ++++---
src/page-worker/impls/homedepot.ts | 4 +-
src/page-worker/impls/lowes.ts | 30 +++
src/page-worker/interfaces/common.ts | 25 ---
src/page-worker/interfaces/homedepot.ts | 2 +-
src/page-worker/interfaces/lowes.ts | 15 +-
src/page-worker/web-injectors/homedepot.ts | 9 +-
src/router/index.ts | 1 +
src/sidepanel/views/HomedepotSidepanel.vue | 8 +
src/storages/amazon.ts | 6 -
src/storages/homedepot.ts | 30 ++-
14 files changed, 363 insertions(+), 84 deletions(-)
create mode 100644 src/options/views/HomedepotReviews.vue
create mode 100644 src/page-worker/impls/lowes.ts
diff --git a/src/options/Options.vue b/src/options/Options.vue
index ebb4556..b2cc050 100644
--- a/src/options/Options.vue
+++ b/src/options/Options.vue
@@ -13,6 +13,7 @@ const options: { label: string; value: string }[] = [
{ label: 'Amazon', value: '/amazon' },
{ label: 'Amazon Review', value: '/amazon-reviews' },
{ label: 'Homedepot', value: '/homedepot' },
+ { label: 'Homedepot Review', value: '/homedepot-reviews' },
];
watch(opt, (val) => {
diff --git a/src/options/views/HomedepotReviews.vue b/src/options/views/HomedepotReviews.vue
new file mode 100644
index 0000000..15bc34e
--- /dev/null
+++ b/src/options/views/HomedepotReviews.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
筛选器
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (Array.isArray(newRange)) {
+ filter.dateRange = [newRange[0], newRange[1] + (24 * 3600 * 1000 - 1)];
+ } else {
+ filter.dateRange = newRange;
+ }
+ }
+ "
+ />
+
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
diff --git a/src/page-worker/composables/amazon.ts b/src/page-worker/composables/amazon.ts
index a47933d..e3abe16 100644
--- a/src/page-worker/composables/amazon.ts
+++ b/src/page-worker/composables/amazon.ts
@@ -90,9 +90,9 @@ function buildAmazonPageWorker(): WorkerComposable x.id));
+ const addedIds = new Set(reviews.map((x) => x.id));
const origin = reviewItems.value.get(asin)!;
- const newReviews = origin.filter((x) => !addIds.has(x.id)).concat(reviews);
+ const newReviews = origin.filter((x) => !addedIds.has(x.id)).concat(reviews);
newReviews.sort((a, b) => dayjs(b.dateInfo).diff(dayjs(a.dateInfo)));
reviewItems.value.set(asin, newReviews);
} else {
@@ -104,12 +104,9 @@ function buildAmazonPageWorker(): WorkerComposable void)[] = [];
-
// Register all relevant worker event handlers
function registerWorkerEvents() {
- return [
+ const unsubscribes = [
// Stop worker on error
worker.on('error', () => {
worker.stop();
@@ -144,17 +141,18 @@ function buildAmazonPageWorker(): WorkerComposable {
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
+ };
}
// Register event handlers on mount
- onMounted(() => {
- unsubscribes.push(...registerWorkerEvents());
- });
+ const unsbuscribe = registerWorkerEvents();
// Unregister event handlers on unmount
onUnmounted(() => {
- unsubscribes.forEach((unsubscribe) => unsubscribe());
- unsubscribes.splice(0, unsubscribes.length);
+ unsbuscribe();
});
/**
diff --git a/src/page-worker/composables/homedepot.ts b/src/page-worker/composables/homedepot.ts
index dc5b709..5e58c2e 100644
--- a/src/page-worker/composables/homedepot.ts
+++ b/src/page-worker/composables/homedepot.ts
@@ -1,5 +1,5 @@
import { useLongTask } from '~/composables/useLongTask';
-import { detailItems as homedepotDetailItems } from '~/storages/homedepot';
+import { detailItems, reviewItems } from '~/storages/homedepot';
import homedepot from '../impls/homedepot';
import { createGlobalState } from '@vueuse/core';
import { WorkerComposable } from '../interfaces/common';
@@ -16,45 +16,63 @@ function buildHomedepotWorker(): WorkerComposable();
+ const reviewCache = new Map();
- const unsubscribeFuncs = [] as (() => void)[];
+ function registerWorkerEvents() {
+ const unsubscribes = [
+ 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);
+ }
+ }),
+ worker.on('review-collected', (ev) => {
+ const { OSMID, reviews } = ev;
+ reviewCache.set(OSMID, (reviewCache.get(OSMID) || []).concat(reviews));
+ }),
+ ];
+ return () => {
+ unsubscribes.forEach((unsubscribe) => unsubscribe());
+ };
+ }
- 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);
- }
- }),
- ],
- );
- });
+ const unsubscribe = registerWorkerEvents();
onUnmounted(() => {
- unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
- unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
+ unsubscribe();
});
const commitChange = () => {
const { objects } = settings.value;
if (objects?.includes('detail')) {
for (const [k, v] of detailCache.entries()) {
- if (homedepotDetailItems.value.has(k)) {
- const origin = homedepotDetailItems.value.get(k)!;
- homedepotDetailItems.value.set(k, { ...origin, ...v });
+ if (detailItems.value.has(k)) {
+ const origin = detailItems.value.get(k)!;
+ detailItems.value.set(k, { ...origin, ...v });
} else {
- homedepotDetailItems.value.set(k, v);
+ detailItems.value.set(k, v);
}
}
detailCache.clear();
+ for (const [k, v] of reviewCache.entries()) {
+ if (reviewItems.value.has(k)) {
+ const uid = (x: HomedepotReview) => `${x.username}-${x.title}`;
+ const addedUids = new Set(v.map(uid));
+ const origin = reviewItems.value.get(k)!;
+ const newReviews = origin.filter((x) => !addedUids.has(uid(x))).concat(v);
+ newReviews.sort((a, b) => dayjs(b.dateInfo).diff(dayjs(a.dateInfo)));
+ reviewItems.value.set(k, newReviews);
+ } else {
+ reviewItems.value.set(k, v);
+ }
+ }
+ reviewCache.clear();
}
};
diff --git a/src/page-worker/impls/homedepot.ts b/src/page-worker/impls/homedepot.ts
index 9e0f0d4..2787a5c 100644
--- a/src/page-worker/impls/homedepot.ts
+++ b/src/page-worker/impls/homedepot.ts
@@ -50,10 +50,10 @@ class HomedepotWorkerImpl
}
await injector.waitForReviewLoad();
const reviews = await injector.getReviews();
- await this.emit('review-collected', { reviews });
+ await this.emit('review-collected', { OSMID, reviews });
while (await injector.tryJumpToNextPage()) {
const reviews = await injector.getReviews();
- await this.emit('review-collected', { reviews });
+ await this.emit('review-collected', { OSMID, reviews });
}
setTimeout(() => {
browser.tabs.remove(tab.id!);
diff --git a/src/page-worker/impls/lowes.ts b/src/page-worker/impls/lowes.ts
new file mode 100644
index 0000000..07e4b68
--- /dev/null
+++ b/src/page-worker/impls/lowes.ts
@@ -0,0 +1,30 @@
+import { taskOptionBase } from '../interfaces/common';
+import { BaseWorker } from './base';
+import { LowesEvents, LowesWorker } from '../interfaces/lowes';
+
+class LowesWorkerImpl extends BaseWorker implements LowesWorker {
+ private static instance: LowesWorker | null = null;
+ public static getInstance() {
+ if (!this.instance) {
+ this.instance = new LowesWorkerImpl();
+ }
+ return this.instance;
+ }
+ protected constructor() {
+ super();
+ }
+
+ runDetailPageTask(urls: string[], options?: taskOptionBase): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ stop(): Promise {
+ throw new Error('Method not implemented.');
+ }
+}
+
+export default {
+ getLowesWorker() {
+ return LowesWorkerImpl.getInstance();
+ },
+};
diff --git a/src/page-worker/interfaces/common.ts b/src/page-worker/interfaces/common.ts
index ebf4ebd..34de5ac 100644
--- a/src/page-worker/interfaces/common.ts
+++ b/src/page-worker/interfaces/common.ts
@@ -6,31 +6,6 @@ export interface taskOptionBase {
progress?: (remains: string[]) => Promise | void;
}
-export interface LowesEvents {
- /** The event is fired when detail items collect */
- ['detail-item-collected']: { item: LowesDetailItem };
-
- /** The event is fired when error occurs. */
- ['error']: { message: string; url?: string };
-}
-
-export interface LowesWorker {
- /**
- * The channel for communication with the Lowes page worker.
- */
- readonly channel: Emittery;
-
- /**
- * Browsing item detail page and collect target information
- */
- runDetailPageTask(urls: string[], options?: taskOptionBase): Promise;
-
- /**
- * Stop the worker.
- */
- stop(): Promise;
-}
-
export type WorkerComposable = Base & {
settings: Ref;
isRunning: Ref;
diff --git a/src/page-worker/interfaces/homedepot.ts b/src/page-worker/interfaces/homedepot.ts
index fddb89e..62afcb8 100644
--- a/src/page-worker/interfaces/homedepot.ts
+++ b/src/page-worker/interfaces/homedepot.ts
@@ -5,7 +5,7 @@ export interface HomedepotEvents {
['detail-item-collected']: { item: HomedepotDetailItem };
/** The event is fired when reviews collect */
- ['review-collected']: { reviews: HomedepotReview[] };
+ ['review-collected']: { OSMID: string; reviews: HomedepotReview[] };
/** The event is fired when error occurs. */
['error']: { message: string; url?: string };
diff --git a/src/page-worker/interfaces/lowes.ts b/src/page-worker/interfaces/lowes.ts
index 3cf3068..d7a8f67 100644
--- a/src/page-worker/interfaces/lowes.ts
+++ b/src/page-worker/interfaces/lowes.ts
@@ -1,17 +1,18 @@
import { taskOptionBase, Listener } from './common';
-export interface HomedepotEvents {
- /**The event is fired when detail base info collected */
- ['detail-base-info-collect']: { item: LowesDetailItem };
- /**The event is fired when error occur */
- ['error']: { message: string };
+export interface LowesEvents {
+ /** The event is fired when detail items collect */
+ ['detail-item-collected']: { item: LowesDetailItem };
+
+ /** The event is fired when error occurs. */
+ ['error']: { message: string; url?: string };
}
-export interface HomedepotWorker extends Listener {
+export interface LowesWorker {
/**
* Browsing item detail page and collect target information
*/
- runDetailPageTask(urls: string[], options: taskOptionBase): Promise;
+ runDetailPageTask(urls: string[], options?: taskOptionBase): Promise;
/**
* Stop the worker.
diff --git a/src/page-worker/web-injectors/homedepot.ts b/src/page-worker/web-injectors/homedepot.ts
index 07f5837..a972f22 100644
--- a/src/page-worker/web-injectors/homedepot.ts
+++ b/src/page-worker/web-injectors/homedepot.ts
@@ -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[];
+ return (obj['image'] as string[]).map((url) => url.slice(1, -1));
});
}
@@ -125,6 +125,7 @@ export class HomedepotDetailPageInjector extends BaseInjector {
document
.querySelector("#product-section-rr div[role='button']")
?.scrollIntoView({ behavior: 'smooth' });
+ document.querySelector('li:not(.sui-border-accent) .navlink-rr')?.click();
if (el && el.getClientRects().length > 0 && el.getClientRects()[0].height > 0) {
el?.scrollIntoView({ behavior: 'smooth' });
break;
@@ -151,8 +152,9 @@ export class HomedepotDetailPageInjector extends BaseInjector {
const badges = Array.from(
root.querySelectorAll('.review-status-icons__list, li.review-badge > *'),
)
- .map((el) => el.innerText)
- .filter((t) => !["(What's this?)"].includes(t));
+ .map((el) => el.innerText.trim())
+ .filter((t) => !["(What's this?)"].includes(t))
+ .filter((t) => t.length !== 0);
const imageUrls = Array.from(
root.querySelectorAll('.media-carousel__media > button'),
).map((el) => el.style.backgroundImage.split(/[\(\)]/, 3)[1]);
@@ -163,6 +165,7 @@ export class HomedepotDetailPageInjector extends BaseInjector {
public tryJumpToNextPage() {
return this.run(async () => {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
const final = document.querySelector(
'.pager__summary--bold:nth-last-of-type(2)',
)!.innerText;
diff --git a/src/router/index.ts b/src/router/index.ts
index ce4cee7..9679001 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -13,6 +13,7 @@ const routeObj: Record<'sidepanel' | 'options', RouteRecordRaw[]> = {
{ 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-reviews', component: () => import('~/options/views/HomedepotReviews.vue') },
{ path: '/help', component: () => import('~/options/views/help/guide.md') },
],
sidepanel: [
diff --git a/src/sidepanel/views/HomedepotSidepanel.vue b/src/sidepanel/views/HomedepotSidepanel.vue
index 3b7865a..2d04184 100644
--- a/src/sidepanel/views/HomedepotSidepanel.vue
+++ b/src/sidepanel/views/HomedepotSidepanel.vue
@@ -15,6 +15,14 @@ worker.on('detail-item-collected', ({ item }) => {
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([]);
diff --git a/src/storages/amazon.ts b/src/storages/amazon.ts
index 3cc7bae..113092d 100644
--- a/src/storages/amazon.ts
+++ b/src/storages/amazon.ts
@@ -24,17 +24,11 @@ export const searchItems = useWebExtensionStorage('searchIte
export const detailItems = useWebExtensionStorage