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
296f6687df
commit
2bafb403ea
@ -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": {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
|
||||
201
src/options/views/LowesResultTable.vue
Normal file
201
src/options/views/LowesResultTable.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<script setup lang="tsx">
|
||||
import type { TableColumn } from '~/components/ResultTable.vue';
|
||||
import { useExcelHelper } from '~/composables/useExcelHelper';
|
||||
import type { Header } from '~/logic/excel';
|
||||
import { allItems } from '~/storages/lowes';
|
||||
|
||||
const message = useMessage();
|
||||
const excelHelper = useExcelHelper();
|
||||
|
||||
const filter = ref({ timeRange: null as [number, number] | null });
|
||||
|
||||
const resetFilter = () => {
|
||||
filter.value = { timeRange: null };
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: '品牌名称',
|
||||
key: 'brandName',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: 'Item #',
|
||||
key: 'itemSeries',
|
||||
},
|
||||
{
|
||||
title: 'Model #',
|
||||
key: 'modelSeries',
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
key: 'price',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
title: '评分',
|
||||
key: 'rate',
|
||||
minWidth: 60,
|
||||
},
|
||||
{
|
||||
title: '评论数',
|
||||
key: 'reviewCount',
|
||||
minWidth: 75,
|
||||
},
|
||||
{
|
||||
title: '获取日期',
|
||||
key: 'timestamp',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '销量信息',
|
||||
key: 'boughtInfo',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render(row: (typeof allItems.value)[0]) {
|
||||
return (
|
||||
<n-space>
|
||||
<n-button
|
||||
type="primary"
|
||||
text
|
||||
onClick={() => {
|
||||
browser.tabs.create({
|
||||
active: true,
|
||||
url: row.link,
|
||||
});
|
||||
}}
|
||||
>
|
||||
前往
|
||||
</n-button>
|
||||
</n-space>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const extraHeaders: Header[] = [
|
||||
{
|
||||
label: '商品链接',
|
||||
prop: 'link',
|
||||
},
|
||||
{
|
||||
label: '主图链接',
|
||||
prop: 'mainImageUrl',
|
||||
},
|
||||
];
|
||||
|
||||
const filteredData = computed(() => {
|
||||
let data = toRaw(allItems.value);
|
||||
if (filter.value.timeRange) {
|
||||
const start = dayjs(filter.value.timeRange[0]);
|
||||
const end = dayjs(filter.value.timeRange[1]);
|
||||
data = data.filter(
|
||||
(r) => dayjs(r.timestamp).diff(start) >= 0 && dayjs(r.timestamp).diff(end) <= 0,
|
||||
);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
const getItemHeaders = () => {
|
||||
return columns
|
||||
.filter((col: Record<string, any>) => col.key !== 'actions')
|
||||
.reduce((p, v: Record<string, any>) => {
|
||||
if ('key' in v && 'title' in v) {
|
||||
p.push({ label: v.title, prop: v.key });
|
||||
}
|
||||
return p;
|
||||
}, [] as Header[])
|
||||
.concat(extraHeaders);
|
||||
};
|
||||
|
||||
const handleClearData = () => {
|
||||
allItems.value = [];
|
||||
};
|
||||
|
||||
const handleImport = async (file: File) => {
|
||||
const itemHeaders = getItemHeaders();
|
||||
const [dataFragment] = await excelHelper.importFile(file, [itemHeaders]);
|
||||
allItems.value = dataFragment.data as typeof allItems.value;
|
||||
message.info(`成功导入 ${file.name} 文件`);
|
||||
};
|
||||
|
||||
const handleExport = async (opt: 'cloud' | 'local') => {
|
||||
const itemHeaders = getItemHeaders();
|
||||
const fragments = [{ data: filteredData.value, imageColumn: '主图链接', headers: itemHeaders }];
|
||||
await excelHelper.exportFile(fragments, { cloud: opt === 'cloud' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="result-table">
|
||||
<result-table :records="filteredData" :columns="columns">
|
||||
<template #header>
|
||||
<n-space align="center">
|
||||
<h3 class="header-text">Lowes 数据</h3>
|
||||
</n-space>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<control-strip round @clear="handleClearData" @import="handleImport">
|
||||
<template #exporter>
|
||||
<export-panel @export-file="handleExport" />
|
||||
</template>
|
||||
<template #filter>
|
||||
<div class="filter-panel">
|
||||
<div>过滤条件</div>
|
||||
<n-form label-placement="left" :label-width="80" label-align="center">
|
||||
<n-form-item label="采集时间:">
|
||||
<n-date-picker
|
||||
type="datetimerange"
|
||||
v-model:value="filter.timeRange"
|
||||
></n-date-picker>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-space justify="end">
|
||||
<n-button @click="resetFilter">重置</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
</control-strip>
|
||||
</template>
|
||||
</result-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.expoter-progress-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
padding: 10px;
|
||||
gap: 15px;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
|
||||
& > div:first-of-type {
|
||||
font-size: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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!);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ export function usePageWorker(
|
||||
settings?: HomedepotWorkerSettings,
|
||||
): ReturnType<typeof useHomedepotWorker>;
|
||||
export function usePageWorker(
|
||||
type: 'homedepot',
|
||||
type: 'lowes',
|
||||
settings?: LowesWorkerSettings,
|
||||
): ReturnType<typeof useLowesWorker>;
|
||||
export function usePageWorker(type: Website, settings: any) {
|
||||
|
||||
@ -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<HTMLDivElement>('.a-spinner')).filter(
|
||||
const spins = Array.from(document.querySelectorAll<HTMLElement>('.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<HTMLDivElement>(
|
||||
document.querySelectorAll<HTMLElement>(
|
||||
'.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<HTMLDivElement>(
|
||||
document.querySelectorAll<HTMLElement>(
|
||||
'.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<HTMLDivElement>(
|
||||
document.querySelectorAll<HTMLElement>(
|
||||
'.puis-card-container',
|
||||
) as unknown as HTMLDivElement[],
|
||||
) as unknown as HTMLElement[],
|
||||
).filter((e) => e.getClientRects().length > 0);
|
||||
const linkObjs = items.reduce<
|
||||
Pick<AmazonSearchItem, 'link' | 'title' | 'imageSrc' | 'price'>[]
|
||||
@ -113,9 +113,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
|
||||
|
||||
public async getCurrentPage() {
|
||||
return this.run(async () => {
|
||||
const node = document.querySelector<HTMLDivElement>(
|
||||
'.s-pagination-item.s-pagination-selected',
|
||||
);
|
||||
const node = document.querySelector<HTMLElement>('.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<HTMLDivElement>('.overlayRestOfImages');
|
||||
const overlay = document.querySelector<HTMLElement>('.overlayRestOfImages');
|
||||
if (overlay) {
|
||||
if (document.querySelector<HTMLDivElement>('#ivThumbs')!.getClientRects().length === 0) {
|
||||
if (document.querySelector<HTMLElement>('#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<HTMLDivElement>('.cr-widget-FocalReviews');
|
||||
const targetNode = document.querySelector<HTMLElement>('.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<HTMLDivElement>('.a-profile-name')!.innerText;
|
||||
const title = commentNode.querySelector<HTMLDivElement>(
|
||||
const username = commentNode.querySelector<HTMLElement>('.a-profile-name')!.innerText;
|
||||
const title = commentNode.querySelector<HTMLElement>(
|
||||
'[data-hook="review-title"] > span:not(.a-letter-space)',
|
||||
)!.innerText;
|
||||
const rating = commentNode.querySelector<HTMLDivElement>(
|
||||
const rating = commentNode.querySelector<HTMLElement>(
|
||||
'[data-hook*="review-star-rating"]',
|
||||
)!.innerText;
|
||||
const dateInfo = commentNode.querySelector<HTMLDivElement>(
|
||||
const dateInfo = commentNode.querySelector<HTMLElement>(
|
||||
'[data-hook="review-date"]',
|
||||
)!.innerText;
|
||||
const content = commentNode.querySelector<HTMLDivElement>(
|
||||
const content = commentNode.querySelector<HTMLElement>(
|
||||
'[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<HTMLDivElement>('.a-profile-name')!.innerText;
|
||||
const title = commentNode.querySelector<HTMLDivElement>(
|
||||
const username = commentNode.querySelector<HTMLElement>('.a-profile-name')!.innerText;
|
||||
const title = commentNode.querySelector<HTMLElement>(
|
||||
'[data-hook="review-title"] > span:not(.a-letter-space)',
|
||||
)!.innerText;
|
||||
const rating = commentNode.querySelector<HTMLDivElement>(
|
||||
const rating = commentNode.querySelector<HTMLElement>(
|
||||
'[data-hook*="review-star-rating"]',
|
||||
)!.innerText;
|
||||
const dateInfo = commentNode.querySelector<HTMLDivElement>(
|
||||
const dateInfo = commentNode.querySelector<HTMLElement>(
|
||||
'[data-hook="review-date"]',
|
||||
)!.innerText;
|
||||
const content = commentNode.querySelector<HTMLDivElement>(
|
||||
const content = commentNode.querySelector<HTMLElement>(
|
||||
'[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<HTMLDivElement>(
|
||||
const nextPageNode = document.querySelector<HTMLElement>(
|
||||
'[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<HTMLDivElement>('#star-count-dropdown')!;
|
||||
const dropdown = document.querySelector<HTMLElement>('#star-count-dropdown')!;
|
||||
dropdown.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
dropdown.click();
|
||||
if (dropdown.getAttribute('aria-expanded') === 'true') {
|
||||
|
||||
@ -63,23 +63,23 @@ export class HomedepotDetailPageInjector extends BaseInjector {
|
||||
public getInfo() {
|
||||
return this.run(async () => {
|
||||
const link = document.location.toString();
|
||||
const brandName = document.querySelector<HTMLDivElement>(
|
||||
const brandName = document.querySelector<HTMLElement>(
|
||||
`[data-component^="product-details:ProductDetailsBrandCollection"]`,
|
||||
)?.innerText;
|
||||
const title = document.querySelector<HTMLDivElement>(
|
||||
const title = document.querySelector<HTMLElement>(
|
||||
`[data-component^="product-details:ProductDetailsTitle"]`,
|
||||
)!.innerText;
|
||||
const price = document
|
||||
.querySelector<HTMLDivElement>(`#standard-price`)!
|
||||
.querySelector<HTMLElement>(`#standard-price`)!
|
||||
.innerText.replaceAll('\n', '');
|
||||
const rateEl = document.querySelector<HTMLDivElement>(
|
||||
const rateEl = document.querySelector<HTMLElement>(
|
||||
`[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<HTMLDivElement>(
|
||||
document.querySelector<HTMLElement>(
|
||||
`[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<HTMLElement>('.review-content__title')!.innerText;
|
||||
|
||||
@ -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<HTMLElement>(
|
||||
'[data-component-name="RatingsNLinks"] a .label',
|
||||
)?.innerText;
|
||||
|
||||
// 销量信息
|
||||
const boughtInfo = document.querySelector<HTMLElement>(
|
||||
'[data-component-name="ExclusiveBadge"]',
|
||||
)?.innerText;
|
||||
|
||||
// 获取标题
|
||||
const title = document.querySelector<HTMLDivElement>(
|
||||
`h1.product-brand-description`,
|
||||
)!.innerText;
|
||||
const title = document.querySelector<HTMLElement>(`h1.product-brand-description`)!.innerText;
|
||||
|
||||
// 获取价格
|
||||
const price = document
|
||||
.querySelector<HTMLDivElement>(`.screen-reader`)!
|
||||
.querySelector<HTMLElement>(`.screen-reader`)!
|
||||
.innerText.replaceAll('\n', '');
|
||||
|
||||
// 获取评分
|
||||
const rate = document.querySelector<HTMLDivElement>(`.avgrating`)?.innerText;
|
||||
const rate = document.querySelector<HTMLElement>(`.avgrating`)?.innerText;
|
||||
|
||||
// 获取评价数量
|
||||
const reviewCount = Number(
|
||||
document.querySelector<HTMLDivElement>(`[data-testid="rating-trigger"] > div > div > span`)
|
||||
document.querySelector<HTMLElement>(`[data-testid="rating-trigger"] > div > div > span`)
|
||||
?.innerText || '0',
|
||||
);
|
||||
|
||||
@ -77,6 +76,7 @@ export class LowesDetailPageInjector extends BaseInjector {
|
||||
rate,
|
||||
reviewCount,
|
||||
mainImageUrl,
|
||||
boughtInfo,
|
||||
itemSeries,
|
||||
modelSeries,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,25 +1,16 @@
|
||||
<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';
|
||||
import { detailInputText } from '~/storages/lowes';
|
||||
|
||||
const idInputRef = useTemplateRef('id-input');
|
||||
|
||||
const worker = usePageWorker('homedepot', { objects: ['detail'] });
|
||||
const worker = usePageWorker('lowes', { 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}条评论`,
|
||||
title: `信息采集`,
|
||||
content: `成功获取到${item.link}的商品信息`,
|
||||
time: new Date().toLocaleString(),
|
||||
});
|
||||
});
|
||||
@ -50,7 +41,6 @@ const handleStart = async () => {
|
||||
}
|
||||
detailInputText.value = remains.join('\n');
|
||||
},
|
||||
review: detailWorkerSettings.value.review,
|
||||
});
|
||||
timelines.value.push({
|
||||
type: 'info',
|
||||
@ -68,8 +58,8 @@ const handleInterrupt = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="homedepot-sidepanel">
|
||||
<header-title>Lowes</header-title>
|
||||
<div class="lowes-sidepanel">
|
||||
<header-title>Lowes Detail</header-title>
|
||||
<div class="interative-section">
|
||||
<id-input
|
||||
v-model="detailInputText"
|
||||
@ -81,19 +71,16 @@ const handleInterrupt = () => {
|
||||
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-button
|
||||
v-if="!worker.isRunning.value"
|
||||
type="primary"
|
||||
round
|
||||
size="large"
|
||||
@click="handleStart"
|
||||
>
|
||||
<n-icon :size="20"><ant-design-thunderbolt-outlined /></n-icon>
|
||||
开始
|
||||
</optional-button>
|
||||
</n-button>
|
||||
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
||||
<n-icon :size="20"><ant-design-thunderbolt-outlined /></n-icon>
|
||||
停止
|
||||
@ -107,7 +94,7 @@ const handleInterrupt = () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.homedepot-sidepanel {
|
||||
.lowes-sidepanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@ -1,6 +1,20 @@
|
||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||
|
||||
export const detailInputText = useWebExtensionStorage('lowes-detail-input-text', '');
|
||||
|
||||
export const detailItems = useWebExtensionStorage<Map<string, LowesDetailItem>>(
|
||||
'lowes-details',
|
||||
new Map(),
|
||||
);
|
||||
|
||||
export const allItems = computed({
|
||||
get() {
|
||||
return Array.from(detailItems.value.values());
|
||||
},
|
||||
set(newValue) {
|
||||
detailItems.value = newValue.reduce((m, c) => {
|
||||
m.set(c.link, c);
|
||||
return m;
|
||||
}, new Map());
|
||||
},
|
||||
});
|
||||
|
||||
4
src/types/lowes.d.ts
vendored
4
src/types/lowes.d.ts
vendored
@ -1,8 +1,12 @@
|
||||
declare type LowesDetailItem = {
|
||||
link: string;
|
||||
itemSeries?: string;
|
||||
modelSeries?: string;
|
||||
brandName?: string;
|
||||
boughtInfo?: string;
|
||||
title: string;
|
||||
price: string;
|
||||
timestamp: string;
|
||||
rate?: string;
|
||||
reviewCount?: number;
|
||||
mainImageUrl: string;
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
/// <reference types="vitest" />
|
||||
|
||||
import { dirname, relative } from 'node:path';
|
||||
import type { UserConfig } from 'vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user