Module:曲目页面

Admin​(留言 | 贡献)2026年8月18日 (二) 22:16的版本 (JSON 流水线更新曲目页面模块)

此模块的文档可以在Module:曲目页面/doc创建

local p = {}

-- 数据页: 曲目列表 (曲绘/曲师/曲包/定数 的单一来源)
local DATA_TITLE = '模板:曲目列表.json'

local function esc(s)
	return (s:gsub('|', '|'))
end

local function norm(s)
	return (s:gsub(' ', '_'))
end

-- Lua 中空字符串为真值, 必须显式判空
local function arg(frame, k, default)
	local v = frame.args[k]
	if v == nil or v == '' then
		return default
	end
	return v
end

local function fmt(v)
	if type(v) == 'number' then
		return string.format('%.1f', v)
	end
	return v or ''
end

local function loadData()
	local t = mw.title.new(DATA_TITLE)
	if not t or not t.exists then
		return nil, '数据页不存在: [[' .. DATA_TITLE .. ']]'
	end
	local ok, data = pcall(mw.text.jsonDecode, t:getContent())
	if not ok or type(data) ~= 'table' then
		return nil, '数据页 JSON 解析失败: [[' .. DATA_TITLE .. ']]'
	end
	return data
end

local function findSong(data, name, specialOnly)
	if not specialOnly then
		for _, s in ipairs(data.songs or {}) do
			if s['曲名'] == name then
				return s, false
			end
		end
	end
	for _, s in ipairs(data.special or {}) do
		if s['曲名'] == name then
			return s, true
		end
	end
	return nil, false
end

local function langWrapName(song)
	local lang = song['lang']
	if lang == 'ja' then
		return '{{lang|ja|' .. esc(song['曲名']) .. '}}'
	elseif lang == 'zh-Hant' then
		return '{{lang|zh-Hant|' .. esc(song['曲名']) .. '}}'
	end
	return esc(song['曲名'])
end

local function songArt(song, isSpecial)
	local art = song['曲绘'] or song['曲名'] or ''
	if isSpecial then
		art = art:gsub('(石墩子模式)$', '') .. '_石墩子模式'
	end
	return norm(art)
end

function p.render(frame)
	local name = arg(frame, '曲名', '')
	if name == '' then
		return '<span class="error">未指定 |曲名= 参数</span>'
	end

	local data, err = loadData()
	if not data then
		return '<span class="error">' .. tostring(err) .. '</span>'
	end

	local specialOnly = arg(frame, '特殊', '') == '石墩子'
	local song, isSpecial = findSong(data, name, specialOnly)
	if not song then
		return '<span class="error">数据页中找不到曲目: ' .. esc(name)
			.. '(请检查 [[模板:曲目列表.json]])</span>'
	end

	local defaultTitle = isSpecial
		and (langWrapName(song) .. '(石墩子模式)')
		or song['曲名']
	local title = arg(frame, '标题', defaultTitle)
	local art = songArt(song, isSpecial)
	local consts = song['定数'] or {}

	local out = {}

	local tagline = arg(frame, '标语', '')
	if tagline ~= '' then
		table.insert(out, '<div class="song-tagline">' .. tagline .. '</div>')
	end

	-- 信息框: 自动填 标题/图片/曲师/曲包/定数/谱师/时长/BPM, 手工传参可覆盖
	table.insert(out, '{{曲目信息框')
	table.insert(out, '|标题= ' .. esc(title))
	local imgOverride = arg(frame, '图片', '')
	if imgOverride ~= '' then
		table.insert(out, '|图片= ' .. imgOverride)
	else
		table.insert(out, '|图片= [[文件:曲绘-' .. esc(art) .. '.png|256px]]')
	end
	table.insert(out, '|曲师= ' .. esc(arg(frame, '曲师', song['曲师'] or '')))
	table.insert(out, '|曲包= ' .. arg(frame, '曲包', song['曲包'] or ''))
	table.insert(out, '|时长= ' .. esc(arg(frame, '时长', song['时长'] or '')))
	table.insert(out, '|BPM= ' .. esc(arg(frame, 'BPM', song['BPM'] or '')))
	local charters = song['谱师'] or {}
	for _, d in ipairs({ 'CASUAL', 'TOUGH', 'INSANE', 'PRO' }) do
		table.insert(out, '|' .. d .. '定数= ' .. fmt(consts[d]))
		table.insert(out, '|' .. d .. '谱师= ' .. esc(arg(frame, d .. '谱师', charters[d] or '')))
		table.insert(out, '|' .. d .. '连击= ' .. esc(arg(frame, d .. '连击', '')))
	end
	table.insert(out, '}}')

	-- 标准段落
	table.insert(out, '\n== 解锁方式 ==\n' .. arg(frame, '解锁方式', '(待补充)'))
	table.insert(out, '\n== 游戏相关 ==\n=== 定数调整历史 ===\n'
		.. arg(frame, '定数调整历史', '(待补充)'))
	local extra = arg(frame, '游戏相关', '')
	if extra ~= '' then
		table.insert(out, '\n' .. extra)
	end
	table.insert(out, '\n== 歌词 ==\n' .. arg(frame, '歌词', '(待补充)'))
	local other = arg(frame, '其他', '')
	if other ~= '' then
		table.insert(out, '\n== 其他 ==\n' .. other)
	end

	table.insert(out, '\n[[Category:曲目]]')

	-- 展开 {{曲目信息框}} 等模板
	return frame:preprocess(table.concat(out, '\n'))
end

return p