/** * 扫描 public/static 下各赛事文件夹,生成 manifest.json * 约定子目录:背景、口令、赛事LOGO、秩序册 */ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const ROOT = path.join(__dirname, '..', 'public', 'static') /** 优先使用新目录名,兼容旧名称 */ const SUBDIR_CANDIDATES = { backgrounds: ['背景', '背景图'], passwords: ['口令'], logos: ['赛事LOGO', '赛事logo'], rosters: ['秩序册'], } function resolveSubdir(folderPath, candidates) { for (const name of candidates) { if (fs.existsSync(path.join(folderPath, name))) return name } return candidates[0] } const IMAGE_EXT = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp', '.svg']) const ROSTER_EXT = new Set(['.txt', '.csv', '.xlsx', '.xls']) const PASSWORD_EXT = new Set(['.mp3']) const LOGO_EXT = IMAGE_EXT function listFiles(dir, allowedExt) { if (!fs.existsSync(dir)) return [] return fs .readdirSync(dir, { withFileTypes: true }) .filter( (e) => e.isFile() && !e.name.startsWith('~$') && allowedExt.has(path.extname(e.name).toLowerCase()), ) .map((e) => e.name) .sort((a, b) => a.localeCompare(b, 'zh-CN')) } /** 使用原始路径;由前端 resolveStaticUrl 统一编码 */ function toPublicPath(folder, sub, file) { return `/static/${folder}/${sub}/${file}` } function readEventMeta(folderPath) { const metaPath = path.join(folderPath, 'event.json') if (!fs.existsSync(metaPath)) return {} try { return JSON.parse(fs.readFileSync(metaPath, 'utf-8')) } catch { return {} } } function scan() { if (!fs.existsSync(ROOT)) { fs.mkdirSync(ROOT, { recursive: true }) return { generatedAt: new Date().toISOString(), events: [] } } const events = [] for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) { if (!entry.isDirectory()) continue if (entry.name.startsWith('.')) continue const folder = entry.name const folderPath = path.join(ROOT, folder) const meta = readEventMeta(folderPath) const bgDir = resolveSubdir(folderPath, SUBDIR_CANDIDATES.backgrounds) const pwdDir = resolveSubdir(folderPath, SUBDIR_CANDIDATES.passwords) const logoDir = resolveSubdir(folderPath, SUBDIR_CANDIDATES.logos) const rosterDir = resolveSubdir(folderPath, SUBDIR_CANDIDATES.rosters) const backgrounds = listFiles(path.join(folderPath, bgDir), IMAGE_EXT).map((file) => ({ file, path: toPublicPath(folder, bgDir, file), })) const passwords = listFiles(path.join(folderPath, pwdDir), PASSWORD_EXT).map((file) => ({ file, path: toPublicPath(folder, pwdDir, file), })) const logos = listFiles(path.join(folderPath, logoDir), LOGO_EXT).map((file) => ({ file, path: toPublicPath(folder, logoDir, file), })) const rosters = listFiles(path.join(folderPath, rosterDir), ROSTER_EXT).map((file) => ({ file, path: toPublicPath(folder, rosterDir, file), })) events.push({ id: folder, name: meta.displayName || folder, folder, backgrounds, passwords, logos, rosters, }) } events.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN')) return { generatedAt: new Date().toISOString(), events, } } const manifest = scan() const outPath = path.join(ROOT, 'manifest.json') fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2), 'utf-8') console.log(`[static-manifest] ${manifest.events.length} 个赛事 -> ${outPath}`)