124 lines
3.5 KiB
JavaScript
124 lines
3.5 KiB
JavaScript
/**
|
||
* 扫描 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}`)
|