Add column filter

This commit is contained in:
johnathan 2025-06-27 11:35:16 +08:00
parent 2a52150c4a
commit d66c40b9e7
11 changed files with 165 additions and 129 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "azon-seeker", "name": "azon-seeker",
"displayName": "Azon Seeker", "displayName": "Azon Seeker",
"version": "0.3.0", "version": "0.4.1",
"private": true, "private": true,
"description": "Starter modify by honestfox101", "description": "Starter modify by honestfox101",
"scripts": { "scripts": {

View File

@ -76,6 +76,17 @@ const emit = defineEmits<{
</template> </template>
<slot name="filter" /> <slot name="filter" />
</n-popover> </n-popover>
<n-popover v-if="$slots.settings" trigger="hover" placement="bottom" :duration="500">
<template #trigger>
<n-button type="default" ghost :round="round" :size="size">
<template #icon>
<solar-settings-linear />
</template>
设置
</n-button>
</template>
<slot name="settings" />
</n-popover>
</n-button-group> </n-button-group>
</div> </div>
</template> </template>

View File

@ -27,7 +27,7 @@ const formItemRule: FormItemRule = {
trigger: ['submit'], trigger: ['submit'],
message: props.validateMessage, message: props.validateMessage,
validator: () => { validator: () => {
return props.matchPattern.exec(modelValue.value) !== null; return props.matchPattern && props.matchPattern.exec(modelValue.value) !== null;
}, },
}; };

View File

@ -14,4 +14,5 @@ export function isForbiddenUrl(url: string): boolean {
export const isFirefox = navigator.userAgent.includes('Firefox'); export const isFirefox = navigator.userAgent.includes('Firefox');
export const remoteHost = __DEV__ ? '127.0.0.1:8000' : '47.251.4.191:8000'; // export const remoteHost = __DEV__ ? '127.0.0.1:8000' : '47.251.4.191:8000';
export const remoteHost = '47.251.4.191:8000';

View File

@ -47,13 +47,11 @@ export async function exec<T, P extends Record<string, unknown>>(
): Promise<T> { ): Promise<T> {
const { timeout = 30000 } = options; const { timeout = 30000 } = options;
return new Promise<T>(async (resolve, reject) => { return new Promise<T>(async (resolve, reject) => {
if (isFirefox) { while (true) {
while (true) { await new Promise<void>((r) => setTimeout(r, 200));
await new Promise<void>((r) => setTimeout(r, 200)); const tab = await browser.tabs.get(tabId);
const tab = await browser.tabs.get(tabId); if (tab.status === 'complete') {
if (tab.status === 'complete') { break;
break;
}
} }
} }
setTimeout(() => reject('脚本运行超时'), timeout); setTimeout(() => reject('脚本运行超时'), timeout);

View File

@ -6,6 +6,10 @@ export const detailAsinInput = useWebExtensionStorage<string>('detailAsinInputTe
export const reviewAsinInput = useWebExtensionStorage<string>('reviewAsinInputText', ''); export const reviewAsinInput = useWebExtensionStorage<string>('reviewAsinInputText', '');
export const itemColumnSettings = useWebExtensionStorage<
Set<keyof Pick<AmazonItem, 'keywords' | 'page' | 'rank' | 'createTime' | 'timestamp'>>
>('itemColumnSettings', new Set(['keywords', 'page', 'rank', 'createTime']));
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []); export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
export const detailWorkerSettings = useWebExtensionStorage<{ aplus: boolean }>( export const detailWorkerSettings = useWebExtensionStorage<{ aplus: boolean }>(

View File

@ -23,12 +23,13 @@ export async function uploadImage(
formData.append('file', blob, filename); formData.append('file', blob, filename);
const url = `http://${remoteHost}/upload/image/${encodeURIComponent(filename)}`; const url = `http://${remoteHost}/upload/image/${encodeURIComponent(filename)}`;
return fetch(url, { const resp = (await fetch(url, {
method: 'POST', method: 'POST',
body: formData, body: formData,
}) }).catch((err) => undefined)) as Response | undefined;
.then((response) => response.json()) if (!resp) {
.then((data) => { return undefined;
return data.file ? `http://${remoteHost}${data.file}` : undefined; }
}); const data = await resp.json();
return `http://${remoteHost}${data.file}`;
} }

View File

@ -26,14 +26,14 @@ export class BaseInjector {
} }
} }
protected async screenshot(params: ProtocolMap['html-to-image']['data']) { protected async screenshot(
params: ProtocolMap['html-to-image']['data'],
): Promise<ProtocolMap['html-to-image']['return']> {
const sender = await this.getMessageSender(); const sender = await this.getMessageSender();
return Promise.resolve<ProtocolMap['html-to-image']['return']>( return sender!.sendMessage('html-to-image', params, {
sender.sendMessage('html-to-image', params, { context: 'content-script',
context: 'content-script', tabId: this._tab.id!,
tabId: this._tab.id!, });
}),
);
} }
protected run<T, P extends Record<string, unknown>>( protected run<T, P extends Record<string, unknown>>(

View File

@ -3,7 +3,7 @@ import { NButton, NSpace } from 'naive-ui';
import type { TableColumn } from '~/components/ResultTable.vue'; import type { TableColumn } from '~/components/ResultTable.vue';
import { useCloudExporter } from '~/composables/useCloudExporter'; import { useCloudExporter } from '~/composables/useCloudExporter';
import { castRecordsByHeaders, createWorkbook, Header, importFromXLSX } from '~/logic/excel'; import { castRecordsByHeaders, createWorkbook, Header, importFromXLSX } from '~/logic/excel';
import { allItems, reviewItems } from '~/logic/storages/amazon'; import { allItems, itemColumnSettings, reviewItems } from '~/logic/storages/amazon';
const message = useMessage(); const message = useMessage();
const modal = useModal(); const modal = useModal();
@ -21,96 +21,108 @@ const onFilterReset = () => {
filter.value = {}; filter.value = {};
}; };
const columns: TableColumn[] = [ const columns = computed<TableColumn[]>(() => {
{ return [
type: 'expand', {
expandable: (row) => row.hasDetail, type: 'expand',
renderExpand(row) { expandable: (row) => row.hasDetail,
return <amazon-detail-description model={row} />; renderExpand(row) {
return <amazon-detail-description model={row} />;
},
}, },
}, {
{ title: '关键词',
title: '关键词', key: 'keywords',
key: 'keywords', minWidth: 120,
minWidth: 120, hidden: !itemColumnSettings.value.has('keywords'),
},
{
title: '页码',
key: 'page',
minWidth: 60,
},
{
title: '排位',
key: 'rank',
minWidth: 60,
},
{
title: 'ASIN',
key: 'asin',
minWidth: 130,
},
{
title: '标题',
key: 'title',
},
{
title: '价格',
key: 'price',
minWidth: 100,
},
{
title: '封面图',
key: 'imageSrc',
hidden: true,
},
{
title: '获取日期',
key: 'createTime',
minWidth: 160,
},
{
title: '查看',
key: 'actions',
minWidth: 100,
render(row) {
return (
<n-space>
{[
{
text: '评论',
disabled: !reviewItems.value.has(row.asin),
onClick: () => {
const asin = row.asin;
modal.create({
title: `${asin}评论`,
preset: 'card',
style: {
width: '80vw',
height: '85vh',
},
content: () => <amazon-review-preview asin={asin} />,
});
},
},
{
text: '链接',
onClick: () => {
browser.tabs.create({
active: true,
url: row.link,
});
},
},
].map(({ text, onClick, disabled }) => (
<n-button type="primary" text size="small" disabled={disabled} onClick={onClick}>
{text}
</n-button>
))}
</n-space>
);
}, },
}, {
]; title: '页码',
key: 'page',
minWidth: 60,
hidden: !itemColumnSettings.value.has('page'),
},
{
title: '排位',
key: 'rank',
minWidth: 60,
hidden: !itemColumnSettings.value.has('rank'),
},
{
title: 'ASIN',
key: 'asin',
minWidth: 130,
},
{
title: '标题',
key: 'title',
},
{
title: '价格',
key: 'price',
minWidth: 100,
},
{
title: '封面图',
key: 'imageSrc',
hidden: true,
},
{
title: '获取日期',
key: 'createTime',
minWidth: 160,
hidden: !itemColumnSettings.value.has('createTime'),
},
{
title: '获取日期(详情页)',
key: 'timestamp',
minWidth: 160,
hidden: !itemColumnSettings.value.has('timestamp'),
},
{
title: '查看',
key: 'actions',
minWidth: 100,
render(row) {
return (
<n-space>
{[
{
text: '评论',
disabled: !reviewItems.value.has(row.asin),
onClick: () => {
const asin = row.asin;
modal.create({
title: `${asin}评论`,
preset: 'card',
style: {
width: '80vw',
height: '85vh',
},
content: () => <amazon-review-preview asin={asin} />,
});
},
},
{
text: '链接',
onClick: () => {
browser.tabs.create({
active: true,
url: row.link,
});
},
},
].map(({ text, onClick, disabled }) => (
<n-button type="primary" text size="small" disabled={disabled} onClick={onClick}>
{text}
</n-button>
))}
</n-space>
);
},
},
];
});
const extraHeaders: Header<AmazonItem>[] = [ const extraHeaders: Header<AmazonItem>[] = [
{ prop: 'link', label: '商品链接' }, { prop: 'link', label: '商品链接' },
@ -155,7 +167,7 @@ const reviewHeaders: Header<AmazonReview>[] = [
]; ];
const getItemHeaders = () => { const getItemHeaders = () => {
return columns return columns.value
.filter((col: Record<string, any>) => col.key !== 'actions') .filter((col: Record<string, any>) => col.key !== 'actions')
.reduce( .reduce(
(p, v: Record<string, any>) => { (p, v: Record<string, any>) => {
@ -240,7 +252,7 @@ const handleCloudExport = async () => {
const mappedData1 = await castRecordsByHeaders(items, itemHeaders); const mappedData1 = await castRecordsByHeaders(items, itemHeaders);
const mappedData2 = await castRecordsByHeaders(reviews, reviewHeaders); const mappedData2 = await castRecordsByHeaders(reviews, reviewHeaders);
const fragments = [ const fragments = [
{ data: mappedData1, imageColumn: ['商品图片链接', 'A+截图'], name: 'items' }, { data: mappedData1, imageColumn: ['A+截图', '商品图片链接'], name: 'items' },
{ data: mappedData2, imageColumn: '图片链接', name: 'reviews' }, { data: mappedData2, imageColumn: '图片链接', name: 'reviews' },
]; ];
const filename = await cloudExporter.doExport(fragments); const filename = await cloudExporter.doExport(fragments);
@ -369,18 +381,32 @@ const handleClearData = async () => {
</n-form-item> </n-form-item>
<n-form-item label="日期(搜索页)"> <n-form-item label="日期(搜索页)">
<n-date-picker <n-date-picker
type="daterange" type="datetimerange"
clearable clearable
v-model:value="filter.searchDateRange" v-model:value="filter.searchDateRange"
/> />
</n-form-item> </n-form-item>
<n-form-item label="日期(详情页)"> <n-form-item label="日期(详情页)">
<n-date-picker <n-date-picker
type="daterange" type="datetimerange"
clearable clearable
v-model:value="filter.detailDateRange" v-model:value="filter.detailDateRange"
/> />
</n-form-item> </n-form-item>
<n-form-item label="列筛选">
<n-checkbox-group
:value="Array.from(itemColumnSettings)"
@update:value="(val) => (itemColumnSettings = new Set(val) as any)"
>
<n-space item-style="display: flex;">
<n-checkbox value="keywords" label="关键词" />
<n-checkbox value="page" label="页码" />
<n-checkbox value="rank" label="排位" />
<n-checkbox value="createTime" label="获取日期" />
<n-checkbox value="timestamp" label="获取日期(详情)" />
</n-space>
</n-checkbox-group>
</n-form-item>
</n-form> </n-form>
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div> <div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
</div> </div>
@ -445,7 +471,7 @@ const handleClearData = async () => {
} }
.filter-section { .filter-section {
max-width: 360px; max-width: 500px;
.filter-title { .filter-title {
font-size: 18px; font-size: 18px;

View File

@ -1,5 +1,5 @@
export interface ErrorChannelContainer { export interface ErrorChannelContainer {
emit: (event: 'error', error: { message: string }) => void; emit: (event: 'error', error: { message: string }) => Promise<void>;
} }
/** /**
@ -16,7 +16,7 @@ export function withErrorHandling(
try { try {
return await originalMethod.call(this, ...args); // 调用原有方法 return await originalMethod.call(this, ...args); // 调用原有方法
} catch (error) { } catch (error) {
this.emit('error', { message: `发生未知错误:${error}` }); await this.emit('error', { message: `发生未知错误:${error}` });
throw error; throw error;
} }
}; };

View File

@ -47,10 +47,9 @@ class AmazonPageWorkerFactory {
worker.on('item-top-reviews-collected', (ev) => { worker.on('item-top-reviews-collected', (ev) => {
updateDetailCache(ev); updateDetailCache(ev);
}), }),
worker.on('item-aplus-screenshot-collect', (ev) => { worker.on('item-aplus-screenshot-collect', async (ev) => {
uploadImage(ev.base64data, `${ev.asin}.png`).then((url) => { const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
url && updateDetailCache({ asin: ev.asin, aplus: url }); url && updateDetailCache({ asin: ev.asin, aplus: url });
});
}), }),
worker.on('item-review-collected', (ev) => { worker.on('item-review-collected', (ev) => {
updateReviews(ev); updateReviews(ev);
@ -92,14 +91,12 @@ class AmazonPageWorkerFactory {
const { searchItems, detailItems, reviewItems } = this.amazonWorkerSettings; const { searchItems, detailItems, reviewItems } = this.amazonWorkerSettings;
if (typeof searchItems !== 'undefined') { if (typeof searchItems !== 'undefined') {
searchItems.value = searchItems.value.concat(searchCache); searchItems.value = searchItems.value.concat(searchCache);
searchCache.splice(0, searchCache.length);
} }
if (typeof detailItems !== 'undefined') { if (typeof detailItems !== 'undefined') {
for (const [k, v] of detailCache.entries()) { for (const [k, v] of detailCache.entries()) {
detailItems.value.delete(k); // Trigger update detailItems.value.delete(k); // Trigger update
detailItems.value.set(k, v); detailItems.value.set(k, v);
} }
detailCache.clear();
} }
if (typeof reviewItems !== 'undefined') { if (typeof reviewItems !== 'undefined') {
for (const [asin, reviews] of reviewCache.entries()) { for (const [asin, reviews] of reviewCache.entries()) {
@ -116,12 +113,11 @@ class AmazonPageWorkerFactory {
reviewItems.value.set(asin, reviews); reviewItems.value.set(asin, reviews);
} }
} }
reviewCache.clear();
} }
}; };
const taskWrapper = <T extends (...params: any) => any>(func: T) => { const taskWrapper = <T extends (...params: any) => any>(func: T) => {
const { commitChangeIngerval = 1500 } = this.amazonWorkerSettings; const { commitChangeIngerval = 10000 } = this.amazonWorkerSettings;
searchCache.splice(0, searchCache.length); searchCache.splice(0, searchCache.length);
detailCache.clear(); detailCache.clear();
reviewCache.clear(); reviewCache.clear();
@ -215,7 +211,7 @@ class HomedepotWorkerFactory {
}; };
const taskWrapper = <T extends (...params: any) => any>(func: T) => { const taskWrapper = <T extends (...params: any) => any>(func: T) => {
const { commitChangeIngerval = 1500 } = this.homedepotWorkerSettings; const { commitChangeIngerval = 10000 } = this.homedepotWorkerSettings;
return (...params: Parameters<T>) => return (...params: Parameters<T>) =>
startTask(async () => { startTask(async () => {
const interval = setInterval(() => commitChange(), commitChangeIngerval); const interval = setInterval(() => commitChange(), commitChangeIngerval);
@ -249,7 +245,6 @@ class HomedepotWorkerFactory {
} }
const amazonfacotry = new AmazonPageWorkerFactory(); const amazonfacotry = new AmazonPageWorkerFactory();
const homedepotfactory = new HomedepotWorkerFactory(); const homedepotfactory = new HomedepotWorkerFactory();
export function usePageWorker( export function usePageWorker(