mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-01-19 13:13:22 +08:00
Update UI
This commit is contained in:
parent
063aca6b94
commit
80f5fedd0d
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@ -11,6 +11,13 @@
|
||||
"webRoot": "${workspaceFolder}/src/",
|
||||
"port": 9222,
|
||||
"urlFilter": "chrome-extension://fmalpbpehdilmjhnanhpjmnkgbahopfj/*"
|
||||
},
|
||||
{
|
||||
"type": "msedge",
|
||||
"request": "attach",
|
||||
"name": "Attach to Amazon",
|
||||
"port": 9222,
|
||||
"urlFilter": "https://www.amazon.com/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -65,5 +65,8 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
}
|
||||
}
|
||||
|
||||
5379
pnpm-lock.yaml
generated
5379
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
38
src/logic/data-io.ts
Normal file
38
src/logic/data-io.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { utils, writeFileXLSX } from 'xlsx';
|
||||
|
||||
/**
|
||||
* 导出为XLSX文件
|
||||
* @param data 数据数组
|
||||
* @param options 导出选项
|
||||
*/
|
||||
export function exportToXLSX(
|
||||
data: Record<string, unknown>[],
|
||||
options: { fileName?: string; headers?: { label: string; prop: string }[]; index?: boolean } = {},
|
||||
): void {
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = options.headers || Object.keys(data[0]).map((k) => ({ label: k, prop: k }));
|
||||
if (options.index) {
|
||||
headers.unshift({ label: 'Index', prop: 'Index' });
|
||||
data.forEach((item, index) => {
|
||||
item.Index = index + 1;
|
||||
});
|
||||
}
|
||||
const rows = data.map((item) => {
|
||||
const row: Record<string, unknown> = {};
|
||||
headers.forEach((header) => {
|
||||
row[header.label] = item[header.prop];
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
const worksheet = utils.json_to_sheet(rows, {
|
||||
header: headers.map((h) => h.label),
|
||||
});
|
||||
const workbook = utils.book_new();
|
||||
utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
const fileName = options.fileName || `export_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||
writeFileXLSX(workbook, fileName, { bookType: 'xlsx', type: 'binary', compression: true });
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import Emittery from 'emittery';
|
||||
import type { AmazonGoodsLinkItem, AmazonPageWorker, AmazonPageWorkerEvents } from './types';
|
||||
import type { AmazonPageWorker, AmazonPageWorkerEvents } from './types';
|
||||
import type { Tabs } from 'webextension-polyfill';
|
||||
import { exec } from '../execute-script';
|
||||
|
||||
@ -56,9 +56,9 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
const pagePattern = await exec(tabId, async () => {
|
||||
return [
|
||||
...(document.querySelectorAll<HTMLDivElement>(
|
||||
'.a-section.a-spacing-small.puis-padding-left-small',
|
||||
'.puisg-row:has(.a-section.a-spacing-small.a-spacing-top-small:not(.a-text-right))',
|
||||
) as unknown as HTMLDivElement[]),
|
||||
].filter((e) => e.getClientRects().length > 0).length === 0
|
||||
].filter((e) => e.getClientRects().length > 0).length > 0
|
||||
? 'pattern-1'
|
||||
: 'pattern-2';
|
||||
});
|
||||
@ -68,24 +68,28 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
}
|
||||
// #endregion
|
||||
// #region Retrieve key nodes and their information from the critical product search page
|
||||
let data: AmazonGoodsLinkItem[] | null = null;
|
||||
let data: { link: string; title: string; imageSrc: string }[] | null = null;
|
||||
switch (pagePattern) {
|
||||
// 处理商品以列表形式展示的情况
|
||||
case 'pattern-1':
|
||||
data = await exec(tabId, async () => {
|
||||
const items = [
|
||||
...(document.querySelectorAll<HTMLDivElement>(
|
||||
'.a-section.a-spacing-small.a-spacing-top-small:not(.a-text-right)',
|
||||
'.puisg-row:has(.a-section.a-spacing-small.a-spacing-top-small:not(.a-text-right))',
|
||||
) as unknown as HTMLDivElement[]),
|
||||
].filter((e) => e.getClientRects().length > 0);
|
||||
const linkObjs = items.reduce<AmazonGoodsLinkItem[]>((objs, el) => {
|
||||
const link = el.querySelector<HTMLAnchorElement>('a')?.href;
|
||||
const title = el
|
||||
.querySelector<HTMLHeadingElement>('h2.a-color-base')
|
||||
?.getAttribute('aria-label');
|
||||
link && objs.push({ link, title: title || '' });
|
||||
return objs;
|
||||
}, []);
|
||||
const linkObjs = items.reduce<{ link: string; title: string; imageSrc: string }[]>(
|
||||
(objs, el) => {
|
||||
const link = el.querySelector<HTMLAnchorElement>('a')?.href;
|
||||
const title = el
|
||||
.querySelector<HTMLHeadingElement>('h2.a-color-base')!
|
||||
.getAttribute('aria-label')!;
|
||||
const imageSrc = el.querySelector<HTMLImageElement>('img.s-image')!.src!;
|
||||
link && objs.push({ link, title, imageSrc });
|
||||
return objs;
|
||||
},
|
||||
[],
|
||||
);
|
||||
return linkObjs;
|
||||
});
|
||||
break;
|
||||
@ -94,15 +98,19 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
data = await exec(tabId, async () => {
|
||||
const items = [
|
||||
...(document.querySelectorAll<HTMLDivElement>(
|
||||
'.a-section.a-spacing-small.puis-padding-left-small',
|
||||
'.puis-card-container:has(.a-section.a-spacing-small.puis-padding-left-small)',
|
||||
) as unknown as HTMLDivElement[]),
|
||||
].filter((e) => e.getClientRects().length > 0);
|
||||
const linkObjs = items.reduce<AmazonGoodsLinkItem[]>((objs, el) => {
|
||||
const link = el.querySelector<HTMLAnchorElement>('a.a-link-normal')?.href;
|
||||
const title = el.querySelector<HTMLHeadingElement>('h2.a-color-base')?.innerText;
|
||||
link && objs.push({ link, title: title || '' });
|
||||
return objs;
|
||||
}, []);
|
||||
const linkObjs = items.reduce<{ link: string; title: string; imageSrc: string }[]>(
|
||||
(objs, el) => {
|
||||
const link = el.querySelector<HTMLAnchorElement>('a.a-link-normal')?.href;
|
||||
const title = el.querySelector<HTMLHeadingElement>('h2.a-color-base')!.innerText;
|
||||
const imageSrc = el.querySelector<HTMLImageElement>('img.s-image')!.src!;
|
||||
link && objs.push({ link, title, imageSrc });
|
||||
return objs;
|
||||
},
|
||||
[],
|
||||
);
|
||||
return linkObjs;
|
||||
});
|
||||
break;
|
||||
@ -141,21 +149,36 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
stopSignal = true;
|
||||
};
|
||||
this.channel.on('error', stop);
|
||||
let result = { hasNextPage: true, data: [] as AmazonGoodsLinkItem[] };
|
||||
let result = {
|
||||
hasNextPage: true,
|
||||
data: [] as unknown[],
|
||||
};
|
||||
while (result.hasNextPage && !stopSignal) {
|
||||
result = await this.wanderSearchSinglePage(tab);
|
||||
this.channel.emit('item-links-collected', { objs: result.data });
|
||||
this.channel.emit('item-links-collected', {
|
||||
objs: result.data as { link: string; title: string; imageSrc: string }[],
|
||||
});
|
||||
}
|
||||
this.channel.off('error', stop);
|
||||
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
public async wanderDetailPage(asin: string): Promise<void> {
|
||||
public async wanderDetailPage(entry: string): Promise<void> {
|
||||
const tab = await this.getCurrentTab();
|
||||
if (!tab.url?.includes(`/dp/${asin}`)) {
|
||||
const params = { asin: '', url: '' };
|
||||
if (entry.match(/^https?:\/\/www\.amazon\.com.*\/dp\/[A-Z0-9]{10}/)) {
|
||||
const [asin] = /\/\/dp\/[A-Z0-9]{10}/.exec(entry)!;
|
||||
params.asin = asin;
|
||||
params.url = entry;
|
||||
} else if (entry.match(/^[A-Z0-9]{10}$/)) {
|
||||
params.asin = entry;
|
||||
params.url = `https://www.amazon.com/dp/${entry}`;
|
||||
}
|
||||
if (!tab.url?.includes(`www.amazon.com`) || !tab.url?.includes(`/dp/${params.asin}`)) {
|
||||
await browser.tabs.update(tab.id!, {
|
||||
url: `https://www.amazon.com/dp/${asin}?th=1`,
|
||||
url: params.url,
|
||||
});
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
//#region Await Production Introduction Element Loaded and Determine Page Pattern
|
||||
const pattern = await exec(tab.id!, async () => {
|
||||
@ -188,7 +211,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
});
|
||||
if (ratingInfo && (ratingInfo.rating !== 0 || ratingInfo.ratingCount !== 0)) {
|
||||
this.channel.emit('item-rating-collected', {
|
||||
asin,
|
||||
asin: params.asin,
|
||||
...ratingInfo,
|
||||
});
|
||||
}
|
||||
@ -230,7 +253,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
const category2Ranking = Number(/(?<=#)\d+/.exec(category2Statement)?.[0]) || null;
|
||||
const category2Name = /(?<=in\s).+/.exec(category2Statement)?.[0] || null;
|
||||
this.channel.emit('item-category-rank-collected', {
|
||||
asin,
|
||||
asin: params.asin,
|
||||
category1: ![category1Name, category1Ranking].includes(null)
|
||||
? { name: category1Name!, rank: category1Ranking! }
|
||||
: undefined,
|
||||
@ -253,7 +276,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||
});
|
||||
imageUrls &&
|
||||
this.channel.emit('item-images-collected', {
|
||||
asin,
|
||||
asin: entry,
|
||||
urls: imageUrls,
|
||||
});
|
||||
//#endregion
|
||||
|
||||
14
src/logic/page-worker/types.d.ts
vendored
14
src/logic/page-worker/types.d.ts
vendored
@ -1,12 +1,18 @@
|
||||
import type Emittery from 'emittery';
|
||||
|
||||
type AmazonGoodsLinkItem = { link: string; title: string };
|
||||
type AmazonGoodsLinkItem = {
|
||||
link: string;
|
||||
title: string;
|
||||
asin: string;
|
||||
rank: number;
|
||||
imageSrc: string;
|
||||
};
|
||||
|
||||
interface AmazonPageWorkerEvents {
|
||||
/**
|
||||
* The event is fired when worker collected links to items on the Amazon search page.
|
||||
*/
|
||||
['item-links-collected']: { objs: AmazonGoodsLinkItem[] };
|
||||
['item-links-collected']: { objs: { link: string; title: string; imageSrc: string }[] };
|
||||
|
||||
/**
|
||||
* The event is fired when worker collected goods' rating on the Amazon detail page.
|
||||
@ -61,7 +67,7 @@ interface AmazonPageWorker {
|
||||
|
||||
/**
|
||||
* Browsing goods detail page and collect target information.
|
||||
* @param asin Product indentification
|
||||
* @param entry Product link or Amazon Standard Identification Number.
|
||||
*/
|
||||
wanderDetailPage(asin: string): Promise<void>;
|
||||
wanderDetailPage(entry: string): Promise<void>;
|
||||
}
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||
import type { AmazonGoodsLinkItem } from './page-worker/types';
|
||||
|
||||
export const keywords = useWebExtensionStorage<string>('keywords', '');
|
||||
export const keywords = useWebExtensionStorage<string>('keywords', '');
|
||||
|
||||
export const itemList = useWebExtensionStorage<AmazonGoodsLinkItem[]>('itemList', []);
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import SidePanel from './SidePanel.vue';
|
||||
import TestPanel from './TestPanel.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Naive UI Wrapper-->
|
||||
<n-modal-provider>
|
||||
<n-dialog-provider>
|
||||
<n-message-provider>
|
||||
<test-panel />
|
||||
<side-panel />
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-modal-provider>
|
||||
|
||||
49
src/sidepanel/DetailPageWorker.vue
Normal file
49
src/sidepanel/DetailPageWorker.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import pageWorkerFactory from '~/logic/page-worker';
|
||||
|
||||
const inputText = ref('');
|
||||
const data = ref<Record<string, unknown>>({});
|
||||
const worker = pageWorkerFactory.useAmazonPageWorker();
|
||||
|
||||
onMounted(() => {
|
||||
worker.channel.on('item-rating-collected', (ev) => {
|
||||
console.log('item-rating-collected', ev);
|
||||
data.value = { ...data.value, ...ev };
|
||||
});
|
||||
worker.channel.on('item-category-rank-collected', (ev) => {
|
||||
console.log('item-category-rank-collected', ev);
|
||||
data.value = { ...data.value, ...ev };
|
||||
});
|
||||
worker.channel.on('item-images-collected', (ev) => {
|
||||
console.log('item-images-collected', ev);
|
||||
data.value = { ...data.value, ...ev };
|
||||
});
|
||||
});
|
||||
|
||||
const handleGetInfo = async () => {
|
||||
worker.wanderDetailPage(inputText.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-page-worker">
|
||||
<n-form>
|
||||
<n-form-item>
|
||||
<n-input v-model:value="inputText" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button @click="handleGetInfo">Get Info</n-button>
|
||||
<div>{{ data }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-page-worker {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
</style>
|
||||
212
src/sidepanel/SearchPageWorker.vue
Normal file
212
src/sidepanel/SearchPageWorker.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<script setup lang="ts">
|
||||
import { keywords } from '~/logic/storage';
|
||||
import pageWorker from '~/logic/page-worker';
|
||||
import { exportToXLSX } from '~/logic/data-io';
|
||||
import type { AmazonGoodsLinkItem } from '~/logic/page-worker/types';
|
||||
import { NButton } from 'naive-ui';
|
||||
import { itemList as items } from '~/logic/storage';
|
||||
import type { TableColumn } from 'naive-ui/es/data-table/src/interface';
|
||||
|
||||
const message = useMessage();
|
||||
const worker = pageWorker.useAmazonPageWorker();
|
||||
|
||||
const page = reactive({ current: 1, size: 5 });
|
||||
const resultSearchText = ref('');
|
||||
const columns: (TableColumn<AmazonGoodsLinkItem> & { hidden?: boolean })[] = [
|
||||
{
|
||||
title: '排位',
|
||||
key: 'rank',
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
title: 'ASIN',
|
||||
key: 'asin',
|
||||
},
|
||||
{
|
||||
title: '图片',
|
||||
key: 'imageSrc',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
title: '链接',
|
||||
key: 'link',
|
||||
render(row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
size: 'small',
|
||||
onClick: async () => {
|
||||
const tab = await browser.tabs
|
||||
.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
.then((tabs) => tabs[0]);
|
||||
if (tab) {
|
||||
await browser.tabs.update(tab.id, {
|
||||
url: row.link,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
() => '前往',
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const itemView = computed(() => {
|
||||
const { current, size } = page;
|
||||
const searchText = resultSearchText.value;
|
||||
let data = items.value;
|
||||
if (searchText.trim() !== '') {
|
||||
data = data.filter(
|
||||
(r) =>
|
||||
r.title.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
r.asin.toLowerCase().includes(searchText.toLowerCase()),
|
||||
);
|
||||
}
|
||||
let pageCount = ~~(data.length / size);
|
||||
pageCount += data.length % size > 0 ? 1 : 0;
|
||||
data = data.slice((current - 1) * size, current * size);
|
||||
return { data, pageCount };
|
||||
});
|
||||
|
||||
const onItemLinksCollected = (ev: { objs: Record<string, unknown>[] }) => {
|
||||
const addedRows = ev.objs.map((v, i) => {
|
||||
const [asin] = /(?<=\/dp\/)[A-Z0-9]{10}/.exec(v.link as string)!;
|
||||
return { ...v, asin, rank: items.value.length + i + 1 } as AmazonGoodsLinkItem;
|
||||
});
|
||||
items.value = items.value.concat(addedRows);
|
||||
};
|
||||
|
||||
const onCollectStart = async () => {
|
||||
if (keywords.value.trim() === '') {
|
||||
return;
|
||||
}
|
||||
message.info('开始收集');
|
||||
items.value = [];
|
||||
worker.channel.on('error', ({ message: msg }) => {
|
||||
message.error(msg);
|
||||
});
|
||||
worker.channel.on('item-links-collected', onItemLinksCollected);
|
||||
await worker.doSearch(keywords.value);
|
||||
await worker.wanderSearchPage();
|
||||
worker.channel.off('item-links-collected', onItemLinksCollected);
|
||||
message.info('完成');
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const headers = columns.reduce(
|
||||
(p, v: Record<string, any>) => {
|
||||
if ('key' in v && 'title' in v) {
|
||||
p.push({ label: v.title, prop: v.key });
|
||||
}
|
||||
return p;
|
||||
},
|
||||
[] as { label: string; prop: string }[],
|
||||
);
|
||||
exportToXLSX(items.value, { headers });
|
||||
message.info('导出完成');
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="search-page-worker">
|
||||
<n-space class="app-header">
|
||||
<mdi-cat style="font-size: 60px; color: black" />
|
||||
<h1>Azon Seeker</h1>
|
||||
</n-space>
|
||||
<n-space>
|
||||
<n-input
|
||||
v-model:value="keywords"
|
||||
class="search-input-box"
|
||||
autosize
|
||||
size="large"
|
||||
round
|
||||
placeholder="请输入关键词采集信息"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon size="20">
|
||||
<ion-search />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
<n-button type="primary" round size="large" @click="onCollectStart">采集</n-button>
|
||||
</n-space>
|
||||
<div style="height: 10px"></div>
|
||||
<n-card class="result-content-container" title="结果框">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-input
|
||||
v-model:value="resultSearchText"
|
||||
size="small"
|
||||
placeholder="输入关键词查询结果"
|
||||
round
|
||||
/>
|
||||
<n-button type="primary" tertiary round size="small" @click="items = []">
|
||||
<template #icon>
|
||||
<ion-trash-outline />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button type="primary" tertiary round size="small" @click="handleExport">
|
||||
<template #icon>
|
||||
<ion-download-outline />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<n-empty v-if="items.length === 0" size="huge" style="padding-top: 40px">
|
||||
<template #icon>
|
||||
<n-icon size="60">
|
||||
<solar-cat-linear />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #default>
|
||||
<h3>还没有数据哦</h3>
|
||||
</template>
|
||||
</n-empty>
|
||||
<n-space vertical v-else>
|
||||
<n-data-table
|
||||
:columns="columns.filter((col) => col.hidden !== true)"
|
||||
:data="itemView.data"
|
||||
/>
|
||||
<n-pagination
|
||||
v-model:page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:page-count="itemView.pageCount"
|
||||
:page-sizes="[5, 10, 20]"
|
||||
show-size-picker
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-page-worker {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.app-header {
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.search-input-box {
|
||||
min-width: 270px;
|
||||
}
|
||||
|
||||
.result-content-container {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,141 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { keywords } from '~/logic/storage';
|
||||
import pageWorker from '~/logic/page-worker';
|
||||
import type { AmazonGoodsLinkItem } from '~/logic/page-worker/types';
|
||||
import { NButton, type DataTableColumns } from 'naive-ui';
|
||||
import DetailPageWorker from './DetailPageWorker.vue';
|
||||
import SearchPageWorker from './SearchPageWorker.vue';
|
||||
|
||||
const message = useMessage();
|
||||
const worker = pageWorker.useAmazonPageWorker();
|
||||
|
||||
type TableData = AmazonGoodsLinkItem & { rank: number };
|
||||
|
||||
const items = ref<AmazonGoodsLinkItem[]>([]);
|
||||
const page = reactive({ current: 1, size: 5 });
|
||||
const columns: DataTableColumns<TableData> = [
|
||||
const tabs = [
|
||||
{
|
||||
title: '排位',
|
||||
key: 'rank',
|
||||
name: 'Search Page',
|
||||
component: SearchPageWorker,
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
},
|
||||
{
|
||||
title: '链接',
|
||||
key: 'link',
|
||||
render(row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
size: 'small',
|
||||
onClick: async () => {
|
||||
const tab = await browser.tabs
|
||||
.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
.then((tabs) => tabs[0]);
|
||||
if (tab) {
|
||||
await browser.tabs.update(tab.id, {
|
||||
url: row.link,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
() => '前往',
|
||||
);
|
||||
},
|
||||
name: 'Detail Page',
|
||||
component: DetailPageWorker,
|
||||
},
|
||||
];
|
||||
|
||||
const itemView = computed(() => {
|
||||
const { current, size } = page;
|
||||
return items.value
|
||||
.slice((current - 1) * size, current * size)
|
||||
.map((v, i) => ({ ...v, rank: 1 + (current - 1) * size + i }));
|
||||
const selectedTab = ref(tabs[0].name);
|
||||
const currentComponent = computed(() => {
|
||||
const tab = tabs.find((tab) => tab.name === selectedTab.value);
|
||||
return tab ? tab.component : null;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
worker.channel.on('item-links-collected', (ev) => {
|
||||
items.value = items.value.concat(ev.objs);
|
||||
});
|
||||
});
|
||||
|
||||
const onCollect = async () => {
|
||||
if (keywords.value.trim() === '') {
|
||||
return;
|
||||
}
|
||||
message.info('开始收集');
|
||||
worker.channel.on('error', ({ message: msg }) => {
|
||||
message.error(msg);
|
||||
});
|
||||
await worker.doSearch(keywords.value);
|
||||
await worker.wanderSearchPage();
|
||||
message.info('完成');
|
||||
};
|
||||
const showHeader = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="side-panel">
|
||||
<n-space class="app-header">
|
||||
<mdi-cat style="font-size: 60px; color: black" />
|
||||
<h1>Azon Seeker</h1>
|
||||
</n-space>
|
||||
<n-space>
|
||||
<n-input
|
||||
v-model:value="keywords"
|
||||
class="search-input-box"
|
||||
autosize
|
||||
size="large"
|
||||
round
|
||||
placeholder="请输入关键词"
|
||||
/>
|
||||
<n-button type="primary" round size="large" @click="onCollect">采集</n-button>
|
||||
</n-space>
|
||||
<div style="height: 10px"></div>
|
||||
<n-card class="result-content-container" title="结果框">
|
||||
<n-empty v-if="items.length === 0" description="还没有结果哦">
|
||||
<template #icon>
|
||||
<n-icon :size="50">
|
||||
<solar-cat-linear />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-empty>
|
||||
<n-space vertical v-else>
|
||||
<n-data-table :columns="columns" :data="itemView" />
|
||||
<n-pagination
|
||||
v-model:page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:page-count="~~(items.length / page.size) + 1"
|
||||
:page-sizes="[5, 10, 20]"
|
||||
show-size-picker
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</main>
|
||||
<div class="side-panel">
|
||||
<div class="header-menu" v-if="showHeader">
|
||||
<n-tabs placement="top" :default-value="tabs[0].name" type="card" v-model:value="selectedTab">
|
||||
<n-tab v-for="tab in tabs" :name="tab.name" />
|
||||
</n-tabs>
|
||||
</div>
|
||||
<div class="display-header-button" @click="showHeader = !showHeader">
|
||||
<n-icon size="22">
|
||||
<ion-chevron-up v-if="showHeader" />
|
||||
<ion-chevron-down v-else />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<keep-alive>
|
||||
<Component :is="currentComponent" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style scoped lang="scss">
|
||||
.side-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
background-color: #f0f0f0;
|
||||
|
||||
.app-header {
|
||||
margin-top: 100px;
|
||||
.header-menu {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.search-input-box {
|
||||
min-width: 270px;
|
||||
.display-header-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
> .n-icon {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> .n-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
}
|
||||
|
||||
.result-content-container {
|
||||
width: 90%;
|
||||
.main-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<base target="_blank" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user