打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
登录后可编辑和发表评论。

Module:VOCALOID Songbox/new

来自Vocawiki

原设计 by User:Amero

{{VOCALOID Songbox}}的临时fork,待格式整改完成之后合并入原模板。

示例

{{VOCALOID_Songbox/new
|image    = 春风.jpg
|颜色     = #F5DF41;color:#ED3439
|演唱     = [[初音未来]] & '''john'''
|歌曲名称 = {{lj|春嵐}}<br>春日风暴
|P主      = [[john]]
|nnd_id   = sm36053074
|投稿时间 = 2019年12月7日 <!--未填写站点_date参数时将回退到该参数-->
|yt_id    = pUH9vCsvq08
|bb_id    = BV1MqKXz8Epm
|bb_date  = 2025年6月30日
|其他资料 = 仍然保留了其他资料,但不会自动生成文本
|再生 = 将被忽略
|nocate = 1 <!--不添加日期分类-->
}}
春嵐
春日风暴
仍然保留了其他资料,但不会自动生成文本
演唱
P主
投稿
niconico
2019-12-7
--
bilibili
2025-6-30
--
YouTube
2019-12-7
--

投稿平台数小于3时的样式

投稿
niconico
2019-12-7
--
YouTube
2019-12-7
--
投稿
niconico
2019-12-7
--

特殊情况

如果歌曲有多个版本且不宜使用{{tabs}},或是存在删稿、非公开等情况,可以使用 {{VOCALOID Songbox/card}} ,其参数如下:

{{VOCALOID Songbox/card
|nnd           <!--投稿平台,命名与 Songbox 本体的参数一致,目前包括 nnd/yt/bb/ac/wyy-->
|sm964511      <!--稿件 id-->
|2007年9月1日   <!--投稿时间,格式要求与本体一致-->
|初版          <!--显示的版本名称,可省略,省略时显示默认的平台名称-->
|再生 = 11,375 <!--用于删稿、非公开时手动填写再生数-->
|class =       <!--附加样式类-->
}}

目前可用的附加 class 包括:deleted, one-column, two-columns

示例

使用 class = deleted 标记视频已删除,目前会使卡片中的标题变灰并加删除线、播放量加下划线,如果填写了再生参数则还会在再生数之后添加(最终记录)

{{VOCALOID_Songbox/new
|image   = 初音未来V2盒装图像.png
|颜色    = #39C5BB
|演唱    = [[初音未来]]
|歌曲名称 = {{lj|鳥の詩}}<br>鸟之诗
|P主     = {{lj|でんげん}}
|投稿    =
{{VOCALOID Songbox/card|nnd|sm964511|2007年9月1日|初版|再生=11,375|class=deleted}}
{{VOCALOID Songbox/card|nnd|sm973051|2007年9月2日|第一次修正版|再生=100,689|class=deleted}}
{{VOCALOID Songbox/card|nnd|sm1014311|2007年9月8日|第二次修正版|再生=21,975|class=deleted}}
{{VOCALOID Songbox/card|nnd|sm1051563|2007年9月13日|第三次修正版}}
{{VOCALOID Songbox/card|nnd|sm1175168|2007年9月30日|完整版}}
{{VOCALOID Songbox/card|nnd|sm1279356|2007年10月14日|音质提高版}}
}}
鳥の詩
鸟之诗
演唱
P主
でんげん
投稿
初版
2007-9-1
11,375(最终记录)
第一次修正版
2007-9-2
100,689(最终记录)
第二次修正版
2007-9-8
21,975(最终记录)
第三次修正版
2007-9-13
--
完整版
2007-9-30
--
音质提高版
2007-10-14
--

如果出于逻辑或美观需求需要强制卡片以更少的列数显示,可以在第一个card中使用 two-columnsone-column

{{VOCALOID_Songbox/new
|image    = Six Greetings.jpeg
|演唱     = [[初音未来]]
|歌曲名称 = Six Greetings
|P主  = {{lj|[[きくお]]}}
|投稿 =
{{VOCALOID Songbox/card|nnd|so21551768|2013/8/9|Niconico Short ver|class=two-columns}}
{{VOCALOID Songbox/card|yt|CWtHv3Lih_0|2013/8/9|YouTube Short ver}}
{{VOCALOID Songbox/card|nnd|so21659640|2013/8/23|Niconico Long ver}}
{{VOCALOID Songbox/card|yt|3ejUVQGSKq0|2013/8/23|YouTube Long ver}}
}}
Six Greetings
演唱
P主
投稿
Niconico Short ver
2013-8-9
--
YouTube Short ver
2013-8-9
--
Niconico Long ver
2013-8-23
--
YouTube Long ver
2013-8-23
--
local strings = require('Module:Strings')
local get = require('Module:Get')
local acandy = require('Module:ACandy')
local a, Fragment, Raw = acandy.a, acandy.Fragment, acandy.Raw
local div, span = a.div, a.span
local trim = mw.text.trim

