diff --git a/package.json b/package.json
index db61ab2..3b5f8ac 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "azon-seeker",
"displayName": "Azon Seeker",
- "version": "0.0.1",
+ "version": "0.2.0",
"private": true,
"description": "Starter modify by honestfox101",
"scripts": {
@@ -12,6 +12,7 @@
"dev:web": "vite",
"dev:js": "npm run build:js -- --mode development",
"build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:background build:js",
+ "build:firefox": "cross-env NODE_ENV=production EXTENSION=firefox run-s clear build:web build:prepare build:background build:js",
"build:prepare": "esno scripts/prepare.ts",
"build:background": "vite build --config vite.config.background.mts",
"build:web": "vite build",
diff --git a/src/components/DetailDescription.vue b/src/components/DetailDescription.vue
index 32aec08..420c567 100644
--- a/src/components/DetailDescription.vue
+++ b/src/components/DetailDescription.vue
@@ -7,8 +7,9 @@ const props = defineProps<{ model: AmazonDetailItem }>();
const modal = useModal();
const handleLoadMore = () => {
+ const asin = props.model.asin;
modal.create({
- title: `${props.model.asin}全部评论`,
+ title: `${asin}评论`,
preset: 'card',
style: {
width: '80vw',
@@ -16,7 +17,7 @@ const handleLoadMore = () => {
},
content: () =>
h(ReviewPreview, {
- asin: props.model.asin,
+ asin,
}),
});
};
@@ -47,11 +48,9 @@ const handleLoadMore = () => {
{{ model.category2?.rank || '-' }}
-
- {{ link }}
-
+
- {
更多评论
-
+ -->
diff --git a/src/components/ImageLink.vue b/src/components/ImageLink.vue
new file mode 100644
index 0000000..3cf3e34
--- /dev/null
+++ b/src/components/ImageLink.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
v && loadImage()"
+ trigger="hover"
+ placement="right"
+ :delay="1000"
+ :duration="1000"
+ >
+
+ {{ url }}
+
+
+ 加载中...
+
+
+
+
+
diff --git a/src/components/ResultTable.vue b/src/components/ResultTable.vue
index 4cb762a..d42cc4c 100644
--- a/src/components/ResultTable.vue
+++ b/src/components/ResultTable.vue
@@ -1,20 +1,23 @@
@@ -229,6 +300,7 @@ const handleClearData = async () => {
size="small"
placeholder="输入文本过滤结果"
round
+ clearable
style="min-width: 230px"
/>
{{ paragraph }}
-
-
-
+
+
+
{{ model.dateInfo }}
+
+
diff --git a/src/components/ReviewPreview.vue b/src/components/ReviewPreview.vue
index 08de0d1..6d205fc 100644
--- a/src/components/ReviewPreview.vue
+++ b/src/components/ReviewPreview.vue
@@ -90,7 +90,8 @@ const handleImport = async (file: File) => {
};
const handleExport = () => {
- exportToXLSX(allReviews, { headers });
+ const fileName = `${props.asin}Reviews${dayjs().format('YYYY-MM-DD')}.xlsx`;
+ exportToXLSX(allReviews, { headers, fileName });
message.info('导出完成');
};
diff --git a/src/logic/data-io.ts b/src/logic/data-io.ts
index a75e13a..b06be1e 100644
--- a/src/logic/data-io.ts
+++ b/src/logic/data-io.ts
@@ -1,4 +1,99 @@
-import { utils, read, writeFileXLSX } from 'xlsx';
+import { utils, read, writeFileXLSX, WorkSheet, WorkBook } from 'xlsx';
+
+class Worksheet {
+ readonly _raw: WorkSheet;
+
+ constructor(ws: WorkSheet) {
+ this._raw = ws;
+ }
+
+ static fromJson(data: Record[], options: { headers?: Header[] } = {}) {
+ const {
+ headers = data.length > 0
+ ? Object.keys(data[0]).map((k) => ({ label: k, prop: k }) as Header)
+ : [],
+ } = options;
+
+ const rows = data.map((item) => {
+ const row: Record = {};
+ headers.forEach((header) => {
+ const value = getAttribute(item, header.prop);
+ if (header.formatOutputValue) {
+ row[header.label] = header.formatOutputValue(value);
+ } else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
+ row[header.label] = value;
+ } else {
+ row[header.label] = JSON.stringify(value);
+ }
+ });
+ return row;
+ });
+
+ const ws = utils.json_to_sheet(rows, {
+ header: headers.map((h) => h.label),
+ });
+ ws['!autofilter'] = {
+ ref: utils.encode_range({ c: 0, r: 0 }, { c: headers.length - 1, r: rows.length }),
+ }; // Use Auto Filter: https://github.com/SheetJS/sheetjs/issues/472#issuecomment-292852308
+ return new Worksheet(ws);
+ }
+
+ toJson(options: { headers?: Header[] } = {}) {
+ const { headers } = options;
+
+ let jsonData = utils.sheet_to_json>(this._raw);
+ if (headers) {
+ jsonData = jsonData.map((item) => {
+ const mappedItem: Record = {};
+ headers.forEach((header) => {
+ const value = header.parseImportValue
+ ? header.parseImportValue(item[header.label])
+ : item[header.label];
+ setAttribute(mappedItem, header.prop, value);
+ });
+ return mappedItem;
+ });
+ }
+ return jsonData as T[];
+ }
+
+ toWorkbook(sheetName?: string) {
+ const wb = new Workbook(utils.book_new());
+ wb.addSheet(sheetName || 'Sheet1', this);
+ return wb;
+ }
+}
+
+class Workbook {
+ readonly _raw: WorkBook;
+
+ constructor(wb: WorkBook) {
+ this._raw = wb;
+ }
+
+ get sheetCount() {
+ return this._raw.SheetNames.length;
+ }
+
+ static fromArrayBuffer(bf: ArrayBuffer) {
+ const data = new Uint8Array(bf);
+ const wb = read(data, { type: 'array' });
+ return new Workbook(wb);
+ }
+
+ getSheet(index: number) {
+ const sheetName = this._raw.SheetNames[index];
+ return new Worksheet(this._raw.Sheets[sheetName]);
+ }
+
+ addSheet(name: string, sheet: Worksheet) {
+ utils.book_append_sheet(this._raw, sheet._raw, name);
+ }
+
+ exportFile(fileName: string) {
+ writeFileXLSX(this._raw, fileName, { bookType: 'xlsx', type: 'binary', compression: true });
+ }
+}
function getAttribute(
obj: Record,
@@ -40,49 +135,45 @@ export type Header = {
formatOutputValue?: (val: any) => any;
};
+export type ExportBaseOptions = {
+ fileName?: string;
+ headers?: Header[];
+};
+
+export type ImportBaseOptions = {
+ headers?: Header[];
+};
+
/**
- * 导出为XLSX文件
+ * 导出为XLSX
* @param data 数据数组
* @param options 导出选项
*/
export function exportToXLSX(
data: Record[],
- options: {
- fileName?: string;
- headers?: Header[];
- } = {},
-): void {
- if (!data.length) {
- return;
+ options?: ExportBaseOptions & { asWorkSheet?: false },
+): void;
+export function exportToXLSX(
+ data: Record[],
+ options: Omit & { asWorkSheet: true },
+): Worksheet;
+export function exportToXLSX(
+ data: Record[],
+ options: ExportBaseOptions & { asWorkSheet?: boolean } = {},
+) {
+ const {
+ headers,
+ fileName = `export_${new Date().toISOString().slice(0, 10)}.xlsx`,
+ asWorkSheet,
+ } = options;
+
+ const worksheet = Worksheet.fromJson(data, { headers: headers });
+
+ if (asWorkSheet) {
+ return worksheet;
}
-
- const headers: Header[] =
- options.headers || Object.keys(data[0]).map((k) => ({ label: k, prop: k }));
- const rows = data.map((item) => {
- const row: Record = {};
- headers.forEach((header) => {
- const value = getAttribute(item, header.prop);
- if (header.formatOutputValue) {
- row[header.label] = header.formatOutputValue(value);
- } else if (['string', 'number', 'bigint', 'boolean'].includes(typeof value)) {
- row[header.label] = value;
- } else {
- row[header.label] = JSON.stringify(value);
- }
- });
- return row;
- });
-
- const worksheet = utils.json_to_sheet(rows, {
- header: headers.map((h) => h.label),
- });
- worksheet['!autofilter'] = {
- ref: utils.encode_range({ c: 0, r: 0 }, { c: headers.length - 1, r: rows.length }),
- }; // Use Auto Filter: https://github.com/SheetJS/sheetjs/issues/472#issuecomment-292852308
- const workbook = utils.book_new();
- utils.book_append_sheet(workbook, worksheet, 'Sheet1');
- const fileName = options.fileName || `export_${new Date().toISOString().slice(0, 10)}.xlsx`;
- writeFileXLSX(workbook, fileName, { bookType: 'xlsx', type: 'binary', compression: true });
+ const workbook = worksheet.toWorkbook();
+ workbook.exportFile(fileName);
}
/**
@@ -93,41 +184,34 @@ export function exportToXLSX(
*/
export async function importFromXLSX>(
file: File,
- options: { headers?: Header[] } = {},
-): Promise {
+ options?: ImportBaseOptions,
+): Promise;
+export async function importFromXLSX(file: File, options: { asWorkBook: true }): Promise;
+export async function importFromXLSX>(
+ file: File,
+ options: ImportBaseOptions & { asWorkBook?: boolean } = {},
+) {
+ const { headers, asWorkBook } = options;
+
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
try {
- const data = new Uint8Array(event.target?.result as ArrayBuffer);
- const workbook = read(data, { type: 'array' });
- const sheetName = workbook.SheetNames[0]; // 默认读取第一个工作表
- const worksheet = workbook.Sheets[sheetName];
- let jsonData = utils.sheet_to_json(worksheet);
-
- if (options.headers) {
- jsonData = jsonData.map((item) => {
- const mappedItem: Record = {};
- options.headers?.forEach((header) => {
- const value = header.parseImportValue
- ? header.parseImportValue(item[header.label])
- : item[header.label];
- setAttribute(mappedItem, header.prop, value);
- });
- return mappedItem as T;
- });
+ const wb = Workbook.fromArrayBuffer(event.target?.result as ArrayBuffer);
+ if (asWorkBook) {
+ resolve(wb);
}
+ const ws = wb.getSheet(0); // 默认读取第一个工作表
+ const jsonData = ws.toJson({ headers });
resolve(jsonData);
} catch (error) {
reject(error);
}
};
-
reader.onerror = (error) => {
reject(error);
};
-
reader.readAsArrayBuffer(file);
});
}
diff --git a/src/logic/web-injectors.ts b/src/logic/web-injectors.ts
index 24e81f8..452609d 100644
--- a/src/logic/web-injectors.ts
+++ b/src/logic/web-injectors.ts
@@ -95,7 +95,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
data = await this.run(async () => {
const items = Array.from(
document.querySelectorAll(
- '.puis-card-container:has(.a-section.a-spacing-small.puis-padding-left-small)',
+ '.puis-card-container',
) as unknown as HTMLDivElement[],
).filter((e) => e.getClientRects().length > 0);
const linkObjs = items.reduce<
@@ -124,7 +124,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
default:
break;
}
- data = data && data.filter((r) => new URL(r.link).pathname.includes('/dp/'));
+ data = data && data.filter((r) => new URL(r.link).pathname.includes('/dp/')); // No advertisement only
return data;
}
@@ -183,7 +183,7 @@ export class AmazonDetailPageInjector extends BaseInjector {
return this.run(async () => {
const title = document.querySelector('#title')!.innerText;
const price = document.querySelector(
- '.a-price:not(.a-text-price) .a-offscreen',
+ '.aok-offscreen, .a-price:not(.a-text-price) .a-offscreen',
)?.innerText;
return { title, price };
});
@@ -313,7 +313,14 @@ export class AmazonDetailPageInjector extends BaseInjector {
commentNode.querySelectorAll(
'.review-image-tile-section img[src] img[src]',
),
- ).map((e) => e.getAttribute('src')!);
+ ).map((e) => {
+ const url = new URL(e.getAttribute('src')!);
+ const paths = url.pathname.split('/');
+ const chunks = paths[paths.length - 1].split('.');
+ paths[paths.length - 1] = `${chunks[0]}.${chunks[chunks.length - 1]}`;
+ url.pathname = paths.join('/');
+ return url.toString();
+ });
items.push({ id, username, title, rating, dateInfo, content, imageSrc });
}
return items;
@@ -387,7 +394,14 @@ export class AmazonReviewPageInjector extends BaseInjector {
)!.innerText;
const imageSrc = Array.from(
commentNode.querySelectorAll('.review-image-tile-section img[src]'),
- ).map((e) => e.getAttribute('src')!);
+ ).map((e) => {
+ const url = new URL(e.getAttribute('src')!);
+ const paths = url.pathname.split('/');
+ const chunks = paths[paths.length - 1].split('.');
+ paths[paths.length - 1] = `${chunks[0]}.${chunks[chunks.length - 1]}`;
+ url.pathname = paths.join('/');
+ return url.toString();
+ });
items.push({ id, username, title, rating, dateInfo, content, imageSrc });
}
return items;
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 716ffbb..0ebb5d6 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -18,3 +18,26 @@ body,
hover:opacity-100 hover:text-teal-600;
font-size: 0.9em;
}
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: #e2e8f0 #f7fafc;
+}
+
+/* For Webkit browsers */
+*::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ background: #f7fafc;
+}
+
+*::-webkit-scrollbar-thumb {
+ background: linear-gradient(135deg, #cbd5e1 40%, #a0aec0 100%);
+ border-radius: 8px;
+ border: 2px solid #f7fafc;
+}
+
+*::-webkit-scrollbar-track {
+ background: #f7fafc;
+ border-radius: 8px;
+}