245 lines
7.8 KiB
JavaScript
245 lines
7.8 KiB
JavaScript
let apiToken = localStorage.getItem('pan_api_token') || '';
|
|
|
|
// 代理 fetch 请求注入凭证
|
|
const originalFetch = window.fetch;
|
|
window.fetch = async function () {
|
|
let [resource, config] = arguments;
|
|
if (!config) config = {};
|
|
if (!config.headers) config.headers = {};
|
|
|
|
if (apiToken) {
|
|
config.headers['Authorization'] = `Bearer ${apiToken}`;
|
|
}
|
|
|
|
const response = await originalFetch(resource, config);
|
|
if (response.status === 403) {
|
|
showAuthModal();
|
|
}
|
|
return response;
|
|
};
|
|
|
|
// 身份验证逻辑
|
|
function showAuthModal() {
|
|
document.getElementById('auth-modal').style.display = 'flex';
|
|
}
|
|
|
|
function saveAuth() {
|
|
const val = document.getElementById('auth-input').value;
|
|
if (val) {
|
|
localStorage.setItem('pan_api_token', val);
|
|
apiToken = val;
|
|
document.getElementById('auth-modal').style.display = 'none';
|
|
showToast('身份验证成功', 'success');
|
|
document.getElementById('auth-input').value = '';
|
|
loadImages();
|
|
} else {
|
|
showToast('Token 不能为空', 'error');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initDragAndDrop();
|
|
loadImages();
|
|
});
|
|
|
|
// Toast 全局提示
|
|
function showToast(message, type = 'info') {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast ${type}`;
|
|
toast.textContent = message;
|
|
|
|
container.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.style.animation = 'slideOutRight 0.3s ease forwards';
|
|
setTimeout(() => toast.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// 拖拽与剪贴板处理
|
|
function initDragAndDrop() {
|
|
const dropZone = document.getElementById('drop-zone');
|
|
const fileInput = document.getElementById('file-input');
|
|
|
|
dropZone.addEventListener('click', () => fileInput.click());
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
handleFiles(e.target.files);
|
|
e.target.value = '';
|
|
});
|
|
|
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, preventDefaults, false);
|
|
});
|
|
|
|
['dragenter', 'dragover'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, () => dropZone.classList.add('drag-over'), false);
|
|
});
|
|
|
|
['dragleave', 'drop'].forEach(eventName => {
|
|
dropZone.addEventListener(eventName, () => dropZone.classList.remove('drag-over'), false);
|
|
});
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
const dt = e.dataTransfer;
|
|
const files = dt.files;
|
|
handleFiles(files);
|
|
}, false);
|
|
|
|
// 全局剪贴板监听
|
|
document.addEventListener('paste', (e) => {
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
|
|
const files = (e.clipboardData || window.clipboardData).files;
|
|
if (files && files.length > 0) {
|
|
e.preventDefault();
|
|
handleFiles(files);
|
|
showToast('正在上传剪贴板图片...', 'info');
|
|
}
|
|
});
|
|
}
|
|
|
|
function preventDefaults(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
function handleFiles(files) {
|
|
const validFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
|
|
|
|
if (validFiles.length === 0) {
|
|
showToast('仅支持上传图片格式', 'error');
|
|
return;
|
|
}
|
|
|
|
validFiles.forEach(uploadFile);
|
|
}
|
|
|
|
// 执行上传流程
|
|
async function uploadFile(file) {
|
|
const queue = document.getElementById('upload-queue');
|
|
const item = document.createElement('div');
|
|
item.className = 'queue-item';
|
|
item.innerHTML = `
|
|
<span>正在上传: ${file.name}</span>
|
|
<div class="loader"></div>
|
|
`;
|
|
queue.appendChild(item);
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const response = await fetch('/api/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
item.style.animation = 'slideOutRight 0.3s forwards';
|
|
setTimeout(() => item.remove(), 300);
|
|
|
|
if (result.code === 0 || result.code === 200) {
|
|
showToast(`上传成功: ${file.name}`, 'success');
|
|
loadImages();
|
|
} else {
|
|
showToast(`上传失败: ${result.message || '未知错误'}`, 'error');
|
|
}
|
|
} catch (err) {
|
|
item.remove();
|
|
showToast(`网络错误: ${err.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// 画廊视图获取
|
|
async function loadImages() {
|
|
try {
|
|
const response = await fetch('/api/images');
|
|
if (response.status !== 200) {
|
|
if (response.status === 403) {
|
|
document.getElementById('stats-counter').textContent = '未授权验证';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.code === 0 || result.code === 200) {
|
|
renderGallery(result.data || []);
|
|
document.getElementById('stats-counter').textContent = `共 ${(result.data || []).length} 张图片`;
|
|
} else {
|
|
showToast(`加载失败: ${result.message}`, 'error');
|
|
document.getElementById('stats-counter').textContent = '获取失败';
|
|
}
|
|
} catch (err) {
|
|
showToast('网络连接异常', 'error');
|
|
document.getElementById('stats-counter').textContent = '离线';
|
|
}
|
|
}
|
|
|
|
// 侧信道XSS安全防备渲染
|
|
function renderGallery(images) {
|
|
const grid = document.getElementById('gallery-grid');
|
|
grid.innerHTML = '';
|
|
|
|
if (images.length === 0) {
|
|
grid.innerHTML = '<span style="color:var(--text-muted); font-size:14px;">暂无图片</span>';
|
|
return;
|
|
}
|
|
|
|
images.forEach(img => {
|
|
const kbSize = (img.size / 1024).toFixed(1);
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'img-card';
|
|
const finalUrl = img.url || img.origin_url;
|
|
|
|
const safeName = (img.name || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
|
|
card.innerHTML = `
|
|
<img src="${finalUrl}" alt="${safeName}" loading="lazy" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=\\'http://www.w3.org/2000/svg\\' viewBox=\\'0 0 24 24\\' fill=\\'%23ef4444\\'%3E%3Cpath d=\\'M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z\\'/%3E%3C/svg%3E';">
|
|
<div class="card-overlay">
|
|
<div class="card-info">
|
|
<strong>${safeName}</strong> <br/>
|
|
${kbSize} KB • ${new Date(img.created_at).toLocaleDateString()}
|
|
</div>
|
|
<div class="card-actions">
|
|
<button class="btn-action btn-copy" onclick="copyUrl('${finalUrl}')">复制链接</button>
|
|
<button class="btn-action btn-del" onclick="deleteImage('${img.id}', '${safeName}')">删除</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
grid.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function copyUrl(url) {
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
showToast('直链已复制到剪贴板', 'success');
|
|
}).catch(err => {
|
|
showToast('复制失败,请手动选取', 'error');
|
|
});
|
|
}
|
|
|
|
async function deleteImage(id, name) {
|
|
if (!confirm(`确定要永久删除图片 [${name}] 吗?`)) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/images/${id}`, { method: 'DELETE' });
|
|
if (response.status !== 200) return;
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.code === 0 || result.code === 200) {
|
|
showToast(`[${name}] 已删除`, 'success');
|
|
loadImages();
|
|
} else {
|
|
showToast(`删除失败: ${result.message}`, 'error');
|
|
}
|
|
} catch (err) {
|
|
showToast('删除网络请求失败', 'error');
|
|
}
|
|
}
|