---@alias Image {
---	file: string,
---	caption: string?,
---	size: string?,
---}

---@alias Date {
--- year: number,
--- month: number,
--- day: number,
---}

---@alias Platform { id: string, date: Date }

---@alias Platforms {
---	nico: Platform?,
---	bili: Platform?,
---	acfun: Platform?,
---	ncm: Platform?,
---	ytb: Platform?,
---}

---@alias Style {
---	general: string?,
---	[1]: string?,
---	[2]: string?,
---	[3]: string?,
---}

---@param str string?
---@return string?
local function clean(str)
	if not str then return nil end
	str = trim(str)
	return str ~= '' and str or nil
end

---@param s1 string?
---@param s2 string?
---@return string?
local function merge(s1, s2)
	local merged = trim((s1 or '')..(s2 or ''))
	return merged ~= '' and merged or nil
end

local function raw_or_empty(v)
	return v ~= nil and Raw(v) or ''
end


local c = {}  -- components

---@param props {
---	image: Image?,
---	tabs: string?,
---	title: string?,
---	uploader: string?,
---	producer: string?,
---	note: string?,
---	singer: string?,
---	albums: string?,
---	platforms: Platforms,
---	submissions: string?,
---	colors: Style,
---	text_styles: Style,
---}
function c.SongBox(props, frame)
	local colors = props.colors
	local text_styles = props.text_styles

	local function THeader(nth, content)
		local th_style = colors[nth] or colors.general
		if th_style then
			th_style = 'background:'..th_style
		else
			th_style = 'background: #66CDAA; color: #FFF;'
		end
		return div['songbox_th'] {
			style = th_style,
			span {
				style = 'display: flex; align-items: center;'..(text_styles[nth] or text_styles.general or ''),
				content,
			},
		}
	end

	return div['songbox'] {
		props.image and c.Image(props.image) or '',
		raw_or_empty(props.tabs),
		c.Title(props.title),
		c.Note(props),
		THeader(1, '演唱'),
		div['songbox_td'] / raw_or_empty(props.singer),
		THeader(2, props.uploader and 'UP主' or 'P主'),
		div['songbox_td'] / raw_or_empty(props.uploader or props.producer),
		THeader(3, props.albums and '收录专辑' or '投稿'),
		div['songbox_td'] /
		  (props.albums
		 	and Raw(props.albums)
		 	or div['songbox_submission_container'] /
			  { c.Submissions(props.platforms), raw_or_empty(props.submissions) }
		  ),
	}
end

---@param props Image
function c.Image(props)
	local frame = mw.getCurrentFrame()
	return Raw(frame:expandTemplate { title = '氛围图片', args = props })
end

---@param title string?
function c.Title(title)
	if not title then
		return div['songbox_titles unknown']
	end

	local titles = strings.split(title, '%s*<br%s*/?>%s*')
	local main_title = table.remove(titles, 1)
	return div['songbox_titles'] {
		div['songbox_main-title'] / Raw(main_title),
		Raw(table.concat(titles, '<br>')),
	}
end

---@param props {note: string?}
function c.Note(props)
	return props.note and div['songbox_note'] {
		raw_or_empty(props.note),
	} or ''
end

local platform_data = get(
	{ key = 'nico',  url = 'https://www.nicovideo.jp/watch/%s',  name = 'niconico' },
	{ key = 'bili',  url = 'https://www.bilibili.com/video/%s',  name = 'bilibili' },
	{ key = 'acfun', url = 'http://www.acfun.cn/v/%s',           name = 'AcFun'    },
	{ key = 'ncm',   url = 'https://music.163.com/#/song?id=%s', name = '网易云'   },
	{ key = 'ytb',   url = 'https://www.youtube.com/watch?v=%s', name = 'YouTube'  })

---@param props Platforms
function c.Submissions(props)
	local submissions = Fragment()
	for platform in platform_data:filter(function (x) return props[x.key] end) do
		---@cast platform { key: string, url: string, name: string }
		local data = props[platform.key]  ---@cast data Platform
		submissions:insert(c.Card(platform, data))
	end
	return submissions
end

