模块:CardData:修订间差异
来自夜幕之下
更多操作
重写:用 mw.html.create 生成完整骨架,对应 Sandbox/2.0 HTML 结构 (via update-page on MediaWiki MCP Server) |
重写:ExternalData 服务端拉取 Directus 数据,mw.html.create 生成完整 HTML (via update-page on MediaWiki MCP Server) |
||
| 第1行: | 第1行: | ||
-- Module:CardData | -- Module:CardData | ||
-- 用法:{{#invoke:CardData|render|复仇童谣}} | -- 用法:{{#invoke:CardData|render|复仇童谣}} | ||
-- | -- 通过 ExternalData 扩展从 Directus API 拉取数据,在服务端渲染完整 HTML | ||
-- | -- 小传、邀约、映像、认知、属性等 API 暂无数据,保留骨架占位 | ||
local p = {} | local p = {} | ||
-- ══════════════════════════════════════════════ | |||
-- 工具函数 | |||
-- ══════════════════════════════════════════════ | |||
-- 安全取值:若为 nil 则返回 fallback(默认 "—") | |||
local function val(v, fallback) | |||
if v == nil or v == "" then | |||
return fallback or "—" | |||
end | |||
return v | |||
end | |||
-- 将稀有度数字转为星号字符串,例如 6 → "★★★★★★" | |||
local function rarityStars(n) | |||
n = tonumber(n) or 0 | |||
return string.rep("★", n) | |||
end | |||
-- 解析 JSON 字符串,失败时返回 nil | |||
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 | |||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
-- | -- 辅助:段落标题(如「战斗技能 Tactical」) | ||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
local function makeTitle(titleText, subtitleText, extraClass) | local function makeTitle(titleText, subtitleText, extraClass) | ||
local div = mw.html.create("div") | local div = mw.html.create("div") | ||
:addClass("card_content-item-title") | :addClass("card_content-item-title") | ||
:addClass("card_content-item-title--mt") | |||
if extraClass then div:addClass(extraClass) end | if extraClass then div:addClass(extraClass) end | ||
div:wikitext(titleText) | div:wikitext(titleText) | ||
| 第23行: | 第47行: | ||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
-- | -- 辅助:分隔线 | ||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
local function makeHr(extraClass) | local function makeHr(extraClass) | ||
| 第33行: | 第57行: | ||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
-- | -- 辅助:被动触发条件文字 | ||
-- | -- trigger_type: "normal_attack" 等 | ||
-- trigger_value: 数字 | |||
-- ────────────────────────────────────────────── | |||
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 | |||
-- ────────────────────────────────────────────── | |||
-- 辅助:技能 tags 行 | |||
-- tagsJson: JSON 字符串数组,如 '["持续伤害","群攻"]' | |||
-- ────────────────────────────────────────────── | |||
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 | |||
-- ────────────────────────────────────────────── | |||
-- 辅助:技能升级表格 | |||
-- levelsJson: JSON 数组,每项 { name, levels: [...] } | |||
-- 列数根据第一行 levels 数组长度 +1 决定(+1 是标签列) | |||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
local function | local function makeUpgradeTable(levelsJson) | ||
-- | local wrap = mw.html.create("div"):addClass("card_content_skill-upgrade") | ||
local | local rows = parseJson(levelsJson) | ||
:addClass("card_content_skill- | |||
-- 无数据时返回空占位 | |||
if not rows or type(rows) ~= "table" or #rows == 0 then | |||
return wrap | |||
end | |||
-- 计算列数:第一行的 levels 数组长度(即升级档位数) | |||
local firstRow = rows[1] | |||
local colCount = 0 | |||
if firstRow and firstRow.levels then | |||
colCount = #firstRow.levels | |||
end | |||
-- 总格数 = 标签列 + 数据列,对应 grid-template-columns | |||
local gridClass = "card_content_skill-upgrade-grid" | |||
if colCount == 9 then | |||
-- 终结技 9 档(1~9级) | |||
gridClass = gridClass -- 默认样式 | |||
elseif colCount >= 9 then | |||
-- 普攻/被动 10 档(1~10级,但第1格是标签) | |||
gridClass = gridClass .. " card_content_skill-upgrade-grid--10" | |||
end | |||
local grid = wrap:tag("div"):addClass(gridClass) | |||
-- 表头行:空格 + 2~N 级 | |||
grid:tag("div") -- 左上角空格 | |||
local startLv = 2 | |||
if colCount == 9 then startLv = 2 end -- 终结技从 Lv2 开始 | |||
for i = startLv, 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("→") | |||
-- | -- 数据列(从第 2 个值开始,第 1 个是基础值显示在描述里) | ||
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 | |||
-- ────────────────────────────────────────────── | |||
-- 辅助:生成一张技能卡 | |||
-- skillData: Lua table,包含技能字段 | |||
-- skillType: "normal_attack" | "passive" | "ultimate" | |||
-- ────────────────────────────────────────────── | |||
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 left = card:tag("div"):addClass("card_content_skill-left") | ||
-- | -- 标题行 | ||
local | local titleRow = left:tag("div"):addClass("card_content_skill-title") | ||
titleRow:tag("div"):addClass("card_content_skill-icon") -- 图标占位 | |||
local titleText = | |||
local titleText = titleRow:tag("div"):addClass("card_content_skill-title-text") | |||
-- 技能名 + 类型 | -- 技能名 + 类型 | ||
local nameRow = titleText:tag("div") | local nameRow = titleText:tag("div") | ||
nameRow:tag("span") | nameRow:tag("span") | ||
:addClass("card_content_skill-name") | :addClass("card_content_skill-name") | ||
: | :wikitext(val(skillData.name)) | ||
nameRow:tag("span") | nameRow:tag("span") | ||
:addClass("card_content_skill-type card_accent--type") | :addClass("card_content_skill-type card_accent--type") | ||
: | :wikitext("「" .. val(skillData.type) .. "」") | ||
-- meta | -- meta 行 | ||
local metaRow = titleText:tag("div"):addClass("card_content_skill-meta") | local metaRow = titleText:tag("div"):addClass("card_content_skill-meta") | ||
if | if skillType == "normal_attack" then | ||
metaRow:wikitext("使用武器 ") | metaRow:wikitext("使用武器 ") | ||
metaRow:tag("span") | metaRow:tag("span") | ||
:addClass("card_content_skill-meta-val card_accent") | :addClass("card_content_skill-meta-val card_accent") | ||
: | :wikitext(val(skillData.weapon)) | ||
elseif skillType == "passive" then | |||
elseif | |||
metaRow:wikitext("触发条件 ") | metaRow:wikitext("触发条件 ") | ||
metaRow: | metaRow:node(makeTriggerLabel(skillData.trigger_type, skillData.trigger_value)) | ||
elseif skillType == "ultimate" then | |||
elseif | |||
metaRow:wikitext("欲火消耗 ") | metaRow:wikitext("欲火消耗 ") | ||
metaRow:tag("span") | local costSpan = metaRow:tag("span"):addClass("card_content_skill-meta-val") | ||
costSpan:tag("span"):addClass("card_accent"):wikitext(val(skillData.desire_cost)) | |||
end | end | ||
-- | -- tags(被动和终结技才有) | ||
if | if skillType == "passive" or skillType == "ultimate" then | ||
left: | left:node(makeTagsRow(skillData.tags)) | ||
end | end | ||
| 第95行: | 第201行: | ||
left:tag("div") | left:tag("div") | ||
:addClass("card_content_skill_effect") | :addClass("card_content_skill_effect") | ||
: | :wikitext(val(skillData.description)) | ||
-- | -- 升级表格 | ||
card: | card:node(makeUpgradeTable(skillData.levels)) | ||
return card | return card | ||
| 第107行: | 第210行: | ||
-- ────────────────────────────────────────────── | -- ────────────────────────────────────────────── | ||
-- 辅助:觉醒列表 | |||
-- stagesJson: JSON 字符串,feats[0].stages 数组 | |||
-- ────────────────────────────────────────────── | |||
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") | |||
-- 左侧:STAGE + 序号 | |||
local stageWrap = row:tag("div"):addClass("card_content_feat-stage-wrap") | |||
stageWrap:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE") | |||
local stageNum = stage.stage or 0 | |||
stageWrap:tag("div"):addClass("card_content_feat-number") | |||
:wikitext(string.format("%02d", stageNum)) | |||
-- 右侧:效果文字 + 属性加成 + 觉醒名 | |||
local rightDiv = row:tag("div") | |||
-- 特殊效果(value[].description),用 card_accent 高亮 | |||
if stage.value and type(stage.value) == "table" and #stage.value > 0 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 | |||
-- 属性加成(stat_boosts[]) | |||
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 | |||
-- 觉醒名(extra_name) | |||
if stage.extra_name then | |||
rightDiv:tag("span") | |||
:addClass("card_content_feat-stage-cn") | |||
:wikitext("「" .. stage.extra_name .. "」") | |||
end | |||
end | |||
return ul | |||
end | |||
-- ══════════════════════════════════════════════ | |||
-- 主渲染函数 | -- 主渲染函数 | ||
-- | -- ══════════════════════════════════════════════ | ||
function p.render(frame) | function p.render(frame) | ||
local name = 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 | ||
| 第117行: | 第274行: | ||
end | end | ||
-- ── | -- ── 1. 用 ExternalData 拉取数据 ── | ||
-- 构造请求字段列表(只取需要的字段,减少响应体积) | |||
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" | |||
}, ",") | |||
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 | |||
-- 调用 ExternalData 的 #get_web_data,将整个 JSON 存入变量 cardJson | |||
frame:callParserFunction("#get_web_data", { | |||
url = apiUrl, | |||
format = "JSON", | |||
data = "cardJson=data.0" | |||
}) | |||
-- 读取变量(ExternalData 把结果存在全局) | |||
local rawJson = mw.ext.externalData.getVar("cardJson") or "" | |||
-- ── 2. 解析 JSON ── | |||
local card = parseJson(rawJson) | |||
-- 数据拉取失败时的错误提示 | |||
if not card then | |||
return '<span class="error">⚠ 无法加载卡片数据:' .. mw.text.nowiki(name) .. '</span>' | |||
end | |||
-- 取各子对象(可能是 table 或 nil) | |||
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 {} | |||
-- feats 是数组,取第一项的 stages | |||
local featsStagesJson = "[]" | |||
if card.feats and type(card.feats) == "table" and card.feats[1] then | |||
local stagesRaw = card.feats[1].stages | |||
if type(stagesRaw) == "table" then | |||
featsStagesJson = mw.text.jsonEncode(stagesRaw) | |||
elseif type(stagesRaw) == "string" then | |||
featsStagesJson = stagesRaw | |||
end | |||
end | |||
-- 技能 levels:ExternalData 可能把它解析成 table,转回 JSON 字符串给 makeUpgradeTable | |||
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) | |||
-- 技能 tags 同理 | |||
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) | |||
-- ── 3. 组装 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") | local root = mw.html.create("div") | ||
:addClass("ron-card") | :addClass("ron-card") | ||
:attr("data- | :addClass("ron-card--rarity-" .. rarity) -- 星级样式钩子 | ||
:attr("data-rarity", rarity) | |||
-- | -- 全屏卡面图(暂无 API 数据,留空占位) | ||
root:tag("div") | root:tag("div"):addClass("card_fullscreen-img") | ||
-- | -- 英雄区占位 | ||
root:tag("div") | root:tag("div"):addClass("card_hero"):css("height", "100vh") | ||
-- | -- 主内容容器 | ||
local container = root:tag("div"):addClass("card_content-container") | local container = root:tag("div"):addClass("card_content-container") | ||
local bg = container:tag("div"):addClass("card_content-background") | local bg = container:tag("div"):addClass("card_content-background") | ||
| 第139行: | 第391行: | ||
-- ════════════════════════════════ | -- ════════════════════════════════ | ||
-- | -- 左栏 | ||
-- ════════════════════════════════ | -- ════════════════════════════════ | ||
local | local leftCol = layout:tag("div"):addClass("card_content_left") | ||
-- 1. 卡名区 | -- 1. 卡名区 | ||
local nameContainer = | local nameContainer = leftCol:tag("div"):addClass("card_content_name-container") | ||
local nameRow = nameContainer:tag("div"):addClass("card_content_name-row") | local nameRow = nameContainer:tag("div"):addClass("card_content_name-row") | ||
-- | -- 职业图标占位 + 职业名 | ||
nameRow:tag("div") | nameRow:tag("div"):addClass("card_content_name-icon") | ||
nameRow:tag("div"):addClass("card_content_name-classtext"):wikitext(profName) | |||
nameRow:tag("div") | |||
-- | -- 欲望图标占位 + 欲望名 | ||
nameRow:tag("div") | nameRow:tag("div"):addClass("card_content_name-icon card_content_name-icon--ml") | ||
nameRow:tag("div"):addClass("card_content_name-classtext"):wikitext(desName) | |||
nameRow:tag("div") | |||
-- | -- 风格名 · 角色名 | ||
nameContainer:tag("div") | local nameText = nameContainer:tag("div"):addClass("card_content_name-text") | ||
nameText:wikitext(stylename .. " ") | |||
nameText:tag("span"):addClass("card_accent"):wikitext("·") | |||
nameText:wikitext(" " .. charName) | |||
-- 2. | -- 2. 信息(稀有度) | ||
leftCol:node(makeTitle("信息", "Info")) | |||
leftCol:node(makeHr()) | |||
local infoGrid = | local infoGrid = leftCol:tag("div"):addClass("card_content_info") | ||
- | infoGrid:tag("div"):addClass("card_content_info-item") | ||
:tag("div"):wikitext("稀有度"):done() | |||
:tag("div"):wikitext(rarityStars(rarity)) | |||
-- 3. | -- 3. 满级属性(暂无数据) | ||
leftCol:node(makeTitle("等级 100", "Lv. Max")) | |||
leftCol:node(makeHr()) | |||
leftCol:tag("div"):addClass("card_content_attribute-item") | |||
:wikitext("(数据待补充)") | |||
:wikitext(" | |||
-- 4. | -- 4. 映像(暂无数据) | ||
leftCol:node(makeTitle("映像", "Reflection")) | |||
leftCol:node(makeHr()) | |||
leftCol:tag("div"):addClass("card_content_attribute-item") | |||
:wikitext("(数据待补充)") | |||
:wikitext(" | |||
-- 5. | -- 5. 认知(暂无数据) | ||
leftCol:node(makeTitle("认知", "Cognition")) | |||
leftCol:node(makeHr()) | |||
leftCol:tag("div"):addClass("card_content_attribute-item") | |||
:wikitext("(数据待补充)") | |||
:wikitext(" | |||
-- ════════════════════════════════ | -- ════════════════════════════════ | ||
-- 右栏 | -- 右栏 | ||
-- ════════════════════════════════ | -- ════════════════════════════════ | ||
local | 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 = | local storySection = rightCol:tag("div"):addClass("card_content_story") | ||
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")) | ||
storyLeft:tag("div") | storyLeft:tag("div"):addClass("card_content_story-toggle") | ||
:attr("data-collapsed", "false") | :attr("data-collapsed", "false") | ||
:tag("span"):addClass("story-toggle-text"):wikitext("收起"):done() | :tag("span"):addClass("story-toggle-text"):wikitext("收起"):done() | ||
| 第247行: | 第478行: | ||
storyHeader:tag("div"):addClass("card_content_story-header-spacer") | storyHeader:tag("div"):addClass("card_content_story-header-spacer") | ||
-- | -- 时间线骨架(暂无数据) | ||
storyHeader:tag("div") | local timeline = storyHeader:tag("div") | ||
:addClass("card_timeline card_content_story-timeline") | :addClass("card_timeline card_content_story-timeline") | ||
timeline:tag("div"):addClass("card_timeline-line") | |||
timeline:tag("div"):addClass("card_timeline-item") | |||
:wikitext("(小传数据待补充)") | |||
: | |||
-- | -- ── 邀约(暂无数据,保留骨架) ── | ||
rightCol:tag("div"):css("height", "12px"):css("width", "100%") | |||
rightCol:node(makeTitle("邀约", "Date")) | |||
rightCol:node(makeHr("card_content-item-hr--mb12")) | |||
local dateWrap = rightCol:tag("div"):addClass("card_content_date") | |||
dateWrap:tag("div"):addClass("card_content_date-track") | |||
:addClass(" | :tag("ul"):addClass("card_content_date-list") | ||
:wikitext(" | :tag("li"):addClass("card_content_date-item") | ||
:wikitext("(邀约数据待补充)") | |||
-- | -- ── 完成,输出 HTML + CSS 模板 ── | ||
return tostring(root) .. frame:expandTemplate{ title = "cardcss" } | return tostring(root) .. frame:expandTemplate{ title = "cardcss" } | ||
end | end | ||
return p | return p | ||
2026年3月10日 (二) 23:46的版本
此模块的文档可以在模块:CardData/doc创建
-- Module:CardData
-- 用法:{{#invoke:CardData|render|复仇童谣}}
-- 通过 ExternalData 扩展从 Directus API 拉取数据,在服务端渲染完整 HTML
-- 小传、邀约、映像、认知、属性等 API 暂无数据,保留骨架占位
local p = {}
-- ══════════════════════════════════════════════
-- 工具函数
-- ══════════════════════════════════════════════
-- 安全取值:若为 nil 则返回 fallback(默认 "—")
local function val(v, fallback)
if v == nil or v == "" then
return fallback or "—"
end
return v
end
-- 将稀有度数字转为星号字符串,例如 6 → "★★★★★★"
local function rarityStars(n)
n = tonumber(n) or 0
return string.rep("★", n)
end
-- 解析 JSON 字符串,失败时返回 nil
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
-- ──────────────────────────────────────────────
-- 辅助:段落标题(如「战斗技能 Tactical」)
-- ──────────────────────────────────────────────
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
-- ──────────────────────────────────────────────
-- 辅助:被动触发条件文字
-- trigger_type: "normal_attack" 等
-- trigger_value: 数字
-- ──────────────────────────────────────────────
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
-- ──────────────────────────────────────────────
-- 辅助:技能 tags 行
-- tagsJson: JSON 字符串数组,如 '["持续伤害","群攻"]'
-- ──────────────────────────────────────────────
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
-- ──────────────────────────────────────────────
-- 辅助:技能升级表格
-- levelsJson: JSON 数组,每项 { name, levels: [...] }
-- 列数根据第一行 levels 数组长度 +1 决定(+1 是标签列)
-- ──────────────────────────────────────────────
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
-- 计算列数:第一行的 levels 数组长度(即升级档位数)
local firstRow = rows[1]
local colCount = 0
if firstRow and firstRow.levels then
colCount = #firstRow.levels
end
-- 总格数 = 标签列 + 数据列,对应 grid-template-columns
local gridClass = "card_content_skill-upgrade-grid"
if colCount == 9 then
-- 终结技 9 档(1~9级)
gridClass = gridClass -- 默认样式
elseif colCount >= 9 then
-- 普攻/被动 10 档(1~10级,但第1格是标签)
gridClass = gridClass .. " card_content_skill-upgrade-grid--10"
end
local grid = wrap:tag("div"):addClass(gridClass)
-- 表头行:空格 + 2~N 级
grid:tag("div") -- 左上角空格
local startLv = 2
if colCount == 9 then startLv = 2 end -- 终结技从 Lv2 开始
for i = startLv, 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("→")
-- 数据列(从第 2 个值开始,第 1 个是基础值显示在描述里)
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
-- ──────────────────────────────────────────────
-- 辅助:生成一张技能卡
-- skillData: Lua table,包含技能字段
-- skillType: "normal_attack" | "passive" | "ultimate"
-- ──────────────────────────────────────────────
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) .. "」")
-- meta 行
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
-- tags(被动和终结技才有)
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
-- ──────────────────────────────────────────────
-- 辅助:觉醒列表
-- stagesJson: JSON 字符串,feats[0].stages 数组
-- ──────────────────────────────────────────────
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")
-- 左侧:STAGE + 序号
local stageWrap = row:tag("div"):addClass("card_content_feat-stage-wrap")
stageWrap:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE")
local stageNum = stage.stage or 0
stageWrap:tag("div"):addClass("card_content_feat-number")
:wikitext(string.format("%02d", stageNum))
-- 右侧:效果文字 + 属性加成 + 觉醒名
local rightDiv = row:tag("div")
-- 特殊效果(value[].description),用 card_accent 高亮
if stage.value and type(stage.value) == "table" and #stage.value > 0 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
-- 属性加成(stat_boosts[])
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
-- 觉醒名(extra_name)
if stage.extra_name then
rightDiv:tag("span")
:addClass("card_content_feat-stage-cn")
:wikitext("「" .. stage.extra_name .. "」")
end
end
return ul
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. 用 ExternalData 拉取数据 ──
-- 构造请求字段列表(只取需要的字段,减少响应体积)
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"
}, ",")
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
-- 调用 ExternalData 的 #get_web_data,将整个 JSON 存入变量 cardJson
frame:callParserFunction("#get_web_data", {
url = apiUrl,
format = "JSON",
data = "cardJson=data.0"
})
-- 读取变量(ExternalData 把结果存在全局)
local rawJson = mw.ext.externalData.getVar("cardJson") or ""
-- ── 2. 解析 JSON ──
local card = parseJson(rawJson)
-- 数据拉取失败时的错误提示
if not card then
return '<span class="error">⚠ 无法加载卡片数据:' .. mw.text.nowiki(name) .. '</span>'
end
-- 取各子对象(可能是 table 或 nil)
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 {}
-- feats 是数组,取第一项的 stages
local featsStagesJson = "[]"
if card.feats and type(card.feats) == "table" and card.feats[1] then
local stagesRaw = card.feats[1].stages
if type(stagesRaw) == "table" then
featsStagesJson = mw.text.jsonEncode(stagesRaw)
elseif type(stagesRaw) == "string" then
featsStagesJson = stagesRaw
end
end
-- 技能 levels:ExternalData 可能把它解析成 table,转回 JSON 字符串给 makeUpgradeTable
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)
-- 技能 tags 同理
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)
-- ── 3. 组装 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")
:addClass("ron-card--rarity-" .. rarity) -- 星级样式钩子
:attr("data-rarity", rarity)
-- 全屏卡面图(暂无 API 数据,留空占位)
root:tag("div"):addClass("card_fullscreen-img")
-- 英雄区占位
root:tag("div"):addClass("card_hero"):css("height", "100vh")
-- 主内容容器
local container = root:tag("div"):addClass("card_content-container")
local bg = container:tag("div"):addClass("card_content-background")
local inner = bg:tag("div"):addClass("card_content-inner")
local layout = inner:tag("div"):addClass("card_content card_content--layout")
-- ════════════════════════════════
-- 左栏
-- ════════════════════════════════
local leftCol = layout:tag("div"):addClass("card_content_left")
-- 1. 卡名区
local nameContainer = leftCol:tag("div"):addClass("card_content_name-container")
local nameRow = nameContainer:tag("div"):addClass("card_content_name-row")
-- 职业图标占位 + 职业名
nameRow:tag("div"):addClass("card_content_name-icon")
nameRow:tag("div"):addClass("card_content_name-classtext"):wikitext(profName)
-- 欲望图标占位 + 欲望名
nameRow:tag("div"):addClass("card_content_name-icon card_content_name-icon--ml")
nameRow:tag("div"):addClass("card_content_name-classtext"):wikitext(desName)
-- 风格名 · 角色名
local nameText = nameContainer:tag("div"):addClass("card_content_name-text")
nameText:wikitext(stylename .. " ")
nameText:tag("span"):addClass("card_accent"):wikitext("·")
nameText:wikitext(" " .. charName)
-- 2. 信息(稀有度)
leftCol:node(makeTitle("信息", "Info"))
leftCol:node(makeHr())
local infoGrid = leftCol:tag("div"):addClass("card_content_info")
infoGrid:tag("div"):addClass("card_content_info-item")
:tag("div"):wikitext("稀有度"):done()
:tag("div"):wikitext(rarityStars(rarity))
-- 3. 满级属性(暂无数据)
leftCol:node(makeTitle("等级 100", "Lv. Max"))
leftCol:node(makeHr())
leftCol:tag("div"):addClass("card_content_attribute-item")
:wikitext("(数据待补充)")
-- 4. 映像(暂无数据)
leftCol:node(makeTitle("映像", "Reflection"))
leftCol:node(makeHr())
leftCol:tag("div"):addClass("card_content_attribute-item")
:wikitext("(数据待补充)")
-- 5. 认知(暂无数据)
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")
-- 时间线骨架(暂无数据)
local timeline = storyHeader:tag("div")
:addClass("card_timeline card_content_story-timeline")
timeline:tag("div"):addClass("card_timeline-line")
timeline:tag("div"):addClass("card_timeline-item")
:wikitext("(小传数据待补充)")
-- ── 邀约(暂无数据,保留骨架) ──
rightCol:tag("div"):css("height", "12px"):css("width", "100%")
rightCol:node(makeTitle("邀约", "Date"))
rightCol:node(makeHr("card_content-item-hr--mb12"))
local dateWrap = rightCol:tag("div"):addClass("card_content_date")
dateWrap:tag("div"):addClass("card_content_date-track")
:tag("ul"):addClass("card_content_date-list")
:tag("li"):addClass("card_content_date-item")
:wikitext("(邀约数据待补充)")
-- ── 完成,输出 HTML + CSS 模板 ──
return tostring(root) .. frame:expandTemplate{ title = "cardcss" }
end
return p