<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>工作区文件管理器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f5f5f5;
color: #333;
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
background: white;
}
/* 顶部标题栏 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fff;
border-bottom: 1px solid #e5e7eb;
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 8px;
}
.folder-icon {
font-size: 24px;
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.header-right {
display: flex;
gap: 8px;
}
.icon-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
transition: background 0.2s;
}
.icon-btn:hover {
background: #f3f4f6;
}
/* 主内容区 */
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* 左侧文件列表 */
.file-panel {
width: 400px;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
background: #fff;
}
.file-toolbar {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
gap: 8px;
}
.search-box {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
background: #fff;
}
.search-box input {
flex: 1;
border: none;
outline: none;
font-size: 14px;
}
.toolbar-buttons {
display: flex;
gap: 8px;
}
.toolbar-btn {
flex: 1;
padding: 6px 12px;
border: 1px solid #d1d5db;
background: #fff;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.toolbar-btn:hover {
background: #f9fafb;
border-color: #9ca3af;
}
/* 文件列表 */
.file-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.file-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 2px;
}
.file-item:hover {
background: #f3f4f6;
}
.file-item.selected {
background: #dbeafe;
}
.file-item.folder {
font-weight: 500;
}
.file-icon {
font-size: 18px;
width: 20px;
text-align: center;
}
.file-info {
flex: 1;
min-width: 0;
}
.file-name {
font-size: 14px;
color: #374151;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-size {
font-size: 12px;
color: #6b7280;
margin-top: 2px;
}
.file-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.file-item:hover .file-actions {
opacity: 1;
}
.file-action-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.file-action-btn:hover {
background: #e5e7eb;
}
/* 子文件夹 */
.file-children {
margin-left: 24px;
}
.file-item .toggle-icon {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition: transform 0.2s;
}
.file-item.expanded .toggle-icon {
transform: rotate(90deg);
}
/* 底部统计 */
.file-stats {
padding: 12px;
border-top: 1px solid #e5e7eb;
font-size: 13px;
color: #6b7280;
}
/* 右侧预览区 */
.preview-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #fafafa;
overflow: hidden;
}
.preview-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #9ca3af;
}
.preview-empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.preview-empty-text {
font-size: 16px;
}
.preview-content {
flex: 1;
overflow: auto;
padding: 24px;
}
.preview-header {
padding: 16px 24px;
background: #fff;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: space-between;
}
.preview-title {
font-size: 14px;
font-weight: 500;
color: #374151;
}
.preview-actions {
display: flex;
gap: 8px;
}
/* 加载状态 */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
color: #6b7280;
}
.spinner {
border: 2px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 0.8s linear infinite;
margin-right: 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 预览内容样式 */
.preview-text {
background: #fff;
padding: 20px;
border-radius: 8px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
.preview-image {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #fff;
padding: 20px;
border-radius: 8px;
min-height: 400px;
}
.preview-image img {
max-width: 100%;
max-height: 70vh;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
cursor: zoom-in;
transition: transform 0.2s;
}
.preview-image img:hover {
transform: scale(1.02);
}
.preview-image.zoomed img {
cursor: zoom-out;
max-width: none;
max-height: none;
width: auto;
height: auto;
}
.image-controls {
display: flex;
gap: 8px;
margin-top: 16px;
}
.image-control-btn {
padding: 8px 16px;
border: 1px solid #d1d5db;
background: #fff;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.image-control-btn:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.image-info {
margin-top: 12px;
font-size: 13px;
color: #6b7280;
text-align: center;
}
.preview-markdown {
background: #fff;
padding: 32px;
border-radius: 8px;
line-height: 1.8;
color: #1f2937;
max-width: 900px;
margin: 0 auto;
}
.preview-markdown h1 {
font-size: 2em;
font-weight: 700;
margin-top: 0;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 2px solid #e5e7eb;
color: #111827;
}
.preview-markdown h2 {
font-size: 1.5em;
font-weight: 600;
margin-top: 32px;
margin-bottom: 16px;
color: #1f2937;
}
.preview-markdown h3 {
font-size: 1.25em;
font-weight: 600;
margin-top: 24px;
margin-bottom: 12px;
color: #374151;
}
.preview-markdown h4,
.preview-markdown h5,
.preview-markdown h6 {
font-weight: 600;
margin-top: 20px;
margin-bottom: 10px;
color: #4b5563;
}
.preview-markdown p {
margin-bottom: 16px;
line-height: 1.8;
}
.preview-markdown ul,
.preview-markdown ol {
margin-bottom: 16px;
padding-left: 28px;
}
.preview-markdown li {
margin-bottom: 8px;
}
.preview-markdown blockquote {
border-left: 4px solid #3b82f6;
margin: 16px 0;
padding: 12px 20px;
background: #f0f9ff;
color: #1e40af;
border-radius: 4px;
}
.preview-markdown code {
background: #f3f4f6;
padding: 3px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9em;
color: #e11d48;
}
.preview-markdown pre {
background: #1f2937;
color: #f9fafb;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
line-height: 1.6;
}
.preview-markdown pre code {
background: none;
padding: 0;
color: inherit;
font-size: 0.95em;
}
.preview-markdown a {
color: #3b82f6;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
.preview-markdown a:hover {
border-bottom-color: #3b82f6;
}
.preview-markdown table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
border: 1px solid #e5e7eb;
}
.preview-markdown table th {
background: #f9fafb;
padding: 10px 12px;
text-align: left;
font-weight: 600;
border: 1px solid #e5e7eb;
}
.preview-markdown table td {
padding: 10px 12px;
border: 1px solid #e5e7eb;
}
.preview-markdown table tr:hover {
background: #f9fafb;
}
.preview-markdown hr {
border: none;
border-top: 2px solid #e5e7eb;
margin: 32px 0;
}
.preview-markdown img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 16px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 表格预览 */
.preview-table {
background: #fff;
padding: 20px;
border-radius: 8px;
overflow: auto;
}
.preview-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.preview-table th,
.preview-table td {
padding: 8px 12px;
border: 1px solid #e5e7eb;
text-align: left;
}
.preview-table th {
background: #f9fafb;
font-weight: 600;
}
.preview-table tr:hover {
background: #f9fafb;
}
/* 响应式 */
@media (max-width: 768px) {
.file-panel {
width: 100%;
border-right: none;
}
.preview-panel {
display: none;
}
.file-panel.mobile-hide {
display: none;
}
.preview-panel.mobile-show {
display: flex;
}
}
/* 上传区域 */
.upload-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.upload-overlay.active {
display: flex;
}
.upload-box {
background: white;
padding: 40px;
border-radius: 12px;
text-align: center;
min-width: 400px;
}
.upload-icon {
font-size: 48px;
margin-bottom: 16px;
}
.upload-text {
font-size: 16px;
color: #374151;
margin-bottom: 8px;
}
.upload-hint {
font-size: 14px;
color: #6b7280;
}
/* 隐藏文件输入 */
#fileInput {
display: none;
}
</style>
</head>
<body>
<div class="container">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-left">
<span class="folder-icon">📁</span>
<span class="header-title">文件</span>
</div>
<div class="header-right">
<button class="icon-btn" id="downloadAllBtn" title="下载工作区">⬇️</button>
<button class="icon-btn" id="fullscreenBtn" title="全屏">⛶</button>
<button class="icon-btn" id="closeBtn" title="关闭">✕</button>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 左侧文件列表 -->
<div class="file-panel">
<div class="file-toolbar">
<div class="search-box">
<span>🔍</span>
<input type="text" id="searchInput" placeholder="搜索文件...">
</div>
<div class="toolbar-buttons">
<button class="toolbar-btn" id="refreshBtn">🔄 刷新</button>
<button class="toolbar-btn" id="uploadBtn">⬆️ 上传</button>
</div>
</div>
<div class="file-list" id="fileList">
<div class="loading">
<div class="spinner"></div>
<span>加载中...</span>
</div>
</div>
<div class="file-stats" id="fileStats">
<span>0 个文件</span>
</div>
</div>
<!-- 右侧预览区 -->
<div class="preview-panel" id="previewPanel">
<div class="preview-empty">
<div class="preview-empty-icon">📁</div>
<div class="preview-empty-text">选择要查看的文件</div>
</div>
</div>
</div>
</div>
<!-- 上传遮罩 -->
<div class="upload-overlay" id="uploadOverlay">
<div class="upload-box">
<div class="upload-icon">📤</div>
<div class="upload-text">拖放文件到此处</div>
<div class="upload-hint">或点击上传按钮选择文件</div>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input type="file" id="fileInput" multiple>
<!-- Markdown 渲染库 -->
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
<script>
// 全局变量
let userId = '';
let chatId = '';
let fileTree = null;
let selectedFile = null;
// 初始化
function init() {
// 从 URL 参数获取 user_id 和 chat_id
const urlParams = new URLSearchParams(window.location.search);
userId = urlParams.get('user_id') || '';
chatId = urlParams.get('chat_id') || '';
if (!userId || !chatId) {
showError('缺少必需参数: user_id 和 chat_id');
return;
}
// 绑定事件
bindEvents();
// 加载文件树
loadFileTree();
}
// 绑定事件
function bindEvents() {
// 刷新按钮
document.getElementById('refreshBtn').addEventListener('click', loadFileTree);
// 上传按钮
document.getElementById('uploadBtn').addEventListener('click', () => {
document.getElementById('fileInput').click();
});
// 文件输入
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
// 下载工作区
document.getElementById('downloadAllBtn').addEventListener('click', downloadWorkspace);
// 全屏按钮
document.getElementById('fullscreenBtn').addEventListener('click', toggleFullscreen);
// 关闭按钮
document.getElementById('closeBtn').addEventListener('click', () => {
window.close();
});
// 搜索框
document.getElementById('searchInput').addEventListener('input', handleSearch);
// 拖放上传
const container = document.querySelector('.container');
container.addEventListener('dragover', handleDragOver);
container.addEventListener('dragleave', handleDragLeave);
container.addEventListener('drop', handleDrop);
}
// 加载文件树
async function loadFileTree() {
const fileList = document.getElementById('fileList');
fileList.innerHTML = '<div class="loading"><div class="spinner"></div><span>加载中...</span></div>';
try {
const response = await fetch(`/api/workspace/tree?user_id=${userId}&chat_id=${chatId}`);
const data = await response.json();
if (data.success) {
fileTree = data.tree;
renderFileTree(fileTree);
updateStats(data.tree);
} else {
showError(data.error || '加载文件树失败');
}
} catch (error) {
showError('网络错误: ' + error.message);
}
}
// 渲染文件树
function renderFileTree(tree, container = null, parentPath = '') {
if (!container) {
container = document.getElementById('fileList');
container.innerHTML = '';
}
if (!tree || !tree.children || tree.children.length === 0) {
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #9ca3af;">没有文件</div>';
return;
}
tree.children.forEach(item => {
const itemPath = parentPath ? `${parentPath}/${item.name}` : `/${item.name}`;
const itemEl = createFileItem(item, itemPath);
container.appendChild(itemEl);
});
}
// 创建文件项
function createFileItem(item, itemPath) {
// 创建容器(对于文件夹,包含文件夹行和子项;对于文件,只是文件行)
const container = document.createElement('div');
// 创建文件/文件夹行
const itemRow = document.createElement('div');
itemRow.className = 'file-item' + (item.type === 'directory' ? ' folder' : '');
itemRow.dataset.path = itemPath;
itemRow.dataset.type = item.type;
const icon = getFileIcon(item);
const size = item.size_human || '';
itemRow.innerHTML = `
${item.type === 'directory' ? '<span class="toggle-icon">▶</span>' : '<span style="width:16px"></span>'}
<span class="file-icon">${icon}</span>
<div class="file-info">
<div class="file-name">${item.name}</div>
${size ? `<div class="file-size">${size}</div>` : ''}
</div>
<div class="file-actions">
${item.type === 'file' ? '<button class="file-action-btn" onclick="previewFile(event, this)" title="预览">👁️</button>' : ''}
${item.type === 'file' ? '<button class="file-action-btn" onclick="downloadFile(event, this)" title="下载">⬇️</button>' : ''}
</div>
`;
// 点击事件
itemRow.addEventListener('click', (e) => {
// 阻止事件冒泡到父级文件夹
e.stopPropagation();
// 如果点击的是操作按钮,不处理
if (e.target.closest('.file-actions')) return;
handleFileClick(item, itemRow);
});
container.appendChild(itemRow);
// 如果是文件夹且有子项,创建子项容器
if (item.type === 'directory' && item.children && item.children.length > 0) {
const childrenContainer = document.createElement('div');
childrenContainer.className = 'file-children';
childrenContainer.style.display = 'none';
item.children.forEach(child => {
const childPath = `${itemPath}/${child.name}`;
childrenContainer.appendChild(createFileItem(child, childPath));
});
container.appendChild(childrenContainer);
}
return item.type === 'directory' && item.children && item.children.length > 0 ? container : itemRow;
}
// 获取文件图标
function getFileIcon(item) {
if (item.type === 'directory') return '📁';
const ext = item.name.split('.').pop().toLowerCase();
const iconMap = {
'md': '📝',
'txt': '📄',
'pdf': '📕',
'doc': '📘',
'docx': '📘',
'xls': '📗',
'xlsx': '📗',
'csv': '📊',
'png': '🖼️',
'jpg': '🖼️',
'jpeg': '🖼️',
'gif': '🖼️',
'svg': '🖼️',
'py': '🐍',
'js': '📜',
'html': '🌐',
'css': '🎨',
'json': '📋',
'zip': '📦',
'tar': '📦',
'gz': '📦'
};
return iconMap[ext] || '📄';
}
// 处理文件点击
function handleFileClick(item, element) {
if (item.type === 'directory') {
// 切换文件夹展开/折叠
element.classList.toggle('expanded');
// 查找同级的 .file-children 容器
const parent = element.parentElement;
const children = parent ? parent.querySelector('.file-children') : null;
if (children) {
children.style.display = children.style.display === 'none' ? 'block' : 'none';
}
} else {
// 选中文件
document.querySelectorAll('.file-item').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
selectedFile = item;
previewFile(null, element);
}
}
// 预览文件
async function previewFile(event, element) {
if (event) {
event.stopPropagation();
}
const path = element.closest('.file-item').dataset.path;
const previewPanel = document.getElementById('previewPanel');
previewPanel.innerHTML = '<div class="loading"><div class="spinner"></div><span>加载预览...</span></div>';
try {
const response = await fetch(`/api/workspace/file/preview?user_id=${userId}&chat_id=${chatId}&file_path=${encodeURIComponent(path)}`);
// 检查响应类型
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
// JSON 响应(错误情况)
const data = await response.json();
showError(data.error || '预览失败');
} else {
// 文本/Markdown 响应(成功情况)
const content = await response.text();
renderPreview({ content }, path);
}
} catch (error) {
showError('预览失败: ' + error.message);
}
}
// 渲染预览
function renderPreview(data, path) {
const previewPanel = document.getElementById('previewPanel');
const filename = path.split('/').pop();
const ext = filename.split('.').pop().toLowerCase();
let content = '';
// 预览头部
const header = `
<div class="preview-header">
<div class="preview-title">${filename}</div>
<div class="preview-actions">
<button class="icon-btn" onclick="downloadFile(event, document.querySelector('.file-item.selected'))" title="下载">⬇️</button>
</div>
</div>
`;
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'ico'].includes(ext)) {
// 图片预览 - 去掉路径开头的斜杠
const imagePath = path.startsWith('/') ? path.substring(1) : path;
const imageUrl = `/api/workspace/image/${imagePath}?user_id=${userId}&chat_id=${chatId}`;
content = `
${header}
<div class="preview-content">
<div class="preview-image" id="imagePreview">
<img src="${imageUrl}" alt="${filename}" id="previewImg" onload="updateImageInfo(this)">
<div class="image-controls">
<button class="image-control-btn" onclick="toggleImageZoom()">🔍 缩放</button>
<button class="image-control-btn" onclick="downloadCurrentImage()">⬇️ 下载</button>
<button class="image-control-btn" onclick="openImageInNewTab()">🔗 新窗口打开</button>
</div>
<div class="image-info" id="imageInfo">加载中...</div>
</div>
</div>
`;
} else if (data.content) {
// 文本内容
if (ext === 'md') {
// Markdown 渲染(使用 marked.js)
let renderedHtml = '';
try {
if (typeof marked !== 'undefined') {
// 配置 marked
marked.setOptions({
breaks: true,
gfm: true,
headerIds: true,
mangle: false
});
renderedHtml = marked.parse(data.content);
} else {
// 降级到纯文本
renderedHtml = `<pre>${escapeHtml(data.content)}</pre>`;
}
} catch (e) {
renderedHtml = `<pre>${escapeHtml(data.content)}</pre>`;
}
content = `
${header}
<div class="preview-content">
<div class="preview-markdown">${renderedHtml}</div>
</div>
`;
} else {
// 普通文本
content = `
${header}
<div class="preview-content">
<div class="preview-text">${escapeHtml(data.content)}</div>
</div>
`;
}
} else if (data.preview_html) {
// HTML 预览(Excel等)
content = `
${header}
<div class="preview-content">
${data.preview_html}
</div>
`;
} else {
content = `
${header}
<div class="preview-content">
<div style="text-align: center; padding: 40px; color: #9ca3af;">
<div style="font-size: 48px; margin-bottom: 16px;">📄</div>
<div>此文件类型无法预览</div>
<div style="margin-top: 8px; font-size: 14px;">请下载后查看</div>
</div>
</div>
`;
}
previewPanel.innerHTML = content;
}
// 下载文件
async function downloadFile(event, element) {
event.stopPropagation();
const path = element.closest('.file-item').dataset.path;
const url = `/api/workspace/file/download?user_id=${userId}&chat_id=${chatId}&file_path=${encodeURIComponent(path)}`;
window.open(url, '_blank');
}
// 下载工作区
function downloadWorkspace() {
const url = `/api/workspace/download?user_id=${userId}&chat_id=${chatId}`;
window.open(url, '_blank');
}
// 处理文件选择
async function handleFileSelect(event) {
const files = event.target.files;
if (files.length === 0) return;
await uploadFiles(Array.from(files));
event.target.value = ''; // 清空输入
}
// 上传文件
async function uploadFiles(files) {
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
try {
const response = await fetch(`/api/workspace/file/upload?user_id=${userId}&chat_id=${chatId}`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert(`成功上传 ${data.uploaded_count} 个文件`);
loadFileTree(); // 重新加载文件树
} else {
showError(data.error || '上传失败');
}
} catch (error) {
showError('上传失败: ' + error.message);
}
}
// 拖放处理
function handleDragOver(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('uploadOverlay').classList.add('active');
}
function handleDragLeave(e) {
e.preventDefault();
e.stopPropagation();
if (e.target === document.querySelector('.container')) {
document.getElementById('uploadOverlay').classList.remove('active');
}
}
async function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
document.getElementById('uploadOverlay').classList.remove('active');
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
await uploadFiles(files);
}
}
// 搜索处理
function handleSearch(e) {
const keyword = e.target.value.toLowerCase();
const items = document.querySelectorAll('.file-item');
items.forEach(item => {
const name = item.querySelector('.file-name').textContent.toLowerCase();
if (name.includes(keyword)) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
}
// 全屏切换
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
}
// 更新统计
function updateStats(tree) {
let fileCount = 0;
let totalSize = 0;
function count(node) {
if (node.type === 'file') {
fileCount++;
totalSize += node.size || 0;
}
if (node.children) {
node.children.forEach(count);
}
}
count(tree);
const sizeStr = formatSize(totalSize);
document.getElementById('fileStats').innerHTML = `<span>${fileCount} 个文件</span> <span>${sizeStr}</span>`;
}
// 格式化大小
function formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示错误
function showError(message) {
const previewPanel = document.getElementById('previewPanel');
previewPanel.innerHTML = `
<div style="flex: 1; display: flex; align-items: center; justify-content: center; color: #ef4444;">
<div style="text-align: center;">
<div style="font-size: 48px; margin-bottom: 16px;">⚠️</div>
<div style="font-size: 16px;">${escapeHtml(message)}</div>
</div>
</div>
`;
}
// ========== 图片预览功能 ==========
let isImageZoomed = false;
// 更新图片信息
function updateImageInfo(img) {
const info = document.getElementById('imageInfo');
if (info && img.complete) {
const naturalWidth = img.naturalWidth;
const naturalHeight = img.naturalHeight;
const displayWidth = img.width;
const displayHeight = img.height;
const fileSize = img.dataset.fileSize || '未知';
info.innerHTML = `
原始尺寸: ${naturalWidth} × ${naturalHeight} px
${displayWidth !== naturalWidth ? `| 显示: ${displayWidth} × ${displayHeight} px` : ''}
`;
}
}
// 切换图片缩放
function toggleImageZoom() {
const container = document.getElementById('imagePreview');
const img = document.getElementById('previewImg');
if (!container || !img) return;
isImageZoomed = !isImageZoomed;
if (isImageZoomed) {
container.classList.add('zoomed');
container.style.overflow = 'auto';
img.style.cursor = 'zoom-out';
} else {
container.classList.remove('zoomed');
container.style.overflow = 'hidden';
img.style.cursor = 'zoom-in';
}
}
// 下载当前图片
function downloadCurrentImage() {
const img = document.getElementById('previewImg');
if (!img) return;
// 创建一个临时链接来触发下载
const link = document.createElement('a');
link.href = img.src;
link.download = img.alt || 'image';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// 在新窗口打开图片
function openImageInNewTab() {
const img = document.getElementById('previewImg');
if (!img) return;
window.open(img.src, '_blank');
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>