Lumin Admin 媒体库改造:全站媒体文件管理
📌 概述
媒体库是后台管理系统中管理图片、音频、视频等文件的核心模块。本次改造将媒体库从仅扫描 static/images/uploads/ 单一目录,升级为递归扫描整个 static/ 目录,自动识别文件类型,支持按类型筛选、来源切换(本地/远程图床)、智能上传路由等功能。
本功能基于 Vue 3 + Element Plus 前端 + Node.js Express 后端实现,支持图片/音频/视频三种媒体类型的统一管理。
一、Hugo 静态目录结构
Hugo 的 static/ 目录下的文件在构建时直接映射到站点根路径,这是媒体文件存放的标准位置:
1static/
2├── images/ # 图片目录
3│ ├── avatar/ # 头像
4│ ├── banners/ # 横幅
5│ ├── icons/ # 图标
6│ ├── reward/ # 赞赏码
7│ └── uploads/ # 上传图片
8├── audios/ # 音频目录(新建)
9├── videos/ # 视频目录(新建)
10└── libs/ # 第三方库
访问路径映射规则:
| 本地路径 | 网站访问路径 |
|---|---|
static/images/photo.jpg | /images/photo.jpg |
static/audios/song.mp3 | /audios/song.mp3 |
static/videos/demo.mp4 | /videos/demo.mp4 |
二、后端改造
2.1 媒体类型识别
定义媒体扩展名映射表,自动识别文件类型:
1const MEDIA_EXTENSIONS = {
2 image: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif'],
3 audio: ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma'],
4 video: ['.mp4', '.webm', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v']
5}
6
7function getMediaType(ext) {
8 const e = ext.toLowerCase()
9 for (const [type, exts] of Object.entries(MEDIA_EXTENSIONS)) {
10 if (exts.includes(e)) return type
11 }
12 return 'other'
13}
2.2 递归目录扫描
1function scanMediaDir(dir, basePath = '') {
2 const results = []
3 if (!existsSync(dir)) return results
4 for (const entry of readdirSync(dir, { withFileTypes: true })) {
5 if (entry.name.startsWith('.')) continue
6 const fp = join(dir, entry.name)
7 const relPath = basePath ? basePath + '/' + entry.name : entry.name
8 if (entry.isDirectory()) {
9 results.push(...scanMediaDir(fp, relPath))
10 } else {
11 const ext = extname(entry.name)
12 const s = statSync(fp)
13 results.push({
14 name: entry.name,
15 path: relPath,
16 url: '/' + relPath,
17 type: getMediaType(ext),
18 size: s.size,
19 modified: s.mtime.toISOString().replace('T', ' ').slice(0, 19)
20 })
21 }
22 }
23 return results
24}
2.3 API 接口
1app.get('/api/media', (req, res) => {
2 let files = scanMediaDir(STATIC_DIR)
3
4 // 类型筛选
5 if (req.query.type && req.query.type !== 'all') {
6 files = files.filter(f => f.type === req.query.type)
7 }
8
9 // 搜索
10 if (req.query.search) {
11 const s = req.query.search.toLowerCase()
12 files = files.filter(f => f.name.toLowerCase().includes(s) || f.path.toLowerCase().includes(s))
13 }
14
15 // 按修改时间倒序
16 files.sort((a, b) => b.modified.localeCompare(a.modified))
17
18 // 统计各类型数量
19 const counts = { image: 0, audio: 0, video: 0, other: 0 }
20 const allFiles = scanMediaDir(STATIC_DIR)
21 allFiles.forEach(f => { counts[f.type] = (counts[f.type] || 0) + 1 })
22
23 ok(res, { files, counts, total: allFiles.length })
24})
2.4 智能上传路由
根据文件类型自动路由到不同目录:
1app.post('/api/media/upload', upload.single('file'), (req, res) => {
2 const ext = extname(req.file.originalname)
3 const mediaType = getMediaType(ext)
4
5 let targetDir = UPLOAD_DIR // 默认 images/uploads
6 if (mediaType === 'audio') targetDir = join(STATIC_DIR, 'audios')
7 else if (mediaType === 'video') targetDir = join(STATIC_DIR, 'videos')
8
9 if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true })
10
11 const newName = Date.now() + '-' + Math.random().toString(36).slice(2, 8) + ext
12 // ... 重命名并返回 URL
13})
2.5 安全删除
删除接口支持按完整路径删除,增加路径安全检查:
1app.delete('/api/media/:filename', (req, res) => {
2 const decodedName = decodeURIComponent(req.params.filename)
3 const filePath = join(STATIC_DIR, decodedName)
4 if (!filePath.startsWith(STATIC_DIR)) return fail(res, 403, 'Access denied')
5 if (!existsSync(filePath)) return fail(res, 404, 'File not found')
6 unlinkSync(filePath)
7 ok(res, { status: 'ok' })
8})
2.6 静态文件服务
为音频和视频目录添加静态文件服务:
1app.use('/images', express.static(IMAGES_DIR))
2app.use('/audios', express.static(AUDIOS_DIR))
3app.use('/videos', express.static(VIDEOS_DIR))
2.7 文件上传限制
multer 配置从 20MB 提升到 100MB,支持所有主流媒体格式:
1const upload = multer({
2 dest: UPLOAD_DIR,
3 limits: { fileSize: 100 * 1024 * 1024 },
4 fileFilter: (req, file, cb) => {
5 const allowed = [
6 '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.avif',
7 '.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.wma',
8 '.mp4', '.webm', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.m4v'
9 ]
10 cb(null, allowed.includes(extname(file.originalname).toLowerCase()))
11 }
12})
三、前端改造
3.1 类型筛选栏
支持按媒体类型筛选,显示各类型数量:
1<el-radio-group v-model="typeFilter" @change="loadMedia">
2 <el-radio-button value="all">全部 {{ totalCounts.total || 0 }}</el-radio-button>
3 <el-radio-button value="image">图片 {{ totalCounts.image || 0 }}</el-radio-button>
4 <el-radio-button value="audio">音频 {{ totalCounts.audio || 0 }}</el-radio-button>
5 <el-radio-button value="video">视频 {{ totalCounts.video || 0 }}</el-radio-button>
6</el-radio-group>
3.2 来源切换
支持本地和远程图床切换:
1<el-radio-group v-model="sourceFilter" @change="handleSourceChange">
2 <el-radio-button value="local">本地</el-radio-button>
3 <el-radio-button v-for="p in remoteProviders" :key="p.key" :value="p.key">
4 {{ p.name }}
5 </el-radio-button>
6</el-radio-group>
远程图床列表从图床配置 API 动态获取已启用且已配置的图床。
3.3 不同类型的预览
网格视图中,不同类型使用不同的预览方式:
| 类型 | 预览方式 | 背景色 |
|---|---|---|
| 图片 | 缩略图 | 默认 #f5f5f8 |
| 音频 | 耳机图标 | 绿色渐变 #e6f9f0 → #d1fae5 |
| 视频 | 播放图标 | 黄色渐变 #fff8e6 → #fef3c7 |
| 其他 | 文档图标 | 灰色渐变 #f1f5f9 → #e2e8f0 |
3.4 预览弹窗
点击文件弹出预览弹窗,不同类型使用不同播放器:
1<img v-if="previewFileData?.type === 'image'" :src="previewFileData.url" />
2<audio v-else-if="previewFileData?.type === 'audio'" :src="previewFileData.url" controls />
3<video v-else-if="previewFileData?.type === 'video'" :src="previewFileData.url" controls />
弹窗底部显示文件元数据:路径、大小、修改时间、可点击的链接。
四、文件变更清单
| 文件 | 变更 |
|---|---|
admin/backend/server.js | 媒体 API 全面重构,递归扫描 + 类型识别 + 智能路由 |
admin/frontend/src/views/Media/index.vue | 前端页面重构,类型筛选 + 来源切换 + 多类型预览 |
admin/frontend/src/api/index.js | API 函数参数调整 |
myblog/static/audios/ | 新建音频目录 |
myblog/static/videos/ | 新建视频目录 |
留言评论
期待你的想法评论加载中