mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-01-19 13:13:22 +08:00
Update
This commit is contained in:
parent
6b0aef185f
commit
219b34661a
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "azon-seeker",
|
"name": "azon-seeker",
|
||||||
"displayName": "Azon Seeker",
|
"displayName": "Azon Seeker",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Starter modify by honestfox101",
|
"description": "Starter modify by honestfox101",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -45,6 +45,7 @@
|
|||||||
"esno": "^4.8.0",
|
"esno": "^4.8.0",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
|
"github-markdown-css": "^5.8.1",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"kolorist": "^1.8.0",
|
"kolorist": "^1.8.0",
|
||||||
@ -59,6 +60,7 @@
|
|||||||
"unplugin-icons": "^22.1.0",
|
"unplugin-icons": "^22.1.0",
|
||||||
"unplugin-vue-components": "^28.8.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"vite": "^7.0.2",
|
"vite": "^7.0.2",
|
||||||
|
"vite-plugin-md": "^0.21.5",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-demi": "^0.14.10",
|
"vue-demi": "^0.14.10",
|
||||||
|
|||||||
1137
pnpm-lock.yaml
generated
1137
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Size } from 'naive-ui/es/button/src/interface';
|
import { Size } from 'naive-ui/es/button/src/interface';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ disabled?: boolean; round?: boolean; size?: Size }>(), {});
|
withDefaults(defineProps<{ disabled?: boolean; round?: boolean; size?: Size }>(), {});
|
||||||
|
|
||||||
const emit = defineEmits<{ click: [ev: MouseEvent] }>();
|
const emit = defineEmits<{ click: [ev: MouseEvent] }>();
|
||||||
</script>
|
</script>
|
||||||
@ -48,6 +48,10 @@ const emit = defineEmits<{ click: [ev: MouseEvent] }>();
|
|||||||
|
|
||||||
> button:first-of-type {
|
> button:first-of-type {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
width: 88%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> button:last-of-type {
|
> button:last-of-type {
|
||||||
@ -61,6 +65,10 @@ const emit = defineEmits<{ click: [ev: MouseEvent] }>();
|
|||||||
&:has(> button:last-of-type:hover) > button:first-of-type {
|
&:has(> button:last-of-type:hover) > button:first-of-type {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:has(> button:first-of-type:hover) > button:last-of-type {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
## Components
|
|
||||||
|
|
||||||
Components in this dir will be auto-registered and on-demand, powered by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components).
|
|
||||||
|
|
||||||
Components can be shared in all views.
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
|
|
||||||
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
|
|
||||||
|
|
||||||
It will only bundle the icons you use. Check out [unplugin-icons](https://github.com/unplugin/unplugin-icons) for more details.
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useElementBounding, useParentElement } from '@vueuse/core';
|
||||||
import type { EllipsisProps } from 'naive-ui';
|
import type { EllipsisProps } from 'naive-ui';
|
||||||
|
|
||||||
export type TableColumn =
|
export type TableColumn =
|
||||||
@ -17,6 +18,17 @@ export type TableColumn =
|
|||||||
renderExpand: (row: any) => VNode;
|
renderExpand: (row: any) => VNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parentEl = useParentElement();
|
||||||
|
const currentRootEl = useTemplateRef('result-table');
|
||||||
|
const { height: parentHeight } = useElementBounding(parentEl);
|
||||||
|
const tableHeight = computed(() => {
|
||||||
|
let contentHeight = 0;
|
||||||
|
currentRootEl.value?.childNodes.forEach((element) => {
|
||||||
|
contentHeight += (element as Element).getBoundingClientRect().height;
|
||||||
|
});
|
||||||
|
return ~~Math.max(parentHeight.value, contentHeight);
|
||||||
|
});
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
records: Record<string, unknown>[];
|
records: Record<string, unknown>[];
|
||||||
@ -46,7 +58,7 @@ function generateUUID() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="result-table">
|
<div class="result-table" ref="result-table" :style="{ height: `${tableHeight}px` }">
|
||||||
<n-card class="result-content-container">
|
<n-card class="result-content-container">
|
||||||
<template #header><slot name="header" /></template>
|
<template #header><slot name="header" /></template>
|
||||||
<template #header-extra><slot name="header-extra" /></template>
|
<template #header-extra><slot name="header-extra" /></template>
|
||||||
@ -81,13 +93,8 @@ function generateUUID() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.result-table {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-content-container {
|
.result-content-container {
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
:deep(.n-card__content:has(.n-empty)) {
|
:deep(.n-card__content:has(.n-empty)) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -8,8 +8,8 @@ defineProps<{ model: AmazonDetailItem }>();
|
|||||||
<n-descriptions-item label="ASIN">
|
<n-descriptions-item label="ASIN">
|
||||||
{{ model.asin }}
|
{{ model.asin }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="销量信息">
|
<n-descriptions-item label="获取日期">
|
||||||
{{ model.boughtInfo || '-' }}
|
{{ model.timestamp }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="评价">
|
<n-descriptions-item label="评价">
|
||||||
{{ model.rating || '-' }}
|
{{ model.rating || '-' }}
|
||||||
@ -17,9 +17,12 @@ defineProps<{ model: AmazonDetailItem }>();
|
|||||||
<n-descriptions-item label="评论数">
|
<n-descriptions-item label="评论数">
|
||||||
{{ model.ratingCount || '-' }}
|
{{ model.ratingCount || '-' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="分类信息" :span="3">
|
<n-descriptions-item label="分类信息" :span="2">
|
||||||
{{ model.categories || '-' }}
|
{{ model.categories || '-' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="销量信息">
|
||||||
|
{{ model.boughtInfo || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="上架日期">
|
<n-descriptions-item label="上架日期">
|
||||||
{{ model.availableDate || '-' }}
|
{{ model.availableDate || '-' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
@ -36,13 +39,43 @@ defineProps<{ model: AmazonDetailItem }>();
|
|||||||
{{ model.category2?.rank || '-' }}
|
{{ model.category2?.rank || '-' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="图片链接" :span="4">
|
<n-descriptions-item label="图片链接" :span="4">
|
||||||
<image-link v-for="link in model.imageUrls" :url="link" />
|
<ul>
|
||||||
|
<li v-for="link in model.imageUrls">
|
||||||
|
<image-link :url="link" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item v-if="model.aplus" label="A+" :span="2">
|
<n-descriptions-item v-if="model.abouts && model.abouts.length" label="About" :span="4">
|
||||||
<image-link :url="model.aplus" />
|
<ul>
|
||||||
|
<li v-for="(about, idx) in model.abouts" :key="idx">{{ about }}</li>
|
||||||
|
</ul>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="获取日期" :span="2">
|
<n-descriptions-item label="A+" :span="4">
|
||||||
{{ model.timestamp }}
|
<ul v-if="model.aplus">
|
||||||
|
<li><image-link :url="model.aplus" /></li>
|
||||||
|
</ul>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="发货快递">
|
||||||
|
{{ model.shipFrom || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="卖家">
|
||||||
|
{{ model.soldBy || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="品牌名称">
|
||||||
|
{{ model.brand || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="口味/风味">
|
||||||
|
{{ model.flavor || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="单位数量">
|
||||||
|
{{ model.unitCount || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="形态/剂型">
|
||||||
|
{{ model.itemForm || '-' }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="商品尺寸" :span="2">
|
||||||
|
{{ model.productDimensions || '-' }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,24 +5,48 @@ import { useRouter } from 'vue-router';
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const headerText = ref('采集结果');
|
||||||
|
const version = __VERSION__;
|
||||||
|
const opt = ref<string | undefined>(`/${site.value}`);
|
||||||
|
|
||||||
const options: { label: string; value: string }[] = [
|
const options: { label: string; value: string }[] = [
|
||||||
{ label: 'Amazon', value: 'amazon' },
|
{ label: 'Amazon', value: '/amazon' },
|
||||||
{ label: 'Amazon Review', value: 'amazon-reviews' },
|
{ label: 'Amazon Review', value: '/amazon-reviews' },
|
||||||
{ label: 'Homedepot', value: 'homedepot' },
|
{ label: 'Homedepot', value: '/homedepot' },
|
||||||
];
|
];
|
||||||
|
|
||||||
watch(site, (val) => {
|
watch(opt, (val) => {
|
||||||
router.push(val);
|
if (val) {
|
||||||
|
router.push(val);
|
||||||
|
headerText.value = '采集结果';
|
||||||
|
}
|
||||||
|
switch (val) {
|
||||||
|
case '/amazon':
|
||||||
|
case '/amazon-reviews':
|
||||||
|
site.value = 'amazon';
|
||||||
|
break;
|
||||||
|
case '/homedepot':
|
||||||
|
site.value = 'homedepot';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleHelpButtonClick = () => {
|
||||||
|
opt.value = undefined;
|
||||||
|
router.push('/help');
|
||||||
|
headerText.value = '帮助';
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<span>
|
<span>
|
||||||
<n-popselect v-model:value="site" :options="options" placement="bottom-start">
|
<n-popselect v-model:value="opt" :options="options" placement="bottom-start">
|
||||||
<n-button>
|
<n-button>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon size="20">
|
<n-icon :size="20">
|
||||||
<garden-menu-fill-12 />
|
<garden-menu-fill-12 />
|
||||||
</n-icon>
|
</n-icon>
|
||||||
</template>
|
</template>
|
||||||
@ -30,13 +54,27 @@ watch(site, (val) => {
|
|||||||
</n-popselect>
|
</n-popselect>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<h1 class="header-title">采集结果</h1>
|
<h1 class="header-title">{{ headerText }}</h1>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<n-button @click="handleHelpButtonClick" round>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :size="20">
|
||||||
|
<ion-help />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
</span>
|
</span>
|
||||||
<span> </span>
|
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
<footer>
|
||||||
|
<span
|
||||||
|
>Azon Seeker v{{ version }} powered by
|
||||||
|
<a href="https://github.com/honestfox101">@HonestFox101</a></span
|
||||||
|
>
|
||||||
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -49,6 +87,8 @@ header {
|
|||||||
.header-title {
|
.header-title {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
height: 8vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@ -56,7 +96,27 @@ main {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
height: 90vh;
|
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
|
min-height: 87vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
color: rgba(128, 128, 128, 0.68);
|
||||||
|
height: 5vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(128, 128, 128, 0.68);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1a73e8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { setupApp } from '~/logic/common-setup';
|
import { setupApp } from '~/logic/common-setup';
|
||||||
import '../styles';
|
import '~/styles';
|
||||||
|
|
||||||
// This is the options page of the extension.
|
// This is the options page of the extension.
|
||||||
Object.assign(self, { appContext: 'options' });
|
Object.assign(self, { appContext: 'options' });
|
||||||
|
|||||||
@ -150,6 +150,19 @@ const extraHeaders: Header<AmazonItem>[] = [
|
|||||||
prop: 'aplus',
|
prop: 'aplus',
|
||||||
label: 'A+截图',
|
label: 'A+截图',
|
||||||
},
|
},
|
||||||
|
{ prop: 'shipFrom', label: '发货快递' },
|
||||||
|
{ prop: 'soldBy', label: '卖家' },
|
||||||
|
{ prop: 'brand', label: '品牌名称' },
|
||||||
|
{ prop: 'flavor', label: '商品口味' },
|
||||||
|
{ prop: 'unitCount', label: '商品单位数量' },
|
||||||
|
{ prop: 'itemForm', label: '商品形态' },
|
||||||
|
{ prop: 'productDimensions', label: '商品尺寸' },
|
||||||
|
{
|
||||||
|
prop: 'abouts',
|
||||||
|
label: '关于',
|
||||||
|
formatOutputValue: (val?: string[]) => val?.join('\n'),
|
||||||
|
parseImportValue: (val?: string) => val?.split('\n'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const getItemHeaders = () => {
|
const getItemHeaders = () => {
|
||||||
@ -223,45 +236,39 @@ const handleClearData = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="result-table">
|
<result-table :columns="columns" :records="filteredData">
|
||||||
<result-table :columns="columns" :records="filteredData">
|
<template #header>
|
||||||
<template #header>
|
<n-space align="center">
|
||||||
<n-space align="center">
|
<h3 class="header-text">Amazon Items</h3>
|
||||||
<h3 class="header-text">Amazon Items</h3>
|
<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>
|
</n-switch>
|
||||||
</n-switch>
|
</n-space>
|
||||||
</n-space>
|
</template>
|
||||||
</template>
|
<template #header-extra>
|
||||||
<template #header-extra>
|
<n-space size="small">
|
||||||
<n-space size="small">
|
<n-input
|
||||||
<n-input
|
v-model:value="filter.search"
|
||||||
v-model:value="filter.search"
|
placeholder="输入文本过滤结果"
|
||||||
placeholder="输入文本过滤结果"
|
round
|
||||||
round
|
clearable
|
||||||
clearable
|
style="min-width: 230px"
|
||||||
style="min-width: 230px"
|
/>
|
||||||
/>
|
<control-strip round @clear="handleClearData" @import="handleImport">
|
||||||
<control-strip round @clear="handleClearData" @import="handleImport">
|
<template #exporter>
|
||||||
<template #exporter>
|
<export-panel @export-file="handleExport" />
|
||||||
<export-panel @export-file="handleExport" />
|
</template>
|
||||||
</template>
|
<template #filter>
|
||||||
<template #filter>
|
<div class="filter-section">
|
||||||
<div class="filter-section">
|
<div class="filter-title">筛选器</div>
|
||||||
<div class="filter-title">筛选器</div>
|
<n-form :model="filter" label-placement="left" label-align="center" :label-width="95">
|
||||||
<n-form
|
<n-form-item label="关键词">
|
||||||
:model="filter"
|
<n-select
|
||||||
label-placement="left"
|
placeholder=""
|
||||||
label-align="center"
|
v-model:value="filter.keywords"
|
||||||
:label-width="95"
|
clearable
|
||||||
>
|
:options="
|
||||||
<n-form-item label="关键词">
|
|
||||||
<n-select
|
|
||||||
placeholder=""
|
|
||||||
v-model:value="filter.keywords"
|
|
||||||
clearable
|
|
||||||
:options="
|
|
||||||
Array.from(allItems.reduce((o, c) => {
|
Array.from(allItems.reduce((o, c) => {
|
||||||
c.keywords && o.add(c.keywords);
|
c.keywords && o.add(c.keywords);
|
||||||
return o;
|
return o;
|
||||||
@ -270,55 +277,55 @@ const handleClearData = async () => {
|
|||||||
label: opt,
|
label: opt,
|
||||||
value: opt,
|
value: opt,
|
||||||
}))"
|
}))"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="日期(搜索页)">
|
<n-form-item label="日期(搜索页)">
|
||||||
<n-date-picker
|
<n-date-picker
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
clearable
|
clearable
|
||||||
v-model:value="filter.searchDateRange"
|
v-model:value="filter.searchDateRange"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="日期(详情页)">
|
<n-form-item label="日期(详情页)">
|
||||||
<n-date-picker
|
<n-date-picker
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
clearable
|
clearable
|
||||||
v-model:value="filter.detailDateRange"
|
v-model:value="filter.detailDateRange"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="列展示">
|
<n-form-item label="列展示">
|
||||||
<n-checkbox-group
|
<n-checkbox-group
|
||||||
:value="Array.from(itemColumnSettings)"
|
:value="Array.from(itemColumnSettings)"
|
||||||
@update:value="(val: any) => (itemColumnSettings = new Set(val) as any)"
|
@update:value="(val: any) => (itemColumnSettings = new Set(val) as any)"
|
||||||
>
|
>
|
||||||
<n-space item-style="display: flex;">
|
<n-space item-style="display: flex;">
|
||||||
<n-checkbox value="keywords" label="关键词" />
|
<n-checkbox value="keywords" label="关键词" />
|
||||||
<n-checkbox value="page" label="页码" />
|
<n-checkbox value="page" label="页码" />
|
||||||
<n-checkbox value="rank" label="排位" />
|
<n-checkbox value="rank" label="排位" />
|
||||||
<n-checkbox value="createTime" label="获取日期" />
|
<n-checkbox value="createTime" label="获取日期" />
|
||||||
<n-checkbox value="timestamp" label="获取日期(详情)" />
|
<n-checkbox value="timestamp" label="获取日期(详情)" />
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-checkbox-group>
|
</n-checkbox-group>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
|
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</control-strip>
|
</control-strip>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</result-table>
|
</result-table>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.result-table {
|
.result-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.filter-switch) {
|
:deep(.filter-switch) {
|
||||||
|
|||||||
@ -207,11 +207,11 @@ const handleClear = () => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.result-table {
|
.result-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-panel {
|
.filter-panel {
|
||||||
|
|||||||
@ -173,11 +173,11 @@ const handleExport = async (opt: 'cloud' | 'local') => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.result-table {
|
.result-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.expoter-progress-panel {
|
.expoter-progress-panel {
|
||||||
|
|||||||
123
src/options/views/help/guide.md
Normal file
123
src/options/views/help/guide.md
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# 软件目的
|
||||||
|
|
||||||
|
本软件的开发旨在自动采集Amazon电商平台的数据,并提供导出和预览数据的功能。
|
||||||
|
|
||||||
|
# 安装步骤
|
||||||
|
|
||||||
|
下载好插件解压缩,然后在浏览器中按照一下步骤操作。
|
||||||
|
|
||||||
|
<img width="955" alt="Image" src="https://github.com/user-attachments/assets/95c02ca5-3139-49bb-88fe-1fa57415e650" />
|
||||||
|
|
||||||
|
选择包含有**`manifest.json`**文件的文件夹,然后点击确定即可安装完成。
|
||||||
|
|
||||||
|
# 界面预览
|
||||||
|
|
||||||
|
## 侧边栏
|
||||||
|
|
||||||
|
<img width="201" alt="Image" src="https://github.com/user-attachments/assets/ba28ca54-40d3-45bf-90e3-bb5fd8d454bc" />
|
||||||
|
|
||||||
|
侧边栏布局整体如上图所示。从上往下的布局单元介绍如下。
|
||||||
|
|
||||||
|
- **a. 切换栏**,切换可选择执行不同的采集任务。
|
||||||
|
- **b. 标题栏**,其中右上角的“数据”按钮可前往结果页。
|
||||||
|
- **c. 互动栏**,上半部分为输入,下半部分为开始执行按钮。
|
||||||
|
- **d. 状态栏**,报告采集数据中途的数据采集情况。
|
||||||
|
|
||||||
|
## 结果页
|
||||||
|
|
||||||
|
<img width="1250" alt="Image" src="https://github.com/user-attachments/assets/c7df21e6-dc75-4355-bcb9-f23ffc66d9b6" />
|
||||||
|
|
||||||
|
- **a. 菜单栏**,切换展示不同的数据。
|
||||||
|
- **b. 表头功能栏**,对数据作操作包含清空数据、导入导出、以及筛选数据。
|
||||||
|
- **c. 数据表**,用于展示获取到数据。
|
||||||
|
|
||||||
|
# 功能介绍
|
||||||
|
|
||||||
|
## 数据采集
|
||||||
|
|
||||||
|
打开**侧边栏**,可以根据输入自动运行程序采集网站数据。
|
||||||
|
**需要注意的是,当插件的采集任务运行时,请保持执行窗口在前台。**
|
||||||
|
|
||||||
|
### 搜索页
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
位于搜索页面板,可以根据输入的关键词采集搜索页中呈现的商品信息,主要为**关键词排名**。
|
||||||
|
输入:关键词(最多十个)
|
||||||
|
输出:商品搜索页信息,包含关键词、页码、排位、 ASIN、标题、 价格、 封面图、获取日期。
|
||||||
|
|
||||||
|
### 详情页
|
||||||
|
|
||||||
|
<img width="376" height="317" alt="Image" src="https://github.com/user-attachments/assets/a3e7211a-61b7-4fc4-b38f-0d4d1d8f2489" />
|
||||||
|
|
||||||
|
位于详情页面板,可以根据输入的多个ASIN采集商品详情页的商品信息。
|
||||||
|
输入:ASIN(每行输入一个,可以输入多个)
|
||||||
|
输出:商品详情页信息,包含ASIN、标题、 价格、 封面图、获取日期、评分、评论数、 大类、大类排行、小类、 小类排行、 商品图片链接、A+截图等等。
|
||||||
|
|
||||||
|
### 评论页
|
||||||
|
|
||||||
|
<img width="365" height="308" alt="Image" src="https://github.com/user-attachments/assets/0b0b1d48-ed87-409a-8625-7b06d3ed7d9b" />
|
||||||
|
|
||||||
|
位于评论页面板,可以根据输入的多个ASIN采集商品详情页的评论信息。
|
||||||
|
输入:ASIN(每行输入一个,可以输入多个)
|
||||||
|
输出:用户对商品的评论,包含用户名、标题 、评分、内容、日期、图片链接。
|
||||||
|
|
||||||
|
## 数据展示
|
||||||
|
|
||||||
|
商品信息采集结果展示在软件的结果页。视图如下图所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
可以在数据行最右侧的操作栏打开评论。商品评论的视图如下所示。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
全部评论采集结果信息如下图所示。
|
||||||
|
|
||||||
|
<img width="1474" height="743" alt="Image" src="https://github.com/user-attachments/assets/99293fef-5618-48d4-b2be-b922c9a50aef" />
|
||||||
|
|
||||||
|
## 数据导出
|
||||||
|
|
||||||
|
数据导出功能在结果页,数据导出会将采集到的数据导出到Excel表格。
|
||||||
|
分为本地导出和云端导出。
|
||||||
|
Items表记录商品信息,表头信息如下。
|
||||||
|
| 名称 | 描述 |
|
||||||
|
|--------------------|--------------------------|
|
||||||
|
| 关键词 | 产品的关键词 |
|
||||||
|
| 页码 | 产品在关键词搜索列表中的页码 |
|
||||||
|
| 排位 | 产品的排名 |
|
||||||
|
| ASIN | 亚马逊标准识别号 |
|
||||||
|
| 标题 | 产品的标题 |
|
||||||
|
| 价格 | 产品的价格 |
|
||||||
|
| 封面图 | 产品的封面图片链接 |
|
||||||
|
| 获取日期 | 数据获取的日期 |
|
||||||
|
| 商品链接 | 产品的链接 |
|
||||||
|
| 有详情 | 是否有详细信息 |
|
||||||
|
| 评分 | 产品的评分 |
|
||||||
|
| 评论数 | 产品的评论数量 |
|
||||||
|
| 大类 | 产品所属的大类 |
|
||||||
|
| 大类排行 | 产品在大类中的排名 |
|
||||||
|
| 小类 | 产品所属的小类 |
|
||||||
|
| 小类排行 | 产品在小类中的排名 |
|
||||||
|
| 获取日期(详情页) | 详情页数据获取的日期 |
|
||||||
|
| 商品图片链接 | 产品的图片链接,以分号作为间隔 |
|
||||||
|
| A+截图 | 产品的A+内容截图链接 |
|
||||||
|
| 发货快递 | 物流方式(如FBA/FBM) |
|
||||||
|
| 卖家 |销售该产品的卖家名称 |
|
||||||
|
| 关于信息 | 商品详情页的"About this item"信息 |
|
||||||
|
| 品牌名称 | 产品的品牌名 |
|
||||||
|
| 商品口味 | 药品的商品的风味属性 |
|
||||||
|
| 商品单位数量 | 包装内包含的单位数量(如"100片") |
|
||||||
|
| 商品形态| 产品形态(如胶囊/液体/粉末) |
|
||||||
|
| 商品尺寸 | 产品的物理尺寸或规格 |
|
||||||
|
|
||||||
|
Reviews表记录商品的评论信息,表头信息如下所示。
|
||||||
|
| 名称 | 描述 |
|
||||||
|
|--------------|--------------------------|
|
||||||
|
| ASIN | 亚马逊标准识别号 |
|
||||||
|
| 用户名 | 用户的名称 |
|
||||||
|
| 标题 | 评论的标题 |
|
||||||
|
| 评分 | 评论的评分 |
|
||||||
|
| 内容 | 评论的具体内容 |
|
||||||
|
| 日期 | 评论的日期 |
|
||||||
|
| 图片链接 | 图片的链接(多个链接用分号分隔) |
|
||||||
@ -4,7 +4,7 @@ import { uploadImage } from '~/logic/upload';
|
|||||||
import { detailItems, reviewItems, searchItems } from '~/storages/amazon';
|
import { detailItems, reviewItems, searchItems } from '~/storages/amazon';
|
||||||
import { createGlobalState } from '@vueuse/core';
|
import { createGlobalState } from '@vueuse/core';
|
||||||
import { useAmazonService } from '~/services/amazon';
|
import { useAmazonService } from '~/services/amazon';
|
||||||
import { LanchTaskBaseOptions } from '../types';
|
import { LanchTaskBaseOptions } from '../interfaces';
|
||||||
|
|
||||||
export interface AmazonPageWorkerSettings {
|
export interface AmazonPageWorkerSettings {
|
||||||
objects?: ('search' | 'detail' | 'review')[];
|
objects?: ('search' | 'detail' | 'review')[];
|
||||||
@ -109,6 +109,9 @@ function buildAmazonPageWorker() {
|
|||||||
const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
|
const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
|
||||||
url && updateDetailCache({ asin: ev.asin, aplus: url });
|
url && updateDetailCache({ asin: ev.asin, aplus: url });
|
||||||
}),
|
}),
|
||||||
|
worker.on('item-extra-info-collect', (ev) => {
|
||||||
|
updateDetailCache({ asin: ev.asin, ...ev.info });
|
||||||
|
}),
|
||||||
worker.on('item-review-collected', (ev) => {
|
worker.on('item-review-collected', (ev) => {
|
||||||
updateReviews(ev);
|
updateReviews(ev);
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { AmazonPageWorker, AmazonPageWorkerEvents, LanchTaskBaseOptions } from '../types';
|
import type { AmazonPageWorker, AmazonPageWorkerEvents, LanchTaskBaseOptions } from '../interfaces';
|
||||||
import type { Tabs } from 'webextension-polyfill';
|
import type { Tabs } from 'webextension-polyfill';
|
||||||
import { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
import {
|
import {
|
||||||
@ -104,8 +104,12 @@ class AmazonPageWorkerImpl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
public async wanderDetailPage(entry: string, aplus: boolean = false) {
|
public async wanderDetailPage(
|
||||||
|
entry: string,
|
||||||
|
options: Parameters<typeof this.runDetailPageTask>[1] = {},
|
||||||
|
) {
|
||||||
//#region Initial Meta Info
|
//#region Initial Meta Info
|
||||||
|
const { aplus = false, extra = false } = options;
|
||||||
const params = { asin: '', url: '' };
|
const params = { asin: '', url: '' };
|
||||||
if (entry.match(/^https?:\/\/www\.amazon\.com.*\/dp\/[A-Z0-9]{10}/)) {
|
if (entry.match(/^https?:\/\/www\.amazon\.com.*\/dp\/[A-Z0-9]{10}/)) {
|
||||||
const [asin] = /\/\/dp\/[A-Z0-9]{10}/.exec(entry)!;
|
const [asin] = /\/\/dp\/[A-Z0-9]{10}/.exec(entry)!;
|
||||||
@ -189,6 +193,12 @@ class AmazonPageWorkerImpl
|
|||||||
await this.emit('item-aplus-screenshot-collect', { asin: params.asin, base64data });
|
await this.emit('item-aplus-screenshot-collect', { asin: params.asin, base64data });
|
||||||
}
|
}
|
||||||
// #endregion
|
// #endregion
|
||||||
|
//#region Get Extra Info
|
||||||
|
if (extra) {
|
||||||
|
const extraInfo = await injector.getExtraInfo();
|
||||||
|
this.emit('item-extra-info-collect', { asin: params.asin, info: extraInfo });
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
@ -238,7 +248,7 @@ class AmazonPageWorkerImpl
|
|||||||
|
|
||||||
public async runDetailPageTask(
|
public async runDetailPageTask(
|
||||||
asins: string[],
|
asins: string[],
|
||||||
options: LanchTaskBaseOptions & { aplus?: boolean } = {},
|
options: LanchTaskBaseOptions & { aplus?: boolean; extra?: boolean } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { progress, aplus = false } = options;
|
const { progress, aplus = false } = options;
|
||||||
const remains = [...asins];
|
const remains = [...asins];
|
||||||
@ -248,7 +258,7 @@ class AmazonPageWorkerImpl
|
|||||||
});
|
});
|
||||||
while (remains.length > 0 && !interrupt) {
|
while (remains.length > 0 && !interrupt) {
|
||||||
const asin = remains.shift()!;
|
const asin = remains.shift()!;
|
||||||
await this.wanderDetailPage(asin, aplus);
|
await this.wanderDetailPage(asin, options);
|
||||||
progress && progress(remains);
|
progress && progress(remains);
|
||||||
}
|
}
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } from '../types';
|
import type { HomedepotEvents, HomedepotWorker, LanchTaskBaseOptions } from '../interfaces';
|
||||||
import { Tabs } from 'webextension-polyfill';
|
import { Tabs } from 'webextension-polyfill';
|
||||||
import { withErrorHandling } from '../error-handler';
|
import { withErrorHandling } from '../error-handler';
|
||||||
import { HomedepotDetailPageInjector } from '../web-injectors/homedepot';
|
import { HomedepotDetailPageInjector } from '../web-injectors/homedepot';
|
||||||
|
|||||||
@ -5,13 +5,10 @@ type Listener<T> = Pick<Emittery<T>, 'on' | 'off' | 'once'>;
|
|||||||
export type LanchTaskBaseOptions = { progress?: (remains: string[]) => Promise<void> | void };
|
export type LanchTaskBaseOptions = { progress?: (remains: string[]) => Promise<void> | void };
|
||||||
|
|
||||||
export interface AmazonPageWorkerEvents {
|
export interface AmazonPageWorkerEvents {
|
||||||
/**
|
/** The event is fired when worker collected links to items on the Amazon search page. */
|
||||||
* The event is fired when worker collected links to items on the Amazon search page.
|
|
||||||
*/
|
|
||||||
['item-links-collected']: { objs: AmazonSearchItem[] };
|
['item-links-collected']: { objs: AmazonSearchItem[] };
|
||||||
/**
|
|
||||||
* The event is fired when worker collected goods' base info on the Amazon detail page.
|
/** The event is fired when worker collected goods' base info on the Amazon detail page.*/
|
||||||
*/
|
|
||||||
['item-base-info-collected']: Pick<
|
['item-base-info-collected']: Pick<
|
||||||
AmazonDetailItem,
|
AmazonDetailItem,
|
||||||
| 'asin'
|
| 'asin'
|
||||||
@ -22,30 +19,35 @@ export interface AmazonPageWorkerEvents {
|
|||||||
| 'ratingCount'
|
| 'ratingCount'
|
||||||
| 'categories'
|
| 'categories'
|
||||||
| 'timestamp'
|
| 'timestamp'
|
||||||
|
| 'shipFrom'
|
||||||
|
| 'soldBy'
|
||||||
>;
|
>;
|
||||||
/**
|
|
||||||
* The event is fired when worker
|
/** The event is fired when worker */
|
||||||
*/
|
|
||||||
['item-category-rank-collected']: Pick<AmazonDetailItem, 'asin' | 'category1' | 'category2'>;
|
['item-category-rank-collected']: Pick<AmazonDetailItem, 'asin' | 'category1' | 'category2'>;
|
||||||
/**
|
|
||||||
* The event is fired when images collected
|
/** The event is fired when images collected */
|
||||||
*/
|
|
||||||
['item-images-collected']: Pick<AmazonDetailItem, 'asin' | 'imageUrls'>;
|
['item-images-collected']: Pick<AmazonDetailItem, 'asin' | 'imageUrls'>;
|
||||||
/**
|
|
||||||
* The event is fired when top reviews collected in detail page
|
/** The event is fired when top reviews collected in detail page*/
|
||||||
*/
|
|
||||||
// ['item-top-reviews-collected']: Pick<AmazonDetailItem, 'asin' | 'topReviews'>;
|
// ['item-top-reviews-collected']: Pick<AmazonDetailItem, 'asin' | 'topReviews'>;
|
||||||
/**
|
|
||||||
* The event is fired when aplus screenshot-collect
|
/** The event is fired when aplus screenshot collect */
|
||||||
*/
|
|
||||||
['item-aplus-screenshot-collect']: { asin: string; base64data: string };
|
['item-aplus-screenshot-collect']: { asin: string; base64data: string };
|
||||||
/**
|
|
||||||
* The event is fired when reviews collected in all review page
|
/** The event is fired when extra amazon info collected*/
|
||||||
*/
|
['item-extra-info-collect']: {
|
||||||
|
asin: string;
|
||||||
|
info: Pick<
|
||||||
|
AmazonDetailItem,
|
||||||
|
'abouts' | 'brand' | 'flavor' | 'unitCount' | 'itemForm' | 'productDimensions'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The event is fired when reviews collected in all review page */
|
||||||
['item-review-collected']: { asin: string; reviews: AmazonReview[] };
|
['item-review-collected']: { asin: string; reviews: AmazonReview[] };
|
||||||
/**
|
|
||||||
* Error event that occurs when there is an issue with the Amazon page worker
|
/** Error event that occurs when there is an issue with the Amazon page worker*/
|
||||||
*/
|
|
||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +66,7 @@ export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
|||||||
*/
|
*/
|
||||||
runDetailPageTask(
|
runDetailPageTask(
|
||||||
asins: string[],
|
asins: string[],
|
||||||
options?: LanchTaskBaseOptions & { aplus?: boolean },
|
options?: LanchTaskBaseOptions & { aplus?: boolean; extra?: boolean },
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,17 +86,13 @@ export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HomedepotEvents {
|
export interface HomedepotEvents {
|
||||||
/**
|
/** The event is fired when detail items collect */
|
||||||
* The event is fired when detail items collect
|
|
||||||
*/
|
|
||||||
['detail-item-collected']: { item: HomedepotDetailItem };
|
['detail-item-collected']: { item: HomedepotDetailItem };
|
||||||
/**
|
|
||||||
* The event is fired when reviews collect
|
/** The event is fired when reviews collect */
|
||||||
*/
|
|
||||||
['review-collected']: { reviews: HomedepotReview[] };
|
['review-collected']: { reviews: HomedepotReview[] };
|
||||||
/**
|
|
||||||
* The event is fired when error occurs.
|
/** The event is fired when error occurs. */
|
||||||
*/
|
|
||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,13 +112,10 @@ export interface HomedepotWorker extends Listener<HomedepotEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LowesEvents {
|
export interface LowesEvents {
|
||||||
/**
|
/** The event is fired when detail items collect */
|
||||||
* The event is fired when detail items collect
|
|
||||||
*/
|
|
||||||
['detail-item-collected']: { item: LowesDetailItem };
|
['detail-item-collected']: { item: LowesDetailItem };
|
||||||
/**
|
|
||||||
* The event is fired when error occurs.
|
/** The event is fired when error occurs. */
|
||||||
*/
|
|
||||||
['error']: { message: string; url?: string };
|
['error']: { message: string; url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +184,11 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
const categories = document
|
const categories = document
|
||||||
.querySelector<HTMLElement>('#wayfinding-breadcrumbs_feature_div')
|
.querySelector<HTMLElement>('#wayfinding-breadcrumbs_feature_div')
|
||||||
?.innerText.replaceAll('\n', '');
|
?.innerText.replaceAll('\n', '');
|
||||||
return { title, price, boughtInfo, availableDate, categories };
|
const shipFrom = document.querySelector<HTMLElement>(
|
||||||
|
'#fulfillerInfoFeature_feature_div > *:last-of-type',
|
||||||
|
)?.innerText;
|
||||||
|
const soldBy = document.querySelector<HTMLElement>(`#sellerProfileTriggerId`)?.innerText;
|
||||||
|
return { title, price, boughtInfo, availableDate, categories, shipFrom, soldBy };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,9 +373,6 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
}
|
}
|
||||||
return nodes.length > 0 ? nodes : undefined;
|
return nodes.length > 0 ? nodes : undefined;
|
||||||
};
|
};
|
||||||
const shipFrom = document.querySelector<HTMLElement>(
|
|
||||||
'#fulfillerInfoFeature_feature_div > *:last-of-type',
|
|
||||||
)?.innerText;
|
|
||||||
const abouts = $x(
|
const abouts = $x(
|
||||||
`//*[normalize-space(text())='About this item']/following-sibling::ul[1]/li`,
|
`//*[normalize-space(text())='About this item']/following-sibling::ul[1]/li`,
|
||||||
)?.map((el) => el.innerText);
|
)?.map((el) => el.innerText);
|
||||||
@ -386,18 +387,16 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
|||||||
const itemForm = $x(
|
const itemForm = $x(
|
||||||
`//*[./span[normalize-space(text())='Item Form']]/following-sibling::*[1]`,
|
`//*[./span[normalize-space(text())='Item Form']]/following-sibling::*[1]`,
|
||||||
)?.[0].innerText;
|
)?.[0].innerText;
|
||||||
const productDemensions = $x(
|
const productDimensions = $x(
|
||||||
`//span[contains(text(), 'Dimensions')]/following-sibling::*[1]`,
|
`//span[contains(text(), 'Dimensions')]/following-sibling::*[1]`,
|
||||||
)?.[0].innerText;
|
)?.[0].innerText;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abouts,
|
abouts,
|
||||||
shipFrom,
|
|
||||||
brand,
|
brand,
|
||||||
flavor,
|
flavor,
|
||||||
unitCount,
|
unitCount,
|
||||||
itemForm,
|
itemForm,
|
||||||
productDemensions,
|
productDimensions,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ const routeObj: Record<'sidepanel' | 'options', RouteRecordRaw[]> = {
|
|||||||
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
|
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
|
||||||
{ path: '/amazon-reviews', component: () => import('~/options/views/AmazonReviews.vue') },
|
{ path: '/amazon-reviews', component: () => import('~/options/views/AmazonReviews.vue') },
|
||||||
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.vue') },
|
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.vue') },
|
||||||
|
{ path: '/help', component: () => import('~/options/views/help/guide.md') },
|
||||||
],
|
],
|
||||||
sidepanel: [
|
sidepanel: [
|
||||||
{ path: '/', redirect: `/${site.value}` },
|
{ path: '/', redirect: `/${site.value}` },
|
||||||
|
|||||||
@ -23,17 +23,28 @@ watch(currentUrl, (newVal) => {
|
|||||||
switch (url.hostname) {
|
switch (url.hostname) {
|
||||||
case 'www.amazon.com':
|
case 'www.amazon.com':
|
||||||
site.value = 'amazon';
|
site.value = 'amazon';
|
||||||
router.push({ path: '/amazon' });
|
|
||||||
break;
|
break;
|
||||||
case 'www.homedepot.com':
|
case 'www.homedepot.com':
|
||||||
site.value = 'homedepot';
|
site.value = 'homedepot';
|
||||||
router.push({ path: '/homedepot' });
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(site, (newVal) => {
|
||||||
|
switch (newVal) {
|
||||||
|
case 'amazon':
|
||||||
|
router.push('/amazon');
|
||||||
|
break;
|
||||||
|
case 'homedepot':
|
||||||
|
router.push('/homedepot');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { setupApp } from '~/logic/common-setup';
|
import { setupApp } from '~/logic/common-setup';
|
||||||
import '../styles';
|
import '~/styles';
|
||||||
|
|
||||||
// This is the sidepanel page of the extension.
|
// This is the sidepanel page of the extension.
|
||||||
Object.assign(self, { appContext: 'sidepanel' });
|
Object.assign(self, { appContext: 'sidepanel' });
|
||||||
|
|||||||
@ -58,9 +58,17 @@ worker.on('item-images-collected', (ev) => {
|
|||||||
worker.on('item-aplus-screenshot-collect', (ev) => {
|
worker.on('item-aplus-screenshot-collect', (ev) => {
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: `商品${ev.asin} A+信息`,
|
title: `商品${ev.asin}的A+截图`,
|
||||||
time: new Date().toLocaleString(),
|
time: new Date().toLocaleString(),
|
||||||
content: `获取到A+信息`,
|
content: `获取到A+截图`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
worker.on('item-extra-info-collect', (ev) => {
|
||||||
|
timelines.value.push({
|
||||||
|
type: 'success',
|
||||||
|
title: `商品${ev.asin}额外信息`,
|
||||||
|
time: new Date().toLocaleString(),
|
||||||
|
content: `获取商品的额外信息`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
@ -80,6 +88,7 @@ const launch = async () => {
|
|||||||
detailAsinInput.value = remains.join('\n');
|
detailAsinInput.value = remains.join('\n');
|
||||||
},
|
},
|
||||||
aplus: detailWorkerSettings.value.aplus,
|
aplus: detailWorkerSettings.value.aplus,
|
||||||
|
extra: detailWorkerSettings.value.extra,
|
||||||
});
|
});
|
||||||
timelines.value.push({
|
timelines.value.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -104,33 +113,40 @@ const handleInterrupt = () => {
|
|||||||
<div class="detail-page-entry">
|
<div class="detail-page-entry">
|
||||||
<header-title>Amazon Detail</header-title>
|
<header-title>Amazon Detail</header-title>
|
||||||
<div class="interative-section">
|
<div class="interative-section">
|
||||||
<ids-input v-model="detailAsinInput" :disabled="worker.isRunning.value" ref="asin-input">
|
<ids-input v-model="detailAsinInput" :disabled="worker.isRunning.value" ref="asin-input" />
|
||||||
<template #extra-settings>
|
<optional-button
|
||||||
<div class="setting-panel">
|
|
||||||
<n-form label-placement="left">
|
|
||||||
<n-form-item label="Aplus:" :feedback-style="{ display: 'none' }">
|
|
||||||
<n-switch v-model:value="detailWorkerSettings.aplus" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ids-input>
|
|
||||||
<n-button
|
|
||||||
v-if="!worker.isRunning.value"
|
v-if="!worker.isRunning.value"
|
||||||
round
|
round
|
||||||
size="large"
|
size="large"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleStart"
|
@click="handleStart"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #popover>
|
||||||
<ant-design-thunderbolt-outlined />
|
<div class="setting-panel">
|
||||||
|
<n-form
|
||||||
|
label-placement="left"
|
||||||
|
:label-width="60"
|
||||||
|
label-align="center"
|
||||||
|
:show-feedback="false"
|
||||||
|
>
|
||||||
|
<n-form-item label="Aplus:">
|
||||||
|
<n-switch v-model:value="detailWorkerSettings.aplus" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Extra:">
|
||||||
|
<n-switch v-model:value="detailWorkerSettings.extra" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<n-icon :size="20">
|
||||||
|
<ant-design-thunderbolt-outlined />
|
||||||
|
</n-icon>
|
||||||
开始
|
开始
|
||||||
</n-button>
|
</optional-button>
|
||||||
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
||||||
<template #icon>
|
<n-icon :size="20">
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
</template>
|
</n-icon>
|
||||||
停止
|
停止
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
@ -175,6 +191,6 @@ const handleInterrupt = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setting-panel {
|
.setting-panel {
|
||||||
padding: 7px 10px;
|
padding: 7px 5px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -65,8 +65,15 @@ const handleInterrupt = () => {
|
|||||||
<div class="review-page-entry">
|
<div class="review-page-entry">
|
||||||
<header-title>Amazon Review</header-title>
|
<header-title>Amazon Review</header-title>
|
||||||
<div class="interative-section">
|
<div class="interative-section">
|
||||||
<ids-input v-model="reviewAsinInput" :disabled="worker.isRunning.value" ref="asin-input">
|
<ids-input v-model="reviewAsinInput" :disabled="worker.isRunning.value" ref="asin-input" />
|
||||||
<template #extra-settings>
|
<optional-button
|
||||||
|
v-if="!worker.isRunning.value"
|
||||||
|
round
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
@click="handleStart"
|
||||||
|
>
|
||||||
|
<template #popover>
|
||||||
<div class="setting-panel">
|
<div class="setting-panel">
|
||||||
<n-form label-placement="left">
|
<n-form label-placement="left">
|
||||||
<n-form-item label="模式:" :feedback-style="{ display: 'none' }">
|
<n-form-item label="模式:" :feedback-style="{ display: 'none' }">
|
||||||
@ -78,19 +85,11 @@ const handleInterrupt = () => {
|
|||||||
</n-form>
|
</n-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ids-input>
|
<n-icon :size="20">
|
||||||
<n-button
|
|
||||||
v-if="!worker.isRunning.value"
|
|
||||||
round
|
|
||||||
size="large"
|
|
||||||
type="primary"
|
|
||||||
@click="handleStart"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
</template>
|
</n-icon>
|
||||||
开始
|
开始
|
||||||
</n-button>
|
</optional-button>
|
||||||
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<ant-design-thunderbolt-outlined />
|
<ant-design-thunderbolt-outlined />
|
||||||
|
|||||||
@ -74,7 +74,6 @@ const handleInterrupt = () => {
|
|||||||
<optional-button v-if="!worker.isRunning.value" round size="large" @click="handleStart">
|
<optional-button v-if="!worker.isRunning.value" round size="large" @click="handleStart">
|
||||||
<template #popover>
|
<template #popover>
|
||||||
<div class="setting-panel">
|
<div class="setting-panel">
|
||||||
<div>设置</div>
|
|
||||||
<n-form :label-width="50" label-placement="left" :show-feedback="false">
|
<n-form :label-width="50" label-placement="left" :show-feedback="false">
|
||||||
<n-form-item label="评论:">
|
<n-form-item label="评论:">
|
||||||
<n-switch v-model:value="detailWorkerSettings.review" />
|
<n-switch v-model:value="detailWorkerSettings.review" />
|
||||||
@ -128,11 +127,4 @@ const handleInterrupt = () => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-panel {
|
|
||||||
> *:first-of-type {
|
|
||||||
font-size: larger;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -10,15 +10,14 @@ export const itemColumnSettings = useWebExtensionStorage<
|
|||||||
Set<keyof Pick<AmazonItem, 'keywords' | 'page' | 'rank' | 'createTime' | 'timestamp'>>
|
Set<keyof Pick<AmazonItem, 'keywords' | 'page' | 'rank' | 'createTime' | 'timestamp'>>
|
||||||
>('itemColumnSettings', new Set(['keywords', 'page', 'rank', 'createTime']));
|
>('itemColumnSettings', new Set(['keywords', 'page', 'rank', 'createTime']));
|
||||||
|
|
||||||
export const detailWorkerSettings = useWebExtensionStorage<{ aplus: boolean }>(
|
export const detailWorkerSettings = useWebExtensionStorage('amazon-detail-worker-settings', {
|
||||||
'amazon-detail-worker-settings',
|
aplus: false,
|
||||||
{ aplus: false },
|
extra: false,
|
||||||
);
|
});
|
||||||
|
|
||||||
export const reviewWorkerSettings = useWebExtensionStorage<{ recent: boolean }>(
|
export const reviewWorkerSettings = useWebExtensionStorage('amazon-review-worker-settings', {
|
||||||
'amazon-review-worker-settings',
|
recent: true,
|
||||||
{ recent: true },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||||
|
|
||||||
export const site = useWebExtensionStorage<Website>('site', 'amazon');
|
export const site = useWebExtensionStorage<Website>('site', 'amazon', { flush: 'sync' });
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
import './main.scss';
|
import './main.scss';
|
||||||
|
import 'github-markdown-css';
|
||||||
|
|||||||
14
src/types/amazon.d.ts
vendored
14
src/types/amazon.d.ts
vendored
@ -54,8 +54,22 @@ declare type AmazonDetailItem = {
|
|||||||
aplus?: string;
|
aplus?: string;
|
||||||
// /** 顶部评论数组 */
|
// /** 顶部评论数组 */
|
||||||
// topReviews?: AmazonReview[];
|
// topReviews?: AmazonReview[];
|
||||||
|
/** 发货快递 */
|
||||||
|
shipFrom?: string;
|
||||||
|
/** 卖家 */
|
||||||
|
soldBy?: string;
|
||||||
/**关于信息 */
|
/**关于信息 */
|
||||||
abouts?: string[];
|
abouts?: string[];
|
||||||
|
/** 品牌名称 */
|
||||||
|
brand?: string;
|
||||||
|
/** 商品口味/风味 */
|
||||||
|
flavor?: string;
|
||||||
|
/** 商品单位数量 */
|
||||||
|
unitCount?: string;
|
||||||
|
/** 商品形态/剂型 */
|
||||||
|
itemForm?: string;
|
||||||
|
/** 商品尺寸 */
|
||||||
|
productDimensions?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type AmazonReview = BaseReview & {
|
declare type AmazonReview = BaseReview & {
|
||||||
|
|||||||
@ -4,11 +4,19 @@ declare const __DEV__: boolean;
|
|||||||
/** Extension name, defined in packageJson.name */
|
/** Extension name, defined in packageJson.name */
|
||||||
declare const __NAME__: string;
|
declare const __NAME__: string;
|
||||||
|
|
||||||
|
declare const __VERSION__: string;
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
const component: any;
|
const component: any;
|
||||||
export default component;
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '*.md' {
|
||||||
|
import type { ComponentOptions } from 'vue';
|
||||||
|
const Component: ComponentOptions;
|
||||||
|
export default Component;
|
||||||
|
}
|
||||||
|
|
||||||
declare type AppContext = 'options' | 'sidepanel' | 'background' | 'content script';
|
declare type AppContext = 'options' | 'sidepanel' | 'background' | 'content script';
|
||||||
|
|
||||||
declare type Website = 'amazon' | 'homedepot';
|
declare type Website = 'amazon' | 'homedepot';
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
__DEV__: isDev,
|
__DEV__: isDev,
|
||||||
__NAME__: JSON.stringify(packageJson.name),
|
__NAME__: JSON.stringify(packageJson.name),
|
||||||
|
__VERSION__: JSON.stringify(packageJson.version),
|
||||||
// https://github.com/vitejs/vite/issues/9320
|
// https://github.com/vitejs/vite/issues/9320
|
||||||
// https://github.com/vitejs/vite/issues/9186
|
// https://github.com/vitejs/vite/issues/9186
|
||||||
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
__DEV__: isDev,
|
__DEV__: isDev,
|
||||||
__NAME__: JSON.stringify(packageJson.name),
|
__NAME__: JSON.stringify(packageJson.name),
|
||||||
|
__VERSION__: JSON.stringify(packageJson.version),
|
||||||
// https://github.com/vitejs/vite/issues/9320
|
// https://github.com/vitejs/vite/issues/9320
|
||||||
// https://github.com/vitejs/vite/issues/9186
|
// https://github.com/vitejs/vite/issues/9186
|
||||||
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import AutoImport from 'unplugin-auto-import/vite';
|
|||||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
import { isDev, outputDir, port, r } from './scripts/utils.js';
|
import { isDev, outputDir, port, r } from './scripts/utils.js';
|
||||||
import packageJson from './package.json';
|
import packageJson from './package.json';
|
||||||
|
import Markdown from 'vite-plugin-md';
|
||||||
|
|
||||||
export const sharedConfig: UserConfig = {
|
export const sharedConfig: UserConfig = {
|
||||||
root: r('src'),
|
root: r('src'),
|
||||||
@ -23,9 +24,13 @@ export const sharedConfig: UserConfig = {
|
|||||||
define: {
|
define: {
|
||||||
__DEV__: isDev,
|
__DEV__: isDev,
|
||||||
__NAME__: JSON.stringify(packageJson.name),
|
__NAME__: JSON.stringify(packageJson.name),
|
||||||
|
__VERSION__: JSON.stringify(packageJson.version),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
Vue(),
|
Vue({
|
||||||
|
include: [/\.vue$/, /\.md$/],
|
||||||
|
}),
|
||||||
|
Markdown(),
|
||||||
VueJsx(),
|
VueJsx(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user