new
All checks were successful
Deploy to Server / deploy (push) Successful in 32s

This commit is contained in:
jeremygan2021
2026-03-22 22:04:13 +08:00
parent 2e05322909
commit 2104e7b7dc
8 changed files with 525 additions and 112 deletions

View File

@@ -352,11 +352,253 @@
</div>
</div>
<!-- File Preview Modal -->
<div id="filePreviewModal" class="modal fixed inset-0 z-[70] flex items-center justify-center p-4" style="background-color: rgba(0,0,0,0.7);">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-5xl max-h-[95vh] flex flex-col relative animate-fade-in overflow-hidden">
<button class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 z-10 p-2 rounded-full hover:bg-gray-100 transition-colors" onclick="closeModal('filePreviewModal')">
<i class="fas fa-times text-xl"></i>
</button>
<div class="px-6 py-4 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
<h2 id="filePreviewTitle" class="text-lg font-bold text-gray-900 flex items-center truncate">
<i id="filePreviewIcon" class="fas fa-file-pdf text-red-500 mr-2"></i>
<span id="filePreviewName">文件预览</span>
</h2>
<div class="flex items-center gap-2 ml-4">
<a id="fileDownloadBtn" href="#" download class="inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
<i class="fas fa-download mr-1.5"></i>下载
</a>
<button onclick="openFileNewTab()" class="inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
<i class="fas fa-external-link-alt mr-1.5"></i>新窗口打开
</button>
</div>
</div>
<div class="flex-1 overflow-hidden bg-gray-100 relative">
<iframe id="filePreviewFrame" class="w-full h-full min-h-[60vh]" frameborder="0"></iframe>
<div id="imagePreviewContainer" class="hidden w-full h-full overflow-auto flex items-center justify-center p-4">
<img id="imagePreviewImg" class="max-w-full max-h-full object-contain rounded shadow-lg" src="" alt="图片预览">
</div>
<div id="filePreviewLoading" class="absolute inset-0 flex items-center justify-center bg-gray-100">
<div class="text-center">
<i class="fas fa-spinner fa-spin text-4xl text-blue-500 mb-3"></i>
<p class="text-gray-500">正在加载文件...</p>
</div>
</div>
<div id="filePreviewError" class="hidden absolute inset-0 flex items-center justify-center bg-gray-100">
<div class="text-center">
<i class="fas fa-exclamation-circle text-4xl text-red-500 mb-3"></i>
<p class="text-gray-600 mb-3">文件加载失败</p>
<p class="text-sm text-gray-400">请尝试下载文件或在新窗口中打开</p>
</div>
</div>
</div>
<div class="px-4 py-3 bg-white border-t border-gray-200 text-xs text-gray-500 flex justify-between">
<span>如文件无法预览,请直接下载或在浏览器中打开</span>
<span id="filePreviewHint"></span>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.staticfile.net/marked/11.1.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script>
<script>
let currentPreviewFile = { url: '', name: '', type: '' };
function previewFile(url, name, type) {
currentPreviewFile = { url: decodeURIComponent(url), name: decodeURIComponent(name), type: type };
const modal = document.getElementById('filePreviewModal');
const titleEl = document.getElementById('filePreviewTitle');
const iconEl = document.getElementById('filePreviewIcon');
const nameEl = document.getElementById('filePreviewName');
const downloadBtn = document.getElementById('fileDownloadBtn');
const frame = document.getElementById('filePreviewFrame');
const imageContainer = document.getElementById('imagePreviewContainer');
const imageImg = document.getElementById('imagePreviewImg');
const loading = document.getElementById('filePreviewLoading');
const error = document.getElementById('filePreviewError');
const hint = document.getElementById('filePreviewHint');
frame.style.display = 'none';
imageContainer.classList.add('hidden');
loading.classList.remove('hidden');
error.classList.add('hidden');
const fileUrl = decodeURIComponent(url);
const fileName = decodeURIComponent(name);
const fileType = type || '';
titleEl.className = 'text-lg font-bold text-gray-900 flex items-center truncate';
nameEl.textContent = fileName;
downloadBtn.href = fileUrl;
downloadBtn.download = fileName;
loading.classList.remove('hidden');
error.classList.add('hidden');
frame.style.display = 'block';
let iconClass = 'fas fa-file text-gray-500';
let previewUrl = fileUrl;
let hintText = '';
if (fileType === 'pdf') {
iconClass = 'fas fa-file-pdf text-red-500';
previewUrl = fileUrl;
hintText = '提示PDF文件使用本地PDF.js预览器加载';
} else if (fileType === 'ppt' || fileType === 'pptx') {
iconClass = 'fas fa-file-powerpoint text-orange-500';
previewUrl = '';
hintText = '提示PPT文件需要下载后查看或使用WPS/Office打开';
} else if (fileType === 'image') {
iconClass = 'fas fa-image text-green-500';
previewUrl = 'image';
hintText = '提示:点击图片可查看大图';
} else if (fileType === 'doc' || fileType === 'docx') {
iconClass = 'fas fa-file-word text-blue-500';
previewUrl = '';
hintText = '提示Word文档需要下载后查看或使用WPS/Word打开';
} else {
previewUrl = fileUrl;
hintText = '此文件类型不支持在线预览,请下载后查看';
}
iconEl.className = iconClass + ' mr-2';
hint.textContent = hintText;
if (!previewUrl) {
loading.classList.add('hidden');
error.classList.remove('hidden');
frame.style.display = 'none';
document.querySelector('#filePreviewError p:first-of-type').textContent = '此文件类型暂不支持在线预览';
document.querySelector('#filePreviewError p:last-of-type').textContent = '请点击上方"下载"按钮保存文件后查看';
modal.classList.add('active');
return;
}
if (fileType === 'pdf') {
loading.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin text-4xl text-blue-500 mb-3"></i><p class="text-gray-500">正在加载PDF文件...</p></div>';
renderPdfPreview(fileUrl, frame, loading, error);
} else if (fileType === 'image') {
imageImg.src = fileUrl;
imageImg.onload = () => {
loading.classList.add('hidden');
imageContainer.classList.remove('hidden');
};
imageImg.onerror = () => {
loading.classList.add('hidden');
error.classList.remove('hidden');
error.querySelector('p:first-of-type').textContent = '图片加载失败';
error.querySelector('p:last-of-type').textContent = '请点击"下载"按钮保存图片后查看';
};
} else {
frame.style.display = 'block';
frame.onload = function() {
loading.classList.add('hidden');
};
frame.onerror = function() {
loading.classList.add('hidden');
};
frame.src = previewUrl;
}
modal.classList.add('active');
}
async function renderPdfPreview(url, frameEl, loadingEl, errorEl) {
try {
const loadingText = loadingEl.querySelector('p');
let arrayBuffer;
try {
const response = await fetch(url);
if (!response.ok) throw new Error('无法加载PDF文件');
arrayBuffer = await response.arrayBuffer();
} catch (fetchErr) {
console.warn('PDF fetch failed, trying iframe method:', fetchErr);
frameEl.style.display = 'block';
frameEl.src = url;
loadingEl.classList.add('hidden');
frameEl.onload = () => loadingEl.classList.add('hidden');
frameEl.onerror = () => {
loadingEl.classList.add('hidden');
showPdfError(errorEl, 'PDF文件加载失败请下载后查看');
};
return;
}
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const container = document.createElement('div');
container.style.cssText = 'width:100%;height:100%;overflow:auto;background:#525659;padding:20px;';
const scale = 1.2;
const pageContainer = document.createElement('div');
pageContainer.style.cssText = 'background:white;margin:0 auto;box-shadow:0 2px 10px rgba(0,0,0,0.3);width:' + (595 * scale) + 'px;';
const pageNum = Math.min(pdf.numPages, 20);
for (let i = 1; i <= pageNum; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: scale });
const canvas = document.createElement('canvas');
canvas.style.cssText = 'display:block;margin:0 auto;width:100%;';
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport }).promise;
pageContainer.appendChild(canvas);
}
if (pdf.numPages > 20) {
const moreInfo = document.createElement('div');
moreInfo.style.cssText = 'text-align:center;padding:20px;color:#999;background:white;margin-top:10px;';
moreInfo.textContent = `... 还有 ${pdf.numPages - 20} 页未显示,请下载完整查看`;
pageContainer.appendChild(moreInfo);
}
container.appendChild(pageContainer);
frameEl.style.display = 'none';
loadingEl.classList.add('hidden');
const parent = frameEl.parentElement;
const oldContainer = parent.querySelector('#pdfRenderContainer');
if (oldContainer) oldContainer.remove();
parent.insertBefore(container, frameEl);
container.id = 'pdfRenderContainer';
} catch (err) {
console.error('PDF加载失败:', err);
loadingEl.classList.add('hidden');
frameEl.style.display = 'block';
frameEl.src = url;
frameEl.onload = () => loadingEl.classList.add('hidden');
frameEl.onerror = () => {
loadingEl.classList.add('hidden');
showPdfError(errorEl, 'PDF文件无法预览请下载后查看');
};
}
}
function showPdfError(errorEl, message) {
errorEl.classList.remove('hidden');
errorEl.querySelector('p:first-of-type').textContent = message;
errorEl.querySelector('p:last-of-type').textContent = '请点击"下载"按钮保存文件后查看';
}
function openFileNewTab() {
window.open(currentPreviewFile.url, '_blank');
}
/**
* 更新单个维度分数显示并计算总分
*/
@@ -563,25 +805,49 @@ async function viewProject(id) {
filesList.innerHTML = data.files.map(file => {
let iconClass = 'fas fa-file';
let iconColor = 'text-gray-500';
if (file.file_type === 'ppt' || file.file_type === 'pptx') {
let canPreview = false;
let fileType = file.file_type || '';
if (fileType === 'ppt' || fileType === 'pptx') {
iconClass = 'fas fa-file-powerpoint';
iconColor = 'text-orange-500';
} else if (file.file_type === 'pdf') {
canPreview = true;
} else if (fileType === 'pdf') {
iconClass = 'fas fa-file-pdf';
iconColor = 'text-red-500';
} else if (file.file_type === 'video') {
canPreview = true;
} else if (fileType === 'video') {
iconClass = 'fas fa-video';
iconColor = 'text-purple-500';
} else if (file.file_type === 'image') {
} else if (fileType === 'image') {
iconClass = 'fas fa-image';
iconColor = 'text-green-500';
canPreview = true;
} else if (fileType === 'doc' || fileType === 'docx') {
iconClass = 'fas fa-file-word';
iconColor = 'text-blue-500';
canPreview = true;
}
const fileName = file.name || '未命名文件';
const previewBtn = canPreview ? `
<button onclick="previewFile('${encodeURIComponent(file.file_url)}', '${encodeURIComponent(fileName)}', '${fileType}')"
class="ml-2 px-2 py-1 text-xs font-medium text-blue-600 bg-blue-50 rounded hover:bg-blue-100 transition-colors">
<i class="fas fa-eye mr-1"></i>预览
</button>
` : '';
return `
<a href="${file.file_url}" target="_blank" class="flex items-center p-3 bg-white rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-blue-300 transition-colors group">
<div class="flex items-center p-3 bg-white rounded-lg border border-gray-200 hover:bg-gray-50 hover:border-blue-300 transition-colors group">
<i class="${iconClass} ${iconColor} text-xl mr-3 group-hover:scale-110 transition-transform"></i>
<span class="flex-1 text-sm font-medium text-gray-700 truncate">${file.name || '未命名文件'}</span>
<i class="fas fa-external-link-alt text-gray-400 group-hover:text-blue-500"></i>
</a>
<div class="flex-1 min-w-0">
<span class="text-sm font-medium text-gray-700 block truncate">${fileName}</span>
</div>
<a href="${file.file_url}" download="${fileName}" class="ml-2 p-2 text-gray-400 hover:text-blue-500 transition-colors" title="下载">
<i class="fas fa-download"></i>
</a>
${previewBtn}
</div>
`;
}).join('');
} else {