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",
"displayName": "Azon Seeker",
"version": "0.3.0",
"version": "0.4.1",
"private": true,
"description": "Starter modify by honestfox101",
"scripts": {

View File

@ -76,6 +76,17 @@ const emit = defineEmits<{
</template>
<slot name="filter" />
</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>
</div>
</template>

View File

@ -27,7 +27,7 @@ const formItemRule: FormItemRule = {
trigger: ['submit'],
message: props.validateMessage,
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 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> {
const { timeout = 30000 } = options;
return new Promise<T>(async (resolve, reject) => {
if (isFirefox) {
while (true) {
await new Promise<void>((r) => setTimeout(r, 200));
const tab = await browser.tabs.get(tabId);
if (tab.status === 'complete') {
break;
}
while (true) {
await new Promise<void>((r) => setTimeout(r, 200));
const tab = await browser.tabs.get(tabId);
if (tab.status === 'complete') {
break;
}
}
setTimeout(() => reject('脚本运行超时'), timeout);

View File

@ -6,6 +6,10 @@ export const detailAsinInput = useWebExtensionStorage<string>('detailAsinInputTe
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 detailWorkerSettings = useWebExtensionStorage<{ aplus: boolean }>(

View File

@ -23,12 +23,13 @@ export async function uploadImage(
formData.append('file', blob, filename);
const url = `http://${remoteHost}/upload/image/${encodeURIComponent(filename)}`;
return fetch(url, {
const resp = (await fetch(url, {
method: 'POST',
body: formData,
})
.then((response) => response.json())
.then((data) => {
return data.file ? `http://${remoteHost}${data.file}` : undefined;
});
}).catch((err) => undefined)) as Response | undefined;
if (!resp) {
return 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();
return Promise.resolve<ProtocolMap['html-to-image']['return']>(
sender.sendMessage('html-to-image', params, {
context: 'content-script',
tabId: this._tab.id!,
}),
);
return sender!.sendMessage('html-to-image', params, {
context: 'content-script',
tabId: this._tab.id!,
});
}
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 { useCloudExporter } from '~/composables/useCloudExporter';
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 modal = useModal();
@ -21,96 +21,108 @@ const onFilterReset = () => {
filter.value = {};
};
const columns: TableColumn[] = [
{
type: 'expand',
expandable: (row) => row.hasDetail,
renderExpand(row) {
return <amazon-detail-description model={row} />;
const columns = computed<TableColumn[]>(() => {
return [
{
type: 'expand',
expandable: (row) => row.hasDetail,
renderExpand(row) {
return <amazon-detail-description model={row} />;
},
},
},
{
title: '关键词',
key: 'keywords',
minWidth: 120,
},
{
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: 'keywords',
minWidth: 120,
hidden: !itemColumnSettings.value.has('keywords'),
},
},
];
{
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>[] = [
{ prop: 'link', label: '商品链接' },
@ -155,7 +167,7 @@ const reviewHeaders: Header<AmazonReview>[] = [
];
const getItemHeaders = () => {
return columns
return columns.value
.filter((col: Record<string, any>) => col.key !== 'actions')
.reduce(
(p, v: Record<string, any>) => {
@ -240,7 +252,7 @@ const handleCloudExport = async () => {
const mappedData1 = await castRecordsByHeaders(items, itemHeaders);
const mappedData2 = await castRecordsByHeaders(reviews, reviewHeaders);
const fragments = [
{ data: mappedData1, imageColumn: ['商品图片链接', 'A+截图'], name: 'items' },
{ data: mappedData1, imageColumn: ['A+截图', '商品图片链接'], name: 'items' },
{ data: mappedData2, imageColumn: '图片链接', name: 'reviews' },
];
const filename = await cloudExporter.doExport(fragments);
@ -369,18 +381,32 @@ const handleClearData = async () => {
</n-form-item>
<n-form-item label="日期(搜索页)">
<n-date-picker
type="daterange"
type="datetimerange"
clearable
v-model:value="filter.searchDateRange"
/>
</n-form-item>
<n-form-item label="日期(详情页)">
<n-date-picker
type="daterange"
type="datetimerange"
clearable
v-model:value="filter.detailDateRange"
/>
</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>
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
</div>
@ -445,7 +471,7 @@ const handleClearData = async () => {
}
.filter-section {
max-width: 360px;
max-width: 500px;
.filter-title {
font-size: 18px;

View File

@ -1,5 +1,5 @@
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 {
return await originalMethod.call(this, ...args); // 调用原有方法
} catch (error) {
this.emit('error', { message: `发生未知错误:${error}` });
await this.emit('error', { message: `发生未知错误:${error}` });
throw error;
}
};

View File

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