This commit is contained in:
johnathan 2025-07-10 15:45:31 +08:00
parent 6b0aef185f
commit 219b34661a
31 changed files with 1656 additions and 239 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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.

View File

@ -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;

View File

@ -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>

View File

@ -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) => {
router.push(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>

View File

@ -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' });

View File

@ -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,45 +236,39 @@ const handleClearData = async () => {
</script>
<template>
<div class="result-table">
<result-table :columns="columns" :records="filteredData">
<template #header>
<n-space align="center">
<h3 class="header-text">Amazon Items</h3>
<n-switch size="small" class="filter-switch" v-model:value="filter.detailOnly">
<template #checked> 详情 </template>
<template #unchecked> 全部</template>
</n-switch>
</n-space>
</template>
<template #header-extra>
<n-space size="small">
<n-input
v-model:value="filter.search"
placeholder="输入文本过滤结果"
round
clearable
style="min-width: 230px"
/>
<control-strip round @clear="handleClearData" @import="handleImport">
<template #exporter>
<export-panel @export-file="handleExport" />
</template>
<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-item label="关键词">
<n-select
placeholder=""
v-model:value="filter.keywords"
clearable
:options="
<result-table :columns="columns" :records="filteredData">
<template #header>
<n-space align="center">
<h3 class="header-text">Amazon Items</h3>
<n-switch size="small" class="filter-switch" v-model:value="filter.detailOnly">
<template #checked> 详情 </template>
<template #unchecked> 全部</template>
</n-switch>
</n-space>
</template>
<template #header-extra>
<n-space size="small">
<n-input
v-model:value="filter.search"
placeholder="输入文本过滤结果"
round
clearable
style="min-width: 230px"
/>
<control-strip round @clear="handleClearData" @import="handleImport">
<template #exporter>
<export-panel @export-file="handleExport" />
</template>
<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-item label="关键词">
<n-select
placeholder=""
v-model:value="filter.keywords"
clearable
:options="
Array.from(allItems.reduce((o, c) => {
c.keywords && o.add(c.keywords);
return o;
@ -270,55 +277,55 @@ const handleClearData = async () => {
label: opt,
value: opt,
}))"
/>
</n-form-item>
<n-form-item label="日期(搜索页)">
<n-date-picker
type="datetimerange"
clearable
v-model:value="filter.searchDateRange"
/>
</n-form-item>
<n-form-item label="日期(详情页)">
<n-date-picker
type="datetimerange"
clearable
v-model:value="filter.detailDateRange"
/>
</n-form-item>
<n-form-item label="列展示">
<n-checkbox-group
:value="Array.from(itemColumnSettings)"
@update:value="(val: any) => (itemColumnSettings = new Set(val) as any)"
>
<n-space item-style="display: flex;">
<n-checkbox value="keywords" label="关键词" />
<n-checkbox value="page" label="页码" />
<n-checkbox value="rank" label="排位" />
<n-checkbox value="createTime" label="获取日期" />
<n-checkbox value="timestamp" label="获取日期(详情)" />
</n-space>
</n-checkbox-group>
</n-form-item>
</n-form>
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
</div>
</template>
</control-strip>
</n-space>
</template>
</result-table>
</div>
/>
</n-form-item>
<n-form-item label="日期(搜索页)">
<n-date-picker
type="datetimerange"
clearable
v-model:value="filter.searchDateRange"
/>
</n-form-item>
<n-form-item label="日期(详情页)">
<n-date-picker
type="datetimerange"
clearable
v-model:value="filter.detailDateRange"
/>
</n-form-item>
<n-form-item label="列展示">
<n-checkbox-group
:value="Array.from(itemColumnSettings)"
@update:value="(val: any) => (itemColumnSettings = new Set(val) as any)"
>
<n-space item-style="display: flex;">
<n-checkbox value="keywords" label="关键词" />
<n-checkbox value="page" label="页码" />
<n-checkbox value="rank" label="排位" />
<n-checkbox value="createTime" label="获取日期" />
<n-checkbox value="timestamp" label="获取日期(详情)" />
</n-space>
</n-checkbox-group>
</n-form-item>
</n-form>
<div class="filter-footer" @click="onFilterReset"><n-button>重置</n-button></div>
</div>
</template>
</control-strip>
</n-space>
</template>
</result-table>
</template>
<style scoped lang="scss">
.result-table {
width: 100%;
height: 100%;
}
.header-text {
padding: 0px;
margin: 0px;
}
.header-text {
padding: 0px;
margin: 0px;
}
:deep(.filter-switch) {

View File

@ -207,11 +207,11 @@ const handleClear = () => {
<style lang="scss" scoped>
.result-table {
width: 100%;
}
.header-text {
padding: 0px;
margin: 0px;
}
.header-text {
padding: 0px;
margin: 0px;
}
.filter-panel {

View File

@ -173,11 +173,11 @@ const handleExport = async (opt: 'cloud' | 'local') => {
<style scoped lang="scss">
.result-table {
width: 100%;
}
.header-text {
padding: 0px;
margin: 0px;
}
.header-text {
padding: 0px;
margin: 0px;
}
.expoter-progress-panel {

View 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. 数据表**,用于展示获取到数据。
# 功能介绍
## 数据采集
打开**侧边栏**,可以根据输入自动运行程序采集网站数据。
**需要注意的是,当插件的采集任务运行时,请保持执行窗口在前台。**
### 搜索页
![Image](https://github.com/user-attachments/assets/6163c055-64e3-4bab-b3f8-e09aebd15ac0)
位于搜索页面板,可以根据输入的关键词采集搜索页中呈现的商品信息,主要为**关键词排名**。
输入:关键词(最多十个)
输出:商品搜索页信息,包含关键词、页码、排位、 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每行输入一个可以输入多个
输出:用户对商品的评论,包含用户名、标题 、评分、内容、日期、图片链接。
## 数据展示
商品信息采集结果展示在软件的结果页。视图如下图所示。
![Image](https://github.com/user-attachments/assets/5bf7c929-790a-412b-a144-ebe7a9b913d9)
可以在数据行最右侧的操作栏打开评论。商品评论的视图如下所示。
![Image](https://github.com/user-attachments/assets/1c61d4d3-3055-4769-bc64-5eaf651969ad)
全部评论采集结果信息如下图所示。
<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 | 亚马逊标准识别号 |
| 用户名 | 用户的名称 |
| 标题 | 评论的标题 |
| 评分 | 评论的评分 |
| 内容 | 评论的具体内容 |
| 日期 | 评论的日期 |
| 图片链接 | 图片的链接(多个链接用分号分隔) |

View File

@ -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);
}),

View File

@ -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();

View File

@ -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';

View File

@ -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 };
}

View File

@ -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,
};
});
}

View File

@ -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}` },

View File

@ -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>

View File

@ -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' });

View File

@ -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>

View File

@ -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 />

View File

@ -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>

View File

@ -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', []);

View File

@ -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' });

View File

@ -1 +1,2 @@
import './main.scss';
import 'github-markdown-css';

14
src/types/amazon.d.ts vendored
View File

@ -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 & {

View File

@ -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';

View File

@ -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'),

View File

@ -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'),

View File

@ -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: [