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
9f296e337b
commit
127fb5866a
@ -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",
|
||||
|
||||
@ -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 || '-' }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="图片链接" :span="4">
|
||||
<div v-for="link in model.imageUrls">
|
||||
{{ link }}
|
||||
</div>
|
||||
<image-link v-for="link in model.imageUrls" :url="link" />
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item
|
||||
<!-- <n-descriptions-item
|
||||
v-if="model.topReviews && model.topReviews.length > 0"
|
||||
label="精选评论"
|
||||
:span="4"
|
||||
@ -69,7 +68,7 @@ const handleLoadMore = () => {
|
||||
更多评论
|
||||
</n-button>
|
||||
</div>
|
||||
</n-descriptions-item>
|
||||
</n-descriptions-item> -->
|
||||
</n-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
78
src/components/ImageLink.vue
Normal file
78
src/components/ImageLink.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
url: string;
|
||||
}>();
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const cached = ref<string | undefined>();
|
||||
|
||||
watch(
|
||||
() => props.url,
|
||||
(newVal, oldVal) => {
|
||||
if (newVal !== oldVal && cached.value) {
|
||||
URL.revokeObjectURL(cached.value);
|
||||
cached.value = undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const loadImage = () => {
|
||||
if (cached.value) {
|
||||
return;
|
||||
}
|
||||
fetch(props.url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('加载失败');
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
cached.value = URL.createObjectURL(blob);
|
||||
})
|
||||
.catch((_error) => {
|
||||
message.error(`加载图片失败`);
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (cached.value) {
|
||||
URL.revokeObjectURL(cached.value);
|
||||
cached.value = undefined;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-popover
|
||||
@update:show="(v) => v && loadImage()"
|
||||
trigger="hover"
|
||||
placement="right"
|
||||
:delay="1000"
|
||||
:duration="1000"
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="link-text">{{ url }}</span>
|
||||
</template>
|
||||
<img v-if="cached" :src="cached" />
|
||||
<n-text v-else>加载中...</n-text>
|
||||
</n-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.link-text {
|
||||
cursor: default;
|
||||
font-family: v-mono;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 50vw;
|
||||
max-height: 50vh;
|
||||
}
|
||||
</style>
|
||||
@ -1,20 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton } from 'naive-ui';
|
||||
import { NButton, NSpace } from 'naive-ui';
|
||||
import type { TableColumn } from 'naive-ui/es/data-table/src/interface';
|
||||
import { exportToXLSX, Header, importFromXLSX } from '~/logic/data-io';
|
||||
import type { AmazonDetailItem, AmazonItem } from '~/logic/page-worker/types';
|
||||
import { allItems } from '~/logic/storage';
|
||||
import type { AmazonDetailItem, AmazonItem, AmazonReview } from '~/logic/page-worker/types';
|
||||
import { allItems, reviewItems } from '~/logic/storage';
|
||||
import DetailDescription from './DetailDescription.vue';
|
||||
import ReviewPreview from './ReviewPreview.vue';
|
||||
|
||||
const message = useMessage();
|
||||
const modal = useModal();
|
||||
|
||||
const page = reactive({ current: 1, size: 10 });
|
||||
|
||||
const filter = reactive({
|
||||
keywords: null as string | null,
|
||||
const defaultFilter = {
|
||||
keywords: undefined as string | undefined,
|
||||
search: '',
|
||||
detailOnly: false,
|
||||
});
|
||||
};
|
||||
const filter = reactive(defaultFilter);
|
||||
const filterFormItems = computed(() => {
|
||||
const records = allItems.value;
|
||||
return [
|
||||
@ -38,11 +41,7 @@ const filterFormItems = computed(() => {
|
||||
});
|
||||
|
||||
const onFilterReset = () => {
|
||||
Object.assign(filter, {
|
||||
keywords: null as string | null,
|
||||
search: '',
|
||||
detailOnly: false,
|
||||
});
|
||||
Object.assign(filter, defaultFilter);
|
||||
};
|
||||
|
||||
const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
||||
@ -96,23 +95,53 @@ const columns: (TableColumn<AmazonItem> & { hidden?: boolean })[] = [
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
title: '链接',
|
||||
key: 'link',
|
||||
title: '查看',
|
||||
key: 'actions',
|
||||
minWidth: 100,
|
||||
render(row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
size: 'small',
|
||||
onClick: async () => {
|
||||
await browser.tabs.create({
|
||||
active: true,
|
||||
url: row.link,
|
||||
});
|
||||
return h(NSpace, {}, () =>
|
||||
[
|
||||
{
|
||||
text: '评论',
|
||||
disabled: !reviewItems.value.has(row.asin),
|
||||
onClick: () => {
|
||||
const asin = row.asin;
|
||||
modal.create({
|
||||
title: `${asin}评论`,
|
||||
preset: 'card',
|
||||
style: {
|
||||
width: '80vw',
|
||||
height: '85vh',
|
||||
},
|
||||
content: () =>
|
||||
h(ReviewPreview, {
|
||||
asin,
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
() => '前往',
|
||||
{
|
||||
text: '链接',
|
||||
onClick: () => {
|
||||
browser.tabs.create({
|
||||
active: true,
|
||||
url: row.link,
|
||||
});
|
||||
},
|
||||
},
|
||||
].map(({ text, onClick, disabled }) =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
text: true,
|
||||
size: 'small',
|
||||
disabled: disabled,
|
||||
onClick: onClick,
|
||||
},
|
||||
() => text,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
@ -133,6 +162,7 @@ const itemView = computed<{ records: AmazonItem[]; pageCount: number; origin: Am
|
||||
);
|
||||
|
||||
const extraHeaders: Header[] = [
|
||||
{ prop: 'link', label: '商品链接' },
|
||||
{
|
||||
prop: 'hasDetail',
|
||||
label: '有详情',
|
||||
@ -153,6 +183,36 @@ const extraHeaders: Header[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const reviewHeaders: Header[] = [
|
||||
{ prop: 'asin', label: 'ASIN' },
|
||||
{ prop: 'username', label: '用户名' },
|
||||
{ prop: 'title', label: '标题' },
|
||||
{ prop: 'rating', label: '评分' },
|
||||
{ prop: 'content', label: '内容' },
|
||||
{ prop: 'dateInfo', label: '日期' },
|
||||
{
|
||||
prop: 'imageSrc',
|
||||
label: '图片链接',
|
||||
formatOutputValue: (val?: string[]) => val?.join(';'),
|
||||
parseImportValue: (val?: string) => val?.split(';'),
|
||||
},
|
||||
];
|
||||
|
||||
const getItemHeaders = () => {
|
||||
return columns
|
||||
.filter((col: Record<string, any>) => col.key !== 'actions')
|
||||
.reduce(
|
||||
(p, v: Record<string, any>) => {
|
||||
if ('key' in v && 'title' in v) {
|
||||
p.push({ label: v.title, prop: v.key });
|
||||
}
|
||||
return p;
|
||||
},
|
||||
[] as { label: string; prop: string }[],
|
||||
)
|
||||
.concat(extraHeaders) as Header[];
|
||||
};
|
||||
|
||||
const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
|
||||
const { search, detailOnly, keywords } = filter;
|
||||
if (search.trim() !== '') {
|
||||
@ -172,41 +232,52 @@ const filterItemData = (data: AmazonItem[]): AmazonItem[] => {
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const headers: Header[] = columns
|
||||
.reduce(
|
||||
(p, v: Record<string, any>) => {
|
||||
if ('key' in v && 'title' in v) {
|
||||
p.push({ label: v.title, prop: v.key });
|
||||
}
|
||||
return p;
|
||||
},
|
||||
[] as { label: string; prop: string }[],
|
||||
)
|
||||
.concat(extraHeaders);
|
||||
const { origin: data } = itemView.value;
|
||||
exportToXLSX(data, { headers });
|
||||
const itemHeaders = getItemHeaders();
|
||||
const items = toRaw(itemView.value).origin;
|
||||
const asins = new Set(items.map((e) => e.asin));
|
||||
const reviews = toRaw(reviewItems.value)
|
||||
.entries()
|
||||
.filter(([asin]) => asins.has(asin))
|
||||
.reduce<(AmazonReview & { asin: string })[]>((a, [asin, reviews]) => {
|
||||
a.push(...reviews.map((r) => ({ asin, ...r })));
|
||||
return a;
|
||||
}, []);
|
||||
|
||||
const sheet1 = exportToXLSX(items, { headers: itemHeaders, asWorkSheet: true });
|
||||
const wb = sheet1.toWorkbook('items');
|
||||
const sheet2 = exportToXLSX(reviews, { headers: reviewHeaders, asWorkSheet: true });
|
||||
wb.addSheet('reviews', sheet2);
|
||||
wb.exportFile(`Items ${dayjs().format('YYYY-MM-DD')}.xlsx`);
|
||||
message.info('导出完成');
|
||||
};
|
||||
|
||||
const handleImport = async (file: File) => {
|
||||
const headers: Header[] = columns
|
||||
.reduce(
|
||||
(p, v: Record<string, any>) => {
|
||||
if ('key' in v && 'title' in v) {
|
||||
p.push({ label: v.title, prop: v.key });
|
||||
}
|
||||
return p;
|
||||
},
|
||||
[] as { label: string; prop: string }[],
|
||||
)
|
||||
.concat(extraHeaders);
|
||||
const importedData = await importFromXLSX<AmazonItem>(file, { headers });
|
||||
allItems.value = importedData; // 覆盖原数据
|
||||
message.info(`成功导入 ${file.name} 文件 ${importedData.length} 条数据`);
|
||||
const itemHeaders = getItemHeaders();
|
||||
const wb = await importFromXLSX(file, { asWorkBook: true });
|
||||
|
||||
const sheet1 = wb.getSheet(0);
|
||||
const items = sheet1.toJson<AmazonItem>({ headers: itemHeaders });
|
||||
allItems.value = items;
|
||||
|
||||
if (wb.sheetCount > 1) {
|
||||
const sheet2 = wb.getSheet(1);
|
||||
const reviews = sheet2.toJson<AmazonReview & { asin?: string }>({ headers: reviewHeaders });
|
||||
reviewItems.value = reviews.reduce((m, r) => {
|
||||
const asin = r.asin!;
|
||||
delete r.asin;
|
||||
m.has(asin) || m.set(asin, []);
|
||||
const arr = m.get(asin)!;
|
||||
arr.push(r);
|
||||
return m;
|
||||
}, new Map<string, AmazonReview[]>());
|
||||
}
|
||||
|
||||
message.info(`成功导入 ${file.name} 文件`);
|
||||
};
|
||||
|
||||
const handleClearData = async () => {
|
||||
allItems.value = [];
|
||||
reviewItems.value = new Map();
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -229,6 +300,7 @@ const handleClearData = async () => {
|
||||
size="small"
|
||||
placeholder="输入文本过滤结果"
|
||||
round
|
||||
clearable
|
||||
style="min-width: 230px"
|
||||
/>
|
||||
<control-strip
|
||||
|
||||
@ -13,9 +13,16 @@ defineProps<{
|
||||
<div v-for="paragraph in model.content.split('\n')">
|
||||
{{ paragraph }}
|
||||
</div>
|
||||
<template v-if="model.imageSrc">
|
||||
<n-code :code="model.imageSrc.join('\n')" />
|
||||
</template>
|
||||
<div class="image-link-container" v-if="model.imageSrc">
|
||||
<image-link v-for="link in model.imageSrc" :url="link" />
|
||||
</div>
|
||||
<div style="color: gray; font-size: smaller">{{ model.dateInfo }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-link-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -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('导出完成');
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -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<string, unknown>[], 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<string, unknown> = {};
|
||||
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<T>(options: { headers?: Header[] } = {}) {
|
||||
const { headers } = options;
|
||||
|
||||
let jsonData = utils.sheet_to_json<Record<string, unknown>>(this._raw);
|
||||
if (headers) {
|
||||
jsonData = jsonData.map((item) => {
|
||||
const mappedItem: Record<string, unknown> = {};
|
||||
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<T extends unknown>(
|
||||
obj: Record<string, unknown>,
|
||||
@ -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<string, unknown>[],
|
||||
options: {
|
||||
fileName?: string;
|
||||
headers?: Header[];
|
||||
} = {},
|
||||
): void {
|
||||
if (!data.length) {
|
||||
return;
|
||||
options?: ExportBaseOptions & { asWorkSheet?: false },
|
||||
): void;
|
||||
export function exportToXLSX(
|
||||
data: Record<string, unknown>[],
|
||||
options: Omit<ExportBaseOptions, 'fileName'> & { asWorkSheet: true },
|
||||
): Worksheet;
|
||||
export function exportToXLSX(
|
||||
data: Record<string, unknown>[],
|
||||
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<string, unknown> = {};
|
||||
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<T extends Record<string, unknown>>(
|
||||
file: File,
|
||||
options: { headers?: Header[] } = {},
|
||||
): Promise<T[]> {
|
||||
options?: ImportBaseOptions,
|
||||
): Promise<T[]>;
|
||||
export async function importFromXLSX(file: File, options: { asWorkBook: true }): Promise<Workbook>;
|
||||
export async function importFromXLSX<T extends Record<string, unknown>>(
|
||||
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<T>(worksheet);
|
||||
|
||||
if (options.headers) {
|
||||
jsonData = jsonData.map((item) => {
|
||||
const mappedItem: Record<string, unknown> = {};
|
||||
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<T>({ headers });
|
||||
resolve(jsonData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (error) => {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
@ -95,7 +95,7 @@ export class AmazonSearchPageInjector extends BaseInjector {
|
||||
data = await this.run(async () => {
|
||||
const items = Array.from(
|
||||
document.querySelectorAll<HTMLDivElement>(
|
||||
'.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<HTMLElement>('#title')!.innerText;
|
||||
const price = document.querySelector<HTMLElement>(
|
||||
'.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<HTMLImageElement>(
|
||||
'.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<HTMLImageElement>('.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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user