class BackgroundService {
constructor() {
this.translations = {};
this.currentLanguage = 'zh-CN';
this.init();
}
async init() {
console.log('BackgroundService 初始化中...');
await this.initLanguageManager();
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('收到消息:', request.action, sender.tab?.id);
if (request.action === 'captureElement') {
this.handleElementCapture(request.data, sender.tab.id)
.then(result => {
console.log('截图处理成功:', result);
sendResponse(result);
})
.catch(error => {
console.error('截图处理失败:', error);
sendResponse({ success: false, error: error.message });
});
return true;
} else if (request.action === 'downloadImage') {
this.handleDirectDownload(request.data)
.then(result => {
console.log('下载处理成功:', result);
sendResponse(result);
})
.catch(error => {
console.error('处理下载请求失败:', error);
sendResponse({ success: false, error: error.message });
});
return true;
} else if (request.action === 'updateContextMenus') {
const language = request.language;
this.updateContextMenus(language)
.then(() => {
sendResponse({ success: true });
})
.catch(error => {
console.error('更新右键菜单失败:', error);
sendResponse({ success: false, error: error.message });
});
return true;
}
return false;
});
chrome.action.onClicked.addListener((tab) => {
console.log('插件图标被点击,标签页ID:', tab.id);
this.toggleCapture(tab.id);
});
this.createContextMenus();
this.updateActionTitle();
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'startElementCapture') {
console.log('右键菜单被点击,标签页ID:', tab.id);
this.toggleCapture(tab.id);
} else if (info.menuItemId === 'captureSettings') {
console.log('截图设置被点击');
this.openSettings();
}
});
console.log('BackgroundService 初始化完成');
}
async initLanguageManager() {
try {
const result = await chrome.storage.sync.get(['language']);
this.currentLanguage = result.language || 'zh-CN';
const response = await fetch(chrome.runtime.getURL(`lang/${this.currentLanguage}.json`));
if (response.ok) {
this.translations = await response.json();
console.log('语言加载成功:', this.currentLanguage);
} else {
console.error('语言文件加载失败:', this.currentLanguage);
this.translations = await this.loadDefaultTranslations();
this.currentLanguage = 'zh-CN';
}
} catch (error) {
console.error('初始化语言管理器失败:', error);
this.translations = await this.loadDefaultTranslations();
this.currentLanguage = 'zh-CN';
}
}
async loadDefaultTranslations() {
try {
const response = await fetch(chrome.runtime.getURL('lang/zh-CN.json'));
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error('加载默认翻译失败:', error);
}
return {};
}
t(key, params = {}) {
if (!this.translations) return key;
const keys = key.split('.');
let value = this.translations;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
console.warn('翻译键不存在:', key);
return key;
}
}
if (typeof value === 'string') {
return value.replace(/\{(\w+)\}/g, (match, param) => {
return params[param] || match;
});
}
return value || key;
}
createContextMenus() {
chrome.contextMenus.removeAll(() => {
chrome.contextMenus.create({
id: 'elementCaptureMain',
title: this.t('app.name'),
contexts: ['page']
});
chrome.contextMenus.create({
id: 'startElementCapture',
parentId: 'elementCaptureMain',
title: this.t('contextMenu.startElementCapture'),
contexts: ['page']
});
chrome.contextMenus.create({
id: 'captureSettings',
parentId: 'elementCaptureMain',
title: this.t('contextMenu.captureSettings'),
contexts: ['page']
});
console.log('右键菜单创建完成');
});
}
async updateContextMenus(language = null) {
try {
if (language) {
this.currentLanguage = language;
const response = await fetch(chrome.runtime.getURL(`lang/${language}.json`));
if (response.ok) {
this.translations = await response.json();
console.log('语言加载成功:', language);
} else {
console.error('语言文件加载失败:', language);
return;
}
} else {
await this.initLanguageManager();
}
const startTitle = this.t('contextMenu.startElementCapture');
const settingsTitle = this.t('contextMenu.captureSettings');
console.log('准备更新右键菜单:');
console.log('- startElementCapture:', startTitle);
console.log('- captureSettings:', settingsTitle);
chrome.contextMenus.removeAll(() => {
const mainTitle = this.t('app.name');
chrome.contextMenus.create({
id: 'elementCaptureMain',
title: mainTitle,
contexts: ['page']
});
chrome.contextMenus.create({
id: 'startElementCapture',
parentId: 'elementCaptureMain',
title: startTitle,
contexts: ['page']
});
chrome.contextMenus.create({
id: 'captureSettings',
parentId: 'elementCaptureMain',
title: settingsTitle,
contexts: ['page']
});
console.log('右键菜单已重新创建,当前语言:', this.currentLanguage);
this.updateActionTitle();
});
} catch (error) {
console.error('更新右键菜单失败:', error);
}
}
updateActionTitle() {
try {
const title = this.t('app.name');
chrome.action.setTitle({ title: title });
console.log('插件标题已更新:', title);
} catch (error) {
console.error('更新插件标题失败:', error);
}
}
async handleElementCapture(captureData, tabId) {
try {
const dataUrl = await chrome.tabs.captureVisibleTab(null, {
format: 'png',
quality: 100
});
const croppedDataUrl = await this.cropImage(dataUrl, captureData);
const filename = await this.downloadImage(croppedDataUrl, captureData);
return { success: true, filename };
} catch (error) {
console.error('截图失败:', error);
throw error;
}
}
async cropImage(dataUrl, captureData) {
try {
console.log('裁剪数据:', captureData);
const response = await fetch(dataUrl);
const blob = await response.blob();
const imageBitmap = await createImageBitmap(blob);
console.log('原始图片尺寸:', imageBitmap.width, 'x', imageBitmap.height);
const scale = captureData.devicePixelRatio || 1;
const sourceX = Math.max(0, Math.round(captureData.viewportX * scale));
const sourceY = Math.max(0, Math.round(captureData.viewportY * scale));
const sourceWidth = Math.max(1, Math.round(captureData.width * scale));
const sourceHeight = Math.max(1, Math.round(captureData.height * scale));
const finalSourceX = Math.max(0, Math.min(sourceX, imageBitmap.width));
const finalSourceY = Math.max(0, Math.min(sourceY, imageBitmap.height));
const finalWidth = Math.max(1, Math.min(sourceWidth, imageBitmap.width - finalSourceX));
const finalHeight = Math.max(1, Math.min(sourceHeight, imageBitmap.height - finalSourceY));
console.log('裁剪参数详情:', { sourceX, sourceY, sourceWidth, sourceHeight, finalSourceX, finalSourceY, finalWidth, finalHeight, scale, captureData, imageBitmap: { width: imageBitmap.width, height: imageBitmap.height } });
if (finalWidth <= 0 || finalHeight <= 0) {
throw new Error(`无效的裁剪区域: ${finalWidth}x${finalHeight}`);
}
const croppedBitmap = await createImageBitmap(
imageBitmap,
finalSourceX,
finalSourceY,
finalWidth,
finalHeight
);
const outputWidth = Math.max(1, Math.round(finalWidth));
const outputHeight = Math.max(1, Math.round(finalHeight));
const canvas = new OffscreenCanvas(outputWidth, outputHeight);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(croppedBitmap, 0, 0, outputWidth, outputHeight);
const croppedBlob = await canvas.convertToBlob({
type: 'image/png',
quality: 1.0
});
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(croppedBlob);
});
} catch (error) {
console.error('图片裁剪失败:', error);
throw error;
}
}
async downloadImage(dataUrl, captureData) {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const elementInfo = captureData.elementInfo || {};
const elementName = elementInfo.tagName || 'element';
const elementId = elementInfo.id ? `-${elementInfo.id}` : '';
const elementClass = this.safeClassName(elementInfo.className);
const classPart = elementClass ? `-${elementClass}` : '';
const filename = `${elementName.toLowerCase()}${elementId}${classPart}-${timestamp}.png`;
await chrome.downloads.download({
url: dataUrl,
filename: filename,
saveAs: false
});
return filename;
} catch (error) {
console.error('下载失败:', error);
throw error;
}
}
safeString(value, defaultValue = '') {
if (typeof value === 'string') {
return value;
}
if (value && typeof value.toString === 'function') {
return value.toString();
}
return defaultValue;
}
safeClassName(className) {
const safeClass = this.safeString(className);
if (safeClass && safeClass.includes(' ')) {
return safeClass.split(' ')[0];
}
return safeClass;
}
getElementDescription(elementInfo) {
if (!elementInfo) return 'unknown';
const tagName = this.safeString(elementInfo.tagName, 'element');
const id = this.safeString(elementInfo.id, '');
const className = this.safeClassName(elementInfo.className);
const idPart = id ? `-${id}` : '';
const classPart = className ? `-${className}` : '';
return `${tagName.toLowerCase()}${idPart}${classPart}`;
}
async handleDirectDownload(data) {
try {
const { dataUrl, elementInfo } = data;
if (!dataUrl) {
throw new Error('无效的图片数据');
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const elementDesc = this.getElementDescription(elementInfo);
const filename = `element-capture-${elementDesc}-${timestamp}.png`;
return new Promise((resolve) => {
chrome.downloads.download({
url: dataUrl,
filename: filename,
saveAs: false
}, (downloadId) => {
if (chrome.runtime.lastError) {
console.error('下载失败:', chrome.runtime.lastError);
resolve({ success: false, error: chrome.runtime.lastError.message });
} else {
console.log('下载成功, ID:', downloadId);
resolve({ success: true, filename: filename, downloadId: downloadId });
}
});
});
} catch (error) {
console.error('直接下载失败:', error);
throw error;
}
}
generateFilename() {
const now = new Date();
const timestamp = now.getFullYear() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') + '_' +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0');
return `element_screenshot_${timestamp}.png`;
}
async openSettings() {
try {
await chrome.tabs.create({
url: chrome.runtime.getURL('popup.html'),
active: true
});
} catch (error) {
console.error('无法打开设置页面:', error);
}
}
async toggleCapture(tabId) {
console.log('开始启动截图功能,标签页ID:', tabId);
try {
const tab = await chrome.tabs.get(tabId);
console.log('目标标签页:', tab.url);
if (tab.url.startsWith('chrome://') ||
tab.url.startsWith('chrome-extension://') ||
tab.url.startsWith('edge://') ||
tab.url.startsWith('about:')) {
console.log('特殊页面,无法截图:', tab.url);
return;
}
const result = await chrome.storage.sync.get(['defaultCaptureMode']);
const defaultMode = result.defaultCaptureMode || 'snapdom';
console.log('使用默认模式:', defaultMode);
await chrome.tabs.sendMessage(tabId, {
action: 'startCapture',
mode: defaultMode
});
console.log('消息发送成功');
} catch (error) {
console.error('无法启动截图功能:', error);
try {
console.log('尝试注入content script...');
await chrome.scripting.executeScript({
target: { tabId: tabId },
files: ['libs/html2canvas.min.js', 'libs/snapdom.min.js', 'content.js']
});
await chrome.scripting.insertCSS({
target: { tabId: tabId },
files: ['content.css']
});
console.log('Content script注入成功,等待加载...');
setTimeout(async () => {
try {
const result = await chrome.storage.sync.get(['defaultCaptureMode']);
const defaultMode = result.defaultCaptureMode || 'snapdom';
console.log('重新发送消息,模式:', defaultMode);
await chrome.tabs.sendMessage(tabId, {
action: 'startCapture',
mode: defaultMode
});
console.log('重新发送消息成功');
} catch (e) {
console.error('注入后仍无法启动截图功能:', e);
}
}, 500);
} catch (injectError) {
console.error('无法注入content script:', injectError);
}
}
}
}
const backgroundService = new BackgroundService();
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('元素截图插件已安装');
} else if (details.reason === 'update') {
console.log('元素截图插件已更新');
}
});