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",
|
||||
"displayName": "Azon Seeker",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"description": "Starter modify by honestfox101",
|
||||
"scripts": {
|
||||
@ -45,6 +45,7 @@
|
||||
"esno": "^4.8.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"kolorist": "^1.8.0",
|
||||
@ -59,6 +60,7 @@
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.0.2",
|
||||
"vite-plugin-md": "^0.21.5",
|
||||
"vitest": "^3.2.4",
|
||||
"vue": "^3.5.17",
|
||||
"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">
|
||||
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] }>();
|
||||
</script>
|
||||
@ -48,6 +48,10 @@ const emit = defineEmits<{ click: [ev: MouseEvent] }>();
|
||||
|
||||
> button:first-of-type {
|
||||
width: 85%;
|
||||
|
||||
&:hover {
|
||||
width: 88%;
|
||||
}
|
||||
}
|
||||
|
||||
> button:last-of-type {
|
||||
@ -61,6 +65,10 @@ const emit = defineEmits<{ click: [ev: MouseEvent] }>();
|
||||
&:has(> button:last-of-type:hover) > button:first-of-type {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&:has(> button:first-of-type:hover) > button:last-of-type {
|
||||
width: 12%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { useElementBounding, useParentElement } from '@vueuse/core';
|
||||
import type { EllipsisProps } from 'naive-ui';
|
||||
|
||||
export type TableColumn =
|
||||
@ -17,6 +18,17 @@ export type TableColumn =
|
||||
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(
|
||||
defineProps<{
|
||||
records: Record<string, unknown>[];
|
||||
@ -46,7 +58,7 @@ function generateUUID() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="result-table">
|
||||
<div class="result-table" ref="result-table" :style="{ height: `${tableHeight}px` }">
|
||||
<n-card class="result-content-container">
|
||||
<template #header><slot name="header" /></template>
|
||||
<template #header-extra><slot name="header-extra" /></template>
|
||||
@ -81,13 +93,8 @@ function generateUUID() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.result-content-container {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
:deep(.n-card__content:has(.n-empty)) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -8,8 +8,8 @@ defineProps<{ model: AmazonDetailItem }>();
|
||||
<n-descriptions-item label="ASIN">
|
||||
{{ model.asin }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="销量信息">
|
||||
{{ model.boughtInfo || '-' }}
|
||||
<n-descriptions-item label="获取日期">
|
||||
{{ model.timestamp }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="评价">
|
||||
{{ model.rating || '-' }}
|
||||
@ -17,9 +17,12 @@ defineProps<{ model: AmazonDetailItem }>();
|
||||
<n-descriptions-item label="评论数">
|
||||
{{ model.ratingCount || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="分类信息" :span="3">
|
||||
<n-descriptions-item label="分类信息" :span="2">
|
||||
{{ model.categories || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="销量信息">
|
||||
{{ model.boughtInfo || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="上架日期">
|
||||
{{ model.availableDate || '-' }}
|
||||
</n-descriptions-item>
|
||||
@ -36,13 +39,43 @@ defineProps<{ model: AmazonDetailItem }>();
|
||||
{{ model.category2?.rank || '-' }}
|
||||
</n-descriptions-item>
|
||||
<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 v-if="model.aplus" label="A+" :span="2">
|
||||
<image-link :url="model.aplus" />
|
||||
<n-descriptions-item v-if="model.abouts && model.abouts.length" label="About" :span="4">
|
||||
<ul>
|
||||
<li v-for="(about, idx) in model.abouts" :key="idx">{{ about }}</li>
|
||||
</ul>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="获取日期" :span="2">
|
||||
{{ model.timestamp }}
|
||||
<n-descriptions-item label="A+" :span="4">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -5,24 +5,48 @@ import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const headerText = ref('采集结果');
|
||||
const version = __VERSION__;
|
||||
const opt = ref<string | undefined>(`/${site.value}`);
|
||||
|
||||
const options: { label: string; value: string }[] = [
|
||||
{ label: 'Amazon', value: 'amazon' },
|
||||
{ label: 'Amazon Review', value: 'amazon-reviews' },
|
||||
{ label: 'Homedepot', value: 'homedepot' },
|
||||
{ label: 'Amazon', value: '/amazon' },
|
||||
{ label: 'Amazon Review', value: '/amazon-reviews' },
|
||||
{ label: 'Homedepot', value: '/homedepot' },
|
||||
];
|
||||
|
||||
watch(site, (val) => {
|
||||
watch(opt, (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>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<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>
|
||||
<template #icon>
|
||||
<n-icon size="20">
|
||||
<n-icon :size="20">
|
||||
<garden-menu-fill-12 />
|
||||
</n-icon>
|
||||
</template>
|
||||
@ -30,13 +54,27 @@ watch(site, (val) => {
|
||||
</n-popselect>
|
||||
</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>
|
||||
</header>
|
||||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
<footer>
|
||||
<span
|
||||
>Azon Seeker v{{ version }} powered by
|
||||
<a href="https://github.com/honestfox101">@HonestFox101</a></span
|
||||
>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -49,6 +87,8 @@ header {
|
||||
.header-title {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
height: 8vh;
|
||||
}
|
||||
|
||||
main {
|
||||
@ -56,7 +96,27 @@ main {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
height: 90vh;
|
||||
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>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import App from './App.vue';
|
||||
import { setupApp } from '~/logic/common-setup';
|
||||
import '../styles';
|
||||
import '~/styles';
|
||||
|
||||
// This is the options page of the extension.
|
||||
Object.assign(self, { appContext: 'options' });
|
||||
|
||||
@ -150,6 +150,19 @@ const extraHeaders: Header<AmazonItem>[] = [
|
||||
prop: 'aplus',
|
||||
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 = () => {
|
||||
@ -223,7 +236,6 @@ const handleClearData = async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="result-table">
|
||||
<result-table :columns="columns" :records="filteredData">
|
||||
<template #header>
|
||||
<n-space align="center">
|
||||
@ -250,12 +262,7 @@ const handleClearData = async () => {
|
||||
<template #filter>
|
||||
<div class="filter-section">
|
||||
<div class="filter-title">筛选器</div>
|
||||
<n-form
|
||||
:model="filter"
|
||||
label-placement="left"
|
||||
label-align="center"
|
||||
:label-width="95"
|
||||
>
|
||||
<n-form :model="filter" label-placement="left" label-align="center" :label-width="95">
|
||||
<n-form-item label="关键词">
|
||||
<n-select
|
||||
placeholder=""
|
||||
@ -308,18 +315,18 @@ const handleClearData = async () => {
|
||||
</n-space>
|
||||
</template>
|
||||
</result-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.result-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.filter-switch) {
|
||||
font-size: 15px;
|
||||
|
||||
@ -207,12 +207,12 @@ const handleClear = () => {
|
||||
<style lang="scss" scoped>
|
||||
.result-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
min-width: 100px;
|
||||
|
||||
@ -173,12 +173,12 @@ const handleExport = async (opt: 'cloud' | 'local') => {
|
||||
<style scoped lang="scss">
|
||||
.result-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.expoter-progress-panel {
|
||||
display: flex;
|
||||
|
||||
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 { createGlobalState } from '@vueuse/core';
|
||||
import { useAmazonService } from '~/services/amazon';
|
||||
import { LanchTaskBaseOptions } from '../types';
|
||||
import { LanchTaskBaseOptions } from '../interfaces';
|
||||
|
||||
export interface AmazonPageWorkerSettings {
|
||||
objects?: ('search' | 'detail' | 'review')[];
|
||||
@ -109,6 +109,9 @@ function buildAmazonPageWorker() {
|
||||
const url = await uploadImage(ev.base64data, `${ev.asin}.png`);
|
||||
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) => {
|
||||
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 { withErrorHandling } from '../error-handler';
|
||||
import {
|
||||
@ -104,8 +104,12 @@ class AmazonPageWorkerImpl
|
||||
}
|
||||
|
||||
@withErrorHandling
|
||||
public async wanderDetailPage(entry: string, aplus: boolean = false) {
|
||||
public async wanderDetailPage(
|
||||
entry: string,
|
||||
options: Parameters<typeof this.runDetailPageTask>[1] = {},
|
||||
) {
|
||||
//#region Initial Meta Info
|
||||
const { aplus = false, extra = false } = options;
|
||||
const params = { asin: '', url: '' };
|
||||
if (entry.match(/^https?:\/\/www\.amazon\.com.*\/dp\/[A-Z0-9]{10}/)) {
|
||||
const [asin] = /\/\/dp\/[A-Z0-9]{10}/.exec(entry)!;
|
||||
@ -189,6 +193,12 @@ class AmazonPageWorkerImpl
|
||||
await this.emit('item-aplus-screenshot-collect', { asin: params.asin, base64data });
|
||||
}
|
||||
// #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
|
||||
@ -238,7 +248,7 @@ class AmazonPageWorkerImpl
|
||||
|
||||
public async runDetailPageTask(
|
||||
asins: string[],
|
||||
options: LanchTaskBaseOptions & { aplus?: boolean } = {},
|
||||
options: LanchTaskBaseOptions & { aplus?: boolean; extra?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const { progress, aplus = false } = options;
|
||||
const remains = [...asins];
|
||||
@ -248,7 +258,7 @@ class AmazonPageWorkerImpl
|
||||
});
|
||||
while (remains.length > 0 && !interrupt) {
|
||||
const asin = remains.shift()!;
|
||||
await this.wanderDetailPage(asin, aplus);
|
||||
await this.wanderDetailPage(asin, options);
|
||||
progress && progress(remains);
|
||||
}
|
||||
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 { withErrorHandling } from '../error-handler';
|
||||
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 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[] };
|
||||
/**
|
||||
* 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<
|
||||
AmazonDetailItem,
|
||||
| 'asin'
|
||||
@ -22,30 +19,35 @@ export interface AmazonPageWorkerEvents {
|
||||
| 'ratingCount'
|
||||
| 'categories'
|
||||
| 'timestamp'
|
||||
| 'shipFrom'
|
||||
| 'soldBy'
|
||||
>;
|
||||
/**
|
||||
* The event is fired when worker
|
||||
*/
|
||||
|
||||
/** The event is fired when worker */
|
||||
['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'>;
|
||||
/**
|
||||
* 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'>;
|
||||
/**
|
||||
* The event is fired when aplus screenshot-collect
|
||||
*/
|
||||
|
||||
/** The event is fired when aplus screenshot collect */
|
||||
['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[] };
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
||||
@ -64,7 +66,7 @@ export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
||||
*/
|
||||
runDetailPageTask(
|
||||
asins: string[],
|
||||
options?: LanchTaskBaseOptions & { aplus?: boolean },
|
||||
options?: LanchTaskBaseOptions & { aplus?: boolean; extra?: boolean },
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
@ -84,17 +86,13 @@ export interface AmazonPageWorker extends Listener<AmazonPageWorkerEvents> {
|
||||
}
|
||||
|
||||
export interface HomedepotEvents {
|
||||
/**
|
||||
* The event is fired when detail items collect
|
||||
*/
|
||||
/** The event is fired when detail items collect */
|
||||
['detail-item-collected']: { item: HomedepotDetailItem };
|
||||
/**
|
||||
* The event is fired when reviews collect
|
||||
*/
|
||||
|
||||
/** The event is fired when reviews collect */
|
||||
['review-collected']: { reviews: HomedepotReview[] };
|
||||
/**
|
||||
* The event is fired when error occurs.
|
||||
*/
|
||||
|
||||
/** The event is fired when error occurs. */
|
||||
['error']: { message: string; url?: string };
|
||||
}
|
||||
|
||||
@ -114,13 +112,10 @@ export interface HomedepotWorker extends Listener<HomedepotEvents> {
|
||||
}
|
||||
|
||||
export interface LowesEvents {
|
||||
/**
|
||||
* The event is fired when detail items collect
|
||||
*/
|
||||
/** The event is fired when detail items collect */
|
||||
['detail-item-collected']: { item: LowesDetailItem };
|
||||
/**
|
||||
* The event is fired when error occurs.
|
||||
*/
|
||||
|
||||
/** The event is fired when error occurs. */
|
||||
['error']: { message: string; url?: string };
|
||||
}
|
||||
|
||||
@ -184,7 +184,11 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
||||
const categories = document
|
||||
.querySelector<HTMLElement>('#wayfinding-breadcrumbs_feature_div')
|
||||
?.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;
|
||||
};
|
||||
const shipFrom = document.querySelector<HTMLElement>(
|
||||
'#fulfillerInfoFeature_feature_div > *:last-of-type',
|
||||
)?.innerText;
|
||||
const abouts = $x(
|
||||
`//*[normalize-space(text())='About this item']/following-sibling::ul[1]/li`,
|
||||
)?.map((el) => el.innerText);
|
||||
@ -386,18 +387,16 @@ export class AmazonDetailPageInjector extends BaseInjector {
|
||||
const itemForm = $x(
|
||||
`//*[./span[normalize-space(text())='Item Form']]/following-sibling::*[1]`,
|
||||
)?.[0].innerText;
|
||||
const productDemensions = $x(
|
||||
const productDimensions = $x(
|
||||
`//span[contains(text(), 'Dimensions')]/following-sibling::*[1]`,
|
||||
)?.[0].innerText;
|
||||
|
||||
return {
|
||||
abouts,
|
||||
shipFrom,
|
||||
brand,
|
||||
flavor,
|
||||
unitCount,
|
||||
itemForm,
|
||||
productDemensions,
|
||||
productDimensions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ const routeObj: Record<'sidepanel' | 'options', RouteRecordRaw[]> = {
|
||||
{ path: '/amazon', component: () => import('~/options/views/AmazonResultTable.vue') },
|
||||
{ path: '/amazon-reviews', component: () => import('~/options/views/AmazonReviews.vue') },
|
||||
{ path: '/homedepot', component: () => import('~/options/views/HomedepotResultTable.vue') },
|
||||
{ path: '/help', component: () => import('~/options/views/help/guide.md') },
|
||||
],
|
||||
sidepanel: [
|
||||
{ path: '/', redirect: `/${site.value}` },
|
||||
|
||||
@ -23,17 +23,28 @@ watch(currentUrl, (newVal) => {
|
||||
switch (url.hostname) {
|
||||
case 'www.amazon.com':
|
||||
site.value = 'amazon';
|
||||
router.push({ path: '/amazon' });
|
||||
break;
|
||||
case 'www.homedepot.com':
|
||||
site.value = 'homedepot';
|
||||
router.push({ path: '/homedepot' });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(site, (newVal) => {
|
||||
switch (newVal) {
|
||||
case 'amazon':
|
||||
router.push('/amazon');
|
||||
break;
|
||||
case 'homedepot':
|
||||
router.push('/homedepot');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import App from './App.vue';
|
||||
import { setupApp } from '~/logic/common-setup';
|
||||
import '../styles';
|
||||
import '~/styles';
|
||||
|
||||
// This is the sidepanel page of the extension.
|
||||
Object.assign(self, { appContext: 'sidepanel' });
|
||||
|
||||
@ -58,9 +58,17 @@ worker.on('item-images-collected', (ev) => {
|
||||
worker.on('item-aplus-screenshot-collect', (ev) => {
|
||||
timelines.value.push({
|
||||
type: 'success',
|
||||
title: `商品${ev.asin} A+信息`,
|
||||
title: `商品${ev.asin}的A+截图`,
|
||||
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
|
||||
@ -80,6 +88,7 @@ const launch = async () => {
|
||||
detailAsinInput.value = remains.join('\n');
|
||||
},
|
||||
aplus: detailWorkerSettings.value.aplus,
|
||||
extra: detailWorkerSettings.value.extra,
|
||||
});
|
||||
timelines.value.push({
|
||||
type: 'info',
|
||||
@ -104,33 +113,40 @@ const handleInterrupt = () => {
|
||||
<div class="detail-page-entry">
|
||||
<header-title>Amazon Detail</header-title>
|
||||
<div class="interative-section">
|
||||
<ids-input v-model="detailAsinInput" :disabled="worker.isRunning.value" ref="asin-input">
|
||||
<template #extra-settings>
|
||||
<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
|
||||
<ids-input v-model="detailAsinInput" :disabled="worker.isRunning.value" ref="asin-input" />
|
||||
<optional-button
|
||||
v-if="!worker.isRunning.value"
|
||||
round
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="handleStart"
|
||||
>
|
||||
<template #icon>
|
||||
<ant-design-thunderbolt-outlined />
|
||||
<template #popover>
|
||||
<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>
|
||||
<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">
|
||||
<template #icon>
|
||||
<n-icon :size="20">
|
||||
<ant-design-thunderbolt-outlined />
|
||||
</template>
|
||||
</n-icon>
|
||||
停止
|
||||
</n-button>
|
||||
</div>
|
||||
@ -175,6 +191,6 @@ const handleInterrupt = () => {
|
||||
}
|
||||
|
||||
.setting-panel {
|
||||
padding: 7px 10px;
|
||||
padding: 7px 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -65,8 +65,15 @@ const handleInterrupt = () => {
|
||||
<div class="review-page-entry">
|
||||
<header-title>Amazon Review</header-title>
|
||||
<div class="interative-section">
|
||||
<ids-input v-model="reviewAsinInput" :disabled="worker.isRunning.value" ref="asin-input">
|
||||
<template #extra-settings>
|
||||
<ids-input v-model="reviewAsinInput" :disabled="worker.isRunning.value" ref="asin-input" />
|
||||
<optional-button
|
||||
v-if="!worker.isRunning.value"
|
||||
round
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="handleStart"
|
||||
>
|
||||
<template #popover>
|
||||
<div class="setting-panel">
|
||||
<n-form label-placement="left">
|
||||
<n-form-item label="模式:" :feedback-style="{ display: 'none' }">
|
||||
@ -78,19 +85,11 @@ const handleInterrupt = () => {
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
</ids-input>
|
||||
<n-button
|
||||
v-if="!worker.isRunning.value"
|
||||
round
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="handleStart"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon :size="20">
|
||||
<ant-design-thunderbolt-outlined />
|
||||
</template>
|
||||
</n-icon>
|
||||
开始
|
||||
</n-button>
|
||||
</optional-button>
|
||||
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
|
||||
<template #icon>
|
||||
<ant-design-thunderbolt-outlined />
|
||||
|
||||
@ -74,7 +74,6 @@ const handleInterrupt = () => {
|
||||
<optional-button v-if="!worker.isRunning.value" round size="large" @click="handleStart">
|
||||
<template #popover>
|
||||
<div class="setting-panel">
|
||||
<div>设置</div>
|
||||
<n-form :label-width="50" label-placement="left" :show-feedback="false">
|
||||
<n-form-item label="评论:">
|
||||
<n-switch v-model:value="detailWorkerSettings.review" />
|
||||
@ -128,11 +127,4 @@ const handleInterrupt = () => {
|
||||
margin-top: 10px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.setting-panel {
|
||||
> *:first-of-type {
|
||||
font-size: larger;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -10,15 +10,14 @@ export const itemColumnSettings = useWebExtensionStorage<
|
||||
Set<keyof Pick<AmazonItem, 'keywords' | 'page' | 'rank' | 'createTime' | 'timestamp'>>
|
||||
>('itemColumnSettings', new Set(['keywords', 'page', 'rank', 'createTime']));
|
||||
|
||||
export const detailWorkerSettings = useWebExtensionStorage<{ aplus: boolean }>(
|
||||
'amazon-detail-worker-settings',
|
||||
{ aplus: false },
|
||||
);
|
||||
export const detailWorkerSettings = useWebExtensionStorage('amazon-detail-worker-settings', {
|
||||
aplus: false,
|
||||
extra: false,
|
||||
});
|
||||
|
||||
export const reviewWorkerSettings = useWebExtensionStorage<{ recent: boolean }>(
|
||||
'amazon-review-worker-settings',
|
||||
{ recent: true },
|
||||
);
|
||||
export const reviewWorkerSettings = useWebExtensionStorage('amazon-review-worker-settings', {
|
||||
recent: true,
|
||||
});
|
||||
|
||||
export const searchItems = useWebExtensionStorage<AmazonSearchItem[]>('searchItems', []);
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
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 '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;
|
||||
// /** 顶部评论数组 */
|
||||
// topReviews?: AmazonReview[];
|
||||
/** 发货快递 */
|
||||
shipFrom?: string;
|
||||
/** 卖家 */
|
||||
soldBy?: string;
|
||||
/**关于信息 */
|
||||
abouts?: string[];
|
||||
/** 品牌名称 */
|
||||
brand?: string;
|
||||
/** 商品口味/风味 */
|
||||
flavor?: string;
|
||||
/** 商品单位数量 */
|
||||
unitCount?: string;
|
||||
/** 商品形态/剂型 */
|
||||
itemForm?: string;
|
||||
/** 商品尺寸 */
|
||||
productDimensions?: string;
|
||||
};
|
||||
|
||||
declare type AmazonReview = BaseReview & {
|
||||
|
||||
@ -4,11 +4,19 @@ declare const __DEV__: boolean;
|
||||
/** Extension name, defined in packageJson.name */
|
||||
declare const __NAME__: string;
|
||||
|
||||
declare const __VERSION__: string;
|
||||
|
||||
declare module '*.vue' {
|
||||
const component: any;
|
||||
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 Website = 'amazon' | 'homedepot';
|
||||
|
||||
@ -9,6 +9,7 @@ export default defineConfig({
|
||||
define: {
|
||||
__DEV__: isDev,
|
||||
__NAME__: JSON.stringify(packageJson.name),
|
||||
__VERSION__: JSON.stringify(packageJson.version),
|
||||
// https://github.com/vitejs/vite/issues/9320
|
||||
// https://github.com/vitejs/vite/issues/9186
|
||||
'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'),
|
||||
|
||||
@ -9,6 +9,7 @@ export default defineConfig({
|
||||
define: {
|
||||
__DEV__: isDev,
|
||||
__NAME__: JSON.stringify(packageJson.name),
|
||||
__VERSION__: JSON.stringify(packageJson.version),
|
||||
// https://github.com/vitejs/vite/issues/9320
|
||||
// https://github.com/vitejs/vite/issues/9186
|
||||
'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 { isDev, outputDir, port, r } from './scripts/utils.js';
|
||||
import packageJson from './package.json';
|
||||
import Markdown from 'vite-plugin-md';
|
||||
|
||||
export const sharedConfig: UserConfig = {
|
||||
root: r('src'),
|
||||
@ -23,9 +24,13 @@ export const sharedConfig: UserConfig = {
|
||||
define: {
|
||||
__DEV__: isDev,
|
||||
__NAME__: JSON.stringify(packageJson.name),
|
||||
__VERSION__: JSON.stringify(packageJson.version),
|
||||
},
|
||||
plugins: [
|
||||
Vue(),
|
||||
Vue({
|
||||
include: [/\.vue$/, /\.md$/],
|
||||
}),
|
||||
Markdown(),
|
||||
VueJsx(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user