Files
custom-plugin/components/pin-collector.ts
2026-05-04 22:36:37 +08:00

212 lines
5.8 KiB
TypeScript
Raw Permalink 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.
/**
* 花瓣网 Pin 收集器 —— 自动提取页面中所有 Pin 卡片信息
*/
export interface PinData {
/** 相对路径,如 "/pins/7118240882" */
url: string;
/** 图片缩略图地址 */
imgSmallSrc: string;
/** 图片源地址 */
imgSrc: string;
/** 图片原始宽度 */
imgWidth: number;
/** 图片原始高度 */
imgHeight: number;
/** alt 原始文本 */
alt: string;
/** title 原始文本 */
title: string;
/** 提取的作者名 */
author?: string;
/** 提取的发布时间,如 "15小时" */
time?: string;
/** 提取的标签,不含 # 号 */
tags: string[];
}
export class PinCollector {
private pins: PinData[] = [];
/**
* 扫描当前文档,收集所有 Pin 数据
* @returns 收集到的 Pin 数组
*/
collect(): PinData[] {
this.pins = [];
const anchors = document.querySelectorAll<HTMLAnchorElement>(
'a.__7D5D_BHJ'
);
anchors.forEach((anchor) => {
try {
const pin = this.parsePin(anchor);
if (pin) this.pins.push(pin);
} catch (e) {
console.warn('解析 Pin 失败', e);
}
});
return this.pins;
}
/**
* 获取最近一次收集的结果(不重新扫描)
*/
getPins(): PinData[] {
return this.pins;
}
/**
* 将收集到的 Pin 保存到 browser.storage.local
* @param key 存储键名,默认 "collectedPins"
*/
async saveToStorage(key = 'collectedPins'): Promise<void> {
await browser.storage.local.set({ [key]: this.pins });
console.log(`已保存 ${this.pins.length} 个 Pin 到存储`);
}
/**
* 从 browser.storage.local 加载之前保存的 Pin
* @param key 存储键名
* @returns 加载到的 Pin 数组
*/
async loadFromStorage(key = 'collectedPins'): Promise<PinData[]> {
const result = await browser.storage.local.get(key);
if (result[key]) {
this.pins = result[key] as PinData[];
}
return this.pins;
}
/**
* 解析单个 a 元素为 PinData
*/
private parsePin(anchor: HTMLAnchorElement): PinData | null {
const href = anchor.getAttribute('href');
if (!href) return null;
const img = anchor.querySelector<HTMLImageElement>('img.hb-image');
if (!img) return null;
const altText = img.getAttribute('alt') || '';
const titleText = img.getAttribute('title') || '';
const fullText = altText || titleText;
const parts = fullText
.split('\n')
.map((p) => p.trim())
.filter(Boolean);
// 解析作者和发布时间
let author: string | undefined;
let time: string | undefined;
if (parts.length >= 1) {
// 第一行格式通常为:作者名 @username
const authorMatch = parts[0].match(/^(.+?)\s*@/);
author = authorMatch ? authorMatch[1].trim() : parts[0];
}
if (parts.length >= 2 && parts[1].startsWith('·')) {
time = parts[1].replace(/^·\s*/, '').trim();
}
// 提取标签 #xxx (支持中文)
const tags: string[] = [];
const tagRegex = /#([\w\u4e00-\u9fff]+)/g;
let match;
while ((match = tagRegex.exec(fullText)) !== null) {
tags.push(match[1]);
}
const width = parseFloat(img.getAttribute('width') || '0');
const height = parseFloat(img.getAttribute('height') || '0');
const originalSrc = img.src.replace(/_fw\d+(webp)?/, '');
return {
url: href,
imgSmallSrc: img.src,
imgSrc: originalSrc,
imgWidth: width,
imgHeight: height,
alt: altText,
title: titleText,
author,
time,
tags,
};
}
}
/**
* 快捷函数:直接返回当前页面的所有 Pin 数据
*/
export function collectPins(): PinData[] {
const collector = new PinCollector();
return collector.collect();
}
/**
* 从花瓣 API 响应中的单个 pin 对象提取 PinData
* @param pin - /v3/boards/{id}/pins 返回的 pins 数组元素
*/
export function extractPinFromApi(pin: any): PinData {
return {
url: `/pins/${pin.pin_id}/`,
imgSmallSrc: pin.file?.url ?? '',
imgSrc: (pin.file?.url ?? '').replace(/_fw\d+webp|_png/, ''),
imgWidth: pin.file?.width ?? 0,
imgHeight: pin.file?.height ?? 0,
alt: pin.raw_text ?? '',
title: pin.raw_text ?? '',
author: parseAuthorFromRawText(pin.raw_text) ?? '',
time: formatTimestamp(pin.created_at) ?? '',
tags: extractTagsFromText(pin.raw_text),
};
}
function formatTimestamp(ts: number): string {
if (!ts) return '';
const date = new Date(ts * 1000); // API 给的是秒JS 需要毫秒
const now = Date.now();
const diff = now - date.getTime();
const hours = Math.floor(diff / 3600000);
if (hours < 24) return `${hours}小时`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}`;
return date.toLocaleDateString('zh-CN'); // 超过30天直接显示日期
}
/**
* 从 raw_text 中解析作者名
*/
function parseAuthorFromRawText(rawText: string): string {
if (!rawText) return '';
const lines = rawText.split('\n').map(s => s.trim()).filter(Boolean);
if (lines.length === 0) return '';
const firstLine = lines[0];
// 格式: "北道(きたみち)かえるAiart" → 去掉 @xxx 后缀
const atIndex = firstLine.indexOf('@');
if (atIndex > 0) return firstLine.slice(0, atIndex).trim();
// 格式: "@无硫火花 的个人主页 - 微博" → 取 @ 后到空格前的用户名
if (atIndex === 0) {
const username = firstLine.slice(1).split(/\s/)[0];
return username || firstLine;
}
// 格式: "Z3zz_的照片 - 微相册"
return firstLine;
}
/**
* 从文本中提取 #标签
*/
export function extractTagsFromText(text: string): string[] {
const tags: string[] = [];
const tagRegex = /#([\w\u4e00-\u9fff]+)/g;
let match;
while ((match = tagRegex.exec(text)) !== null) {
tags.push(match[1]);
}
return tags;
}