一个前后端一体的图片上传网站:
后端 (server.js,Node + Express + Multer)
POST /api/upload:接收图片,支持多文件、拖拽、进度反馈GET /api/images:返回已上传图片列表/uploads:对外提供已保存的图片访问- 安全限制:仅允许图片类型(jpg/png/gif/webp/bmp/svg)、单文件最大 20MB、用随机文件名存储避免覆盖
- 文件落盘到项目内的
uploads/目录 
前端 (public/)
- 拖拽 / 点击上传,实时进度条
- 上传后自动刷新画廊,缩略图网格展示,点击可查看原图
- 干净的移动端自适应界面

server.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 3000;
// 上传目录:放在项目根目录下的 uploads 文件夹(也作为静态资源对外提供)
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// 磁盘存储 + 唯一文件名,避免覆盖
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadsDir),
filename: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase() || '.bin';
const name = crypto.randomBytes(16).toString('hex') + ext;
cb(null, name);
}
});
// 不限制文件类型,仅按大小限制(单文件最大 20MB)
const upload = multer({
storage,
limits: { fileSize: 8000 * 1024 * 1024 }
});
// 托管前端静态资源
app.use(express.static(path.join(__dirname, 'public')));
// 对外提供已上传图片
app.use('/uploads', express.static(uploadsDir));
// 上传接口:支持多文件,字段名 images
app.post('/api/upload', (req, res) => {
upload.array('images', 30)(req, res, (err) => {
if (err) {
return res.status(400).json({ success: false, message: err.message });
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({ success: false, message: '未选择任何文件' });
}
const files = req.files.map(f => ({
url: '/uploads/' + f.filename,
name: f.originalname,
size: f.size
}));
res.json({ success: true, files });
});
});
// 列出已上传图片
app.get('/api/images', (req, res) => {
fs.readdir(uploadsDir, (err, items) => {
if (err) return res.status(500).json({ success: false, message: '读取失败' });
const files = items
.filter(f => !f.startsWith('.'))
.map(f => ({
url: '/uploads/' + f,
name: f,
size: fs.statSync(path.join(uploadsDir, f)).size
}));
res.json({ success: true, files });
});
});
app.listen(PORT, () => {
console.log(`图片上传服务已启动: http://localhost:${PORT}`);
});
app.js
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const progressWrap = document.getElementById('progressWrap');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const statusEl = document.getElementById('status');
const gallery = document.getElementById('gallery');
const refreshBtn = document.getElementById('refreshBtn');
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
}
function setStatus(msg, type) {
statusEl.textContent = msg;
statusEl.className = 'status' + (type ? ' ' + type : '');
}
// 点击选择文件
dropzone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
if (fileInput.files.length) uploadFiles(fileInput.files);
fileInput.value = '';
});
// 拖拽
['dragenter', 'dragover'].forEach(evt =>
dropzone.addEventListener(evt, e => {
e.preventDefault();
dropzone.classList.add('dragover');
})
);
['dragleave', 'drop'].forEach(evt =>
dropzone.addEventListener(evt, e => {
e.preventDefault();
dropzone.classList.remove('dragover');
})
);
dropzone.addEventListener('drop', e => {
const files = e.dataTransfer.files;
if (files.length) uploadFiles(files);
});
function uploadFiles(fileList) {
const files = Array.from(fileList);
if (!files.length) {
setStatus('未选择任何文件', 'err');
return;
}
const form = new FormData();
files.forEach(f => form.append('images', f));
progressWrap.classList.remove('hidden');
progressFill.style.width = '0%';
progressText.textContent = '0%';
setStatus('上传中...');
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100);
progressFill.style.width = pct + '%';
progressText.textContent = pct + '%';
}
};
xhr.onload = () => {
let data;
try { data = JSON.parse(xhr.responseText); } catch { data = {}; }
if (xhr.status === 200 && data.success) {
setStatus(`上传成功,共 ${data.files.length} 张`, 'ok');
setTimeout(() => progressWrap.classList.add('hidden'), 800);
loadGallery();
} else {
setStatus('上传失败:' + (data.message || xhr.status), 'err');
}
};
xhr.onerror = () => setStatus('网络错误,上传失败', 'err');
xhr.send(form);
}
async function loadGallery() {
try {
const res = await fetch('/api/images');
const data = await res.json();
if (!data.success) return;
renderGallery(data.files);
} catch {
gallery.innerHTML = '<p class="empty">加载失败</p>';
}
}
const IMG_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'avif', 'ico'];
function isImage(name) {
const ext = name.split('.').pop().toLowerCase();
return IMG_EXT.includes(ext);
}
function renderGallery(files) {
if (!files.length) {
gallery.innerHTML = '<p class="empty">还没有文件,快上传一个吧~</p>';
return;
}
gallery.innerHTML = files.map(f => {
const thumb = isImage(f.name)
? `<img src="${f.url}" alt="${f.name}" loading="lazy" />`
: `<div class="file-thumb">📄</div>`;
return `
<a class="card" href="${f.url}" target="_blank" title="${f.name}">
${thumb}
<div class="info">
<span class="name">${f.name}</span>
<span>${formatSize(f.size)}</span>
</div>
</a>`;
}).join('');
}
refreshBtn.addEventListener('click', loadGallery);
loadGallery();
style.css
:root {
--bg: #f5f7fa;
--card: #ffffff;
--primary: #2f6fed;
--primary-dark: #1f56c4;
--text: #1f2937;
--muted: #6b7280;
--border: #e5e7eb;
--danger: #e23b3b;
--success: #1f9d55;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
}
.container {
max-width: 880px;
margin: 0 auto;
padding: 32px 20px 64px;
}
header { text-align: center; margin-bottom: 28px; }
header h1 { margin: 0 0 6px; font-size: 28px; }
.subtitle { margin: 0; color: var(--muted); font-size: 14px; }
/* 上传区 */
.uploader { margin-bottom: 36px; }
.dropzone {
background: var(--card);
border: 2px dashed var(--border);
border-radius: 14px;
padding: 48px 20px;
text-align: center;
cursor: pointer;
transition: border-color .2s, background .2s;
}
.dropzone:hover { border-color: var(--primary); }
.dropzone.dragover {
border-color: var(--primary);
background: #eef3ff;
}
.dropzone-inner .icon { font-size: 40px; margin-bottom: 8px; }
.hint { margin: 0 0 6px; font-size: 16px; }
.hint .link { color: var(--primary); text-decoration: underline; }
.meta { margin: 0; font-size: 12px; color: var(--muted); }
/* 进度条 */
.progress-wrap {
display: flex;
align-items: center;
gap: 12px;
margin-top: 16px;
}
.progress-bar {
flex: 1;
height: 10px;
background: var(--border);
border-radius: 6px;
overflow: hidden;
}
.progress-fill {
height: 100%;
width: 0%;
background: var(--primary);
transition: width .15s ease;
}
.progress-text { font-size: 13px; color: var(--muted); min-width: 38px; }
/* 状态提示 */
.status { margin-top: 14px; font-size: 14px; min-height: 20px; }
.status.ok { color: var(--success); }
.status.err { color: var(--danger); }
/* 画廊 */
.gallery-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.gallery-head h2 { margin: 0; font-size: 18px; }
.btn-ghost {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
padding: 6px 14px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
}
.btn-ghost:hover { border-color: var(--primary); color: var(--primary); }
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 14px;
}
.gallery .empty {
grid-column: 1 / -1;
text-align: center;
color: var(--muted);
padding: 30px 0;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
}
.card img {
width: 100%;
height: 130px;
object-fit: cover;
display: block;
background: #f0f0f0;
}
.card .file-thumb {
width: 100%;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
background: #f0f0f0;
}
.card .info {
padding: 8px 10px;
font-size: 12px;
color: var(--muted);
}
.card .info .name {
color: var(--text);
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 2px;
}
.hidden { display: none !important; }
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>图片上传服务</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<div class="container">
<header>
<h1>📷 图片上传服务</h1>
<p class="subtitle">拖拽或点击上传图片,自动保存到服务器</p>
</header>
<section class="uploader">
<div id="dropzone" class="dropzone">
<input type="file" id="fileInput" multiple hidden />
<div class="dropzone-inner">
<div class="icon">⬆️</div>
<p class="hint">把图片拖到这里,或 <span class="link">点击选择</span></p>
<p class="meta">支持任意文件类型,单文件最大 20MB</p>
</div>
</div>
<div id="progressWrap" class="progress-wrap hidden">
<div class="progress-bar"><div id="progressFill" class="progress-fill"></div></div>
<span id="progressText" class="progress-text">0%</span>
</div>
<div id="status" class="status"></div>
</section>
<section class="gallery-section">
<div class="gallery-head">
<h2>已上传图片</h2>
<button id="refreshBtn" class="btn-ghost">刷新</button>
</div>
<div id="gallery" class="gallery">
<p class="empty">还没有图片,快上传一张吧~</p>
</div>
</section>
</div>
<script src="/app.js"></script>
</body>
</html>
启动:
node.exe server.js

