This commit is contained in:
johnathan 2025-06-20 17:26:24 +08:00
parent 64b41f5841
commit 6645cd2b75
23 changed files with 543 additions and 190 deletions

View File

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

View File

@ -1,26 +1,7 @@
<script lang="ts" setup>
import type { AmazonDetailItem } from '~/logic/page-worker/types';
import { reviewItems } from '~/logic/storage';
import ReviewPreview from './ReviewPreview.vue';
const props = defineProps<{ model: AmazonDetailItem }>();
const modal = useModal();
const handleLoadMore = () => {
const asin = props.model.asin;
modal.create({
title: `${asin}评论`,
preset: 'card',
style: {
width: '80vw',
height: '85vh',
},
content: () =>
h(ReviewPreview, {
asin,
}),
});
};
defineProps<{ model: AmazonDetailItem }>();
</script>
<template>
@ -50,25 +31,6 @@ const handleLoadMore = () => {
<n-descriptions-item label="图片链接" :span="4">
<image-link v-for="link in model.imageUrls" :url="link" />
</n-descriptions-item>
<!-- <n-descriptions-item
v-if="model.topReviews && model.topReviews.length > 0"
label="精选评论"
:span="4"
>
<n-scrollbar style="max-height: 350px">
<div class="review-item-cotent">
<template v-for="review in model.topReviews">
<review-card :model="review" />
<div style="height: 7px"></div>
</template>
</div>
</n-scrollbar>
<div class="review-item-footer">
<n-button :disabled="!reviewItems.has(model.asin)" @click="handleLoadMore" size="small">
更多评论
</n-button>
</div>
</n-descriptions-item> -->
</n-descriptions>
</div>
</template>

View File

@ -2,12 +2,18 @@
import { useFileDialog } from '@vueuse/core';
import type { FormItemRule } from 'naive-ui';
withDefaults(
const props = withDefaults(
defineProps<{
disabled?: boolean;
matchPattern?: RegExp;
placeholder?: string;
validateMessage?: string;
}>(),
{
disabled: false,
matchPattern: () => /^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g,
placeholder: '输入ASINs',
validateMessage: '请输入格式正确的ASIN',
},
);
@ -19,20 +25,20 @@ const formItemRef = useTemplateRef('detail-form-item');
const formItemRule: FormItemRule = {
required: true,
trigger: ['submit', 'blur'],
message: '请输入格式正确的ASIN',
message: props.validateMessage,
validator: () => {
return modelValue.value.match(/^[A-Z0-9]{10}((\n|\s|,|;)[A-Z0-9]{10})*\n?$/g) !== null;
return props.matchPattern.exec(modelValue.value) !== null;
},
};
const fileDialog = useFileDialog({ accept: '.txt', multiple: false });
fileDialog.onChange((fileList) => {
const file = fileList?.item(0);
file && handleImportAsin(file);
file && handleImportIds(file);
fileDialog.reset();
});
const handleImportAsin = (file: File) => {
const handleImportIds = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result;
@ -43,10 +49,10 @@ const handleImportAsin = (file: File) => {
reader.readAsText(file, 'utf-8');
};
const handleExportAsin = () => {
const handleExportIds = () => {
const blob = new Blob([modelValue.value], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const filename = `asin-${new Date().toISOString()}.txt`;
const filename = `${new Date().toISOString()}.txt`;
const link = document.createElement('a');
link.href = url;
link.download = filename;
@ -75,7 +81,7 @@ defineExpose({
</script>
<template>
<div class="asin-input">
<div class="ids-input">
<n-space>
<n-button @click="fileDialog.open()" :disabled="disabled" round size="small">
<template #icon>
@ -83,7 +89,7 @@ defineExpose({
</template>
导入
</n-button>
<n-button :disabled="disabled" @click="handleExportAsin" round size="small">
<n-button :disabled="disabled" @click="handleExportIds" round size="small">
<template #icon>
<ion-arrow-up-right-box-outline />
</template>
@ -95,7 +101,7 @@ defineExpose({
<n-input
:disabled="disabled"
v-model:value="modelValue"
placeholder="输入ASINs"
:placeholder="placeholder"
type="textarea"
size="large"
/>

View File

@ -1,11 +1,13 @@
<script setup lang="ts">
export type Timeline = {
type: 'default' | 'error' | 'success' | 'warning' | 'info';
title: string;
time: string;
content: string;
};
defineProps<{
timelines: {
type: 'default' | 'error' | 'success' | 'warning' | 'info';
title: string;
time: string;
content: string;
}[];
timelines: Timeline[];
}>();
</script>

View File

@ -0,0 +1,96 @@
<script setup lang="ts">
const page = reactive({ current: 1, size: 10 });
export type TableColumn =
| {
title: string;
key: string;
minWidth?: number;
hidden?: boolean;
render?: (row: any) => VNode;
}
| {
type: 'expand';
hidden?: boolean;
expandable: (row: any) => boolean;
renderExpand: (row: any) => VNode;
};
const props = defineProps<{
records: Record<string, unknown>[];
columns: TableColumn[];
}>();
const itemView = computed(() => {
const { current, size } = page;
const data = props.records;
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 };
});
function generateUUID() {
return crypto.randomUUID();
}
</script>
<template>
<div class="result-table">
<n-card class="result-content-container">
<template #header><slot name="header" /></template>
<template #header-extra><slot name="header-extra" /></template>
<n-empty v-if="itemView.records.length === 0" size="huge">
<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
:row-key="() => `${generateUUID()}`"
:columns="columns.filter((col) => col.hidden !== true)"
:data="itemView.records"
/>
<div class="data-pagination">
<n-pagination
v-model:page="page.current"
v-model:page-size="page.size"
:page-count="itemView.pageCount"
:page-sizes="[5, 10, 15, 20, 25]"
show-size-picker
/>
</div>
</n-space>
</n-card>
</div>
</template>
<style scoped lang="scss">
.result-table {
width: 100%;
height: 100%;
}
.result-content-container {
min-height: 100%;
:deep(.n-card__content:has(.n-empty)) {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
}
}
.data-pagination {
display: flex;
flex-direction: row-reverse;
}
</style>

View File

@ -2,7 +2,7 @@
import { useElementSize } from '@vueuse/core';
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
import type { AmazonReview } from '~/logic/page-worker/types';
import { reviewItems } from '~/logic/storage';
import { reviewItems } from '~/logic/storages/amazon';
const props = defineProps<{ asin: string }>();

View File

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

View File

@ -121,7 +121,6 @@ type HomedepotDetailItem = {
title: string;
price: string;
rate?: string;
innerText: string;
reviewCount?: number;
mainImageUrl: string;
modelInfo?: string;

View File

@ -4,7 +4,7 @@ import type {
AmazonItem,
AmazonReview,
AmazonSearchItem,
} from './page-worker/types';
} from '~/logic/page-worker/types';
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);

View File

@ -0,0 +1,3 @@
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
export const site = useWebExtensionStorage<'amazon' | 'homedepot'>('site', 'amazon');

View File

@ -0,0 +1,22 @@
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
import { HomedepotDetailItem } from '../page-worker/types';
export const detailItems = useWebExtensionStorage<Map<string, HomedepotDetailItem>>(
'homedepot-details',
new Map(),
{
listenToStorageChanges: 'options',
},
);
export const allItems = computed({
get() {
return Array.from(detailItems.value.values());
},
set(newValue) {
detailItems.value = newValue.reduce((m, c) => {
m.set(c.OSMID, c);
return m;
}, new Map());
},
});

View File

@ -226,17 +226,22 @@ export class AmazonDetailPageInjector extends BaseInjector {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
const script = document.evaluate(
`//script[starts-with(text(), "\nP.when(\'A\').register")]`,
let script = document.evaluate(
`//script[starts-with(text(), "\nP.when(\'A\').register") or contains(text(), "\nP.when('A').register")]`,
document,
null,
XPathResult.STRING_TYPE,
).stringValue;
const urls = [
...script.matchAll(
/(?<="hiRes":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
),
].map((e) => e[0]);
const extractUrls = (pattern: RegExp) =>
Array.from(script.matchAll(pattern)).map((e) => e[0]);
let urls = extractUrls(
/(?<="hiRes":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
);
if (urls.length === 0) {
urls = extractUrls(
/(?<="large":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
);
}
return urls;
});
}

View File

@ -1,12 +1,37 @@
<script setup lang="ts">
import { RouterView } from 'vue-router';
import { site } from '~/logic/storages/global';
import { useRouter } from 'vue-router';
const router = useRouter();
const options: { label: string; value: 'amazon' | 'homedepot' }[] = [
{ label: 'Amazon', value: 'amazon' },
{ label: 'Homedepot', value: 'homedepot' },
];
watch(site, (newVal) => {
router.push(`/${newVal}`);
});
</script>
<template>
<header>
<span></span>
<h1 class="header-title">采集结果</h1>
<span></span>
<span>
<n-popselect v-model:value="site" :options="options" placement="bottom-start">
<n-button>
<template #icon>
<n-icon size="20">
<garden-menu-fill-12 />
</n-icon>
</template>
</n-button>
</n-popselect>
</span>
<span>
<h1 class="header-title">采集结果</h1>
</span>
<span> </span>
</header>
<main>
<router-view />
@ -18,6 +43,8 @@ header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.header-title {
cursor: default;
}
@ -28,9 +55,7 @@ main {
flex-direction: column;
align-items: center;
.result-table {
height: 90vh;
width: 95vw;
}
height: 90vh;
width: 95vw;
}
</style>

View File

@ -1,17 +1,15 @@
<script setup lang="tsx">
import { NButton, NSpace } from 'naive-ui';
import type { TableColumn } from 'naive-ui/es/data-table/src/interface';
import type { TableColumn } from '~/components/ResultTable.vue';
import { useCloudExporter } from '~/composables/useCloudExporter';
import { castRecordsByHeaders, createWorkbook, Header, importFromXLSX } from '~/logic/data-io';
import type { AmazonItem, AmazonReview } from '~/logic/page-worker/types';
import { allItems, reviewItems } from '~/logic/storage';
import { allItems, reviewItems } from '~/logic/storages/amazon';
const message = useMessage();
const modal = useModal();
const cloudExporter = useCloudExporter();
const page = reactive({ current: 1, size: 10 });
const defaultFilter = {
keywords: undefined as string | undefined,
search: '',
@ -44,7 +42,7 @@ const onFilterReset = () => {
Object.assign(filter, defaultFilter);
};
const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
const columns: TableColumn[] = [
{
type: 'expand',
expandable: (row) => row.hasDetail,
@ -75,9 +73,6 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
{
title: '标题',
key: 'title',
render(row) {
return <div>{row.title}</div>;
},
},
{
title: '价格',
@ -138,20 +133,6 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
},
];
const itemView = computed<{ records: AmazonItem[]; pageCount: number; origin: AmazonItem[] }>(
() => {
const { current, size } = page;
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 };
},
);
const extraHeaders: Header[] = [
{ prop: 'link', label: '商品链接' },
{
@ -204,8 +185,9 @@ const getItemHeaders = () => {
.concat(extraHeaders) as Header[];
};
const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
const filteredData = computed(() => {
const { search, detailOnly, keywords } = filter;
let data = toRaw(allItems.value);
if (search.trim() !== '') {
data = data.filter((r) => {
return [r.title, r.asin, r.keywords].some((field) =>
@ -220,11 +202,11 @@ const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
data = data.filter((r) => r.keywords === keywords);
}
return data;
};
});
const handleLocalExport = async () => {
const itemHeaders = getItemHeaders();
const items = toRaw(itemView.value).origin;
const items = toRaw(filteredData.value);
const asins = new Set(items.map((e) => e.asin));
const reviews = toRaw(reviewItems.value)
.entries()
@ -248,7 +230,7 @@ const handleCloudExport = async () => {
message.warning('正在导出,请勿关闭当前页面!', { duration: 2000 });
const itemHeaders = getItemHeaders();
const items = toRaw(itemView.value).origin;
const items = toRaw(filteredData.value);
const asins = new Set(items.map((e) => e.asin));
const reviews = toRaw(reviewItems.value)
.entries()
@ -302,10 +284,10 @@ const handleClearData = async () => {
<template>
<div class="result-table">
<n-card class="result-content-container">
<result-table :columns="columns" :records="filteredData">
<template #header>
<n-space>
<div style="padding-right: 10px">结果数据表</div>
<div style="padding-right: 10px">Amazon数据</div>
<n-switch size="small" class="filter-switch" v-model:value="filter.detailOnly">
<template #checked> 详情 </template>
<template #unchecked> 全部</template>
@ -382,56 +364,15 @@ const handleClearData = async () => {
</control-strip>
</n-space>
</template>
<n-empty v-if="itemView.records.length === 0" size="huge">
<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
:row-key="(row: any) => `${row.asin}-${~~(Math.random() * 10000)}`"
:columns="columns.filter((col) => col.hidden !== true)"
:data="itemView.records"
/>
<div class="data-pagination">
<n-pagination
v-model:page="page.current"
v-model:page-size="page.size"
:page-count="itemView.pageCount"
:page-sizes="[5, 10, 15, 20, 25]"
show-size-picker
/>
</div>
</n-space>
</n-card>
</result-table>
</div>
</template>
<style lang="scss" scoped>
.result-content-container {
min-height: 100%;
:deep(.n-card__content:has(.n-empty)) {
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
}
}
:deep(.filter-switch) {
font-size: 15px;
}
.data-pagination {
display: flex;
flex-direction: row-reverse;
}
.exporter-menu {
display: flex;
flex-direction: column;

View File

@ -0,0 +1,220 @@
<script setup lang="tsx">
import type { TableColumn } from '~/components/ResultTable.vue';
import { useCloudExporter } from '~/composables/useCloudExporter';
import { castRecordsByHeaders, exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
import { allItems } from '~/logic/storages/homedepot';
const message = useMessage();
const cloudExporter = useCloudExporter();
const columns: TableColumn[] = [
{
title: 'OSMID',
key: 'OSMID',
},
{
title: '品牌名称',
key: 'brandName',
},
{
title: '型号信息',
key: 'modelInfo',
},
{
title: '标题',
key: 'title',
},
{
title: '价格',
key: 'price',
},
{
title: '评分',
key: 'rate',
minWidth: 60,
},
{
title: '评论数',
key: 'reviewCount',
minWidth: 75,
},
{
title: '商品链接',
key: 'link',
hidden: true,
},
{
title: '主图链接',
key: 'mainImageUrl',
hidden: true,
},
{
title: '操作',
key: 'action',
render(row: (typeof allItems.value)[0]) {
return (
<n-space>
<n-button
type="primary"
text
onClick={() => {
browser.tabs.create({
active: true,
url: row.link,
});
}}
>
前往
</n-button>
</n-space>
);
},
},
];
const filteredData = computed(() => {
return allItems.value;
});
const getItemHeaders = () => {
return columns
.filter((col: Record<string, any>) => col.key !== 'actions')
.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 }[],
)
.concat([]) as Header[];
};
const handleClearData = () => {
allItems.value = [];
};
const handleImport = async (file: File) => {
const itemHeaders = getItemHeaders();
allItems.value = await importFromXLSX<(typeof allItems.value)[0]>(file, { headers: itemHeaders });
message.info(`成功导入 ${file.name} 文件`);
};
const handleLocalExport = async () => {
const itemHeaders = getItemHeaders();
await exportToXLSX(filteredData.value, { headers: itemHeaders });
message.info(`导出完成`);
};
const handleCloudExport = async () => {
message.warning('正在导出,请勿关闭当前页面!', { duration: 2000 });
const itemHeaders = getItemHeaders();
const mappedData = await castRecordsByHeaders(filteredData.value, itemHeaders);
const fragments = [{ data: mappedData, imageColumn: '主图链接' }];
const filename = await cloudExporter.doExport(fragments);
filename && message.info(`导出完成`);
};
</script>
<template>
<div class="result-table">
<result-table :records="filteredData" :columns="columns">
<template #header>
<n-space>
<div style="padding-right: 10px">Homedepot数据</div>
</n-space>
</template>
<template #header-extra>
<control-strip round size="small" @clear="handleClearData" @import="handleImport">
<template #exporter>
<ul v-if="!cloudExporter.isRunning.value" class="exporter-menu">
<li @click="handleLocalExport">
<n-tooltip :delay="1000" placement="right">
<template #trigger>
<div class="menu-item">
<n-icon><lucide-sheet /></n-icon>
<span>本地导出</span>
</div>
</template>
不包含图片
</n-tooltip>
</li>
<li @click="handleCloudExport">
<n-tooltip :delay="1000" placement="right">
<template #trigger>
<div class="menu-item">
<n-icon><ic-outline-cloud /></n-icon>
<span>云端导出</span>
</div>
</template>
包含图片
</n-tooltip>
</li>
</ul>
<div v-else class="expoter-progress-panel">
<n-progress
type="circle"
:percentage="(cloudExporter.progress.current * 100) / cloudExporter.progress.total"
>
<span>
{{ cloudExporter.progress.current }}/{{ cloudExporter.progress.total }}
</span>
</n-progress>
<n-button @click="cloudExporter.stop()">停止</n-button>
</div>
</template>
</control-strip>
</template>
</result-table>
</div>
</template>
<style scoped lang="scss">
.result-table {
width: 100%;
}
.exporter-menu {
display: flex;
flex-direction: column;
align-items: center;
background: #fff;
padding: 0;
margin: 0;
list-style: none;
font-size: 15px;
li {
padding: 5px 10px;
cursor: pointer;
transition: background 0.15s;
color: #222;
user-select: none;
border-radius: 6px;
&:hover {
background: #f0f6fa;
color: #007bff;
}
.menu-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
}
}
}
.expoter-progress-panel {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
font-size: 18px;
padding: 10px;
gap: 15px;
cursor: wait;
}
</style>

View File

@ -1,14 +1,21 @@
import { Plugin } from 'vue';
import { createRouter, createMemoryHistory, RouteRecordRaw } from 'vue-router';
import {
createRouter,
createWebHashHistory,
createMemoryHistory,
RouteRecordRaw,
} from 'vue-router';
import { useAppContext } from '~/composables/useAppContext';
import { site } from '~/logic/storages/global';
const routeObj: Record<AppContext, RouteRecordRaw[]> = {
options: [
{ path: '/', redirect: '/amazon' },
{ path: '/', redirect: `/${site.value}` },
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.vue') },
],
sidepanel: [
{ path: '/', redirect: '/amazon' },
{ path: '/', redirect: `/${site.value}` },
{ path: '/amazon', component: () => import('~/sidepanel/views/AmazonSidepanel.vue') },
{ path: '/homedepot', component: () => import('~/sidepanel/views/HomedepotSidepanel.vue') },
],
@ -19,7 +26,7 @@ export const router: Plugin = {
const { appContext: context } = useAppContext();
const routes = routeObj[context];
const router = createRouter({
history: createMemoryHistory(),
history: context === 'sidepanel' ? createMemoryHistory() : createWebHashHistory(),
routes,
});
app.use(router);

View File

@ -2,6 +2,7 @@
import type { GlobalThemeOverrides } from 'naive-ui';
import { useRouter } from 'vue-router';
import { useCurrentUrl } from '~/composables/useCurrentUrl';
import { site } from '~/logic/storages/global';
const theme: GlobalThemeOverrides = {
common: {
@ -21,11 +22,14 @@ watch(currentUrl, (newVal) => {
switch (url.hostname) {
case 'www.amazon.com':
router.push('/amazon');
site.value = 'amazon';
break;
case 'www.homedepot.com':
router.push('/homedepot');
site.value = 'homedepot';
break;
default:
router.push(`/${site.value}`);
break;
}
}

View File

@ -1,19 +1,13 @@
<script setup lang="ts">
import type { Timeline } from '~/components/ProgressReport.vue';
import { useLongTask } from '~/composables/useLongTask';
import { amazon as pageWorker } from '~/logic/page-worker';
import { AmazonDetailItem } from '~/logic/page-worker/types';
import { detailAsinInput, detailItems } from '~/logic/storage';
import { detailAsinInput, detailItems } from '~/logic/storages/amazon';
const message = useMessage();
const timelines = ref<
{
type: 'default' | 'error' | 'success' | 'warning' | 'info';
title: string;
time: string;
content: string;
}[]
>([]);
const timelines = ref<Timeline[]>([]);
const { isRunning, startTask } = useLongTask();
@ -133,9 +127,9 @@ const handleInterrupt = () => {
<template>
<div class="detail-page-entry">
<header-title>Detail Page</header-title>
<header-title>Amazon Detail</header-title>
<div class="interative-section">
<asins-input v-model="detailAsinInput" :disabled="isRunning" ref="asin-input" />
<ids-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 />

View File

@ -1,8 +1,9 @@
<script lang="ts" setup>
import type { Timeline } from '~/components/ProgressReport.vue';
import { useLongTask } from '~/composables/useLongTask';
import { amazon as pageWorker } from '~/logic/page-worker';
import type { AmazonReview } from '~/logic/page-worker/types';
import { reviewAsinInput, reviewItems } from '~/logic/storage';
import { reviewAsinInput, reviewItems } from '~/logic/storages/amazon';
const { isRunning, startTask } = useLongTask();
@ -39,14 +40,7 @@ const asinInputRef = useTemplateRef('asin-input');
const message = useMessage();
const timelines = ref<
{
type: 'default' | 'error' | 'success' | 'warning' | 'info';
title: string;
time: string;
content: string;
}[]
>([]);
const timelines = ref<Timeline[]>([]);
const task = async () => {
const asinList = reviewAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
@ -93,9 +87,9 @@ const updateReviews = (params: { asin: string; reviews: AmazonReview[] }) => {
<template>
<div class="review-page-entry">
<header-title>Review Page</header-title>
<header-title>Amazon Review</header-title>
<div class="interative-section">
<asins-input v-model="reviewAsinInput" :disabled="isRunning" ref="asin-input" />
<ids-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 />

View File

@ -1,9 +1,10 @@
<script setup lang="ts">
import { keywordsList } from '~/logic/storage';
import { keywordsList } from '~/logic/storages/amazon';
import { amazon as pageWorker } from '~/logic/page-worker';
import { NButton } from 'naive-ui';
import { searchItems } from '~/logic/storage';
import { searchItems } from '~/logic/storages/amazon';
import { useLongTask } from '~/composables/useLongTask';
import type { Timeline } from '~/components/ProgressReport.vue';
const message = useMessage();
const { isRunning, startTask } = useLongTask();
@ -40,14 +41,7 @@ worker.channel.on('item-links-collected', ({ objs }) => {
});
//#endregion
const timelines = ref<
{
type: 'default' | 'error' | 'success' | 'warning' | 'info';
title: string;
time: string;
content: string;
}[]
>([]);
const timelines = ref<Timeline[]>([]);
const task = async () => {
const kws = unref(keywordsList);
@ -95,7 +89,7 @@ const handleInterrupt = () => {
<template>
<div class="search-page-entry">
<header-title>Search Page</header-title>
<header-title>Amazon Search</header-title>
<div class="interactive-section">
<n-dynamic-input
:disabled="isRunning"

View File

@ -57,7 +57,7 @@ const running = ref(false);
<style scoped lang="scss">
.side-panel {
width: 100%;
width: 100vw;
height: 100%;
display: flex;
flex-direction: column;

View File

@ -1,25 +1,76 @@
<script setup lang="ts">
import type { Timeline } from '~/components/ProgressReport.vue';
import { useLongTask } from '~/composables/useLongTask';
import { homedepot } from '~/logic/page-worker';
import { detailItems } from '~/logic/storages/homedepot';
const inputText = ref('');
const output = ref(undefined);
const { isRunning, startTask } = useLongTask();
const worker = homedepot.useHomedepotWorker();
worker.channel.on('detail-item-collected', ({ item }) => {
output.value = item;
timelines.value.push({
type: 'success',
title: `成功`,
content: `成功获取到${item.OSMID}的商品信息`,
time: new Date().toLocaleString(),
});
detailItems.value.set(item.OSMID, item);
});
const handleStart = () => {
worker.runDetailPageTask(inputText.value.split('\n').filter((id) => /\d+/.exec(id)));
const timelines = ref<Timeline[]>([]);
const handleStart = () =>
startTask(async () => {
timelines.value.push({
type: 'info',
title: `开始`,
content: '任务开始',
time: new Date().toLocaleString(),
});
await worker.runDetailPageTask(inputText.value.split('\n').filter((id) => /\d+/.exec(id)));
timelines.value.push({
type: 'info',
title: `结束`,
content: '任务完成',
time: new Date().toLocaleString(),
});
});
const handleInterrupt = () => {
worker.stop();
};
</script>
<template>
<div class="homedepot-sidepanel">
<h3>Hello World!</h3>
<n-input type="textarea" v-model:value="inputText" />
<n-button @click="handleStart">Test!</n-button>
<n-code word-wrap :code="JSON.stringify(output)" />
<header-title>Homedepot</header-title>
<div class="interative-section">
<ids-input
v-model="inputText"
:disabled="isRunning"
ref="asin-input"
:match-pattern="/^\d+(\n\d+)*\n?$/g"
placeholder="输入OSMID"
validate-message="请输入格式正确的OSMID"
/>
<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>
@ -28,5 +79,30 @@ const handleStart = () => {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
}
.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;
}
.progress-report {
margin-top: 10px;
width: 95%;
}
</style>

View File

@ -3,6 +3,9 @@ body,
#app {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
}
.btn {