模块:CardData:修订间差异
来自夜幕之下
更多操作
无编辑摘要 |
修复:加 deep filter 绕过 draft 过滤;修正小传结构解析和 CSS 类名以匹配 sandbox/2.0;移除 cardcss 模板引用 (via update-page on MediaWiki MCP Server) |
||
| 第10行: | 第10行: | ||
local function val(v, fallback) | local function val(v, fallback) | ||
if v == nil or v == "" then | if v == nil or v == "" then return fallback or "—" end | ||
return v | return v | ||
end | end | ||
| 第36行: | 第34行: | ||
if extraClass then div:addClass(extraClass) end | if extraClass then div:addClass(extraClass) end | ||
div:wikitext(titleText) | div:wikitext(titleText) | ||
div:tag("span") | div:tag("span"):addClass("card_content-item-subtitle"):wikitext(subtitleText) | ||
return div | return div | ||
end | end | ||
local function makeHr(extraClass) | local function makeHr(extraClass) | ||
local div = mw.html.create("div") | local div = mw.html.create("div"):addClass("card_content-item-hr") | ||
if extraClass then div:addClass(extraClass) end | if extraClass then div:addClass(extraClass) end | ||
return div | return div | ||
| 第50行: | 第45行: | ||
local function makeTriggerLabel(triggerType, triggerValue) | local function makeTriggerLabel(triggerType, triggerValue) | ||
local span = mw.html.create("span") | local span = mw.html.create("span"):addClass("card_content_skill-meta-val") | ||
if triggerType == "normal_attack" then | if triggerType == "normal_attack" then | ||
span:wikitext("每进行") | span:wikitext("每进行") | ||
| 第76行: | 第70行: | ||
local wrap = mw.html.create("div"):addClass("card_content_skill-upgrade") | local wrap = mw.html.create("div"):addClass("card_content_skill-upgrade") | ||
local rows = parseJson(levelsJson) | local rows = parseJson(levelsJson) | ||
if not rows or type(rows) ~= "table" or #rows == 0 then | if not rows or type(rows) ~= "table" or #rows == 0 then return wrap end | ||
local colCount = 0 | local colCount = 0 | ||
if | if rows[1] and rows[1].levels then colCount = #rows[1].levels end | ||
local gridClass = "card_content_skill-upgrade-grid" | local gridClass = "card_content_skill-upgrade-grid" | ||
if colCount >= 10 then | if colCount >= 10 then gridClass = gridClass .. " card_content_skill-upgrade-grid--10" end | ||
local grid = wrap:tag("div"):addClass(gridClass) | local grid = wrap:tag("div"):addClass(gridClass) | ||
| 第102行: | 第89行: | ||
labelCell:tag("span"):addClass("card_content_skill-upgrade-badge-text"):wikitext(val(row.name, "")) | labelCell:tag("span"):addClass("card_content_skill-upgrade-badge-text"):wikitext(val(row.name, "")) | ||
labelCell:tag("span"):addClass("card_content_skill-upgrade-arrow"):wikitext("→") | labelCell:tag("span"):addClass("card_content_skill-upgrade-arrow"):wikitext("→") | ||
if row.levels and type(row.levels) == "table" then | if row.levels and type(row.levels) == "table" then | ||
for _, v in ipairs(row.levels) do | for _, v in ipairs(row.levels) do | ||
| 第114行: | 第100行: | ||
end | end | ||
end | end | ||
return wrap | return wrap | ||
end | end | ||
| 第128行: | 第113行: | ||
local nameRow = titleText:tag("div") | local nameRow = titleText:tag("div") | ||
nameRow:tag("span"):addClass("card_content_skill-name"):wikitext(val(skillData.name)) | nameRow:tag("span"):addClass("card_content_skill-name"):wikitext(val(skillData.name)) | ||
nameRow:tag("span") | nameRow:tag("span"):addClass("card_content_skill-type card_accent--type") | ||
:wikitext("「" .. val(skillData.type) .. "」") | :wikitext("「" .. val(skillData.type) .. "」") | ||
| 第135行: | 第119行: | ||
if skillType == "normal_attack" then | if skillType == "normal_attack" then | ||
metaRow:wikitext("使用武器 ") | metaRow:wikitext("使用武器 ") | ||
metaRow:tag("span") | metaRow:tag("span"):addClass("card_content_skill-meta-val card_accent"):wikitext(val(skillData.weapon)) | ||
elseif skillType == "passive" then | elseif skillType == "passive" then | ||
metaRow:wikitext("触发条件 ") | metaRow:wikitext("触发条件 ") | ||
| 第150行: | 第132行: | ||
left:node(makeTagsRow(skillData.tags)) | left:node(makeTagsRow(skillData.tags)) | ||
end | end | ||
left:tag("div"):addClass("card_content_skill_effect"):wikitext(val(skillData.description)) | |||
left:tag("div") | |||
card:node(makeUpgradeTable(skillData.levels)) | card:node(makeUpgradeTable(skillData.levels)) | ||
return card | return card | ||
| 第162行: | 第140行: | ||
local ul = mw.html.create("ul"):addClass("card_content_feat-ul") | local ul = mw.html.create("ul"):addClass("card_content_feat-ul") | ||
local stages = parseJson(stagesJson) | local stages = parseJson(stagesJson) | ||
if not stages or type(stages) ~= "table" then | if not stages or type(stages) ~= "table" then | ||
ul:tag("li"):wikitext("—") | ul:tag("li"):wikitext("—") | ||
return ul | return ul | ||
end | end | ||
for _, stage in ipairs(stages) do | for _, stage in ipairs(stages) do | ||
local li = ul:tag("li"):addClass("card_content_feat-list") | local li = ul:tag("li"):addClass("card_content_feat-list") | ||
local row = li:tag("div"):addClass("card_content_feat-row") | local row = li:tag("div"):addClass("card_content_feat-row") | ||
local stageWrap = row:tag("div"):addClass("card_content_feat-stage-wrap") | local stageWrap = row:tag("div"):addClass("card_content_feat-stage-wrap") | ||
stageWrap:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE") | stageWrap:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE") | ||
stageWrap:tag("div"):addClass("card_content_feat-number") | stageWrap:tag("div"):addClass("card_content_feat-number") | ||
:wikitext(string.format("%02d", | :wikitext(string.format("%02d", stage.stage or 0)) | ||
local rightDiv = row:tag("div") | local rightDiv = row:tag("div") | ||
if stage.value and type(stage.value) == "table" then | |||
if stage.value and type(stage.value) == "table" | |||
for _, v in ipairs(stage.value) do | for _, v in ipairs(stage.value) do | ||
if v.description then | if v.description then | ||
| 第188行: | 第160行: | ||
end | end | ||
end | end | ||
if stage.stat_boosts and type(stage.stat_boosts) == "table" then | if stage.stat_boosts and type(stage.stat_boosts) == "table" then | ||
for _, boost in ipairs(stage.stat_boosts) do | for _, boost in ipairs(stage.stat_boosts) do | ||
| 第194行: | 第165行: | ||
end | end | ||
end | end | ||
if stage.extra_name then | if stage.extra_name then | ||
rightDiv:tag("span") | rightDiv:tag("span"):addClass("card_content_feat-stage-cn") | ||
:wikitext("「" .. stage.extra_name .. "」") | :wikitext("「" .. stage.extra_name .. "」") | ||
end | end | ||
end | end | ||
return ul | return ul | ||
end | end | ||
| 第207行: | 第175行: | ||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
-- 辅助:小传时间线 | -- 辅助:小传时间线 | ||
-- | -- 数据结构:card.stories = [ { sort, story: [ { id, text, title? }, ... ] } ] | ||
-- 每条 stories 记录对应一张卡,其 story 数组里每一项是一个章节。 | |||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
local function makeStoryTimeline(stories) | local function makeStoryTimeline(stories) | ||
| 第215行: | 第183行: | ||
timeline:tag("div"):addClass("card_timeline-line") | timeline:tag("div"):addClass("card_timeline-line") | ||
-- | -- 取第一条 stories 记录(一张卡只有一条) | ||
local storiesEntry = stories and type(stories) == "table" and stories[1] | |||
local chapters = storiesEntry and type(storiesEntry.story) == "table" and storiesEntry.story or {} | |||
if #chapters == 0 then | |||
timeline:tag("div"):addClass("card_timeline-item"):wikitext("(小传数据待补充)") | timeline:tag("div"):addClass("card_timeline-item"):wikitext("(小传数据待补充)") | ||
return timeline | return timeline | ||
end | end | ||
local romans = { "Ⅰ", "Ⅱ", "Ⅲ", "Ⅳ", "Ⅴ" } | |||
local | local unlockLevels = { "Lv.20 解锁", "Lv.40 解锁", "Lv.60 解锁", "Lv.80 解锁", "Lv.100 解锁" } | ||
for | |||
for idx, chapter in ipairs(chapters) do | |||
local isLast = (idx == #chapters) | |||
local item = timeline:tag("div"):addClass("card_timeline-item") | |||
local | if isLast then item:addClass("card_timeline-item--last") end | ||
item:tag("div"):addClass("card_timeline-dot") | |||
item:tag("div"):addClass("card_timeline-unlock") | |||
:wikitext(unlockLevels[idx] or ("Lv." .. (idx * 20) .. " 解锁")) | |||
-- | -- 标题行:罗马数字 + 章节名(有则显示)+ "/ Story" | ||
item:tag("div") | local titleDiv = item:tag("div"):addClass("card_timeline-title") | ||
titleDiv:tag("span"):addClass("card_timeline-numeral") | |||
:wikitext(tostring(idx)) | :wikitext(romans[idx] or tostring(idx)) | ||
if chapter.title and chapter.title ~= "" then | |||
titleDiv:tag("span"):addClass("card_timeline-chapter"):wikitext(chapter.title) | |||
end | |||
titleDiv:tag("span"):addClass("card_timeline-label"):wikitext("/ Story") | |||
-- | -- 正文:\n 转 <br> | ||
local bodyDiv = item:tag("div"):addClass("card_timeline-body") | local bodyDiv = item:tag("div"):addClass("card_timeline-body") | ||
local text = chapter.text or "" | |||
if text ~= "" then | |||
local lines = mw.text.split(text, "\n") | |||
for i, line in ipairs(lines) do | |||
if line ~= "" then bodyDiv:wikitext(line) end | |||
if i < #lines then bodyDiv:tag("br") end | |||
end | end | ||
end | end | ||
| 第268行: | 第233行: | ||
function p.render(frame) | function p.render(frame) | ||
local name = mw.text.trim(frame.args[1] or frame.args.name or "") | local name = mw.text.trim(frame.args[1] or frame.args.name or "") | ||
if name == "" then | if name == "" then | ||
return '<span class="error">错误:请提供卡片名称,例如 {{#invoke:CardData|render|复仇童谣}}</span>' | return '<span class="error">错误:请提供卡片名称,例如 {{#invoke:CardData|render|复仇童谣}}</span>' | ||
| 第276行: | 第240行: | ||
local fields = table.concat({ | local fields = table.concat({ | ||
"stylename", "rarity", | "stylename", "rarity", | ||
"character.name", | "character.name", "profession.name", "desire.name", | ||
"skill_normal_attack.name", "skill_normal_attack.type", | |||
"skill_normal_attack.weapon", "skill_normal_attack.description", | |||
"skill_normal_attack.name", | |||
"skill_normal_attack.weapon", | |||
"skill_normal_attack.levels.*", | "skill_normal_attack.levels.*", | ||
"skill_passive.name", | "skill_passive.name", "skill_passive.type", | ||
"skill_passive.trigger_type", "skill_passive.trigger_value", | |||
"skill_passive.trigger_type", | "skill_passive.tags", "skill_passive.description", | ||
"skill_passive.tags", | |||
"skill_passive.levels.*", | "skill_passive.levels.*", | ||
"skill_ultimate.name", | "skill_ultimate.name", "skill_ultimate.type", | ||
"skill_ultimate.desire_cost", "skill_ultimate.tags", | |||
"skill_ultimate.desire_cost", | "skill_ultimate.description", "skill_ultimate.levels.*", | ||
"skill_ultimate.description", | |||
"feats.stages", | "feats.stages", | ||
"stories.sort", | "stories.sort", | ||
"stories.story. | "stories.story.*" -- 取 story 数组里所有字段(含 title、text、id) | ||
}, ",") | }, ",") | ||
| 第306行: | 第260行: | ||
.. "?fields=" .. mw.uri.encode(fields, "QUERY") | .. "?fields=" .. mw.uri.encode(fields, "QUERY") | ||
.. "&filter[stylename][_eq]=" .. encodedName | .. "&filter[stylename][_eq]=" .. encodedName | ||
-- deep filter:让 Directus 不过滤掉 draft 状态的 stories 关联记录 | |||
.. "&deep[stories][_filter][status][_neq]=archived" | |||
-- ── 2. | -- ── 2. 拉取原始 JSON ── | ||
local rawResponse = frame:preprocess( | local rawResponse = frame:preprocess( | ||
'{{#get_web_data:url=' .. apiUrl | '{{#get_web_data:url=' .. apiUrl | ||
.. '|format=text | .. '|format=text|data=responseText=__text}}' | ||
.. '{{#external_value:responseText}}' | .. '{{#external_value:responseText}}' | ||
) | ) | ||
rawResponse = mw.text.trim(rawResponse or "") | rawResponse = mw.text.trim(rawResponse or "") | ||
-- ── 3. 解析 ── | -- ── 3. 解析 ── | ||
local parsed = parseJson(rawResponse) | local parsed = parseJson(rawResponse) | ||
local card = parsed and type(parsed.data) == "table" and parsed.data[1] or nil | local card = parsed and type(parsed.data) == "table" and parsed.data[1] or nil | ||
if not card then | if not card then | ||
| 第336行: | 第290行: | ||
local featsStagesJson = "[]" | local featsStagesJson = "[]" | ||
if card.feats and type(card.feats) == "table" and card.feats[1] then | if card.feats and type(card.feats) == "table" and card.feats[1] then | ||
local | local sr = card.feats[1].stages | ||
featsStagesJson = type(sr) == "table" and mw.text.jsonEncode(sr) | |||
or type(sr) == "string" and sr or "[]" | |||
end | end | ||
| 第347行: | 第298行: | ||
if not skill.levels then return "[]" end | if not skill.levels then return "[]" end | ||
if type(skill.levels) == "string" then return skill.levels end | if type(skill.levels) == "string" then return skill.levels end | ||
if type(skill.levels) == "table" then return mw.text.jsonEncode(skill.levels) end | if type(skill.levels) == "table" then return mw.text.jsonEncode(skill.levels) end | ||
return "[]" | return "[]" | ||
end | end | ||
| 第357行: | 第308行: | ||
if not skill.tags then return "[]" end | if not skill.tags then return "[]" end | ||
if type(skill.tags) == "string" then return skill.tags end | if type(skill.tags) == "string" then return skill.tags end | ||
if type(skill.tags) == "table" then return mw.text.jsonEncode(skill.tags) end | if type(skill.tags) == "table" then return mw.text.jsonEncode(skill.tags) end | ||
return "[]" | return "[]" | ||
end | end | ||
| 第371行: | 第322行: | ||
local root = mw.html.create("div") | local root = mw.html.create("div") | ||
:addClass("ron-card | :addClass("ron-card ron-card--rarity-" .. rarity) | ||
:attr("data-rarity", rarity) | :attr("data-rarity", rarity) | ||
| 第378行: | 第328行: | ||
root:tag("div"):addClass("card_hero"):css("height", "100vh") | root:tag("div"):addClass("card_hero"):css("height", "100vh") | ||
local | local layout = root:tag("div"):addClass("card_content-container") | ||
:tag("div"):addClass("card_content-background") | |||
:tag("div"):addClass("card_content-inner") | |||
:tag("div"):addClass("card_content card_content--layout") | |||
-- 左栏 | -- ── 左栏 ── | ||
local leftCol = layout:tag("div"):addClass("card_content_left") | local leftCol = layout:tag("div"):addClass("card_content_left") | ||
local | local nc = leftCol:tag("div"):addClass("card_content_name-container") | ||
local | local nr = nc:tag("div"):addClass("card_content_name-row") | ||
nr:tag("div"):addClass("card_content_name-icon") | |||
nr:tag("div"):addClass("card_content_name-classtext"):wikitext(profName) | |||
nr:tag("div"):addClass("card_content_name-icon card_content_name-icon--ml") | |||
nr:tag("div"):addClass("card_content_name-classtext"):wikitext(desName) | |||
local nt = nc:tag("div"):addClass("card_content_name-text") | |||
nt:wikitext(stylename .. " ") | |||
local | nt:tag("span"):addClass("card_accent"):wikitext("·") | ||
nt:wikitext(" " .. charName) | |||
leftCol:node(makeTitle("信息", "Info")) | leftCol:node(makeTitle("信息", "Info")) | ||
leftCol:node(makeHr()) | leftCol:node(makeHr()) | ||
leftCol:tag("div"):addClass("card_content_info") | |||
:tag("div"):addClass("card_content_info-item") | |||
:tag("div"):wikitext("稀有度"):done() | :tag("div"):wikitext("稀有度"):done() | ||
:tag("div"):wikitext(rarityStars(rarity)) | :tag("div"):wikitext(rarityStars(rarity)) | ||
| 第417行: | 第365行: | ||
leftCol:tag("div"):addClass("card_content_attribute-item"):wikitext("(数据待补充)") | leftCol:tag("div"):addClass("card_content_attribute-item"):wikitext("(数据待补充)") | ||
-- 右栏 | -- ── 右栏 ── | ||
local rightCol = layout:tag("div"):addClass("card_content_right") | local rightCol = layout:tag("div"):addClass("card_content_right") | ||
| 第436行: | 第384行: | ||
local storyHeader = storySection:tag("div"):addClass("card_content_story-header") | local storyHeader = storySection:tag("div"):addClass("card_content_story-header") | ||
local storyLeft = storyHeader:tag("div"):addClass("card_content_story-header-left") | local storyLeft = storyHeader:tag("div"):addClass("card_content_story-header-left") | ||
storyLeft:node(makeTitle("小传", "Story")) | storyLeft:node(makeTitle("小传", "Story")) | ||
storyLeft:node(makeHr("card_content-item-hr--mb12")) | storyLeft:node(makeHr("card_content-item-hr--mb12")) | ||
| 第443行: | 第390行: | ||
:tag("span"):addClass("story-toggle-text"):wikitext("收起"):done() | :tag("span"):addClass("story-toggle-text"):wikitext("收起"):done() | ||
:tag("span"):addClass("story-toggle-icon"):wikitext("-") | :tag("span"):addClass("story-toggle-icon"):wikitext("-") | ||
storyHeader:tag("div"):addClass("card_content_story-header-spacer") | storyHeader:tag("div"):addClass("card_content_story-header-spacer") | ||
storyHeader:node(makeStoryTimeline(stories)) | storyHeader:node(makeStoryTimeline(stories)) | ||
-- ── | -- ── 邀约(待补充) ── | ||
rightCol:tag("div"):css("height", "12px"):css("width", "100%") | rightCol:tag("div"):css("height", "12px"):css("width", "100%") | ||
rightCol:node(makeTitle("邀约", "Date")) | rightCol:node(makeTitle("邀约", "Date")) | ||
rightCol:node(makeHr("card_content-item-hr--mb12")) | rightCol:node(makeHr("card_content-item-hr--mb12")) | ||
rightCol:tag("div"):addClass("card_content_date") | |||
:tag("div"):addClass("card_content_date-track") | |||
:tag("ul"):addClass("card_content_date-list") | :tag("ul"):addClass("card_content_date-list") | ||
:tag("li"):addClass("card_content_date-item") | :tag("li"):addClass("card_content_date-item"):wikitext("(邀约数据待补充)") | ||
return tostring(root) | return tostring(root) | ||
2026年3月11日 (三) 20:26的版本
此模块的文档可以在模块:CardData/doc创建
-- Module:CardData
-- 用法:{{#invoke:CardData|render|复仇童谣}}
-- 通过 ExternalData 扩展从 Directus API 拉取数据,在服务端渲染完整 HTML
local p = {}
-- ══════════════════════════════════════════════
-- 工具函数
-- ══════════════════════════════════════════════
local function val(v, fallback)
if v == nil or v == "" then return fallback or "—" end
return v
end
local function rarityStars(n)
n = tonumber(n) or 0
return string.rep("★", n)
end
local function parseJson(str)
if not str or str == "" then return nil end
local ok, result = pcall(mw.text.jsonDecode, str)
if ok then return result else return nil end
end
-- ──────────────────────────────────────────────
-- 辅助:段落标题
-- ──────────────────────────────────────────────
local function makeTitle(titleText, subtitleText, extraClass)
local div = mw.html.create("div")
:addClass("card_content-item-title")
:addClass("card_content-item-title--mt")
if extraClass then div:addClass(extraClass) end
div:wikitext(titleText)
div:tag("span"):addClass("card_content-item-subtitle"):wikitext(subtitleText)
return div
end
local function makeHr(extraClass)
local div = mw.html.create("div"):addClass("card_content-item-hr")
if extraClass then div:addClass(extraClass) end
return div
end
local function makeTriggerLabel(triggerType, triggerValue)
local span = mw.html.create("span"):addClass("card_content_skill-meta-val")
if triggerType == "normal_attack" then
span:wikitext("每进行")
span:tag("span"):addClass("card_accent"):wikitext(tostring(triggerValue or "?"))
span:wikitext("次普攻")
else
span:wikitext(val(triggerType))
end
return span
end
local function makeTagsRow(tagsJson)
local div = mw.html.create("div"):addClass("card_content_skill-tags")
local tags = parseJson(tagsJson)
if tags and type(tags) == "table" then
for _, tag in ipairs(tags) do
div:tag("div"):addClass("card_content_skill-tag"):wikitext(tag)
end
end
return div
end
local function makeUpgradeTable(levelsJson)
local wrap = mw.html.create("div"):addClass("card_content_skill-upgrade")
local rows = parseJson(levelsJson)
if not rows or type(rows) ~= "table" or #rows == 0 then return wrap end
local colCount = 0
if rows[1] and rows[1].levels then colCount = #rows[1].levels end
local gridClass = "card_content_skill-upgrade-grid"
if colCount >= 10 then gridClass = gridClass .. " card_content_skill-upgrade-grid--10" end
local grid = wrap:tag("div"):addClass(gridClass)
grid:tag("div")
for i = 2, colCount do
grid:tag("div"):addClass("card_content_skill-upgrade-lv"):wikitext(tostring(i))
end
for idx, row in ipairs(rows) do
local labelCell = grid:tag("div"):addClass("card_content_skill-upgrade-row-label")
labelCell:tag("span"):addClass("card_content_skill-upgrade-badge"):wikitext(tostring(idx))
labelCell:tag("span"):addClass("card_content_skill-upgrade-badge-text"):wikitext(val(row.name, ""))
labelCell:tag("span"):addClass("card_content_skill-upgrade-arrow"):wikitext("→")
if row.levels and type(row.levels) == "table" then
for _, v in ipairs(row.levels) do
local cell = grid:tag("div"):addClass("card_content_skill-upgrade-val")
if v == nil or v == "" or v == "—" then
cell:addClass("card_content_skill-upgrade-val--null"):wikitext("—")
else
cell:addClass("card_accent"):wikitext(tostring(v))
end
end
end
end
return wrap
end
local function makeSkillCard(skillData, skillType)
local card = mw.html.create("div"):addClass("card_content_skill-card")
local left = card:tag("div"):addClass("card_content_skill-left")
local titleRow = left:tag("div"):addClass("card_content_skill-title")
titleRow:tag("div"):addClass("card_content_skill-icon")
local titleText = titleRow:tag("div"):addClass("card_content_skill-title-text")
local nameRow = titleText:tag("div")
nameRow:tag("span"):addClass("card_content_skill-name"):wikitext(val(skillData.name))
nameRow:tag("span"):addClass("card_content_skill-type card_accent--type")
:wikitext("「" .. val(skillData.type) .. "」")
local metaRow = titleText:tag("div"):addClass("card_content_skill-meta")
if skillType == "normal_attack" then
metaRow:wikitext("使用武器 ")
metaRow:tag("span"):addClass("card_content_skill-meta-val card_accent"):wikitext(val(skillData.weapon))
elseif skillType == "passive" then
metaRow:wikitext("触发条件 ")
metaRow:node(makeTriggerLabel(skillData.trigger_type, skillData.trigger_value))
elseif skillType == "ultimate" then
metaRow:wikitext("欲火消耗 ")
local costSpan = metaRow:tag("span"):addClass("card_content_skill-meta-val")
costSpan:tag("span"):addClass("card_accent"):wikitext(val(skillData.desire_cost))
end
if skillType == "passive" or skillType == "ultimate" then
left:node(makeTagsRow(skillData.tags))
end
left:tag("div"):addClass("card_content_skill_effect"):wikitext(val(skillData.description))
card:node(makeUpgradeTable(skillData.levels))
return card
end
local function makeFeatList(stagesJson)
local ul = mw.html.create("ul"):addClass("card_content_feat-ul")
local stages = parseJson(stagesJson)
if not stages or type(stages) ~= "table" then
ul:tag("li"):wikitext("—")
return ul
end
for _, stage in ipairs(stages) do
local li = ul:tag("li"):addClass("card_content_feat-list")
local row = li:tag("div"):addClass("card_content_feat-row")
local stageWrap = row:tag("div"):addClass("card_content_feat-stage-wrap")
stageWrap:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE")
stageWrap:tag("div"):addClass("card_content_feat-number")
:wikitext(string.format("%02d", stage.stage or 0))
local rightDiv = row:tag("div")
if stage.value and type(stage.value) == "table" then
for _, v in ipairs(stage.value) do
if v.description then
rightDiv:tag("span"):addClass("card_accent"):wikitext(v.description)
rightDiv:tag("br")
end
end
end
if stage.stat_boosts and type(stage.stat_boosts) == "table" then
for _, boost in ipairs(stage.stat_boosts) do
rightDiv:wikitext((boost.type or "") .. "提升" .. (boost.value or ""))
end
end
if stage.extra_name then
rightDiv:tag("span"):addClass("card_content_feat-stage-cn")
:wikitext("「" .. stage.extra_name .. "」")
end
end
return ul
end
-- ──────────────────────────────────────────────
-- 辅助:小传时间线
-- 数据结构:card.stories = [ { sort, story: [ { id, text, title? }, ... ] } ]
-- 每条 stories 记录对应一张卡,其 story 数组里每一项是一个章节。
-- ──────────────────────────────────────────────
local function makeStoryTimeline(stories)
local timeline = mw.html.create("div")
:addClass("card_timeline card_content_story-timeline")
timeline:tag("div"):addClass("card_timeline-line")
-- 取第一条 stories 记录(一张卡只有一条)
local storiesEntry = stories and type(stories) == "table" and stories[1]
local chapters = storiesEntry and type(storiesEntry.story) == "table" and storiesEntry.story or {}
if #chapters == 0 then
timeline:tag("div"):addClass("card_timeline-item"):wikitext("(小传数据待补充)")
return timeline
end
local romans = { "Ⅰ", "Ⅱ", "Ⅲ", "Ⅳ", "Ⅴ" }
local unlockLevels = { "Lv.20 解锁", "Lv.40 解锁", "Lv.60 解锁", "Lv.80 解锁", "Lv.100 解锁" }
for idx, chapter in ipairs(chapters) do
local isLast = (idx == #chapters)
local item = timeline:tag("div"):addClass("card_timeline-item")
if isLast then item:addClass("card_timeline-item--last") end
item:tag("div"):addClass("card_timeline-dot")
item:tag("div"):addClass("card_timeline-unlock")
:wikitext(unlockLevels[idx] or ("Lv." .. (idx * 20) .. " 解锁"))
-- 标题行:罗马数字 + 章节名(有则显示)+ "/ Story"
local titleDiv = item:tag("div"):addClass("card_timeline-title")
titleDiv:tag("span"):addClass("card_timeline-numeral")
:wikitext(romans[idx] or tostring(idx))
if chapter.title and chapter.title ~= "" then
titleDiv:tag("span"):addClass("card_timeline-chapter"):wikitext(chapter.title)
end
titleDiv:tag("span"):addClass("card_timeline-label"):wikitext("/ Story")
-- 正文:\n 转 <br>
local bodyDiv = item:tag("div"):addClass("card_timeline-body")
local text = chapter.text or ""
if text ~= "" then
local lines = mw.text.split(text, "\n")
for i, line in ipairs(lines) do
if line ~= "" then bodyDiv:wikitext(line) end
if i < #lines then bodyDiv:tag("br") end
end
end
end
return timeline
end
-- ══════════════════════════════════════════════
-- 主渲染函数
-- ══════════════════════════════════════════════
function p.render(frame)
local name = mw.text.trim(frame.args[1] or frame.args.name or "")
if name == "" then
return '<span class="error">错误:请提供卡片名称,例如 {{#invoke:CardData|render|复仇童谣}}</span>'
end
-- ── 1. 构造 API URL ──
local fields = table.concat({
"stylename", "rarity",
"character.name", "profession.name", "desire.name",
"skill_normal_attack.name", "skill_normal_attack.type",
"skill_normal_attack.weapon", "skill_normal_attack.description",
"skill_normal_attack.levels.*",
"skill_passive.name", "skill_passive.type",
"skill_passive.trigger_type", "skill_passive.trigger_value",
"skill_passive.tags", "skill_passive.description",
"skill_passive.levels.*",
"skill_ultimate.name", "skill_ultimate.type",
"skill_ultimate.desire_cost", "skill_ultimate.tags",
"skill_ultimate.description", "skill_ultimate.levels.*",
"feats.stages",
"stories.sort",
"stories.story.*" -- 取 story 数组里所有字段(含 title、text、id)
}, ",")
local encodedName = mw.uri.encode(name, "QUERY")
local apiUrl = "https://data.saltedkiss.org/items/cards"
.. "?fields=" .. mw.uri.encode(fields, "QUERY")
.. "&filter[stylename][_eq]=" .. encodedName
-- deep filter:让 Directus 不过滤掉 draft 状态的 stories 关联记录
.. "&deep[stories][_filter][status][_neq]=archived"
-- ── 2. 拉取原始 JSON ──
local rawResponse = frame:preprocess(
'{{#get_web_data:url=' .. apiUrl
.. '|format=text|data=responseText=__text}}'
.. '{{#external_value:responseText}}'
)
rawResponse = mw.text.trim(rawResponse or "")
-- ── 3. 解析 ──
local parsed = parseJson(rawResponse)
local card = parsed and type(parsed.data) == "table" and parsed.data[1] or nil
if not card then
return '<span class="error">⚠ 无法加载卡片数据:' .. mw.text.nowiki(name)
.. '(response=' .. mw.text.nowiki(rawResponse) .. ')</span>'
end
local charData = card.character or {}
local profData = card.profession or {}
local desireData = card.desire or {}
local skillNA = card.skill_normal_attack or {}
local skillPA = card.skill_passive or {}
local skillUL = card.skill_ultimate or {}
local stories = card.stories or {}
local featsStagesJson = "[]"
if card.feats and type(card.feats) == "table" and card.feats[1] then
local sr = card.feats[1].stages
featsStagesJson = type(sr) == "table" and mw.text.jsonEncode(sr)
or type(sr) == "string" and sr or "[]"
end
local function levelsToJson(skill)
if not skill.levels then return "[]" end
if type(skill.levels) == "string" then return skill.levels end
if type(skill.levels) == "table" then return mw.text.jsonEncode(skill.levels) end
return "[]"
end
skillNA.levels = levelsToJson(skillNA)
skillPA.levels = levelsToJson(skillPA)
skillUL.levels = levelsToJson(skillUL)
local function tagsToJson(skill)
if not skill.tags then return "[]" end
if type(skill.tags) == "string" then return skill.tags end
if type(skill.tags) == "table" then return mw.text.jsonEncode(skill.tags) end
return "[]"
end
skillPA.tags = tagsToJson(skillPA)
skillUL.tags = tagsToJson(skillUL)
-- ── 4. 组装 HTML ──
local rarity = tonumber(card.rarity) or 0
local stylename = val(card.stylename)
local charName = val(charData.name)
local profName = val(profData.name)
local desName = val(desireData.name)
local root = mw.html.create("div")
:addClass("ron-card ron-card--rarity-" .. rarity)
:attr("data-rarity", rarity)
root:tag("div"):addClass("card_fullscreen-img")
root:tag("div"):addClass("card_hero"):css("height", "100vh")
local layout = root:tag("div"):addClass("card_content-container")
:tag("div"):addClass("card_content-background")
:tag("div"):addClass("card_content-inner")
:tag("div"):addClass("card_content card_content--layout")
-- ── 左栏 ──
local leftCol = layout:tag("div"):addClass("card_content_left")
local nc = leftCol:tag("div"):addClass("card_content_name-container")
local nr = nc:tag("div"):addClass("card_content_name-row")
nr:tag("div"):addClass("card_content_name-icon")
nr:tag("div"):addClass("card_content_name-classtext"):wikitext(profName)
nr:tag("div"):addClass("card_content_name-icon card_content_name-icon--ml")
nr:tag("div"):addClass("card_content_name-classtext"):wikitext(desName)
local nt = nc:tag("div"):addClass("card_content_name-text")
nt:wikitext(stylename .. " ")
nt:tag("span"):addClass("card_accent"):wikitext("·")
nt:wikitext(" " .. charName)
leftCol:node(makeTitle("信息", "Info"))
leftCol:node(makeHr())
leftCol:tag("div"):addClass("card_content_info")
:tag("div"):addClass("card_content_info-item")
:tag("div"):wikitext("稀有度"):done()
:tag("div"):wikitext(rarityStars(rarity))
leftCol:node(makeTitle("等级 100", "Lv. Max"))
leftCol:node(makeHr())
leftCol:tag("div"):addClass("card_content_attribute-item"):wikitext("(数据待补充)")
leftCol:node(makeTitle("映像", "Reflection"))
leftCol:node(makeHr())
leftCol:tag("div"):addClass("card_content_attribute-item"):wikitext("(数据待补充)")
leftCol:node(makeTitle("认知", "Cognition"))
leftCol:node(makeHr())
leftCol:tag("div"):addClass("card_content_attribute-item"):wikitext("(数据待补充)")
-- ── 右栏 ──
local rightCol = layout:tag("div"):addClass("card_content_right")
rightCol:node(makeTitle("战斗技能", "Tactical"))
rightCol:node(makeHr("card_content-item-hr--mb12"))
rightCol:node(makeSkillCard(skillNA, "normal_attack"))
rightCol:tag("div"):addClass("card_content_skill-gap")
rightCol:node(makeSkillCard(skillPA, "passive"))
rightCol:tag("div"):addClass("card_content_skill-gap")
rightCol:node(makeSkillCard(skillUL, "ultimate"))
rightCol:node(makeTitle("觉醒", "Feat"))
rightCol:node(makeHr("card_content-item-hr--mb12"))
rightCol:node(makeFeatList(featsStagesJson))
-- ── 小传 ──
local storySection = rightCol:tag("div"):addClass("card_content_story")
local storyHeader = storySection:tag("div"):addClass("card_content_story-header")
local storyLeft = storyHeader:tag("div"):addClass("card_content_story-header-left")
storyLeft:node(makeTitle("小传", "Story"))
storyLeft:node(makeHr("card_content-item-hr--mb12"))
storyLeft:tag("div"):addClass("card_content_story-toggle")
:attr("data-collapsed", "false")
:tag("span"):addClass("story-toggle-text"):wikitext("收起"):done()
:tag("span"):addClass("story-toggle-icon"):wikitext("-")
storyHeader:tag("div"):addClass("card_content_story-header-spacer")
storyHeader:node(makeStoryTimeline(stories))
-- ── 邀约(待补充) ──
rightCol:tag("div"):css("height", "12px"):css("width", "100%")
rightCol:node(makeTitle("邀约", "Date"))
rightCol:node(makeHr("card_content-item-hr--mb12"))
rightCol:tag("div"):addClass("card_content_date")
:tag("div"):addClass("card_content_date-track")
:tag("ul"):addClass("card_content_date-list")
:tag("li"):addClass("card_content_date-item"):wikitext("(邀约数据待补充)")
return tostring(root)
end
return p