mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-02-05 23:10:28 +08:00
Update
This commit is contained in:
parent
64b41f5841
commit
6645cd2b75
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "azon-seeker",
|
"name": "azon-seeker",
|
||||||
"displayName": "Azon Seeker",
|
"displayName": "Azon Seeker",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Starter modify by honestfox101",
|
"description": "Starter modify by honestfox101",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,26 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AmazonDetailItem } from '~/logic/page-worker/types';
|
import type { AmazonDetailItem } from '~/logic/page-worker/types';
|
||||||
import { reviewItems } from '~/logic/storage';
|
|
||||||
import ReviewPreview from './ReviewPreview.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ model: AmazonDetailItem }>();
|
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,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -50,25 +31,6 @@ const handleLoadMore = () => {
|
|||||||
<n-descriptions-item label="图片链接" :span="4">
|
<n-descriptions-item label="图片链接" :span="4">
|
||||||
<image-link v-for="link in model.imageUrls" :url="link" />
|
<image-link v-for="link in model.imageUrls" :url="link" />
|
||||||
</n-descriptions-item>
|
</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>
|
</n-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -2,12 +2,18 @@
|
|||||||
import { useFileDialog } from '@vueuse/core';
|
import { useFileDialog } from '@vueuse/core';
|
||||||
import type { FormItemRule } from 'naive-ui';
|
import type { FormItemRule } from 'naive-ui';
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
matchPattern?: RegExp;
|
||||||
|
placeholder?: string;
|
||||||
|
validateMessage?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
disabled: false,
|
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 = {
|
const formItemRule: FormItemRule = {
|
||||||
required: true,
|
required: true,
|
||||||
trigger: ['submit', 'blur'],
|
trigger: ['submit', 'blur'],
|
||||||
message: '请输入格式正确的ASIN',
|
message: props.validateMessage,
|
||||||
validator: () => {
|
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 });
|
const fileDialog = useFileDialog({ accept: '.txt', multiple: false });
|
||||||
fileDialog.onChange((fileList) => {
|
fileDialog.onChange((fileList) => {
|
||||||
const file = fileList?.item(0);
|
const file = fileList?.item(0);
|
||||||
file && handleImportAsin(file);
|
file && handleImportIds(file);
|
||||||
fileDialog.reset();
|
fileDialog.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleImportAsin = (file: File) => {
|
const handleImportIds = (file: File) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const content = e.target?.result;
|
const content = e.target?.result;
|
||||||
@ -43,10 +49,10 @@ const handleImportAsin = (file: File) => {
|
|||||||
reader.readAsText(file, 'utf-8');
|
reader.readAsText(file, 'utf-8');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportAsin = () => {
|
const handleExportIds = () => {
|
||||||
const blob = new Blob([modelValue.value], { type: 'text/plain' });
|
const blob = new Blob([modelValue.value], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const filename = `asin-${new Date().toISOString()}.txt`;
|
const filename = `${new Date().toISOString()}.txt`;
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
@ -75,7 +81,7 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="asin-input">
|
<div class="ids-input">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button @click="fileDialog.open()" :disabled="disabled" round size="small">
|
<n-button @click="fileDialog.open()" :disabled="disabled" round size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@ -83,7 +89,7 @@ defineExpose({
|
|||||||
</template>
|
</template>
|
||||||
导入
|
导入
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button :disabled="disabled" @click="handleExportAsin" round size="small">
|
<n-button :disabled="disabled" @click="handleExportIds" round size="small">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ion-arrow-up-right-box-outline />
|
<ion-arrow-up-right-box-outline />
|
||||||
</template>
|
</template>
|
||||||
@ -95,7 +101,7 @@ defineExpose({
|
|||||||
<n-input
|
<n-input
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
v-model:value="modelValue"
|
v-model:value="modelValue"
|
||||||
placeholder="输入ASINs"
|
:placeholder="placeholder"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
size="large"
|
size="large"
|
||||||
/>
|
/>
|
||||||
@ -1,11 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
export type Timeline = {
|
||||||
|
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
||||||
|
title: string;
|
||||||
|
time: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
timelines: {
|
timelines: Timeline[];
|
||||||
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
|
||||||
title: string;
|
|
||||||
time: string;
|
|
||||||
content: string;
|
|
||||||
}[];
|
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
96
src/components/ResultTable.vue
Normal file
96
src/components/ResultTable.vue
Normal 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>
|
||||||
@ -2,7 +2,7 @@
|
|||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
||||||
import type { AmazonReview } from '~/logic/page-worker/types';
|
import type { AmazonReview } from '~/logic/page-worker/types';
|
||||||
import { reviewItems } from '~/logic/storage';
|
import { reviewItems } from '~/logic/storages/amazon';
|
||||||
|
|
||||||
const props = defineProps<{ asin: string }>();
|
const props = defineProps<{ asin: string }>();
|
||||||
|
|
||||||
|
|||||||
@ -13,4 +13,4 @@ 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' : '127.0.0.1:8000';
|
export const remoteHost = __DEV__ ? '127.0.0.1:8000' : '47.251.4.191:8000';
|
||||||
|
|||||||
1
src/logic/page-worker/types.d.ts
vendored
1
src/logic/page-worker/types.d.ts
vendored
@ -121,7 +121,6 @@ type HomedepotDetailItem = {
|
|||||||
title: string;
|
title: string;
|
||||||
price: string;
|
price: string;
|
||||||
rate?: string;
|
rate?: string;
|
||||||
innerText: string;
|
|
||||||
reviewCount?: number;
|
reviewCount?: number;
|
||||||
mainImageUrl: string;
|
mainImageUrl: string;
|
||||||
modelInfo?: string;
|
modelInfo?: string;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type {
|
|||||||
AmazonItem,
|
AmazonItem,
|
||||||
AmazonReview,
|
AmazonReview,
|
||||||
AmazonSearchItem,
|
AmazonSearchItem,
|
||||||
} from './page-worker/types';
|
} from '~/logic/page-worker/types';
|
||||||
|
|
||||||
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
export const keywordsList = useWebExtensionStorage<string[]>('keywordsList', ['']);
|
||||||
|
|
||||||
3
src/logic/storages/global.ts
Normal file
3
src/logic/storages/global.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||||
|
|
||||||
|
export const site = useWebExtensionStorage<'amazon' | 'homedepot'>('site', 'amazon');
|
||||||
22
src/logic/storages/homedepot.ts
Normal file
22
src/logic/storages/homedepot.ts
Normal 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());
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -226,17 +226,22 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const script = document.evaluate(
|
let script = document.evaluate(
|
||||||
`//script[starts-with(text(), "\nP.when(\'A\').register")]`,
|
`//script[starts-with(text(), "\nP.when(\'A\').register") or contains(text(), "\nP.when('A').register")]`,
|
||||||
document,
|
document,
|
||||||
null,
|
null,
|
||||||
XPathResult.STRING_TYPE,
|
XPathResult.STRING_TYPE,
|
||||||
).stringValue;
|
).stringValue;
|
||||||
const urls = [
|
const extractUrls = (pattern: RegExp) =>
|
||||||
...script.matchAll(
|
Array.from(script.matchAll(pattern)).map((e) => e[0]);
|
||||||
/(?<="hiRes":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
|
let urls = extractUrls(
|
||||||
),
|
/(?<="hiRes":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
|
||||||
].map((e) => e[0]);
|
);
|
||||||
|
if (urls.length === 0) {
|
||||||
|
urls = extractUrls(
|
||||||
|
/(?<="large":")https:\/\/m.media-amazon.com\/images\/I\/[\w\d\.\-+]+(?=")/g,
|
||||||
|
);
|
||||||
|
}
|
||||||
return urls;
|
return urls;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<span></span>
|
<span>
|
||||||
<h1 class="header-title">采集结果</h1>
|
<n-popselect v-model:value="site" :options="options" placement="bottom-start">
|
||||||
<span></span>
|
<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>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<router-view />
|
<router-view />
|
||||||
@ -18,6 +43,8 @@ header {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
@ -28,9 +55,7 @@ main {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.result-table {
|
height: 90vh;
|
||||||
height: 90vh;
|
width: 95vw;
|
||||||
width: 95vw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import { NButton, NSpace } from 'naive-ui';
|
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 { useCloudExporter } from '~/composables/useCloudExporter';
|
||||||
import { castRecordsByHeaders, createWorkbook, Header, importFromXLSX } from '~/logic/data-io';
|
import { castRecordsByHeaders, createWorkbook, Header, importFromXLSX } from '~/logic/data-io';
|
||||||
import type { AmazonItem, AmazonReview } from '~/logic/page-worker/types';
|
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 message = useMessage();
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
const cloudExporter = useCloudExporter();
|
const cloudExporter = useCloudExporter();
|
||||||
|
|
||||||
const page = reactive({ current: 1, size: 10 });
|
|
||||||
|
|
||||||
const defaultFilter = {
|
const defaultFilter = {
|
||||||
keywords: undefined as string | undefined,
|
keywords: undefined as string | undefined,
|
||||||
search: '',
|
search: '',
|
||||||
@ -44,7 +42,7 @@ const onFilterReset = () => {
|
|||||||
Object.assign(filter, defaultFilter);
|
Object.assign(filter, defaultFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
const columns: TableColumn[] = [
|
||||||
{
|
{
|
||||||
type: 'expand',
|
type: 'expand',
|
||||||
expandable: (row) => row.hasDetail,
|
expandable: (row) => row.hasDetail,
|
||||||
@ -75,9 +73,6 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
|||||||
{
|
{
|
||||||
title: '标题',
|
title: '标题',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
render(row) {
|
|
||||||
return <div>{row.title}</div>;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '价格',
|
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[] = [
|
const extraHeaders: Header[] = [
|
||||||
{ prop: 'link', label: '商品链接' },
|
{ prop: 'link', label: '商品链接' },
|
||||||
{
|
{
|
||||||
@ -204,8 +185,9 @@ const getItemHeaders = () => {
|
|||||||
.concat(extraHeaders) as Header[];
|
.concat(extraHeaders) as Header[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
|
const filteredData = computed(() => {
|
||||||
const { search, detailOnly, keywords } = filter;
|
const { search, detailOnly, keywords } = filter;
|
||||||
|
let data = toRaw(allItems.value);
|
||||||
if (search.trim() !== '') {
|
if (search.trim() !== '') {
|
||||||
data = data.filter((r) => {
|
data = data.filter((r) => {
|
||||||
return [r.title, r.asin, r.keywords].some((field) =>
|
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);
|
data = data.filter((r) => r.keywords === keywords);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
});
|
||||||
|
|
||||||
const handleLocalExport = async () => {
|
const handleLocalExport = async () => {
|
||||||
const itemHeaders = getItemHeaders();
|
const itemHeaders = getItemHeaders();
|
||||||
const items = toRaw(itemView.value).origin;
|
const items = toRaw(filteredData.value);
|
||||||
const asins = new Set(items.map((e) => e.asin));
|
const asins = new Set(items.map((e) => e.asin));
|
||||||
const reviews = toRaw(reviewItems.value)
|
const reviews = toRaw(reviewItems.value)
|
||||||
.entries()
|
.entries()
|
||||||
@ -248,7 +230,7 @@ const handleCloudExport = async () => {
|
|||||||
message.warning('正在导出,请勿关闭当前页面!', { duration: 2000 });
|
message.warning('正在导出,请勿关闭当前页面!', { duration: 2000 });
|
||||||
|
|
||||||
const itemHeaders = getItemHeaders();
|
const itemHeaders = getItemHeaders();
|
||||||
const items = toRaw(itemView.value).origin;
|
const items = toRaw(filteredData.value);
|
||||||
const asins = new Set(items.map((e) => e.asin));
|
const asins = new Set(items.map((e) => e.asin));
|
||||||
const reviews = toRaw(reviewItems.value)
|
const reviews = toRaw(reviewItems.value)
|
||||||
.entries()
|
.entries()
|
||||||
@ -302,10 +284,10 @@ const handleClearData = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="result-table">
|
<div class="result-table">
|
||||||
<n-card class="result-content-container">
|
<result-table :columns="columns" :records="filteredData">
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-space>
|
<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">
|
<n-switch size="small" class="filter-switch" v-model:value="filter.detailOnly">
|
||||||
<template #checked> 详情 </template>
|
<template #checked> 详情 </template>
|
||||||
<template #unchecked> 全部</template>
|
<template #unchecked> 全部</template>
|
||||||
@ -382,56 +364,15 @@ const handleClearData = async () => {
|
|||||||
</control-strip>
|
</control-strip>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
<n-empty v-if="itemView.records.length === 0" size="huge">
|
</result-table>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<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) {
|
:deep(.filter-switch) {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-pagination {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exporter-menu {
|
.exporter-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
220
src/options/views/HomedepotResultTable.vue
Normal file
220
src/options/views/HomedepotResultTable.vue
Normal 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>
|
||||||
@ -1,14 +1,21 @@
|
|||||||
import { Plugin } from 'vue';
|
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 { useAppContext } from '~/composables/useAppContext';
|
||||||
|
import { site } from '~/logic/storages/global';
|
||||||
|
|
||||||
const routeObj: Record<AppContext, RouteRecordRaw[]> = {
|
const routeObj: Record<AppContext, RouteRecordRaw[]> = {
|
||||||
options: [
|
options: [
|
||||||
{ path: '/', redirect: '/amazon' },
|
{ path: '/', redirect: `/${site.value}` },
|
||||||
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
|
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
|
||||||
|
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.vue') },
|
||||||
],
|
],
|
||||||
sidepanel: [
|
sidepanel: [
|
||||||
{ path: '/', redirect: '/amazon' },
|
{ path: '/', redirect: `/${site.value}` },
|
||||||
{ path: '/amazon', component: () => import('~/sidepanel/views/AmazonSidepanel.vue') },
|
{ path: '/amazon', component: () => import('~/sidepanel/views/AmazonSidepanel.vue') },
|
||||||
{ path: '/homedepot', component: () => import('~/sidepanel/views/HomedepotSidepanel.vue') },
|
{ path: '/homedepot', component: () => import('~/sidepanel/views/HomedepotSidepanel.vue') },
|
||||||
],
|
],
|
||||||
@ -19,7 +26,7 @@ export const router: Plugin = {
|
|||||||
const { appContext: context } = useAppContext();
|
const { appContext: context } = useAppContext();
|
||||||
const routes = routeObj[context];
|
const routes = routeObj[context];
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createMemoryHistory(),
|
history: context === 'sidepanel' ? createMemoryHistory() : createWebHashHistory(),
|
||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useCurrentUrl } from '~/composables/useCurrentUrl';
|
import { useCurrentUrl } from '~/composables/useCurrentUrl';
|
||||||
|
import { site } from '~/logic/storages/global';
|
||||||
|
|
||||||
const theme: GlobalThemeOverrides = {
|
const theme: GlobalThemeOverrides = {
|
||||||
common: {
|
common: {
|
||||||
@ -21,11 +22,14 @@ watch(currentUrl, (newVal) => {
|
|||||||
switch (url.hostname) {
|
switch (url.hostname) {
|
||||||
case 'www.amazon.com':
|
case 'www.amazon.com':
|
||||||
router.push('/amazon');
|
router.push('/amazon');
|
||||||
|
site.value = 'amazon';
|
||||||
break;
|
break;
|
||||||
case 'www.homedepot.com':
|
case 'www.homedepot.com':
|
||||||
router.push('/homedepot');
|
router.push('/homedepot');
|
||||||
|
site.value = 'homedepot';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
router.push(`/${site.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { useLongTask } from '~/composables/useLongTask';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import { amazon as pageWorker } from '~/logic/page-worker';
|
import { amazon as pageWorker } from '~/logic/page-worker';
|
||||||
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
import { AmazonDetailItem } from '~/logic/page-worker/types';
|
||||||
import { detailAsinInput, detailItems } from '~/logic/storage';
|
import { detailAsinInput, detailItems } from '~/logic/storages/amazon';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const timelines = ref<
|
const timelines = ref<Timeline[]>([]);
|
||||||
{
|
|
||||||
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
|
||||||
title: string;
|
|
||||||
time: string;
|
|
||||||
content: string;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const { isRunning, startTask } = useLongTask();
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
@ -133,9 +127,9 @@ const handleInterrupt = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="detail-page-entry">
|
<div class="detail-page-entry">
|
||||||
<header-title>Detail Page</header-title>
|
<header-title>Amazon Detail</header-title>
|
||||||
<div class="interative-section">
|
<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">
|
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
import { useLongTask } from '~/composables/useLongTask';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import { amazon as pageWorker } from '~/logic/page-worker';
|
import { amazon as pageWorker } from '~/logic/page-worker';
|
||||||
import type { AmazonReview } from '~/logic/page-worker/types';
|
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();
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
@ -39,14 +40,7 @@ const asinInputRef = useTemplateRef('asin-input');
|
|||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const timelines = ref<
|
const timelines = ref<Timeline[]>([]);
|
||||||
{
|
|
||||||
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
|
||||||
title: string;
|
|
||||||
time: string;
|
|
||||||
content: string;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const task = async () => {
|
const task = async () => {
|
||||||
const asinList = reviewAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
const asinList = reviewAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
|
||||||
@ -93,9 +87,9 @@ const updateReviews = (params: { asin: string; reviews: AmazonReview[] }) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="review-page-entry">
|
<div class="review-page-entry">
|
||||||
<header-title>Review Page</header-title>
|
<header-title>Amazon Review</header-title>
|
||||||
<div class="interative-section">
|
<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">
|
<n-button v-if="!isRunning" round size="large" type="primary" @click="handleStart">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { keywordsList } from '~/logic/storage';
|
import { keywordsList } from '~/logic/storages/amazon';
|
||||||
import { amazon as pageWorker } from '~/logic/page-worker';
|
import { amazon as pageWorker } from '~/logic/page-worker';
|
||||||
import { NButton } from 'naive-ui';
|
import { NButton } from 'naive-ui';
|
||||||
import { searchItems } from '~/logic/storage';
|
import { searchItems } from '~/logic/storages/amazon';
|
||||||
import { useLongTask } from '~/composables/useLongTask';
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const { isRunning, startTask } = useLongTask();
|
const { isRunning, startTask } = useLongTask();
|
||||||
@ -40,14 +41,7 @@ worker.channel.on('item-links-collected', ({ objs }) => {
|
|||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const timelines = ref<
|
const timelines = ref<Timeline[]>([]);
|
||||||
{
|
|
||||||
type: 'default' | 'error' | 'success' | 'warning' | 'info';
|
|
||||||
title: string;
|
|
||||||
time: string;
|
|
||||||
content: string;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
const task = async () => {
|
const task = async () => {
|
||||||
const kws = unref(keywordsList);
|
const kws = unref(keywordsList);
|
||||||
@ -95,7 +89,7 @@ const handleInterrupt = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="search-page-entry">
|
<div class="search-page-entry">
|
||||||
<header-title>Search Page</header-title>
|
<header-title>Amazon Search</header-title>
|
||||||
<div class="interactive-section">
|
<div class="interactive-section">
|
||||||
<n-dynamic-input
|
<n-dynamic-input
|
||||||
:disabled="isRunning"
|
:disabled="isRunning"
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const running = ref(false);
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.side-panel {
|
.side-panel {
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -1,25 +1,76 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Timeline } from '~/components/ProgressReport.vue';
|
||||||
|
import { useLongTask } from '~/composables/useLongTask';
|
||||||
import { homedepot } from '~/logic/page-worker';
|
import { homedepot } from '~/logic/page-worker';
|
||||||
|
import { detailItems } from '~/logic/storages/homedepot';
|
||||||
|
|
||||||
const inputText = ref('');
|
const inputText = ref('');
|
||||||
const output = ref(undefined);
|
const { isRunning, startTask } = useLongTask();
|
||||||
|
|
||||||
const worker = homedepot.useHomedepotWorker();
|
const worker = homedepot.useHomedepotWorker();
|
||||||
worker.channel.on('detail-item-collected', ({ item }) => {
|
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 = () => {
|
const timelines = ref<Timeline[]>([]);
|
||||||
worker.runDetailPageTask(inputText.value.split('\n').filter((id) => /\d+/.exec(id)));
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="homedepot-sidepanel">
|
<div class="homedepot-sidepanel">
|
||||||
<h3>Hello World!</h3>
|
<header-title>Homedepot</header-title>
|
||||||
<n-input type="textarea" v-model:value="inputText" />
|
<div class="interative-section">
|
||||||
<n-button @click="handleStart">Test!</n-button>
|
<ids-input
|
||||||
<n-code word-wrap :code="JSON.stringify(output)" />
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -28,5 +79,30 @@ const handleStart = () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
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>
|
</style>
|
||||||
|
|||||||
@ -3,6 +3,9 @@ body,
|
|||||||
#app {
|
#app {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user