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
1203906072
commit
e444777758
@ -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) => {
|
||||
|
||||
226
src/options/views/HomedepotReviews.vue
Normal file
226
src/options/views/HomedepotReviews.vue
Normal file
@ -0,0 +1,226 @@
|
||||
<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')) {
|
||||
for (const [asin, reviews] of reviewCache.entries()) {
|
||||
if (reviewItems.value.has(asin)) {
|
||||
const addIds = new Set(reviews.map((x) => 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<AmazonPageWorker, AmazonPageW
|
||||
}
|
||||
};
|
||||
|
||||
// Store unsubscribe functions for worker events
|
||||
const unsubscribes: (() => 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<AmazonPageWorker, AmazonPageW
|
||||
updateReviewCache(ev);
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
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();
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@ -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<HomedepotWorker, HomedepotWork
|
||||
const { isRunning, startTask } = useLongTask();
|
||||
|
||||
const detailCache = new Map<string, HomedepotDetailItem>();
|
||||
const reviewCache = new Map<string, HomedepotReview[]>();
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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!);
|
||||
|
||||
30
src/page-worker/impls/lowes.ts
Normal file
30
src/page-worker/impls/lowes.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { taskOptionBase } from '../interfaces/common';
|
||||
import { BaseWorker } from './base';
|
||||
import { LowesEvents, LowesWorker } from '../interfaces/lowes';
|
||||
|
||||
class LowesWorkerImpl extends BaseWorker<LowesEvents> 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<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getLowesWorker() {
|
||||
return LowesWorkerImpl.getInstance();
|
||||
},
|
||||
};
|
||||
@ -6,31 +6,6 @@ export interface taskOptionBase {
|
||||
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 & {
|
||||
settings: Ref<S>;
|
||||
isRunning: Ref<boolean>;
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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<HomedepotEvents> {
|
||||
export interface LowesWorker {
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@ -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<HTMLElement>('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<HTMLElement>('.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<HTMLElement>('.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<HTMLElement>(
|
||||
'.pager__summary--bold:nth-last-of-type(2)',
|
||||
)!.innerText;
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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<Timeline[]>([]);
|
||||
|
||||
|
||||
@ -24,17 +24,11 @@ export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchIte
|
||||
export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>(
|
||||
'detailItems',
|
||||
new Map(),
|
||||
{
|
||||
listenToStorageChanges: 'options',
|
||||
},
|
||||
);
|
||||
|
||||
export const reviewItems = useWebExtensionStorage<Map<string, AmazonReview[]>>(
|
||||
'reviewItems',
|
||||
new Map(),
|
||||
{
|
||||
listenToStorageChanges: 'options',
|
||||
},
|
||||
);
|
||||
|
||||
export const allItems = computed<AmazonItem[]>({
|
||||
|
||||
@ -9,9 +9,11 @@ export const detailWorkerSettings = useWebExtensionStorage('homedepot-detail-wor
|
||||
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
|
||||
'homedepot-details',
|
||||
new Map(),
|
||||
{
|
||||
listenToStorageChanges: 'options',
|
||||
},
|
||||
);
|
||||
|
||||
export const reviewItems = useWebExtensionStorage<Map<string, HomedepotReview[]>>(
|
||||
'homedepot-reviews',
|
||||
new Map(),
|
||||
);
|
||||
|
||||
export const allItems = computed({
|
||||
@ -25,3 +27,25 @@ export const allItems = computed({
|
||||
}, 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());
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user