---@param platform { key: string, url: string, name: string }
---@param data Platform
function c.Card(platform, data, overrides)
	overrides = overrides or {}

	local frame = mw.getCurrentFrame()
	local card = div[('songbox_submission %s %s'):format(platform.key, overrides.class or '')] {
		div['submission_link']
			/ (data.id and Raw(('[%s -{}-]'):format(platform.url:format(data.id)))),
		div['submission_platform'] / Raw(overrides.title or platform.name),
		div['submission_info'] {
			div['submission_date']
				/ Raw(('%s-%s-%s'):format(data.date.year, data.date.month, data.date.day)),
			div['submission_playcount'] /
				(overrides.playcount
				and span['override'] / {
					Raw(overrides.playcount),
					overrides.playcount_note and span['override-note']
						/ Raw(('(%s)'):format(overrides.playcount_note)),
				}
				 or Raw(frame:expandTemplate {
					title = platform.name .. 'Count',
					args = { id = data.id },
				}))
		},
		mw.title.getCurrentTitle()--[[@cast -?]]:inNamespace(0)
			and Raw(
				('[[Category:%s年投稿至%s的歌曲]]'):format(data.date.year, platform.name)
			 .. ('[[Category:%s月%s日投稿至%s的歌曲]]'):format(data.date.month, data.date.day, platform.name)
			) or nil,
	}

	if mw.title.getCurrentTitle()--[[@cast -?]]:inNamespace(0) then
		card['data-json'] = mw.text.jsonEncode({
			id = data.id,
			date = data.date,
			platform = { name = platform.name }
		})
	end

	return card;
end

local p = {
	components = c,
}

local function get_by_aliases(t, ...)
	local n = select('#', ...)
	for i = 1, n do
		local key = select(i, ...)
		local item = t[key]
		if item ~= nil then
			return item
		end
	end
	return nil
end

---@param date_str string?
---@return Date?
local function parse_date(date_str)
	if not date_str then return nil end
	local parts = strings.split(date_str, '%s*[-/年月日]%s*', true)
	local year = tonumber(parts[1])
	local month = tonumber(parts[2])
	local day = tonumber(parts[3])
	if year and month and day then
		return { year = year, month = month, day = day }
	else
		return nil
	end
end

--[[
image
圖片信息 + 图片信息
圖片大小 > 图片大小
tabs

歌曲名称
投稿时间 + 其他资料
演唱
UP主 > P主
收录专辑 > 各平台id&date + 链接

颜色
颜色1
颜色2
颜色3
文字样式
文字样式1
文字样式2
文字样式3
]]
function p.from_args(uncleaned_args)
	local args = {}  ---@type table<string, string | nil>
	for k, v in pairs(uncleaned_args) do
		args[k] = clean(v)
	end

	function platform_from_arg(prefix)
		local id = args[prefix..'_id']
		local date = parse_date(args[prefix..'_date']
			or get_by_aliases(args, '投稿时间', '投稿時間'))
		if not id or not date then return nil end -- TODO: 报错
		return {
			id = id,
			date = date,
		}
	end

	local props = {
		image = args.image and {
			file = args.image,
			caption = merge(args['圖片信息'], args['图片信息']),
			size = get_by_aliases(args, '圖片大小', '图片大小'),
		},
		tabs = args.tabs,
		title = args['歌曲名称'],
		uploader = args['UP主'],
		producer = args['P主'],
		note = args['其他资料'],
		singer = args['演唱'],
		albums = args['收录专辑'],
		platforms = {
			nico  = platform_from_arg('nnd'),
			bili  = platform_from_arg('bb'),
			acfun = platform_from_arg('ac'),
			ncm   = platform_from_arg('wyy'),
			ytb   = platform_from_arg('yt'),
		},
		submissions = args['投稿'],
		colors = {
			general = args['颜色'],
			args['颜色1'],
			args['颜色2'],
			args['颜色3'],
		},
		text_styles = {
			general = args['文字样式'],
			args['文字样式1'],
			args['文字样式2'],
			args['文字样式3'],
		}
	}
	return c.SongBox(props)
end

function p.from_frame(frame)
	return p.from_args(frame.args)
end

function p.from_parent(frame)
	local parent = frame:getParent()
	return p.from_frame(parent)
end

function p.card_from_args(uncleaned_args)
	local args = {}  ---@type table<string, string | nil>
	for k, v in pairs(uncleaned_args) do
		args[k] = clean(v)
	end

	local conversion = {
		nnd = "nico",
		bb  = "bili",
		yt  = "ytb",
		wyy = "ncm",
		ac  = "acfun"
	}
	local key = conversion[args[1]] or args[1]
	local data = {
		id = args[2],
		date = parse_date(args[3])
	}
	local overrides = {
		title = args[4],
		playcount = get_by_aliases(args, '再生', 'count'),
		playcount_note = get_by_aliases(args, '再生提示', 'countnote'),
		class = args['class']
	}
	if overrides.class and overrides.class:find("deleted")
	   and not overrides.playcount_note then
		overrides.playcount_note = "最终记录"
	end
	if overrides.playcount_note == "nil" then
		overrides.playcount_note = nil
	end

	for platform in platform_data:filter(function (x) return x.key == key end) do
		return c.Card(platform, data, overrides)
	end
end

function p.card_from_frame(frame)
	return p.card_from_args(frame.args)
end

function p.card_from_parent(frame)
	local parent = frame:getParent()
	return p.card_from_frame(parent)
end

return p