feat: 初步开发一些特性

This commit is contained in:
meishibiezb
2026-05-01 21:13:16 +08:00
parent a25145638f
commit a6b86934af
6 changed files with 527 additions and 107 deletions

144
components/page-marker.ts Normal file
View File

@@ -0,0 +1,144 @@
/**
* 页面标记组件 —— 在网页上显示一个固定位置的“已锁定”标记
*/
export interface PageMarkerOptions {
/** 标记显示的文本,默认为“🔒 已锁定” */
text?: string;
/** 位置预设:'top-right' | 'top-left' | 'bottom-right' | 'bottom-left',默认 'top-right' */
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
/** 背景色,默认 rgba(0,0,0,0.75) */
backgroundColor?: string;
/** 字体颜色,默认 #fff */
color?: string;
/** 自定义样式,会与默认样式合并 */
customStyle?: Partial<CSSStyleDeclaration>;
/** 几秒后自动消失,设为 0 则永久存在,默认 0 */
autoRemoveSeconds?: number;
/** 是否允许用户点击关闭,默认 false */
closable?: boolean;
}
export class PageMarker {
private element: HTMLDivElement | null = null;
private timer: ReturnType<typeof setTimeout> | null = null;
constructor(private options: PageMarkerOptions = {}) {}
/**
* 在页面上显示标记
*/
show(): void {
if (this.element) this.hide();
const {
text = '🔒 已锁定',
position = 'top-right',
backgroundColor = 'rgba(0,0,0,0.75)',
color = '#fff',
autoRemoveSeconds = 0,
closable = false,
customStyle = {},
} = this.options;
const el = document.createElement('div');
el.textContent = text;
el.title = '此页面已被插件锁定';
const baseStyle: Record<string, string> = {
position: 'fixed',
zIndex: '99999',
padding: '6px 14px',
borderRadius: '4px',
fontSize: '13px',
fontFamily: 'Arial, sans-serif',
fontWeight: '600',
backgroundColor,
color,
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
pointerEvents: closable ? 'auto' : 'none',
userSelect: 'none',
transition: 'opacity 0.2s ease',
...positionStyles(position),
...(customStyle as Record<string, string>),
};
Object.assign(el.style, baseStyle);
// 可关闭按钮
if (closable) {
const closeBtn = document.createElement('span');
closeBtn.textContent = '×';
closeBtn.style.cssText = 'margin-left:8px;cursor:pointer;font-weight:bold;opacity:0.7';
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.hide();
});
el.appendChild(closeBtn);
}
document.body.appendChild(el);
this.element = el;
if (autoRemoveSeconds > 0) {
this.timer = setTimeout(() => this.hide(), autoRemoveSeconds * 1000);
}
}
/**
* 隐藏/移除标记
*/
hide(): void {
if (this.element) {
this.element.style.opacity = '0';
setTimeout(() => {
this.element?.remove();
this.element = null;
}, 200);
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
/**
* 更新标记文本
*/
updateText(text: string): void {
if (!this.element) return;
// 保留关闭按钮
const closeBtn = this.element.querySelector('span');
this.element.childNodes.forEach(node => {
if (node !== closeBtn) node.remove();
});
this.element.prepend(document.createTextNode(text));
}
/**
* 标记当前是否已显示
*/
get isVisible(): boolean {
return !!this.element;
}
}
function positionStyles(pos: string): Record<string, string> {
const offset = '12px';
switch (pos) {
case 'top-right': return { top: offset, right: offset };
case 'top-left': return { top: offset, left: offset };
case 'bottom-right': return { bottom: offset, right: offset };
case 'bottom-left': return { bottom: offset, left: offset };
default: return { top: offset, right: offset };
}
}
/**
* 快捷函数:直接显示一个简单标记
*/
export function showPageMarker(options?: PageMarkerOptions): PageMarker {
const marker = new PageMarker(options);
marker.show();
return marker;
}

143
components/pin-collector.ts Normal file
View File

@@ -0,0 +1,143 @@
/**
* 花瓣网 Pin 收集器 —— 自动提取页面中所有 Pin 卡片信息
*/
export interface PinData {
/** 相对路径,如 "/pins/7118240882" */
url: 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');
return {
url: href,
imgSrc: img.src,
imgWidth: width,
imgHeight: height,
alt: altText,
title: titleText,
author,
time,
tags,
};
}
}
/**
* 快捷函数:直接返回当前页面的所有 Pin 数据
*/
export function collectPins(): PinData[] {
const collector = new PinCollector();
return collector.collect();
}