Update UI & Worker

This commit is contained in:
johnathan 2025-05-26 15:04:01 +08:00
parent a84f148743
commit 9aeb12c7bf
18 changed files with 456 additions and 244 deletions

View File

@ -20,4 +20,16 @@ if (USE_SIDE_PANEL) {
browser.runtime.onInstalled.addListener(() => {
// eslint-disable-next-line no-console
console.log('Azon Seeker installed');
browser.contextMenus.create({
id: 'show-result',
title: '结果页',
contexts: ['action'],
});
});
browser.contextMenus.onClicked.addListener((info) => {
if (info.menuItemId === 'show-result') {
browser.runtime.openOptionsPage();
}
});

View File

@ -0,0 +1,112 @@
<script setup lang="ts">
import { useFileDialog } from '@vueuse/core';
import type { FormItemRule } from 'naive-ui';
withDefaults(
defineProps<{
disabled?: boolean;
}>(),
{
disabled: false,
},
);
const modelValue = defineModel<string>({ required: true });
const message = useMessage();
const formItemRef = useTemplateRef('detail-form-item');
const formItemRule: FormItemRule = {
required: true,
trigger: ['submit', 'blur'],
message: '请输入格式正确的ASIN',
validator: () => {
return modelValue.value.match(/^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g) !== null;
},
};
const fileDialog = useFileDialog({ accept: '.txt', multiple: false });
fileDialog.onChange((fileList) => {
const file = fileList?.item(0);
file && handleImportAsin(file);
fileDialog.reset();
});
const handleImportAsin = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result;
if (typeof content === 'string') {
modelValue.value = content;
}
};
reader.readAsText(file, 'utf-8');
};
const handleExportAsin = () => {
const blob = new Blob([modelValue.value], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const filename = `asin-${new Date().toISOString()}.txt`;
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
message.info('导出完成');
};
defineExpose({
validate: async () =>
new Promise<boolean>((resolve) => {
formItemRef.value?.validate({
trigger: 'submit',
callback: (errors) => {
if (errors) {
resolve(false);
} else {
resolve(true);
}
},
});
}),
});
</script>
<template>
<div class="asin-input">
<n-space>
<n-button @click="fileDialog.open()" :disabled="disabled" round size="small">
<template #icon>
<gg-import />
</template>
导入
</n-button>
<n-button :disabled="disabled" @click="handleExportAsin" round size="small">
<template #icon>
<ion-arrow-up-right-box-outline />
</template>
导出
</n-button>
</n-space>
<div style="height: 7px" />
<n-form-item ref="detail-form-item" label-placement="left" :rule="formItemRule">
<n-input
:disabled="disabled"
v-model:value="modelValue"
placeholder="输入ASINs"
type="textarea"
size="large"
/>
</n-form-item>
</div>
</template>
<style lang="scss">
.asin-input {
width: 100%;
display: flex;
flex-direction: column;
}
</style>

View File

@ -33,18 +33,34 @@ const props = defineProps<{ model: AmazonDetailItem }>();
{{ link }}
</div>
</n-descriptions-item>
<n-descriptions-item v-if="props.model.topReviews" label="精选评论" :span="4">
<div v-for="review in props.model.topReviews" style="margin-bottom: 5px">
<h3 style="margin: 0">{{ review.username }}: {{ review.title }}</h3>
<div style="color: gray; font-size: smaller">{{ review.rating }}</div>
<div v-for="paragraph in review.content.split('\n')">
{{ paragraph }}
<n-descriptions-item
v-if="props.model.topReviews && props.model.topReviews.length > 0"
label="精选评论"
:span="4"
>
<n-scrollbar style="max-height: 500px">
<div class="review-item-cotent">
<div v-for="review in props.model.topReviews" style="margin-bottom: 5px">
<h3 style="margin: 0">{{ review.username }}: {{ review.title }}</h3>
<div style="color: gray; font-size: smaller">{{ review.rating }}</div>
<div v-for="paragraph in review.content.split('\n')">
{{ paragraph }}
</div>
<div style="color: gray; font-size: smaller">{{ review.dateInfo }}</div>
</div>
</div>
<div style="color: gray; font-size: smaller">{{ review.dateInfo }}</div>
</n-scrollbar>
<div class="review-item-footer">
<n-button size="small">Load More</n-button>
</div>
</n-descriptions-item>
</n-descriptions>
</div>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.review-item-footer {
display: flex;
flex-direction: row-reverse;
}
</style>

View File

@ -1,14 +1,26 @@
<script setup lang="ts">
import { NButton, UploadOnChange } from 'naive-ui';
import { NButton } from 'naive-ui';
import type { TableColumn } from 'naive-ui/es/data-table/src/interface';
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
import type { AmazonDetailItem, AmazonItem } from '~/logic/page-worker/types';
import { allItems } from '~/logic/storage';
import DetailDescription from './DetailDescription.vue';
import { useFileDialog } from '@vueuse/core';
const message = useMessage();
const fileDialog = useFileDialog({
accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
multiple: false,
});
fileDialog.onChange((files) => {
const file = files?.item(0);
file && handleImport(file);
fileDialog.reset();
});
const page = reactive({ current: 1, size: 10 });
const filter = reactive({
keywords: null as string | null,
search: '',
@ -120,10 +132,14 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
const itemView = computed<{ records: AmazonItem[]; pageCount: number; origin: AmazonItem[] }>(
() => {
const { current, size } = page;
let data = filterItemData(allItems.value); // Filter Data
let pageCount = ~~(data.length / size);
pageCount += data.length % size > 0 ? 1 : 0;
return { records: data.slice((current - 1) * size, current * size), pageCount, origin: data };
const data = filterItemData(allItems.value); // Filter Data
const pageCount = ~~(data.length / size) + (data.length % size > 0 ? 1 : 0);
const offset = (current - 1) * size;
if (data.length > 0 && offset >= data.length) {
page.current = 1;
}
const records = data.slice(offset, offset + size);
return { records, pageCount, origin: data };
},
);
@ -189,11 +205,7 @@ const handleExport = async () => {
message.info('导出完成');
};
const handleImport: UploadOnChange = async ({ fileList }) => {
if (fileList.length !== 1 || fileList[0].file === null) {
return;
}
const file: File = fileList.pop()!.file!;
const handleImport = async (file: File) => {
const headers: Header[] = columns
.reduce(
(p, v: Record<string, any>) => {
@ -234,65 +246,66 @@ const handleClearData = async () => {
size="small"
placeholder="输入文本过滤结果"
round
style="min-width: 230px"
/>
<n-popconfirm
placement="bottom"
@positive-click="handleClearData"
positive-text="确定"
negative-text="取消"
>
<template #trigger>
<n-button type="error" tertiary circle size="small">
<template #icon>
<ion-trash-outline />
</template>
</n-button>
</template>
确认清空所有数据吗
</n-popconfirm>
<n-upload
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
:max="1"
@change="handleImport"
>
<n-button type="primary" tertiary circle size="small">
<n-button-group class="button-group">
<n-popconfirm
placement="bottom"
@positive-click="handleClearData"
positive-text="确定"
negative-text="取消"
>
<template #trigger>
<n-button type="default" ghost round size="small">
<template #icon>
<ion-trash-outline />
</template>
清空
</n-button>
</template>
确认清空所有数据吗
</n-popconfirm>
<n-button type="default" ghost round @click="fileDialog.open()" size="small">
<template #icon>
<gg-import />
</template>
导入
</n-button>
</n-upload>
<n-button type="primary" tertiary circle size="small" @click="handleExport">
<template #icon>
<ion-arrow-up-right-box-outline />
</template>
</n-button>
<n-popover trigger="hover" placement="bottom">
<template #trigger>
<n-button type="primary" tertiary circle size="small">
<template #icon>
<ant-design-filter-outlined />
</template>
</n-button>
</template>
<div class="filter-section">
<div class="filter-title">筛选器</div>
<n-form :model="filter" label-placement="left">
<n-form-item v-for="item in filterFormItems" :label="item.label">
<n-select
v-if="item.type === 'select'"
placeholder=""
v-model:value="filter.keywords"
clearable
:options="item.params.options"
/>
</n-form-item>
</n-form>
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
</div>
</n-popover>
<n-button type="default" ghost round size="small" @click="handleExport">
<template #icon>
<ion-arrow-up-right-box-outline />
</template>
导出
</n-button>
<n-popover trigger="hover" placement="bottom">
<template #trigger>
<n-button type="default" ghost round size="small">
<template #icon>
<ant-design-filter-outlined />
</template>
过滤
</n-button>
</template>
<div class="filter-section">
<div class="filter-title">筛选器</div>
<n-form :model="filter" label-placement="left">
<n-form-item v-for="item in filterFormItems" :label="item.label">
<n-select
v-if="item.type === 'select'"
placeholder=""
v-model:value="filter.keywords"
clearable
:options="item.params.options"
/>
</n-form-item>
</n-form>
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
</div>
</n-popover>
</n-button-group>
</n-space>
</template>
<n-empty v-if="allItems.length === 0" size="huge" style="padding-top: 40px">
<n-empty v-if="allItems.length === 0" size="huge">
<template #icon>
<n-icon size="60">
<solar-cat-linear />
@ -324,7 +337,13 @@ const handleClearData = async () => {
<style lang="scss" scoped>
.result-content-container {
width: 100%;
min-height: 100%;
:deep(.n-card__content:has(.n-empty)) {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
}
}
:deep(.filter-switch) {

View File

@ -0,0 +1,21 @@
export function useLongTask() {
const isRunning = ref(false);
const startTask = async (task: () => Promise<void>) => {
isRunning.value = true;
try {
await task();
isRunning.value = false;
} catch (error) {
isRunning.value = false;
console.error('Task failed:', error);
throw error;
}
};
return {
isRunning,
startTask,
};
}

View File

@ -37,15 +37,16 @@ export async function exec<T, P extends Record<string, unknown>>(
};
return new Promise<T>(async (resolve, reject) => {
setTimeout(() => reject('脚本运行超时'), timeout);
const injectResults = await browser.scripting.executeScript({
target: { tabId },
func,
args: payload ? [payload] : undefined,
});
const ret = injectResults.pop();
if (ret?.error) {
reject(`注入脚本时发生错误: ${ret.error}`); // 似乎无法走到这一步
try {
const injectResults = await browser.scripting.executeScript({
target: { tabId },
func,
args: payload ? [payload] : undefined,
});
const ret = injectResults.pop();
resolve(ret!.result as T);
} catch (e) {
throw new Error(`入脚本运行失败: ${e}`);
}
resolve(ret!.result as T);
});
}

View File

@ -32,6 +32,7 @@ type AmazonReview = {
rating: string;
dateInfo: string;
content: string;
imageSrc: string[];
};
type AmazonItem = Pick<AmazonSearchItem, 'asin'> &

View File

@ -3,7 +3,9 @@ import type { AmazonDetailItem, AmazonItem, AmazonSearchItem } from './page-work
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
export const asinInputText = useWebExtensionStorage<string>('asinInputText', '');
export const detailAsinInput = useWebExtensionStorage<string>('detailAsinInputText', '');
export const reviewAsinInput = useWebExtensionStorage<string>('reviewAsinInputText', '');
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
@ -17,8 +19,8 @@ export const detailItems = useWebExtensionStorage<Map<string, AmazonDetailItem>>
export const allItems = computed({
get() {
const sItems = searchItems.value;
const dItems = detailItems.value;
const sItems = unref(searchItems);
const dItems = unref(detailItems);
return sItems.map<AmazonItem>((si) => {
const asin = si.asin;
const dItem = dItems.get(asin);
@ -45,6 +47,8 @@ export const allItems = computed({
});
const detailItemsProps: (keyof AmazonDetailItem)[] = [
'asin',
'title',
'price',
'category1',
'category2',
'imageUrls',

View File

@ -306,7 +306,12 @@ export class AmazonDetailPageInjector extends BaseInjector {
const content = commentNode.querySelector<HTMLDivElement>(
'[data-hook="review-body"]',
)!.innerText;
items.push({ id, username, title, rating, dateInfo, content });
const imageSrc = Array.from(
commentNode.querySelectorAll<HTMLImageElement>(
'.review-image-tile-section img[src] img[src]',
),
).map((e) => e.getAttribute('src')!);
items.push({ id, username, title, rating, dateInfo, content, imageSrc });
}
return items;
});
@ -377,7 +382,10 @@ export class AmazonReviewPageInjector extends BaseInjector {
const content = commentNode.querySelector<HTMLDivElement>(
'[data-hook="review-body"]',
)!.innerText;
items.push({ id, username, title, rating, dateInfo, content });
const imageSrc = Array.from(
commentNode.querySelectorAll<HTMLImageElement>('.review-image-tile-section img[src]'),
).map((e) => e.getAttribute('src')!);
items.push({ id, username, title, rating, dateInfo, content, imageSrc });
}
return items;
});

View File

@ -1,5 +1,5 @@
import fs from 'fs-extra';
import type { Manifest } from 'webextension-polyfill';
import { type Manifest } from 'webextension-polyfill';
import type PkgType from '../package.json';
import { isDev, isFirefox, port, r } from '../scripts/utils';
@ -33,7 +33,15 @@ export async function getManifest() {
48: './assets/icon-512.png',
128: './assets/icon-512.png',
},
permissions: ['tabs', 'storage', 'activeTab', 'sidePanel', 'scripting', 'unlimitedStorage'],
permissions: [
'tabs',
'storage',
'activeTab',
'sidePanel',
'scripting',
'unlimitedStorage',
'contextMenus',
],
host_permissions: ['*://*/*'],
content_scripts: [
{

View File

@ -14,7 +14,8 @@ main {
align-items: center;
.result-table {
width: 95%;
height: 90vh;
width: 95vw;
}
}
</style>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { GlobalThemeOverrides } from 'naive-ui';
import SidePanel from './SidePanel.vue';
import SidePanel from './Sidepanel.vue';
const theme: GlobalThemeOverrides = {
common: {

View File

@ -1,21 +1,11 @@
<script setup lang="ts">
import type { FormItemRule, UploadOnChange } from 'naive-ui';
import { useLongTask } from '~/composables/useLongTask';
import pageWorker from '~/logic/page-worker';
import { AmazonDetailItem } from '~/logic/page-worker/types';
import { asinInputText, detailItems } from '~/logic/storage';
import { detailAsinInput, detailItems } from '~/logic/storage';
const message = useMessage();
const formItemRef = useTemplateRef('detail-form-item');
const formItemRule: FormItemRule = {
required: true,
trigger: ['submit', 'blur'],
message: '请输入格式正确的ASIN',
validator: () => {
return asinInputText.value.match(/^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g) !== null;
},
};
const timelines = ref<
{
type: 'default' | 'error' | 'success' | 'warning' | 'info';
@ -25,7 +15,9 @@ const timelines = ref<
}[]
>([]);
const running = ref(false);
const { isRunning, startTask } = useLongTask();
const asinInputRef = useTemplateRef('asin-input');
//#region Page Worker Code
const worker = pageWorker.useAmazonPageWorker(); // Page Worker
@ -37,7 +29,7 @@ worker.channel.on('error', ({ message: msg }) => {
content: msg,
});
message.error(msg);
running.value = false;
worker.stop();
});
worker.channel.on('item-base-info-collected', (ev) => {
timelines.value.push({
@ -98,72 +90,33 @@ const updateDetailItems = (row: { asin: string } & Partial<AmazonDetailItem>) =>
};
//#endregion
const handleImportAsin: UploadOnChange = ({ fileList }) => {
if (fileList.length > 0) {
const file = fileList.pop();
if (file && file.file) {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result;
if (typeof content === 'string') {
asinInputText.value = content;
}
};
reader.readAsText(file.file, 'utf-8');
}
}
};
const handleExportAsin = () => {
const blob = new Blob([asinInputText.value], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const filename = `asin-${new Date().toISOString()}.txt`;
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
message.info('导出完成');
};
const handleFetchInfoFromPage = () => {
const runTask = async () => {
const asinList = asinInputText.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
running.value = true;
timelines.value = [
{
type: 'info',
title: '开始',
time: new Date().toLocaleString(),
content: '开始数据采集',
},
];
await worker.runDetaiPageTask(asinList, async (remains) => {
asinInputText.value = remains.join('\n');
});
timelines.value.push({
const task = async () => {
const asinList = detailAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
timelines.value = [
{
type: 'info',
title: '结束',
title: '开始',
time: new Date().toLocaleString(),
content: '数据采集完成',
});
running.value = false;
};
formItemRef.value?.validate({
callback: (errors) => {
if (errors) {
return;
} else {
runTask();
}
content: '开始数据采集',
},
];
await worker.runDetaiPageTask(asinList, async (remains) => {
detailAsinInput.value = remains.join('\n');
});
timelines.value.push({
type: 'info',
title: '结束',
time: new Date().toLocaleString(),
content: '数据采集完成',
});
};
const handleStart = () => {
asinInputRef.value?.validate().then(async (success) => success && startTask(task));
};
const handleInterrupt = () => {
if (!running.value) return;
if (!isRunning.value) return;
worker.stop();
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
};
@ -173,37 +126,8 @@ const handleInterrupt = () => {
<div class="detail-page-entry">
<header-title>Detail Page</header-title>
<div class="interative-section">
<n-space>
<n-upload @change="handleImportAsin" accept=".txt" :max="1">
<n-button :disabled="running" round size="small">
<template #icon>
<gg-import />
</template>
导入
</n-button>
</n-upload>
<n-button :disabled="running" @click="handleExportAsin" round size="small">
<template #icon>
<ion-arrow-up-right-box-outline />
</template>
导出
</n-button>
</n-space>
<n-form-item
ref="detail-form-item"
label-placement="left"
:rule="formItemRule"
style="padding-top: 0px"
>
<n-input
:disabled="running"
v-model:value="asinInputText"
placeholder="输入ASINs"
type="textarea"
size="large"
/>
</n-form-item>
<n-button v-if="!running" round size="large" type="primary" @click="handleFetchInfoFromPage">
<asins-input v-model="detailAsinInput" :disabled="isRunning" ref="asin-input" />
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
<template #icon>
<ant-design-thunderbolt-outlined />
</template>
@ -216,7 +140,7 @@ const handleInterrupt = () => {
停止
</n-button>
</div>
<div v-if="running" class="running-tip-section">
<div v-if="isRunning" class="running-tip-section">
<n-alert title="Warning" type="warning"> 警告在插件运行期间请勿与浏览器交互 </n-alert>
</div>
<progress-report class="progress-report" :timelines="timelines" />
@ -230,29 +154,29 @@ const handleInterrupt = () => {
display: flex;
flex-direction: column;
align-items: center;
}
.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;
}
.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;
}
.running-tip-section {
margin: 10px 0 0 0;
height: 100px;
border-radius: 10px;
cursor: wait;
}
.progress-report {
margin-top: 10px;
width: 95%;
}
.progress-report {
margin-top: 10px;
width: 95%;
}
</style>

View File

@ -1,35 +1,124 @@
<script lang="ts" setup>
import { useLongTask } from '~/composables/useLongTask';
import pageWorker from '~/logic/page-worker';
import { reviewAsinInput } from '~/logic/storage';
const worker = pageWorker.useAmazonPageWorker();
worker.channel.on('error', ({ message: msg }) => {
timelines.value.push({
type: 'error',
title: '错误发生',
time: new Date().toLocaleString(),
content: msg,
});
});
worker.channel.on('item-review-collected', (ev) => {
output.value = ev;
timelines.value.push({
type: 'success',
title: `商品${ev.asin}评价`,
time: new Date().toLocaleString(),
content: `获取到 ${ev.reviews.length} 条评价`,
});
});
const inputText = ref('');
const output = ref<any>(null);
const { isRunning, startTask } = useLongTask();
const asinInputRef = useTemplateRef('asin-input');
const message = useMessage();
const timelines = ref<
{
type: 'default' | 'error' | 'success' | 'warning' | 'info';
title: string;
time: string;
content: string;
}[]
>([]);
const task = async () => {
const asinList = reviewAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
timelines.value = [
{
type: 'info',
title: '开始',
time: new Date().toLocaleString(),
content: '开始数据采集',
},
];
await worker.runReviewPageTask(asinList, async (remains) => {
reviewAsinInput.value = remains.join('\n');
});
timelines.value.push({
type: 'info',
title: '结束',
time: new Date().toLocaleString(),
content: '数据采集完成',
});
};
const handleStart = () => {
worker.runReviewPageTask(inputText.value.split('\n').filter((t) => /^[A-Z0-9]{10}/.exec(t)));
asinInputRef.value?.validate().then(async (success) => success && startTask(task));
};
const handleInterrupt = () => {
worker.stop();
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
};
</script>
<template>
<div class="review-page-entry">
<header-title>Review Page</header-title>
<n-input type="textarea" v-model:value="inputText" />
<n-button @click="handleStart">测试</n-button>
<div>{{ output }}</div>
<div class="interative-section">
<asins-input v-model="reviewAsinInput" :disabled="isRunning" ref="asin-input" />
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
<template #icon>
<ant-design-thunderbolt-outlined />
</template>
开始
</n-button>
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
<template #icon>
<ant-design-thunderbolt-outlined />
</template>
停止
</n-button>
</div>
<div v-if="isRunning" class="running-tip-section">
<n-alert title="Warning" type="warning"> 警告在插件运行期间请勿与浏览器交互 </n-alert>
</div>
<progress-report class="progress-report" :timelines="timelines" />
</div>
</template>
<style lang="scss" scoped>
.review-page-entry {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
}
.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 {
border-radius: 10px;
cursor: wait;
}
.progress-report {
width: 90%;
}
</style>

View File

@ -3,8 +3,11 @@ import { keywordsList } from '~/logic/storage';
import pageWorker from '~/logic/page-worker';
import { NButton } from 'naive-ui';
import { searchItems } from '~/logic/storage';
import { useLongTask } from '~/composables/useLongTask';
const message = useMessage();
const { isRunning, startTask } = useLongTask();
//#region Initial Page Worker
const worker = pageWorker.useAmazonPageWorker();
worker.channel.on('error', ({ message: msg }) => {
@ -16,7 +19,6 @@ worker.channel.on('error', ({ message: msg }) => {
});
message.error(msg);
worker.stop();
running.value = false;
});
worker.channel.on('item-links-collected', ({ objs }) => {
timelines.value.push({
@ -28,7 +30,6 @@ worker.channel.on('item-links-collected', ({ objs }) => {
searchItems.value = searchItems.value.concat(objs); // Add records
});
//#endregion
const running = ref(false);
const timelines = ref<
{
@ -39,12 +40,8 @@ const timelines = ref<
}[]
>([]);
const handleFetchInfoFromPage = async () => {
if (keywordsList.value.length === 0) {
return;
}
const task = async () => {
const kws = unref(keywordsList);
running.value = true;
timelines.value = [
{
type: 'info',
@ -71,11 +68,17 @@ const handleFetchInfoFromPage = async () => {
time: new Date().toLocaleString(),
content: `搜索任务结束`,
});
running.value = false;
};
const handleStart = async () => {
if (keywordsList.value.length === 0) {
return;
}
startTask(task);
};
const handleInterrupt = () => {
if (!running.value) return;
if (!isRunning.value) return;
worker.stop();
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
};
@ -86,7 +89,7 @@ const handleInterrupt = () => {
<header-title>Search Page</header-title>
<div class="interactive-section">
<n-dynamic-input
:disabled="running"
:disabled="isRunning"
v-model:value="keywordsList"
:min="1"
:max="10"
@ -96,7 +99,7 @@ const handleInterrupt = () => {
round
placeholder="请输入关键词采集信息"
/>
<n-button v-if="!running" type="primary" round size="large" @click="handleFetchInfoFromPage">
<n-button v-if="!isRunning" type="primary" round size="large" @click="handleStart">
<template #icon>
<n-icon>
<ant-design-thunderbolt-outlined />
@ -113,7 +116,7 @@ const handleInterrupt = () => {
中断
</n-button>
</div>
<div v-if="running" class="running-tip-section">
<div v-if="isRunning" class="running-tip-section">
<n-alert title="Warning" type="warning"> 警告在插件运行期间请勿与浏览器交互 </n-alert>
</div>
<progress-report class="progress-report" :timelines="timelines" />

View File

@ -1 +1 @@
import './main.css';
import './main.scss';

View File

@ -1,7 +0,0 @@
import { describe, expect, it } from 'vitest';
describe('demo', () => {
it('should work', () => {
expect(1 + 1).toBe(2);
});
});