增加图片上传管理
This commit is contained in:
+189
-1
@@ -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
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user