readmd/src/popup/Popup.tsx
ddshi bf3b4548b0 fix: 修复下载功能,通过background script直接下载到默认目录
- 下载改由background script处理,使用chrome.downloads.download API
- 添加chrome.runtime.sendMessage类型定义
- 修复SVG图标路径错误
- 使用Data URL方式确保下载可靠性

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 16:55:54 +08:00

220 lines
7.4 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', '已复制到剪贴板!');
setTimeout(() => {
window.close();
}, 1000);
} catch {
showStatus('error', '复制失败');
}
};
// 下载文件 - 通过 background script 直接下载到默认目录
const handleDownload = async () => {
if (!extractedContent) return;
try {
const markdown = generateMarkdown(extractedContent, {
includeTitle: settings.includeTitle,
includeUrl: settings.includeUrl
});
const filename = `${extractedContent.title.replace(/[<>:"/\\|?*]/g, '_').substring(0, 100)}.md`;
// 发送消息给 background script 处理下载
await chrome.runtime.sendMessage({
action: 'download',
filename: filename,
content: markdown
});
showStatus('success', '开始下载...');
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 h-64 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-indigo-500 border-t-transparent"></div>
</div>
);
}
return (
<div className="w-80 bg-gray-50 dark:bg-gray-900">
{/* 头部 */}
<div className="px-5 py-5 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center shadow-lg shadow-indigo-500/30">
<svg className="w-5 h-5 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/>
<path d="M14 2v6h6"/>
<path d="M8 13h8"/>
<path d="M8 17h8"/>
</svg>
</div>
<div>
<h1 className="text-lg font-semibold text-gray-900 dark:text-white">ReadMD</h1>
<p className="text-xs text-gray-500 dark:text-gray-400"> Markdown</p>
</div>
</div>
</div>
{/* 主要内容区 */}
<div className="p-5 space-y-4">
{/* 状态消息 */}
{status.message && (
<div className={`p-3 rounded-lg text-sm font-medium transition-all duration-300 ${
status.type === 'success' ? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400' :
status.type === 'error' ? 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400' :
status.type === 'loading' ? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-900/20 dark:text-indigo-400' :
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
}`}>
{status.message}
</div>
)}
{/* 提取按钮 */}
<ExtractButton
loading={status.type === 'loading'}
onExtract={handleExtract}
mode={mode}
onModeChange={setMode}
/>
{/* 选项面板 */}
<OptionsPanel
settings={settings}
onSettingsChange={saveSettings}
/>
</div>
{/* 页脚 */}
<div className="px-5 py-4 bg-white dark:bg-gray-800 border-t border-gray-100 dark:border-gray-700">
<button
onClick={() => chrome.runtime.openOptionsPage()}
className="w-full py-2.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors flex items-center justify-center gap-2"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12.36 6v1.2M12.36 16.8v1.2M6.36 12H5.16M18.84 12h-1.2M8.4 8.4l-.85.85M16.45 15.55l-.85.85M8.4 15.6l-.85-.85M16.45 8.45l-.85-.85"/>
</svg>
</button>
</div>
{/* 预览弹窗 */}
{showPreview && extractedContent && (
<PreviewModal
content={extractedContent}
onClose={() => setShowPreview(false)}
onCopy={handleCopy}
onDownload={handleDownload}
/>
)}
</div>
);
}