diff --git a/package.json b/package.json
index e6d48ec..db2739d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "azon-seeker",
"displayName": "Azon Seeker",
- "version": "0.6.0",
+ "version": "0.7.0",
"private": true,
"description": "Starter modify by honestfox101",
"scripts": {
diff --git a/src/options/Options.vue b/src/options/Options.vue
index ec8af2f..1a15fb6 100644
--- a/src/options/Options.vue
+++ b/src/options/Options.vue
@@ -14,6 +14,7 @@ const options: { label: string; value: string }[] = [
{ label: 'Amazon Review', value: '/amazon-reviews' },
{ label: 'Homedepot', value: '/homedepot' },
{ label: 'Homedepot Review', value: '/homedepot-reviews' },
+ { label: 'Lowes', value: '/lowes' },
];
watch(opt, (val) => {
@@ -30,6 +31,9 @@ watch(opt, (val) => {
case '/homedepot-reviews':
site.value = 'homedepot';
break;
+ case '/lowes':
+ site.value = 'lowes';
+ break;
default:
break;
}
diff --git a/src/options/views/HomedepotResultTable.vue b/src/options/views/HomedepotResultTable.vue
index 521b274..91e6291 100644
--- a/src/options/views/HomedepotResultTable.vue
+++ b/src/options/views/HomedepotResultTable.vue
@@ -94,7 +94,7 @@ const extraHeaders: Header[] = [
];
const filteredData = computed(() => {
- let data = allItems.value;
+ let data = toRaw(allItems.value);
if (filter.value.timeRange) {
const start = dayjs(filter.value.timeRange[0]);
const end = dayjs(filter.value.timeRange[1]);
diff --git a/src/options/views/LowesResultTable.vue b/src/options/views/LowesResultTable.vue
new file mode 100644
index 0000000..d42bfd7
--- /dev/null
+++ b/src/options/views/LowesResultTable.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
过滤条件
+
+
+
+
+
+
+ 重置
+
+
+
+
+
+
+
+
+
+
diff --git a/src/page-worker/impls/homedepot.ts b/src/page-worker/impls/homedepot.ts
index 2787a5c..661cbcb 100644
--- a/src/page-worker/impls/homedepot.ts
+++ b/src/page-worker/impls/homedepot.ts
@@ -49,11 +49,11 @@ class HomedepotWorkerImpl
return;
}
await injector.waitForReviewLoad();
- const reviews = await injector.getReviews();
- await this.emit('review-collected', { OSMID, reviews });
- while (await injector.tryJumpToNextPage()) {
- const reviews = await injector.getReviews();
- await this.emit('review-collected', { OSMID, reviews });
+ let reviews = await injector.getReviews();
+ reviews.length > 0 && (await this.emit('review-collected', { OSMID, reviews }));
+ while ((await injector.tryJumpToNextPage()) && reviews.length > 0) {
+ reviews = await injector.getReviews();
+ reviews.length > 0 && (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
index 42c4f05..75ff1f7 100644
--- a/src/page-worker/impls/lowes.ts
+++ b/src/page-worker/impls/lowes.ts
@@ -31,8 +31,12 @@ class LowesWorkerImpl
const injector = new LowesDetailPageInjector(tab);
await injector.waitForPageLoad();
const baseInfo = await injector.getBaseInfo();
- await this.emit('detail-item-collected', { item: { ...baseInfo, link: url } });
+ baseInfo &&
+ (await this.emit('detail-item-collected', {
+ item: { ...baseInfo, timestamp: dayjs().format('YYYY/M/D HH:mm:ss'), link: url },
+ }));
progress && progress(remains);
+ await new Promise((resolve) => setTimeout(resolve, 1000));
setTimeout(() => browser.tabs.remove(tab.id!), 1500);
}
}
diff --git a/src/page-worker/index.ts b/src/page-worker/index.ts
index c4e78bb..ccebe6c 100644
--- a/src/page-worker/index.ts
+++ b/src/page-worker/index.ts
@@ -11,7 +11,7 @@ export function usePageWorker(
settings?: HomedepotWorkerSettings,
): ReturnType;
export function usePageWorker(
- type: 'homedepot',
+ type: 'lowes',
settings?: LowesWorkerSettings,
): ReturnType;
export function usePageWorker(type: Website, settings: any) {
diff --git a/src/page-worker/web-injectors/amazon.ts b/src/page-worker/web-injectors/amazon.ts
index 0f40c23..4a2a591 100644
--- a/src/page-worker/web-injectors/amazon.ts
+++ b/src/page-worker/web-injectors/amazon.ts
@@ -15,7 +15,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
}
while (true) {
await new Promise((resolve) => setTimeout(resolve, 500 + ~~(500 * Math.random())));
- const spins = Array.from(document.querySelectorAll('.a-spinner')).filter(
+ const spins = Array.from(document.querySelectorAll('.a-spinner')).filter(
(e) => e.getClientRects().length > 0,
);
if (spins.length === 0) {
@@ -28,7 +28,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
public async getPagePattern() {
return this.run(async () => {
return Array.from(
- document.querySelectorAll(
+ document.querySelectorAll(
'.puisg-row:has(.a-section.a-spacing-small.a-spacing-top-small:not(.a-text-right))',
),
).filter((e) => e.getClientRects().length > 0).length > 0
@@ -44,7 +44,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
case 'pattern-1':
data = await this.run(async () => {
const items = Array.from(
- document.querySelectorAll(
+ document.querySelectorAll(
'.puisg-row:has(.a-section.a-spacing-small.a-spacing-top-small:not(.a-text-right))',
),
).filter((e) => e.getClientRects().length > 0);
@@ -77,9 +77,9 @@ export class AmazonSearchPageInjector extends BaseInjector {
case 'pattern-2':
data = await this.run(async () => {
const items = Array.from(
- document.querySelectorAll(
+ document.querySelectorAll(
'.puis-card-container',
- ) as unknown as HTMLDivElement[],
+ ) as unknown as HTMLElement[],
).filter((e) => e.getClientRects().length > 0);
const linkObjs = items.reduce<
Pick[]
@@ -113,9 +113,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
public async getCurrentPage() {
return this.run(async () => {
- const node = document.querySelector(
- '.s-pagination-item.s-pagination-selected',
- );
+ const node = document.querySelector('.s-pagination-item.s-pagination-selected');
return node ? Number(node.innerText) : 1;
});
}
@@ -228,7 +226,7 @@ export class AmazonDetailPageInjector extends BaseInjector {
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null,
- ).singleNodeValue as HTMLDivElement | null;
+ ).singleNodeValue as HTMLElement | null;
if (targetNode) {
targetNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
return targetNode.innerText;
@@ -241,9 +239,9 @@ export class AmazonDetailPageInjector extends BaseInjector {
/**获取图像链接 */
public async getImageUrls() {
return this.run(async () => {
- const overlay = document.querySelector('.overlayRestOfImages');
+ const overlay = document.querySelector('.overlayRestOfImages');
if (overlay) {
- if (document.querySelector('#ivThumbs')!.getClientRects().length === 0) {
+ if (document.querySelector('#ivThumbs')!.getClientRects().length === 0) {
overlay.click();
await new Promise((resolve) => setTimeout(resolve, 1000));
}
@@ -272,7 +270,7 @@ export class AmazonDetailPageInjector extends BaseInjector {
/**获取精选评论 */
public async getTopReviews() {
return this.run(async () => {
- const targetNode = document.querySelector('.cr-widget-FocalReviews');
+ const targetNode = document.querySelector('.cr-widget-FocalReviews');
if (!targetNode) {
return [];
}
@@ -288,22 +286,22 @@ export class AmazonDetailPageInjector extends BaseInjector {
);
const items: AmazonReview[] = [];
for (let i = 0; i < xResult.snapshotLength; i++) {
- const commentNode = xResult.snapshotItem(i) as HTMLDivElement | null;
+ const commentNode = xResult.snapshotItem(i) as HTMLElement | null;
if (!commentNode) {
continue;
}
const id = commentNode.id.split('-')[0];
- const username = commentNode.querySelector('.a-profile-name')!.innerText;
- const title = commentNode.querySelector(
+ const username = commentNode.querySelector('.a-profile-name')!.innerText;
+ const title = commentNode.querySelector(
'[data-hook="review-title"] > span:not(.a-letter-space)',
)!.innerText;
- const rating = commentNode.querySelector(
+ const rating = commentNode.querySelector(
'[data-hook*="review-star-rating"]',
)!.innerText;
- const dateInfo = commentNode.querySelector(
+ const dateInfo = commentNode.querySelector(
'[data-hook="review-date"]',
)!.innerText;
- const content = commentNode.querySelector(
+ const content = commentNode.querySelector(
'[data-hook="review-body"]',
)!.innerText;
const imageSrc = Array.from(
@@ -446,22 +444,22 @@ export class AmazonReviewPageInjector extends BaseInjector {
);
const items: AmazonReview[] = [];
for (let i = 0; i < xResult.snapshotLength; i++) {
- const commentNode = xResult.snapshotItem(i) as HTMLDivElement;
+ const commentNode = xResult.snapshotItem(i) as HTMLElement;
if (!commentNode) {
continue;
}
const id = commentNode.id.split('-')[0];
- const username = commentNode.querySelector('.a-profile-name')!.innerText;
- const title = commentNode.querySelector(
+ const username = commentNode.querySelector('.a-profile-name')!.innerText;
+ const title = commentNode.querySelector(
'[data-hook="review-title"] > span:not(.a-letter-space)',
)!.innerText;
- const rating = commentNode.querySelector(
+ const rating = commentNode.querySelector(
'[data-hook*="review-star-rating"]',
)!.innerText;
- const dateInfo = commentNode.querySelector(
+ const dateInfo = commentNode.querySelector(
'[data-hook="review-date"]',
)!.innerText;
- const content = commentNode.querySelector(
+ const content = commentNode.querySelector(
'[data-hook="review-body"]',
)!.innerText;
const imageSrc = Array.from(
@@ -490,7 +488,7 @@ export class AmazonReviewPageInjector extends BaseInjector {
).singleNodeValue as HTMLElement | null;
latestReview?.scrollIntoView({ behavior: 'smooth', block: 'center' });
await new Promise((resolve) => setTimeout(resolve, 1000));
- const nextPageNode = document.querySelector(
+ const nextPageNode = document.querySelector(
'[data-hook="pagination-bar"] .a-pagination > *:nth-of-type(2)',
);
nextPageNode?.scrollIntoView({ behavior: 'smooth', block: 'center' });
@@ -504,7 +502,7 @@ export class AmazonReviewPageInjector extends BaseInjector {
public async showStarsDropDownMenu() {
return this.run(async () => {
while (true) {
- const dropdown = document.querySelector('#star-count-dropdown')!;
+ const dropdown = document.querySelector('#star-count-dropdown')!;
dropdown.scrollIntoView({ behavior: 'smooth', block: 'center' });
dropdown.click();
if (dropdown.getAttribute('aria-expanded') === 'true') {
diff --git a/src/page-worker/web-injectors/homedepot.ts b/src/page-worker/web-injectors/homedepot.ts
index 8a2e4a6..0ea81e0 100644
--- a/src/page-worker/web-injectors/homedepot.ts
+++ b/src/page-worker/web-injectors/homedepot.ts
@@ -63,23 +63,23 @@ export class HomedepotDetailPageInjector extends BaseInjector {
public getInfo() {
return this.run(async () => {
const link = document.location.toString();
- const brandName = document.querySelector(
+ const brandName = document.querySelector(
`[data-component^="product-details:ProductDetailsBrandCollection"]`,
)?.innerText;
- const title = document.querySelector(
+ const title = document.querySelector(
`[data-component^="product-details:ProductDetailsTitle"]`,
)!.innerText;
const price = document
- .querySelector(`#standard-price`)!
+ .querySelector(`#standard-price`)!
.innerText.replaceAll('\n', '');
- const rateEl = document.querySelector(
+ const rateEl = document.querySelector(
`[data-component^="ratings-and-reviews"] .sui-mr-1`,
);
const rate = rateEl ? /\d(\.\d)?/.exec(rateEl.innerText)![0] : undefined;
const reviewCount = rateEl
? Number(
/\d+/.exec(
- document.querySelector(
+ document.querySelector(
`[data-component^="ratings-and-reviews"] [name="simple-rating"] + span`,
)!.innerText,
)![0],
@@ -93,7 +93,7 @@ export class HomedepotDetailPageInjector extends BaseInjector {
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
- ).singleNodeValue as HTMLDivElement | null;
+ ).singleNodeValue as HTMLElement | null;
const [modelInfo] = /(?<=#\s).+/.exec(modelInfoEl?.innerText || '') || [];
return {
link,
@@ -138,6 +138,17 @@ export class HomedepotDetailPageInjector extends BaseInjector {
public getReviews() {
return this.run(async () => {
+ const noReview = Boolean(
+ document.evaluate(
+ `//span[text() = 'Write the First Review']`,
+ document,
+ null,
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
+ ).singleNodeValue,
+ );
+ if (noReview) {
+ return [];
+ }
const elements = document.querySelectorAll('.review_item');
return Array.from(elements).map((root) => {
const title = root.querySelector('.review-content__title')!.innerText;
diff --git a/src/page-worker/web-injectors/lowes.ts b/src/page-worker/web-injectors/lowes.ts
index c5162af..87ac509 100644
--- a/src/page-worker/web-injectors/lowes.ts
+++ b/src/page-worker/web-injectors/lowes.ts
@@ -12,6 +12,7 @@ export class LowesDetailPageInjector extends BaseInjector {
if (document.readyState === 'complete') {
break;
}
+ await new Promise((resolve) => setTimeout(resolve, 1000));
}
});
}
@@ -24,8 +25,8 @@ export class LowesDetailPageInjector extends BaseInjector {
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
- ).singleNodeValue as HTMLDivElement | null;
- const itemSeries = itemNumberEl?.innerText.replace('Item #', '').trim();
+ ).singleNodeValue as HTMLElement | null;
+ const itemSeries = itemNumberEl?.innerText.replace('Item #', '').replace('|', '').trim();
// 获取Model #
const modelNumberEl = document.evaluate(
@@ -33,35 +34,33 @@ export class LowesDetailPageInjector extends BaseInjector {
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
- ).singleNodeValue as HTMLDivElement | null;
+ ).singleNodeValue as HTMLElement | null;
const modelSeries = modelNumberEl?.innerText.replace('Model #', '').trim();
// 获取品牌名称
- const brandName = (
- document.evaluate(
- `//h1[contains(@class, "product-brand-description")]/parent::*/parent::*/following-sibling::*[1]//a`,
- document,
- null,
- XPathResult.FIRST_ORDERED_NODE_TYPE,
- ).singleNodeValue as HTMLDivElement
- ).innerText;
+ const brandName = document.querySelector(
+ '[data-component-name="RatingsNLinks"] a .label',
+ )?.innerText;
+
+ // 销量信息
+ const boughtInfo = document.querySelector(
+ '[data-component-name="ExclusiveBadge"]',
+ )?.innerText;
// 获取标题
- const title = document.querySelector(
- `h1.product-brand-description`,
- )!.innerText;
+ const title = document.querySelector(`h1.product-brand-description`)!.innerText;
// 获取价格
const price = document
- .querySelector(`.screen-reader`)!
+ .querySelector(`.screen-reader`)!
.innerText.replaceAll('\n', '');
// 获取评分
- const rate = document.querySelector(`.avgrating`)?.innerText;
+ const rate = document.querySelector(`.avgrating`)?.innerText;
// 获取评价数量
const reviewCount = Number(
- document.querySelector(`[data-testid="rating-trigger"] > div > div > span`)
+ document.querySelector(`[data-testid="rating-trigger"] > div > div > span`)
?.innerText || '0',
);
@@ -77,6 +76,7 @@ export class LowesDetailPageInjector extends BaseInjector {
rate,
reviewCount,
mainImageUrl,
+ boughtInfo,
itemSeries,
modelSeries,
};
diff --git a/src/router/index.ts b/src/router/index.ts
index 9679001..6a220df 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -14,12 +14,14 @@ const routeObj: Record<'sidepanel' | 'options', RouteRecordRaw[]> = {
{ 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: '/lowes', component: () => import('~/options/views/LowesResultTable.vue') },
{ path: '/help', component: () => import('~/options/views/help/guide.md') },
],
sidepanel: [
{ path: '/', redirect: `/${site.value}` },
{ path: '/amazon', component: () => import('~/sidepanel/views/AmazonSidepanel.vue') },
{ path: '/homedepot', component: () => import('~/sidepanel/views/HomedepotSidepanel.vue') },
+ { path: '/lowes', component: () => import('~/sidepanel/views/LowesSidepanel.vue') },
],
};
@@ -29,11 +31,11 @@ export const router: Plugin = {
case 'sidepanel':
case 'options':
const routes = routeObj[appContext];
- const router = createRouter({
+ const vueRouter = createRouter({
history: appContext === 'sidepanel' ? createMemoryHistory() : createWebHashHistory(),
routes,
});
- app.use(router);
+ app.use(vueRouter);
default:
break;
}
diff --git a/src/sidepanel/App.vue b/src/sidepanel/App.vue
index 601d429..0a8461a 100644
--- a/src/sidepanel/App.vue
+++ b/src/sidepanel/App.vue
@@ -27,6 +27,9 @@ watch(currentUrl, (newVal) => {
case 'www.homedepot.com':
site.value = 'homedepot';
break;
+ case 'www.lowes.com':
+ site.value = 'lowes';
+ break;
default:
break;
}
@@ -41,6 +44,9 @@ watch(site, (newVal) => {
case 'homedepot':
router.push('/homedepot');
break;
+ case 'lowes':
+ router.push('/lowes');
+ break;
default:
break;
}
diff --git a/src/sidepanel/views/LowesSidepanel.vue b/src/sidepanel/views/LowesSidepanel.vue
index 4b877a4..bd0c8ed 100644
--- a/src/sidepanel/views/LowesSidepanel.vue
+++ b/src/sidepanel/views/LowesSidepanel.vue
@@ -1,25 +1,16 @@
-
-
Lowes
+
+
Lowes Detail
{
placeholder="输入URL"
validate-message="请输入格式正确的URL"
/>
-
-
-
-
-
-
-
-
-
-
+
开始
-
+
停止
@@ -107,7 +94,7 @@ const handleInterrupt = () => {