增加图片上传管理

This commit is contained in:
2026-07-28 11:23:21 +08:00
parent 8d0b75e484
commit 786f11a2ca
7 changed files with 545 additions and 4 deletions
+189 -1
View File
@@ -42,6 +42,18 @@ document.addEventListener('DOMContentLoaded', () => {
// 模态框表单
document.getElementById('modal-form').addEventListener('submit', handleModalSubmit);
// 封面图上传
const coverFileInput = document.getElementById('post-cover-file');
if (coverFileInput) {
coverFileInput.addEventListener('change', handleCoverFileChange);
}
// 文件管理上传
const fileUploadInput = document.getElementById('file-upload-input');
if (fileUploadInput) {
fileUploadInput.addEventListener('change', handleFileUpload);
}
});
// 显示登录页
@@ -92,6 +104,9 @@ function showPage(pageName) {
case 'comments':
loadComments();
break;
case 'files':
loadFiles();
break;
case 'themes':
loadThemes();
break;
@@ -196,6 +211,7 @@ async function showPostForm(isEdit = false) {
document.getElementById('post-form-title').textContent = isEdit ? '编辑文章' : '新建文章';
document.getElementById('post-form').reset();
document.getElementById('post-id').value = '';
setCover('');
// 加载分类选项
const res = await fetch(`${API_BASE}/categories`);
@@ -217,7 +233,7 @@ async function editPost(id) {
document.getElementById('post-title').value = post.title;
document.getElementById('post-content').value = post.content;
document.getElementById('post-summary').value = post.summary || '';
document.getElementById('post-cover').value = post.cover || '';
setCover(post.cover || '');
document.getElementById('post-status').value = post.status;
document.getElementById('post-istop').checked = post.is_top;
document.getElementById('post-tags').value = post.tags?.map(t => t.name).join(', ') || '';
@@ -986,3 +1002,175 @@ async function saveCurrentFile() {
showThemeMessage('保存文件失败', 'error');
}
}
// ==================== 文件上传与管理 ====================
// 允许的图片类型与大小限制(与后端保持一致)
const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
// 校验图片文件,通过返回 null,否则返回错误信息
function validateImageFile(file) {
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
return '不支持的图片格式,仅支持 jpg/png/gif/webp';
}
if (file.size > MAX_IMAGE_SIZE) {
return '图片大小超过限制(最大 5MB)';
}
return null;
}
// 上传图片到服务器,返回 { url, name, size }
async function uploadImageFile(file) {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${API_BASE}/admin/upload`, {
method: 'POST',
// 注意:上传 FormData 时不能手动设置 Content-Type,浏览器会自动处理 boundary
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') },
body: formData
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || '上传失败');
}
return data;
}
// 封面图:选择文件后上传
async function handleCoverFileChange(e) {
const file = e.target.files[0];
if (!file) return;
const err = validateImageFile(file);
if (err) {
alert(err);
e.target.value = '';
return;
}
try {
const data = await uploadImageFile(file);
setCover(data.url);
} catch (uErr) {
alert(uErr.message);
}
e.target.value = '';
}
// 设置封面图 URL 并更新预览
function setCover(url) {
document.getElementById('post-cover').value = url || '';
const preview = document.getElementById('post-cover-preview');
const removeBtn = document.getElementById('post-cover-remove');
if (!preview || !removeBtn) return;
if (url) {
preview.src = url;
preview.classList.remove('hidden');
removeBtn.classList.remove('hidden');
} else {
preview.src = '';
preview.classList.add('hidden');
removeBtn.classList.add('hidden');
}
}
// 移除封面图
function removeCover() {
setCover('');
}
// 加载已上传文件列表
async function loadFiles() {
try {
const res = await fetch(`${API_BASE}/admin/uploads`, { headers: getHeaders() });
const data = await res.json();
const grid = document.getElementById('file-grid');
if (!data.data || data.data.length === 0) {
grid.innerHTML = '<p class="no-files">暂无上传文件</p>';
return;
}
grid.innerHTML = data.data.map(f => `
<div class="file-card">
<div class="file-thumb"><img src="${f.url}" alt="${f.name}" loading="lazy"></div>
<div class="file-info">
<div class="file-name" title="${f.name}">${f.name}</div>
<div class="file-meta">${formatFileSize(f.size)} · ${f.mod_time}</div>
</div>
<div class="file-actions">
<button class="btn-small btn-secondary" onclick="copyFileURL('${f.url}')">复制链接</button>
<button class="btn-small btn-danger" onclick="deleteFile('${f.name}')">删除</button>
</div>
</div>
`).join('');
} catch (err) {
console.error('加载文件失败:', err);
}
}
// 文件管理:批量上传
async function handleFileUpload(e) {
const files = Array.from(e.target.files);
if (files.length === 0) return;
for (const file of files) {
const err = validateImageFile(file);
if (err) {
alert(`${file.name}:${err}`);
continue;
}
try {
await uploadImageFile(file);
} catch (uErr) {
alert(`${file.name}:${uErr.message}`);
}
}
e.target.value = '';
loadFiles();
}
// 删除已上传文件
async function deleteFile(name) {
if (!confirm('确定要删除这个文件吗?')) return;
try {
const res = await fetch(`${API_BASE}/admin/uploads/${encodeURIComponent(name)}`, {
method: 'DELETE',
headers: getHeaders()
});
if (res.ok) {
loadFiles();
} else {
const data = await res.json().catch(() => ({}));
alert(data.error || '删除失败');
}
} catch (err) {
alert('网络错误');
}
}
// 复制文件链接
function copyFileURL(url) {
const full = window.location.origin + url;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(full)
.then(() => alert('链接已复制:' + full))
.catch(() => prompt('复制链接:', full));
} else {
prompt('复制链接:', full);
}
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
+26 -3
View File
@@ -21,6 +21,7 @@
<a href="#tags" class="menu-item" data-page="tags">标签管理</a>
<a href="#pages" class="menu-item" data-page="pages">页面管理</a>
<a href="#comments" class="menu-item" data-page="comments">评论管理</a>
<a href="#files" class="menu-item" data-page="files">文件管理</a>
<a href="#themes" class="menu-item" data-page="themes">主题管理</a>
</nav>
<div class="user-info">
@@ -123,8 +124,17 @@
<input type="text" id="post-tags" placeholder="标签1, 标签2, 标签3">
</div>
<div class="form-group">
<label>封面图 URL</label>
<input type="text" id="post-cover" placeholder="https://example.com/image.jpg">
<label>封面图</label>
<input type="hidden" id="post-cover">
<input type="file" id="post-cover-file" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none">
<div class="cover-uploader">
<img id="post-cover-preview" class="cover-preview hidden" src="" alt="封面预览">
<div class="cover-uploader-actions">
<button type="button" class="btn-secondary" onclick="document.getElementById('post-cover-file').click()">选择图片</button>
<button type="button" class="btn-danger hidden" id="post-cover-remove" onclick="removeCover()">移除</button>
<span class="upload-hint">支持 jpg/png/gif/webp,最大 5MB</span>
</div>
</div>
</div>
<div class="form-group">
<label>摘要</label>
@@ -221,6 +231,19 @@
</table>
</div>
<!-- 文件管理 -->
<div id="files-page" class="page hidden">
<div class="page-header">
<h1>文件管理</h1>
<div>
<input type="file" id="file-upload-input" accept="image/jpeg,image/png,image/gif,image/webp" style="display:none" multiple>
<button class="btn-primary" onclick="document.getElementById('file-upload-input').click()">上传图片</button>
</div>
</div>
<p class="upload-hint">支持 jpg/png/gif/webp 格式,单个文件最大 5MB</p>
<div class="file-grid" id="file-grid"></div>
</div>
<!-- 主题管理 -->
<div id="themes-page" class="page hidden">
<div class="page-header">
@@ -325,6 +348,6 @@
</div>
</div>
<script src="/static/admin/app.js?v=9"></script>
<script src="/static/admin/app.js?v=10"></script>
</body>
</html>
+108
View File
@@ -662,3 +662,111 @@ body {
min-height: 500px;
}
}
/* 通用隐藏 */
.hidden {
display: none !important;
}
/* 封面图上传 */
.cover-uploader {
display: flex;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
}
.cover-preview {
width: 160px;
height: 100px;
object-fit: cover;
border: 1px solid #d9d9d9;
border-radius: 6px;
background: #fafafa;
}
.cover-uploader-actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.upload-hint {
color: #999;
font-size: 12px;
}
/* 文件管理网格 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.file-card {
background: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
display: flex;
flex-direction: column;
transition: all 0.3s;
}
.file-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateY(-2px);
}
.file-thumb {
width: 100%;
height: 140px;
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.file-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.file-info {
padding: 10px 12px;
flex: 1;
}
.file-name {
font-size: 13px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-meta {
margin-top: 6px;
font-size: 12px;
color: #999;
}
.file-actions {
display: flex;
gap: 8px;
padding: 10px 12px;
border-top: 1px solid #f0f0f0;
}
.file-actions .btn-small {
flex: 1;
}
@media (max-width: 768px) {
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
}