- 复制/下载后自动关闭弹窗并显示提示 - 修复CSS隔离问题,避免影响页面样式 - 使用DOM克隆替代直接操作,避免修改原始页面HTML - 删除未使用的styles.css文件 Co-Authored-By: Claude <noreply@anthropic.com>
216 lines
6.6 KiB
TypeScript
216 lines
6.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { ExtractButton } from './components/ExtractButton';
|
|
import { OptionsPanel } from './components/OptionsPanel';
|
|
import { PreviewModal } from './components/PreviewModal';
|
|
import { useChromeStorage } from '../hooks/useChromeStorage';
|
|
import { copyToClipboard } from '../utils/download';
|
|
import type { ExtractedContent } from '../types';
|
|
|
|
// 状态消息类型
|
|
type StatusType = 'idle' | 'loading' | 'success' | 'error';
|
|
|
|
export function Popup() {
|
|
const { settings, saveSettings, loading: settingsLoading } = useChromeStorage();
|
|
const [extractedContent, setExtractedContent] = useState<ExtractedContent | null>(null);
|
|
const [status, setStatus] = useState<{ type: StatusType; message: string }>({ type: 'idle', message: '' });
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
const [mode, setMode] = useState<'auto' | 'selection'>('auto');
|
|
|
|
// 显示状态消息
|
|
const showStatus = (type: StatusType, message: string) => {
|
|
setStatus({ type, message });
|
|
if (type !== 'loading') {
|
|
setTimeout(() => setStatus({ type: 'idle', message: '' }), 3000);
|
|
}
|
|
};
|
|
|
|
// 获取当前标签页
|
|
const getCurrentTab = async (): Promise<any> => {
|
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
if (!tab) throw new Error('无法获取当前标签页');
|
|
return tab;
|
|
};
|
|
|
|
// 提取内容
|
|
const handleExtract = async () => {
|
|
try {
|
|
showStatus('loading', mode === 'selection' ? '请在页面上选择内容...' : '正在提取内容...');
|
|
|
|
const tab = await getCurrentTab();
|
|
if (!tab.id) throw new Error('标签页无效');
|
|
|
|
// 发送消息给content script
|
|
const response = await chrome.tabs.sendMessage(tab.id, {
|
|
action: mode === 'selection' ? 'extractSelection' : 'extract'
|
|
});
|
|
|
|
if (response.success && response.data) {
|
|
setExtractedContent(response.data);
|
|
setShowPreview(true);
|
|
showStatus('success', '提取成功!');
|
|
} else {
|
|
showStatus('error', response.error || '提取失败');
|
|
}
|
|
} catch (error) {
|
|
console.error('提取失败:', error);
|
|
showStatus('error', error instanceof Error ? error.message : '提取失败');
|
|
}
|
|
};
|
|
|
|
// 复制到剪贴板
|
|
const handleCopy = async () => {
|
|
if (!extractedContent) return;
|
|
|
|
try {
|
|
const markdown = generateMarkdown(extractedContent, {
|
|
includeTitle: settings.includeTitle,
|
|
includeUrl: settings.includeUrl
|
|
});
|
|
await copyToClipboard(markdown);
|
|
showStatus('success', '已复制到剪贴板!');
|
|
// 1秒后关闭弹窗
|
|
setTimeout(() => {
|
|
window.close();
|
|
}, 1000);
|
|
} catch {
|
|
showStatus('error', '复制失败');
|
|
}
|
|
};
|
|
|
|
// 下载文件
|
|
const handleDownload = async () => {
|
|
if (!extractedContent) return;
|
|
|
|
try {
|
|
const markdown = generateMarkdown(extractedContent, {
|
|
includeTitle: settings.includeTitle,
|
|
includeUrl: settings.includeUrl
|
|
});
|
|
|
|
const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// 使用chrome.downloads API
|
|
const filename = `${extractedContent.title.replace(/[<>:"/\\|?*]/g, '_').substring(0, 100)}.md`;
|
|
await chrome.downloads.download({
|
|
url: url,
|
|
filename: filename,
|
|
saveAs: false
|
|
});
|
|
|
|
URL.revokeObjectURL(url);
|
|
showStatus('success', '开始下载...');
|
|
// 1秒后关闭弹窗
|
|
setTimeout(() => {
|
|
window.close();
|
|
}, 1000);
|
|
} catch {
|
|
showStatus('error', '下载失败');
|
|
}
|
|
};
|
|
|
|
// 生成Markdown
|
|
const generateMarkdown = (content: ExtractedContent, options: { includeTitle: boolean; includeUrl: boolean }): string => {
|
|
let md = '';
|
|
|
|
// Frontmatter
|
|
md += '---\n';
|
|
if (options.includeTitle) {
|
|
md += `title: "${content.title.replace(/"/g, '\\"')}"\n`;
|
|
}
|
|
if (options.includeUrl) {
|
|
md += `source: ${content.url}\n`;
|
|
}
|
|
md += '---\n\n';
|
|
|
|
// 标题
|
|
if (options.includeTitle) {
|
|
md += `# ${content.title}\n\n`;
|
|
}
|
|
|
|
// 原文链接
|
|
if (options.includeUrl) {
|
|
md += `> 原文链接: [${content.title}](${content.url})\n\n`;
|
|
}
|
|
|
|
// 内容
|
|
md += content.markdown;
|
|
|
|
return md;
|
|
};
|
|
|
|
if (settingsLoading) {
|
|
return (
|
|
<div className="w-80 p-4 flex items-center justify-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-80 p-4 bg-white dark:bg-gray-900">
|
|
{/* 标题 */}
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<svg className="w-8 h-8 text-green-500" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h6v6h6v10H6z"/>
|
|
<path d="M8 12h8v2H8zm0 4h8v2H8z"/>
|
|
</svg>
|
|
<h1 className="text-xl font-bold text-gray-900 dark:text-white">ReadMD</h1>
|
|
</div>
|
|
|
|
{/* 提取按钮 */}
|
|
<ExtractButton
|
|
loading={status.type === 'loading'}
|
|
onExtract={handleExtract}
|
|
mode={mode}
|
|
onModeChange={setMode}
|
|
/>
|
|
|
|
{/* 状态消息 */}
|
|
{status.message && (
|
|
<div className={`mt-3 p-2 rounded-lg text-sm ${
|
|
status.type === 'success' ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' :
|
|
status.type === 'error' ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' :
|
|
status.type === 'loading' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400' :
|
|
'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300'
|
|
}`}>
|
|
{status.message}
|
|
</div>
|
|
)}
|
|
|
|
{/* 分隔线 */}
|
|
<hr className="my-4 border-gray-200 dark:border-gray-700" />
|
|
|
|
{/* 选项面板 */}
|
|
<OptionsPanel
|
|
settings={settings}
|
|
onSettingsChange={saveSettings}
|
|
/>
|
|
|
|
{/* 预览弹窗 */}
|
|
{showPreview && extractedContent && (
|
|
<PreviewModal
|
|
content={extractedContent}
|
|
onClose={() => setShowPreview(false)}
|
|
onCopy={handleCopy}
|
|
onDownload={handleDownload}
|
|
/>
|
|
)}
|
|
|
|
{/* 页脚 */}
|
|
<div className="mt-4 text-center text-xs text-gray-500 dark:text-gray-400">
|
|
<a
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
chrome.runtime.openOptionsPage();
|
|
}}
|
|
className="hover:text-gray-700 dark:hover:text-gray-200"
|
|
>
|
|
打开设置页面
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|