Ktight 90110a48f0
Feat/advertisement (#26)
* feat: add Amazon collection metadata and reliable sync

* fix: route extension sync through configured server

* chore: release extension v0.7.13

* fix: handle extension ports across BFCache navigation

* fix: classify bottom Sponsored Brands video placements

* fix: exclude hidden Amazon ad cards

* fix: derive Amazon result page from URL

* fix: ignore hidden Amazon sponsored products

* fix: preserve base Amazon product collection

* fix: bind Amazon results to visible page

* fix: classify visible Amazon ad formats

* fix: wait for lazy Amazon ad modules

* perf: reduce Amazon lazy-load scroll stops

* fix: classify Amazon sponsored brand and video search ads

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: collect Shop by type thematic SP ads via redirect chains without data-asin

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: classify sponsored ads across new Amazon templates

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: PetrichorFun <127877748+PetrichorFun@users.noreply.github.com>
2026-09-10 18:03:11 +08:00

221 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import type { Timeline } from '~/components/ProgressReport.vue';
import { usePageWorker } from '~/page-worker';
import { detailAsinInput, detailWorkerSettings } from '~/storages/amazon';
const message = useMessage();
const timelines = ref<Timeline[]>([]);
const asinInputRef = useTemplateRef('asin-input');
//#region Page Worker 初始化Code
const worker = usePageWorker('amazon', { objects: ['detail'] });
worker.on('error', ({ message: msg }) => {
timelines.value.push({
type: 'error',
title: '错误发生',
time: new Date().toLocaleString(),
content: msg,
});
message.error(msg);
});
worker.on('item-detail-redirected', (ev) => {
timelines.value.push({
type: 'warning',
title: '商品发生重定向',
time: new Date().toLocaleString(),
content: `请求 ASIN:${ev.requestedAsin}\n实际 ASIN:${ev.resolvedAsin}\n数据将按实际 ASIN 保存`,
});
});
worker.on('item-base-info-collected', (ev) => {
timelines.value.push({
type: 'success',
title: `商品${ev.asin}基本信息`,
time: new Date().toLocaleString(),
content: `标题: ${ev.title};价格:${ev.price}; 评分: ${ev.rating}; 评论数: ${ev.ratingCount}`,
});
});
worker.on('item-category-rank-collected', (ev) => {
timelines.value.push({
type: 'success',
title: `商品${ev.asin}分类排名`,
time: new Date().toLocaleString(),
content: [
ev.category1 ? `#${ev.category1.rank} in ${ev.category1.name}` : '',
ev.category2 ? `#${ev.category2.rank} in ${ev.category2.name}` : '',
].join('\n'),
});
});
worker.on('item-images-collected', (ev) => {
const imageCount = ev.imageUrls?.length || 0;
timelines.value.push({
type: imageCount > 0 ? 'success' : 'warning',
title: `商品${ev.asin}图像`,
time: new Date().toLocaleString(),
content:
imageCount > 0
? `图片数: ${imageCount}`
: '未找到商品图片;Amazon 页面未提供可识别的图片数据',
});
});
// worker.on('item-top-reviews-collected', (ev) => {
// timelines.value.push({
// type: 'success',
// title: `商品${ev.asin}精选评论`,
// time: new Date().toLocaleString(),
// content: `精选评论数: ${ev.topReviews!.length}`,
// });
// });
worker.on('item-aplus-screenshot-collect', (ev) => {
timelines.value.push({
type: 'success',
title: `商品${ev.asin}的A+截图`,
time: new Date().toLocaleString(),
content: `获取到A+截图`,
});
});
worker.on('item-extra-info-collect', (ev) => {
timelines.value.push({
type: 'success',
title: `商品${ev.asin}额外信息`,
time: new Date().toLocaleString(),
content: `获取商品的额外信息`,
});
});
worker.on('invalid-asin-error', (ev) => {
timelines.value.push({
type: 'warning',
title: `跳过无效ASIN`,
time: new Date().toLocaleString(),
content: `ASIN ${ev.asin} 页面无效,已跳过`,
});
});
//#endregion
const launch = async () => {
const asinList = detailAsinInput.value.split(/\n|\s|,|;/).filter((item) => item.length > 0);
timelines.value = [
{
type: 'info',
title: '开始',
time: new Date().toLocaleString(),
content: '开始数据采集',
},
];
await worker.runDetailPageTask(asinList, {
progress: (remains) => {
detailAsinInput.value = remains.join('\n');
},
...detailWorkerSettings.value,
});
const failed = Boolean(worker.lastError?.value);
timelines.value.push({
type: failed ? 'error' : 'info',
title: '结束',
time: new Date().toLocaleString(),
content: failed ? '数据采集失败,已保留此前成功采集的数据' : '数据采集完成',
});
};
const handleStart = () => {
asinInputRef.value?.validate().then(async (success) => success && launch());
};
const handleInterrupt = () => {
if (!worker.isRunning.value) return;
worker.stop();
message.info('已触发中断,正在等待当前任务完成。', { duration: 2000 });
};
</script>
<template>
<div class="detail-page-entry">
<header-title>Amazon Detail</header-title>
<div class="interative-section">
<id-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 #popover>
<div class="setting-panel">
<n-form
label-placement="left"
:label-width="60"
label-align="center"
:show-feedback="false"
>
<n-form-item label="Image:">
<n-switch v-model:value="detailWorkerSettings.image" />
</n-form-item>
<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>
开始
</optional-button>
<n-button v-else round size="large" type="primary" @click="handleInterrupt">
<n-icon :size="20">
<ant-design-thunderbolt-outlined />
</n-icon>
停止
</n-button>
</div>
<div v-if="worker.isRunning.value" class="running-tip-section">
<n-alert title="Warning" type="warning"> 警告,在插件运行期间请勿与浏览器交互。 </n-alert>
</div>
<progress-report class="progress-report" :timelines="timelines" />
</div>
</template>
<style scoped lang="scss">
.detail-page-entry {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.interative-section {
display: flex;
flex-direction: column;
padding: 15px;
align-items: stretch;
justify-content: center;
width: 85%;
border-radius: 10px;
border: 1px #00000020 dashed;
margin: 0 0 10px 0;
}
.running-tip-section {
margin: 10px 0 0 0;
height: 100px;
border-radius: 10px;
cursor: wait;
}
.progress-report {
margin-top: 10px;
width: 95%;
}
.setting-panel {
padding: 7px 5px;
}
</style>