mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-09 08:53:22 +08:00
Compare commits
No commits in common. "296f6687df8bdc91be6f7a4fd8558e2d94e163f9" and "1203906072fd74c9ea94ee9f04fe840f618374cb" have entirely different histories.
296f6687df
...
1203906072
@ -139,7 +139,7 @@ export function useWebExtensionStorage<T>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void pullFromStorage(); // Init
|
pullFromStorage(); // Init
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ const options: { label: string; value: string }[] = [
|
|||||||
{ label: 'Amazon', value: '/amazon' },
|
{ label: 'Amazon', value: '/amazon' },
|
||||||
{ label: 'Amazon Review', value: '/amazon-reviews' },
|
{ label: 'Amazon Review', value: '/amazon-reviews' },
|
||||||
{ label: 'Homedepot', value: '/homedepot' },
|
{ label: 'Homedepot', value: '/homedepot' },
|
||||||
{ label: 'Homedepot Review', value: '/homedepot-reviews' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
watch(opt, (val) => {
|
watch(opt, (val) => {
|
||||||
@ -27,7 +26,6 @@ watch(opt, (val) => {
|
|||||||
site.value = 'amazon';
|
site.value = 'amazon';
|
||||||
break;
|
break;
|
||||||
case '/homedepot':
|
case '/homedepot':
|
||||||
case '/homedepot-reviews':
|
|
||||||
site.value = 'homedepot';
|
site.value = 'homedepot';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,226 +0,0 @@
|
|||||||
<script setup lang="tsx">
|
|
||||||
import type { TableColumn } from '~/components/ResultTable.vue';
|
|
||||||
import { allReviews } from '~/storages/homedepot';
|
|
||||||
import { useExcelHelper } from '~/composables/useExcelHelper';
|
|
||||||
import type { Header } from '~/logic/excel';
|
|
||||||
|
|
||||||
const excelHelper = useExcelHelper();
|
|
||||||
|
|
||||||
const filter = ref<{
|
|
||||||
keywords: string;
|
|
||||||
rating: string | undefined;
|
|
||||||
OSMIDs: string[];
|
|
||||||
dateRange: [number, number] | undefined;
|
|
||||||
}>({
|
|
||||||
keywords: '',
|
|
||||||
rating: undefined,
|
|
||||||
OSMIDs: [],
|
|
||||||
dateRange: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns = computed<TableColumn[]>(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: 'OSMID',
|
|
||||||
key: 'OSMID',
|
|
||||||
minWidth: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '标题',
|
|
||||||
key: 'title',
|
|
||||||
minWidth: 200,
|
|
||||||
ellipsis: {
|
|
||||||
tooltip: { placement: 'top' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '用户名',
|
|
||||||
key: 'username',
|
|
||||||
minWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '评分',
|
|
||||||
key: 'rating',
|
|
||||||
render(row: HomedepotReview) {
|
|
||||||
return <n-rate readonly size="small" value={Number(row.rating.split(' ')[0])} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容',
|
|
||||||
key: 'content',
|
|
||||||
ellipsis: {
|
|
||||||
tooltip: { placement: 'top', contentStyle: { maxWidth: '60vw' } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '日期信息',
|
|
||||||
key: 'dateInfo',
|
|
||||||
minWidth: 120,
|
|
||||||
render(row: HomedepotReview) {
|
|
||||||
return <span>{dayjs(row.dateInfo).format('YYYY/M/D')}</span>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const extraHeaders: Header[] = [
|
|
||||||
{
|
|
||||||
prop: 'imageUrls',
|
|
||||||
label: '图片链接',
|
|
||||||
formatOutputValue: (val?: string[]) => val?.join(';'),
|
|
||||||
parseImportValue: (val?: string) => val?.split(';'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '标签',
|
|
||||||
prop: 'badges',
|
|
||||||
formatOutputValue: (val?: string[]) => val?.join(';'),
|
|
||||||
parseImportValue: (val?: string) => val?.split(';'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getHeaders = () => {
|
|
||||||
return columns.value
|
|
||||||
.map((col: Record<string, any>) => ({ prop: col.key, label: col.title }) as Header)
|
|
||||||
.concat(extraHeaders);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredData = computed(() => {
|
|
||||||
let reviews = toRaw(allReviews.value);
|
|
||||||
if (filter.value.rating) {
|
|
||||||
reviews = reviews.filter((r) => r.rating === filter.value.rating);
|
|
||||||
}
|
|
||||||
if (filter.value.keywords) {
|
|
||||||
reviews = reviews.filter((r) => {
|
|
||||||
return [
|
|
||||||
r.content.toLocaleLowerCase(),
|
|
||||||
r.title.toLocaleLowerCase(),
|
|
||||||
r.username.toLocaleLowerCase(),
|
|
||||||
].some((s) => s.includes(filter.value.keywords.toLocaleLowerCase()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (filter.value.OSMIDs.length > 0) {
|
|
||||||
reviews = reviews.filter((r) => filter.value.OSMIDs.includes(r.OSMID));
|
|
||||||
}
|
|
||||||
if (filter.value.dateRange) {
|
|
||||||
reviews = reviews.filter((r) => {
|
|
||||||
const date = dayjs(r.dateInfo);
|
|
||||||
const start = dayjs(filter.value.dateRange![0]);
|
|
||||||
const end = dayjs(filter.value.dateRange![1]);
|
|
||||||
return date.diff(start, 'date') >= 0 && date.diff(end, 'date') <= 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return reviews;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleImport = async (file: File) => {
|
|
||||||
const [importedData] = await excelHelper.importFile(file, [getHeaders()]);
|
|
||||||
allReviews.value = importedData.data as typeof allReviews.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = (opt: 'local' | 'cloud') => {
|
|
||||||
excelHelper.exportFile(
|
|
||||||
[{ data: filteredData.value, headers: getHeaders(), imageColumn: '图片链接' }],
|
|
||||||
{
|
|
||||||
cloud: opt === 'cloud',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterReset = () => {
|
|
||||||
filter.value = {
|
|
||||||
keywords: '',
|
|
||||||
rating: undefined,
|
|
||||||
OSMIDs: [],
|
|
||||||
dateRange: undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClear = () => {
|
|
||||||
allReviews.value = [];
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="result-table">
|
|
||||||
<result-table :columns="columns" :records="filteredData">
|
|
||||||
<template #header>
|
|
||||||
<h3 class="header-text">Homedepot Reviews</h3>
|
|
||||||
</template>
|
|
||||||
<template #header-extra>
|
|
||||||
<control-strip round @import="handleImport" @clear="handleClear">
|
|
||||||
<template #exporter>
|
|
||||||
<export-panel @export-file="handleExport" />
|
|
||||||
</template>
|
|
||||||
<template #filter>
|
|
||||||
<div class="filter-panel">
|
|
||||||
<div>筛选器</div>
|
|
||||||
<n-form :label-width="80" label-align="center" label-placement="left">
|
|
||||||
<n-form-item label="评分">
|
|
||||||
<n-select
|
|
||||||
v-model:value="filter.rating"
|
|
||||||
style="width: 200px"
|
|
||||||
round
|
|
||||||
placeholder="选择评分"
|
|
||||||
clearable
|
|
||||||
:options="
|
|
||||||
Array.from({ length: 5 }).map((_, i) => ({
|
|
||||||
label: `${i + 1} 星 ${'★'.repeat(i + 1)}`,
|
|
||||||
value: `${i + 1}.0 out of 5 stars`,
|
|
||||||
}))
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="关键词">
|
|
||||||
<n-input clearable placeholder="请输入关键词" v-model:value="filter.keywords" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="OSMID">
|
|
||||||
<n-dynamic-tags v-model:value="filter.OSMIDs" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="日期范围">
|
|
||||||
<n-date-picker
|
|
||||||
type="daterange"
|
|
||||||
clearable
|
|
||||||
:value="filter.dateRange"
|
|
||||||
@update:value="
|
|
||||||
(newRange) => {
|
|
||||||
if (Array.isArray(newRange)) {
|
|
||||||
filter.dateRange = [newRange[0], newRange[1] + (24 * 3600 * 1000 - 1)];
|
|
||||||
} else {
|
|
||||||
filter.dateRange = newRange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
<n-space justify="end">
|
|
||||||
<n-button @click="handleFilterReset">重置</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</control-strip>
|
|
||||||
</template>
|
|
||||||
</result-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.result-table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-text {
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-panel {
|
|
||||||
min-width: 100px;
|
|
||||||
max-width: 500px;
|
|
||||||
|
|
||||||
& > div:first-of-type {
|
|
||||||
font-size: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -90,9 +90,9 @@ function buildAmazonPageWorker(): WorkerComposable<AmazonPageWorker, AmazonPageW
|
|||||||
if (objects?.includes('review')) {
|
if (objects?.includes('review')) {
|
||||||
for (const [asin, reviews] of reviewCache.entries()) {
|
for (const [asin, reviews] of reviewCache.entries()) {
|
||||||
if (reviewItems.value.has(asin)) {
|
if (reviewItems.value.has(asin)) {
|
||||||
const addedIds = new Set(reviews.map((x) => x.id));
|
const addIds = new Set(reviews.map((x) => x.id));
|
||||||
const origin = reviewItems.value.get(asin)!;
|
const origin = reviewItems.value.get(asin)!;
|
||||||
const newReviews = origin.filter((x) => !addedIds.has(x.id)).concat(reviews);
|
const newReviews = origin.filter((x) => !addIds.has(x.id)).concat(reviews);
|
||||||
newReviews.sort((a, b) => dayjs(b.dateInfo).diff(dayjs(a.dateInfo)));
|
newReviews.sort((a, b) => dayjs(b.dateInfo).diff(dayjs(a.dateInfo)));
|
||||||
reviewItems.value.set(asin, newReviews);
|
reviewItems.value.set(asin, newReviews);
|
||||||
} else {
|
} else {
|
||||||
@ -104,9 +104,12 @@ function buildAmazonPageWorker(): WorkerComposable<AmazonPageWorker, AmazonPageW
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store unsubscribe functions for worker events
|
||||||
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
|
||||||
// Register all relevant worker event handlers
|
// Register all relevant worker event handlers
|
||||||
function registerWorkerEvents() {
|
function registerWorkerEvents() {
|
||||||
const unsubscribes = [
|
return [
|
||||||
// Stop worker on error
|
// Stop worker on error
|
||||||
worker.on('error', () => {
|
worker.on('error', () => {
|
||||||
worker.stop();
|
worker.stop();
|
||||||
@ -141,16 +144,17 @@ function buildAmazonPageWorker(): WorkerComposable<AmazonPageWorker, AmazonPageW
|
|||||||
updateReviewCache(ev);
|
updateReviewCache(ev);
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register event handlers on mount
|
// Register event handlers on mount
|
||||||
const unsbuscribe = registerWorkerEvents();
|
onMounted(() => {
|
||||||
|
unsubscribes.push(...registerWorkerEvents());
|
||||||
|
});
|
||||||
|
|
||||||
// Unregister event handlers on unmount
|
// Unregister event handlers on unmount
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unsbuscribe();
|
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
unsubscribes.splice(0, unsubscribes.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useLongTask } from '~/composables/useLongTask';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import { detailItems, reviewItems } from '~/storages/homedepot';
|
import { detailItems as homedepotDetailItems } from '~/storages/homedepot';
|
||||||
import homedepot from '../impls/homedepot';
|
import homedepot from '../impls/homedepot';
|
||||||
import { createGlobalState } from '@vueuse/core';
|
import { createGlobalState } from '@vueuse/core';
|
||||||
import { WorkerComposable } from '../interfaces/common';
|
import { WorkerComposable } from '../interfaces/common';
|
||||||
@ -16,10 +16,12 @@ function buildHomedepotWorker(): WorkerComposable<HomedepotWorker, HomedepotWork
|
|||||||
const { isRunning, startTask } = useLongTask();
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
const detailCache = new Map<string, HomedepotDetailItem>();
|
const detailCache = new Map<string, HomedepotDetailItem>();
|
||||||
const reviewCache = new Map<string, HomedepotReview[]>();
|
|
||||||
|
|
||||||
function registerWorkerEvents() {
|
const unsubscribeFuncs = [] as (() => void)[];
|
||||||
const unsubscribes = [
|
|
||||||
|
onMounted(() => {
|
||||||
|
unsubscribeFuncs.push(
|
||||||
|
...[
|
||||||
worker.on('error', () => {
|
worker.on('error', () => {
|
||||||
worker.stop();
|
worker.stop();
|
||||||
}),
|
}),
|
||||||
@ -32,45 +34,27 @@ function buildHomedepotWorker(): WorkerComposable<HomedepotWorker, HomedepotWork
|
|||||||
detailCache.set(item.OSMID, item);
|
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsubscribe = registerWorkerEvents();
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unsubscribe();
|
unsubscribeFuncs.forEach((unsubscribe) => unsubscribe());
|
||||||
|
unsubscribeFuncs.splice(0, unsubscribeFuncs.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
const commitChange = () => {
|
const commitChange = () => {
|
||||||
const { objects } = settings.value;
|
const { objects } = settings.value;
|
||||||
if (objects?.includes('detail')) {
|
if (objects?.includes('detail')) {
|
||||||
for (const [k, v] of detailCache.entries()) {
|
for (const [k, v] of detailCache.entries()) {
|
||||||
if (detailItems.value.has(k)) {
|
if (homedepotDetailItems.value.has(k)) {
|
||||||
const origin = detailItems.value.get(k)!;
|
const origin = homedepotDetailItems.value.get(k)!;
|
||||||
detailItems.value.set(k, { ...origin, ...v });
|
homedepotDetailItems.value.set(k, { ...origin, ...v });
|
||||||
} else {
|
} else {
|
||||||
detailItems.value.set(k, v);
|
homedepotDetailItems.value.set(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
detailCache.clear();
|
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();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,94 +0,0 @@
|
|||||||
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);
|
|
||||||
@ -50,10 +50,10 @@ class HomedepotWorkerImpl
|
|||||||
}
|
}
|
||||||
await injector.waitForReviewLoad();
|
await injector.waitForReviewLoad();
|
||||||
const reviews = await injector.getReviews();
|
const reviews = await injector.getReviews();
|
||||||
await this.emit('review-collected', { OSMID, reviews });
|
await this.emit('review-collected', { reviews });
|
||||||
while (await injector.tryJumpToNextPage()) {
|
while (await injector.tryJumpToNextPage()) {
|
||||||
const reviews = await injector.getReviews();
|
const reviews = await injector.getReviews();
|
||||||
await this.emit('review-collected', { OSMID, reviews });
|
await this.emit('review-collected', { reviews });
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
browser.tabs.remove(tab.id!);
|
browser.tabs.remove(tab.id!);
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
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 & { interrupt: undefined }>
|
|
||||||
implements LowesWorker
|
|
||||||
{
|
|
||||||
private static instance: LowesWorker | null = null;
|
|
||||||
public static getInstance() {
|
|
||||||
if (!this.instance) {
|
|
||||||
this.instance = new LowesWorkerImpl();
|
|
||||||
}
|
|
||||||
return this.instance;
|
|
||||||
}
|
|
||||||
protected constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
|
||||||
return this.emit('interrupt');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getLowesWorker() {
|
|
||||||
return LowesWorkerImpl.getInstance();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { AmazonPageWorkerSettings, useAmazonWorker } from './composables/amazon';
|
import { AmazonPageWorkerSettings, useAmazonWorker } from './composables/amazon';
|
||||||
import { HomedepotWorkerSettings, useHomedepotWorker } from './composables/homedepot';
|
import { HomedepotWorkerSettings, useHomedepotWorker } from './composables/homedepot';
|
||||||
import { LowesWorkerSettings, useLowesWorker } from './composables/lowes';
|
|
||||||
|
|
||||||
export function usePageWorker(
|
export function usePageWorker(
|
||||||
type: 'amazon',
|
type: 'amazon',
|
||||||
@ -10,10 +9,6 @@ export function usePageWorker(
|
|||||||
type: 'homedepot',
|
type: 'homedepot',
|
||||||
settings?: HomedepotWorkerSettings,
|
settings?: HomedepotWorkerSettings,
|
||||||
): ReturnType<typeof useHomedepotWorker>;
|
): ReturnType<typeof useHomedepotWorker>;
|
||||||
export function usePageWorker(
|
|
||||||
type: 'homedepot',
|
|
||||||
settings?: LowesWorkerSettings,
|
|
||||||
): ReturnType<typeof useLowesWorker>;
|
|
||||||
export function usePageWorker(type: Website, settings: any) {
|
export function usePageWorker(type: Website, settings: any) {
|
||||||
let worker = null;
|
let worker = null;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -23,9 +18,6 @@ export function usePageWorker(type: Website, settings: any) {
|
|||||||
case 'homedepot':
|
case 'homedepot':
|
||||||
worker = useHomedepotWorker();
|
worker = useHomedepotWorker();
|
||||||
break;
|
break;
|
||||||
case 'lowes':
|
|
||||||
worker = useLowesWorker();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported page worker type: ${type}`);
|
throw new Error(`Unsupported page worker type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,31 @@ export interface taskOptionBase {
|
|||||||
progress?: (remains: string[]) => Promise<void> | void;
|
progress?: (remains: string[]) => Promise<void> | 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<LowesEvents>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsing item detail page and collect target information
|
||||||
|
*/
|
||||||
|
runDetailPageTask(urls: string[], options?: taskOptionBase): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the worker.
|
||||||
|
*/
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkerComposable<Base, S = {}> = Base & {
|
export type WorkerComposable<Base, S = {}> = Base & {
|
||||||
settings: Ref<S>;
|
settings: Ref<S>;
|
||||||
isRunning: Ref<boolean>;
|
isRunning: Ref<boolean>;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export interface HomedepotEvents {
|
|||||||
['detail-item-collected']: { item: HomedepotDetailItem };
|
['detail-item-collected']: { item: HomedepotDetailItem };
|
||||||
|
|
||||||
/** The event is fired when reviews collect */
|
/** The event is fired when reviews collect */
|
||||||
['review-collected']: { OSMID: string; reviews: HomedepotReview[] };
|
['review-collected']: { reviews: HomedepotReview[] };
|
||||||
|
|
||||||
/** The event is fired when error occurs. */
|
/** The event is fired when error occurs. */
|
||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
|
|||||||
@ -1,18 +1,17 @@
|
|||||||
import { taskOptionBase, Listener } from './common';
|
import { taskOptionBase, Listener } from './common';
|
||||||
|
|
||||||
export interface LowesEvents {
|
export interface HomedepotEvents {
|
||||||
/** The event is fired when detail items collect */
|
/**The event is fired when detail base info collected */
|
||||||
['detail-item-collected']: { item: LowesDetailItem };
|
['detail-base-info-collect']: { item: LowesDetailItem };
|
||||||
|
/**The event is fired when error occur */
|
||||||
/** The event is fired when error occurs. */
|
['error']: { message: string };
|
||||||
['error']: { message: string; url?: string };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LowesWorker extends Listener<LowesEvents> {
|
export interface HomedepotWorker extends Listener<HomedepotEvents> {
|
||||||
/**
|
/**
|
||||||
* Browsing item detail page and collect target information
|
* Browsing item detail page and collect target information
|
||||||
*/
|
*/
|
||||||
runDetailPageTask(urls: string[], options?: taskOptionBase): Promise<void>;
|
runDetailPageTask(urls: string[], options: taskOptionBase): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the worker.
|
* Stop the worker.
|
||||||
|
|||||||
@ -125,7 +125,6 @@ export class HomedepotDetailPageInjector extends BaseInjector {
|
|||||||
document
|
document
|
||||||
.querySelector("#product-section-rr div[role='button']")
|
.querySelector("#product-section-rr div[role='button']")
|
||||||
?.scrollIntoView({ behavior: 'smooth' });
|
?.scrollIntoView({ behavior: 'smooth' });
|
||||||
document.querySelector<HTMLElement>('li:not(.sui-border-accent) .navlink-rr')?.click();
|
|
||||||
if (el && el.getClientRects().length > 0 && el.getClientRects()[0].height > 0) {
|
if (el && el.getClientRects().length > 0 && el.getClientRects()[0].height > 0) {
|
||||||
el?.scrollIntoView({ behavior: 'smooth' });
|
el?.scrollIntoView({ behavior: 'smooth' });
|
||||||
break;
|
break;
|
||||||
@ -152,14 +151,11 @@ export class HomedepotDetailPageInjector extends BaseInjector {
|
|||||||
const badges = Array.from(
|
const badges = Array.from(
|
||||||
root.querySelectorAll<HTMLElement>('.review-status-icons__list, li.review-badge > *'),
|
root.querySelectorAll<HTMLElement>('.review-status-icons__list, li.review-badge > *'),
|
||||||
)
|
)
|
||||||
.map((el) => el.innerText.trim())
|
.map((el) => el.innerText)
|
||||||
.filter((t) => !["(What's this?)"].includes(t))
|
.filter((t) => !["(What's this?)"].includes(t));
|
||||||
.filter((t) => t.length !== 0);
|
|
||||||
const imageUrls = Array.from(
|
const imageUrls = Array.from(
|
||||||
root.querySelectorAll<HTMLElement>('.media-carousel__media > button'),
|
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;
|
return { title, content, username, dateInfo, rating, badges, imageUrls } as HomedepotReview;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -167,7 +163,6 @@ export class HomedepotDetailPageInjector extends BaseInjector {
|
|||||||
|
|
||||||
public tryJumpToNextPage() {
|
public tryJumpToNextPage() {
|
||||||
return this.run(async () => {
|
return this.run(async () => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
const final = document.querySelector<HTMLElement>(
|
const final = document.querySelector<HTMLElement>(
|
||||||
'.pager__summary--bold:nth-last-of-type(2)',
|
'.pager__summary--bold:nth-last-of-type(2)',
|
||||||
)!.innerText;
|
)!.innerText;
|
||||||
|
|||||||
@ -49,12 +49,12 @@ export class LowesDetailPageInjector extends BaseInjector {
|
|||||||
// 获取标题
|
// 获取标题
|
||||||
const title = document.querySelector<HTMLDivElement>(
|
const title = document.querySelector<HTMLDivElement>(
|
||||||
`h1.product-brand-description`,
|
`h1.product-brand-description`,
|
||||||
)!.innerText;
|
)?.innerText;
|
||||||
|
|
||||||
// 获取价格
|
// 获取价格
|
||||||
const price = document
|
const price = document
|
||||||
.querySelector<HTMLDivElement>(`.screen-reader`)!
|
.querySelector<HTMLDivElement>(`.screen-reader`)
|
||||||
.innerText.replaceAll('\n', '');
|
?.innerText.replaceAll('\n', '');
|
||||||
|
|
||||||
// 获取评分
|
// 获取评分
|
||||||
const rate = document.querySelector<HTMLDivElement>(`.avgrating`)?.innerText;
|
const rate = document.querySelector<HTMLDivElement>(`.avgrating`)?.innerText;
|
||||||
@ -68,7 +68,7 @@ export class LowesDetailPageInjector extends BaseInjector {
|
|||||||
// 获取图片URL
|
// 获取图片URL
|
||||||
const mainImageUrl = document.querySelector<HTMLImageElement>(
|
const mainImageUrl = document.querySelector<HTMLImageElement>(
|
||||||
`#mfe-gallery .productImage.tile-img`,
|
`#mfe-gallery .productImage.tile-img`,
|
||||||
)!.src;
|
)?.src;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brandName,
|
brandName,
|
||||||
|
|||||||
@ -13,7 +13,6 @@ const routeObj: Record<'sidepanel' | 'options', RouteRecordRaw[]> = {
|
|||||||
{ 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: '/amazon-reviews', component: () => import('~/options/views/AmazonReviews.vue') },
|
||||||
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.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') },
|
{ path: '/help', component: () => import('~/options/views/help/guide.md') },
|
||||||
],
|
],
|
||||||
sidepanel: [
|
sidepanel: [
|
||||||
|
|||||||
@ -15,14 +15,6 @@ worker.on('detail-item-collected', ({ item }) => {
|
|||||||
time: new Date().toLocaleString(),
|
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 timelines = ref<Timeline[]>([]);
|
||||||
|
|
||||||
|
|||||||
@ -1,140 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -24,11 +24,17 @@ export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchIte
|
|||||||
export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>(
|
export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>(
|
||||||
'detailItems',
|
'detailItems',
|
||||||
new Map(),
|
new Map(),
|
||||||
|
{
|
||||||
|
listenToStorageChanges: 'options',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const reviewItems = useWebExtensionStorage<Map<string, AmazonReview[]>>(
|
export const reviewItems = useWebExtensionStorage<Map<string, AmazonReview[]>>(
|
||||||
'reviewItems',
|
'reviewItems',
|
||||||
new Map(),
|
new Map(),
|
||||||
|
{
|
||||||
|
listenToStorageChanges: 'options',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const allItems = computed<AmazonItem[]>({
|
export const allItems = computed<AmazonItem[]>({
|
||||||
|
|||||||
@ -9,11 +9,9 @@ export const detailWorkerSettings = useWebExtensionStorage('homedepot-detail-wor
|
|||||||
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
|
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
|
||||||
'homedepot-details',
|
'homedepot-details',
|
||||||
new Map(),
|
new Map(),
|
||||||
);
|
{
|
||||||
|
listenToStorageChanges: 'options',
|
||||||
export const reviewItems = useWebExtensionStorage<Map<string, HomedepotReview[]>>(
|
},
|
||||||
'homedepot-reviews',
|
|
||||||
new Map(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const allItems = computed({
|
export const allItems = computed({
|
||||||
@ -27,25 +25,3 @@ export const allItems = computed({
|
|||||||
}, new Map());
|
}, new Map());
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const allReviews = computed({
|
|
||||||
get() {
|
|
||||||
const reviewItemMap = toRaw(reviewItems.value);
|
|
||||||
return Array.from(
|
|
||||||
reviewItemMap
|
|
||||||
.entries()
|
|
||||||
.map(([OSMID, reviews]) =>
|
|
||||||
reviews.map<{ OSMID: string } & HomedepotReview>((r) => ({ ...r, OSMID })),
|
|
||||||
),
|
|
||||||
).flat();
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
reviewItems.value = newVal.reduce<typeof reviewItems.value>((m, c) => {
|
|
||||||
const { OSMID, ...review } = c;
|
|
||||||
const reveiws = m.get(OSMID) || [];
|
|
||||||
reveiws.push(review);
|
|
||||||
m.set(OSMID, reveiws);
|
|
||||||
return m;
|
|
||||||
}, new Map());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
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,9 +1,11 @@
|
|||||||
declare type LowesDetailItem = {
|
declare type LowesDetailItem = {
|
||||||
|
OSMID: string;
|
||||||
link: string;
|
link: string;
|
||||||
brandName?: string;
|
brandName?: string;
|
||||||
title: string;
|
title: string;
|
||||||
price: string;
|
price: string;
|
||||||
rate?: string;
|
rate?: string;
|
||||||
|
innerText: string;
|
||||||
reviewCount?: number;
|
reviewCount?: number;
|
||||||
mainImageUrl: string;
|
mainImageUrl: string;
|
||||||
modelInfo?: string;
|
modelInfo?: string;
|
||||||
|
|||||||
@ -19,7 +19,7 @@ declare module '*.md' {
|
|||||||
|
|
||||||
declare type AppContext = 'options' | 'sidepanel' | 'background' | 'content script';
|
declare type AppContext = 'options' | 'sidepanel' | 'background' | 'content script';
|
||||||
|
|
||||||
declare type Website = 'amazon' | 'homedepot' | 'lowes';
|
declare type Website = 'amazon' | 'homedepot';
|
||||||
|
|
||||||
declare const appContext: AppContext;
|
declare const appContext: AppContext;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user