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