feat: 初始化项目,并且接近完成 ozon 部分
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 图片提取 - 主图、SKU、详情图、视频
|
||||
* 从 extension-v1 移植,新增:
|
||||
* - srcset 处理(Ozon 画廊是 <img srcset> / <picture><source>)
|
||||
* - toOriginalUrl 传平台规则(Ozon /wc\d+/)
|
||||
*/
|
||||
import {
|
||||
toAbsoluteUrl,
|
||||
toOriginalUrl,
|
||||
urlInBrackets,
|
||||
looksLikeImageUrl,
|
||||
dedupeKey,
|
||||
pickBestFromSrcset,
|
||||
} from './url';
|
||||
import { queryAllDeep } from './dom';
|
||||
import type { ImageGroupKey, SiteProfile, SrcProp } from '../profiles/types';
|
||||
|
||||
export interface ImageMaterial {
|
||||
key: string; // 'main-001'
|
||||
groupKey: ImageGroupKey; // 'main'
|
||||
groupName: string; // '主图'
|
||||
variantName?: string; // SKU 规格名(仅 sku 组)
|
||||
url: string; // 已还原为原图
|
||||
thumbUrl: string; // 页面上的原始小图地址
|
||||
index: number;
|
||||
type: 'img' | 'video';
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/** 从元素上读出图片地址与名称,按 srcProps 顺序降级 */
|
||||
function readImageSource(
|
||||
el: Element,
|
||||
srcProps: SrcProp[],
|
||||
nameSelectors?: string[]
|
||||
): { url: string; name: string; imgEl: HTMLImageElement | null } {
|
||||
let url = '';
|
||||
let name = '';
|
||||
let imgEl: HTMLImageElement | null = el instanceof HTMLImageElement ? el : null;
|
||||
|
||||
for (const prop of srcProps) {
|
||||
if (url) break;
|
||||
|
||||
if (prop === 'backgroundImage') {
|
||||
if (el.tagName === 'IMG') {
|
||||
const img = el as HTMLImageElement;
|
||||
url = img.currentSrc || img.src || '';
|
||||
name = img.alt || '';
|
||||
} else {
|
||||
const bg = getComputedStyle(el).backgroundImage || '';
|
||||
const cand = (urlInBrackets(bg) || bg).replace(/['"]/g, '');
|
||||
if (looksLikeImageUrl(cand)) url = cand;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prop === 'srcset') {
|
||||
// <img srcset> 或 <source srcset>
|
||||
const raw = el.getAttribute('srcset') || (el as any).srcset || '';
|
||||
if (raw) url = pickBestFromSrcset(raw);
|
||||
continue;
|
||||
}
|
||||
|
||||
const raw = (el as any)[prop] || el.getAttribute(prop);
|
||||
if (raw) {
|
||||
// srcset 场景下 currentSrc 才是实际加载的那张
|
||||
url = prop === 'src' ? ((el as HTMLImageElement).currentSrc || (el as HTMLImageElement).src || '') : raw;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择器命中的是容器、图在子节点上
|
||||
if (!url && el.tagName !== 'IMG') {
|
||||
const inner = el.querySelector('img, source');
|
||||
if (inner) {
|
||||
const srcset = inner.getAttribute('srcset');
|
||||
url = srcset
|
||||
? pickBestFromSrcset(srcset)
|
||||
: inner.getAttribute('data-src') || (inner as HTMLImageElement).currentSrc || (inner as HTMLImageElement).src || '';
|
||||
if (inner instanceof HTMLImageElement) imgEl = inner;
|
||||
if (!name && inner instanceof HTMLImageElement) name = inner.alt || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 名称统一取(SKU 规格名)
|
||||
if (!name && nameSelectors?.length) {
|
||||
for (const sel of nameSelectors) {
|
||||
const t = el.querySelector(sel)?.textContent?.trim();
|
||||
if (t) {
|
||||
name = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { url: url ? toAbsoluteUrl(url) : '', name, imgEl };
|
||||
}
|
||||
|
||||
export function collectImages(profile: SiteProfile): ImageMaterial[] {
|
||||
const result: ImageMaterial[] = [];
|
||||
|
||||
for (const group of profile.imageGroups) {
|
||||
const srcProps = group.srcProps ?? profile.defaultSrcProps;
|
||||
// 去重按组独立:一张图同时是主图和 SKU 图是正常的
|
||||
const seen = new Set<string>();
|
||||
const activeSet = new Set(group.activeSelectors ? queryAllDeep(group.activeSelectors) : []);
|
||||
|
||||
for (const el of queryAllDeep(group.selectors)) {
|
||||
if (activeSet.has(el)) continue;
|
||||
if (group.excludeWithin?.some((sel) => el.closest(sel))) continue;
|
||||
|
||||
const { url: rawUrl, name, imgEl } = readImageSource(el, srcProps, group.nameSelectors);
|
||||
if (!rawUrl) continue;
|
||||
|
||||
if (group.type === 'video' && !/\.(mp4|avi|mov|wmv|m3u8|webm)(\?|$)/i.test(rawUrl) && !/^blob:/i.test(rawUrl)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = group.type === 'img' ? toOriginalUrl(rawUrl, profile.originalUrlRules) : rawUrl;
|
||||
|
||||
// 尺寸过滤
|
||||
if (group.type === 'img' && (group.minWidth || group.minHeight)) {
|
||||
const measured = imgEl ?? (el as HTMLElement);
|
||||
const w = (measured as HTMLImageElement).naturalWidth || (measured as HTMLElement).offsetWidth || 0;
|
||||
const h = (measured as HTMLImageElement).naturalHeight || (measured as HTMLElement).offsetHeight || 0;
|
||||
if (w > 0 && h > 0 && (w < (group.minWidth ?? 0) || h < (group.minHeight ?? 0))) continue;
|
||||
}
|
||||
|
||||
const k = group.key === 'sku' ? `${dedupeKey(url, profile.originalUrlRules)}::${name}` : dedupeKey(url, profile.originalUrlRules);
|
||||
if (seen.has(k)) continue;
|
||||
seen.add(k);
|
||||
|
||||
result.push({
|
||||
key: `${group.key}-${String(result.filter((r) => r.groupKey === group.key).length + 1).padStart(3, '0')}`,
|
||||
groupKey: group.key,
|
||||
groupName: group.name,
|
||||
variantName: group.key === 'sku' ? name || undefined : undefined,
|
||||
url,
|
||||
thumbUrl: rawUrl,
|
||||
index: result.length,
|
||||
type: group.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user