打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

模块:CardData:修订间差异

来自夜幕之下
Rin留言 | 贡献
重写:用 mw.html.create 生成完整骨架,对应 Sandbox/2.0 HTML 结构 (via update-page on MediaWiki MCP Server)
Rin留言 | 贡献
card_fullscreen-img 加上卡面图片,命名规则:卡面_stylename.png (via update-page on MediaWiki MCP Server)
 
(未显示同一用户的39个中间版本)
第1行: 第1行:
-- Module:CardData
-- Module:CardData
-- 生成卡片页面完整 HTML 骨架,数据由前端 JS 从 Directus API 获取后填充
-- 用法:{{#invoke:CardData|render|复仇童谣}}
-- 用法:{{#invoke:CardData|render|复仇童谣}}
--
-- 骨架结构对应 Sandbox/2.0,分为左栏(基础信息)和右栏(技能/觉醒/小传/邀约)
-- JS 会找到所有 [data-cardname] 元素,请求 Directus 后将数据注入各 [data-card-field] 占位节点


local p = {}
local p = {}


-- ──────────────────────────────────────────────
local function val(v, fallback)
-- 辅助:创建"标题行"(如「战斗技能 Tactical」)
    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 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)
     div:tag("span")
     div:tag("span"):addClass("card_content-item-subtitle"):wikitext(subtitleText)
        :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")
        :addClass("card_content-item-hr")
     if extraClass then div:addClass(extraClass) end
     if extraClass then div:addClass(extraClass) end
     return div
     return div
end
end


-- ──────────────────────────────────────────────
-- 单条属性行:图标 + 标签 + 值
-- 辅助:创建一个技能卡片骨架
local function makeAttrRow(label, value)
-- skillKey = "normal_attack" | "passive" | "ultimate"
    local item = mw.html.create("div"):addClass("card_content_attribute-item")
-- ──────────────────────────────────────────────
    local titleDiv = item:tag("div"):addClass("card_content_attribute-title")
local function makeSkillCard(skillKey)
    titleDiv:tag("div"):addClass("card_content_attribute-icon")
     -- 外层卡片
    titleDiv:tag("div"):wikitext(" " .. label)
     local card = mw.html.create("div")
    item:tag("div"):wikitext(value or "—")
         :addClass("card_content_skill-card")
    return item
end
 
-- 信息栏:稀有度 + 四维
local function makeInfoSection(intel, supply, execute, strategy, rarity)
    local infoDiv = mw.html.create("div"):addClass("card_content_info")
 
    local rarityItem = infoDiv:tag("div"):addClass("card_content_info-item")
    rarityItem:tag("div"):wikitext("稀有度")
    rarityItem:tag("div"):wikitext(rarityStars(rarity))
 
    local dims = {
        { label = "情报", value = intel    },
        { label = "物资", value = supply  },
        { label = "执行", value = execute  },
        { label = "策略", value = strategy },
    }
    for _, dim in ipairs(dims) do
        local item = infoDiv:tag("div"):addClass("card_content_info-item")
        item:tag("div"):wikitext(dim.label)
        local n = tonumber(dim.value)
        item:tag("div"):wikitext(n and ("" .. n) or "")
    end
 
    return infoDiv
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 maxCols = 0
    for _, row in ipairs(rows) do
         if type(row.levels) == "table" and #row.levels > maxCols then
            maxCols = #row.levels
        end
    end
 
    local DATA_COLS, gridClass
    if maxCols > 8 then
        DATA_COLS = 9
        gridClass = "card_content_skill-upgrade-grid card_content_skill-upgrade-grid--10"
    else
        DATA_COLS = 8
        gridClass = "card_content_skill-upgrade-grid"
    end
 
    local grid = wrap:tag("div"):addClass(gridClass)
 
    grid:tag("div")
    for i = 2, DATA_COLS + 1 do
        grid:tag("div"):addClass("card_content_skill-upgrade-lv"):wikitext(tostring(i))
    end


     -- 左侧内容区
     for idx, row in ipairs(rows) do
    local left = card:tag("div"):addClass("card_content_skill-left")
        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("→")


    -- 技能标题行
        local levels = (type(row.levels) == "table") and row.levels or {}
    local titleWrap = left:tag("div"):addClass("card_content_skill-title")
        for col = 1, DATA_COLS do
    titleWrap:tag("div"):addClass("card_content_skill-icon") -- 图标占位
            local v = levels[col]
    local titleText = titleWrap:tag("div"):addClass("card_content_skill-title-text")
            local cell = grid:tag("div"):addClass("card_content_skill-upgrade-val")
            if v == nil or v == "" then
                cell:addClass("card_content_skill-upgrade-val--null"):wikitext("")
            else
                cell:addClass("card_accent"):wikitext(tostring(v))
            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")
     local nameRow = titleText:tag("div")
     nameRow:tag("span")
     nameRow:tag("span"):addClass("card_content_skill-name"):wikitext(val(skillData.name))
        :addClass("card_content_skill-name")
     nameRow:tag("span"):addClass("card_content_skill-type card_accent--type")
        :attr("data-card-field", "skill_" .. skillKey .. ".name")
         :wikitext("" .. val(skillData.type) .. "")
        :wikitext("—")
     nameRow:tag("span")
        :addClass("card_content_skill-type card_accent--type")
         :attr("data-card-field", "skill_" .. skillKey .. ".type_label")
        :wikitext("")
 
    -- meta 行(普攻显示武器;被动显示触发条件;终结技显示欲火消耗)
     local metaRow = titleText:tag("div"):addClass("card_content_skill-meta")
     local metaRow = titleText:tag("div"):addClass("card_content_skill-meta")
     if skillKey == "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))
            :addClass("card_content_skill-meta-val card_accent")
     elseif skillType == "passive" then
            :attr("data-card-field", "skill_normal_attack.weapon")
            :wikitext("—")
     elseif skillKey == "passive" then
         metaRow:wikitext("触发条件 ")
         metaRow:wikitext("触发条件 ")
         metaRow:tag("span")
         metaRow:node(makeTriggerLabel(skillData.trigger_type, skillData.trigger_value))
            :addClass("card_content_skill-meta-val")
     elseif skillType == "ultimate" then
            :attr("data-card-field", "skill_passive.trigger_label")
            :wikitext("—")
     elseif skillKey == "ultimate" then
         metaRow:wikitext("欲火消耗 ")
         metaRow:wikitext("欲火消耗 ")
         metaRow:tag("span")
         metaRow:tag("span"):addClass("card_content_skill-meta-val")
            :addClass("card_content_skill-meta-val")
             :tag("span"):addClass("card_accent"):wikitext(val(skillData.desire_cost))
             :tag("span")
    end
                 :addClass("card_accent")
    if skillType == "passive" or skillType == "ultimate" then
                 :attr("data-card-field", "skill_ultimate.desire_cost")
        left:node(makeTagsRow(skillData.tags))
                 :wikitext("")
    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 sw  = row:tag("div"):addClass("card_content_feat-stage-wrap")
        sw:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE")
        sw:tag("div"):addClass("card_content_feat-number")
            :wikitext(string.format("%02d", stage.stage or 0))
        local rd = row:tag("div")
        if stage.value and type(stage.value) == "table" then
            for _, v in ipairs(stage.value) do
                 if v.description then
                    rd:tag("span"):addClass("card_accent"):wikitext(v.description)
                    rd:tag("br")
                 end
            end
        end
        if stage.stat_boosts and type(stage.stat_boosts) == "table" then
            for _, boost in ipairs(stage.stat_boosts) do
                rd:wikitext((boost.type or "") .. "提升" .. (boost.value or ""))
            end
        end
        if stage.extra_name then
            rd:tag("span"):addClass("card_content_feat-stage-cn")
                 :wikitext("「" .. stage.extra_name .. "」")
        end
     end
     end
    return ul
end
local function makeStoryTimeline(stories, cardTitle)
    local timeline = mw.html.create("div")
        :addClass("card_timeline card_content_story-timeline")
    timeline:tag("div"):addClass("card_timeline-line")


    -- 被动/终结技才有 tags 行
     if not stories or type(stories) ~= "table" or #stories == 0 then
     if skillKey == "passive" or skillKey == "ultimate" then
         timeline:tag("div"):addClass("card_timeline-item"):wikitext("(小传数据待补充)")
         left:tag("div")
        return timeline
            :addClass("card_content_skill-tags")
            :attr("data-card-field", "skill_" .. skillKey .. ".tags")
            -- JS 会把 tag 列表渲染到这里
     end
     end


     -- 技能描述
     local romans      = { "Ⅰ", "", "", "", "Ⅴ" }
    left:tag("div")
    local unlockLevels = { "Lv.20 解锁", "Lv.40 解锁", "Lv.60 解锁", "Lv.80 解锁", "Lv.100 解锁" }
        :addClass("card_content_skill_effect")
        :attr("data-card-field", "skill_" .. skillKey .. ".description_html")
        :wikitext("")


     -- 右侧升级表格占位(JS 负责生成具体列数和内容)
     for idx, chapter in ipairs(stories) do
    card:tag("div")
        local item = timeline:tag("div"):addClass("card_timeline-item")
         :addClass("card_content_skill-upgrade")
        if idx == #stories then item:addClass("card_timeline-item--last") end
         :attr("data-card-field", "skill_" .. skillKey .. ".levels_table")
        item:tag("div"):addClass("card_timeline-dot")
        item:tag("div"):addClass("card_timeline-unlock")
            :wikitext(unlockLevels[idx] or ("Lv." .. (idx * 20) .. " 解锁"))
        local titleDiv = item:tag("div"):addClass("card_timeline-title")
        titleDiv:tag("span"):addClass("card_timeline-numeral"):wikitext(romans[idx] or tostring(idx))
         titleDiv:tag("span"):addClass("card_timeline-chapter"):wikitext(cardTitle or "")
         titleDiv:tag("span"):addClass("card_timeline-label"):wikitext("/ Story")
        local bodyDiv = item:tag("div"):addClass("card_timeline-body")
        local text = chapter.text or ""
        if text ~= "" then
            text = text:gsub("\r\n", "\n")
            local paragraphs = mw.text.split(text, "\n+")
            for _, para in ipairs(paragraphs) do
                para = mw.text.trim(para)
                if para ~= "" then bodyDiv:tag("p"):wikitext(para) end
            end
        end
    end


     return card
     return timeline
end
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 "")
    name = mw.text.trim(name)
 
     if name == "" then
     if name == "" then
         return '<span class="error">错误:请提供卡片名称,例如 {{#invoke:CardData|render|复仇童谣}}</span>'
         return '<span class="error">错误:请提供卡片名称,例如 {{#invoke:CardData|render|复仇童谣}}</span>'
     end
     end


    -- ── 最外层容器,JS 以此为根节点 ──
     local encodedName = mw.uri.encode(name, "QUERY")
     local root = mw.html.create("div")
    local apiUrl = "https://data.saltedkiss.org/items/cards"
         :addClass("ron-card")
        .. "?fields=stylename,rarity,character.name,profession.name,desire.name"
         :attr("data-cardname", name)
        .. ",intel,supply,execute,strategy"
         .. ",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.text"
        .. "&filter[stylename][_eq]=" .. encodedName


     -- 全屏卡面图
     local rawResponse = frame:preprocess(
    root:tag("div")
         '{{#get_web_data:url=' .. apiUrl
         :addClass("card_fullscreen-img")
        .. '|format=text|data=responseText=__text}}'
         :attr("data-card-field", "fullscreen_img")
         .. '{{#external_value:responseText}}'
    )
    rawResponse = mw.text.trim(rawResponse or "")


     -- 英雄区占位高度
     local parsed = parseJson(rawResponse)
    root:tag("div")
    local card  = parsed and type(parsed.data) == "table" and parsed.data[1] or nil
        :addClass("card_hero")
        :css("height", "100vh")


     -- ── 主内容容器 ──
     if not card then
    local container = root:tag("div"):addClass("card_content-container")
        return '<span class="error">⚠ 无法加载卡片数据:' .. mw.text.nowiki(name)
    local bg        = container:tag("div"):addClass("card_content-background")
            .. '(response=' .. mw.text.nowiki(rawResponse) .. ')</span>'
     local inner    = bg:tag("div"):addClass("card_content-inner")
     end
    local layout    = inner:tag("div"):addClass("card_content card_content--layout")


     -- ════════════════════════════════
     local charData  = card.character or {}
     -- 左栏(sticky)
    local profData  = card.profession or {}
     -- ════════════════════════════════
    local desireData = card.desire or {}
     local left = layout:tag("div"):addClass("card_content_left")
     local skillNA    = card.skill_normal_attack or {}
     local skillPA    = card.skill_passive or {}
    local skillUL    = card.skill_ultimate or {}
     local stories    = card.stories or {}


    -- 1. 卡名区
     local featsStagesJson = "[]"
     local nameContainer = left:tag("div"):addClass("card_content_name-container")
    if card.feats and type(card.feats) == "table" and card.feats[1] then
    local nameRow = nameContainer:tag("div"):addClass("card_content_name-row")
        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)
    nameRow:tag("div")
         if not skill.levels then return "[]" end
         :addClass("card_content_name-icon")
         if type(skill.levels) == "string" then return skill.levels end
         :attr("data-card-field", "profession_icon")
        if type(skill.levels) == "table" then return mw.text.jsonEncode(skill.levels) end
    nameRow:tag("div")
         return "[]"
         :addClass("card_content_name-classtext")
    end
        :attr("data-card-field", "profession.name")
    skillNA.levels = levelsToJson(skillNA)
        :wikitext("—")
    skillPA.levels = levelsToJson(skillPA)
    skillUL.levels = levelsToJson(skillUL)


     -- 欲望图标 + 文字
     local function tagsToJson(skill)
    nameRow:tag("div")
         if not skill.tags then return "[]" end
         :addClass("card_content_name-icon card_content_name-icon--ml")
         if type(skill.tags) == "string" then return skill.tags end
         :attr("data-card-field", "desire_icon")
         if type(skill.tags) == "table" then return mw.text.jsonEncode(skill.tags) end
    nameRow:tag("div")
         return "[]"
         :addClass("card_content_name-classtext")
    end
         :attr("data-card-field", "desire.name")
    skillPA.tags = tagsToJson(skillPA)
        :wikitext("—")
    skillUL.tags = tagsToJson(skillUL)


     -- 卡名 + 角色名
     local rarity    = tonumber(card.rarity) or 0
     nameContainer:tag("div")
     local stylename = val(card.stylename)
        :addClass("card_content_name-text")
    local charName  = val(charData.name)
        :attr("data-card-field", "card_title")
    local profName  = val(profData.name)
        -- JS 会把「风格名 · 角色名」填进来
    local desName  = val(desireData.name)
        :wikitext("—")


    -- 2. 信息(稀有度等,目前 API 只有 rarity,其他待补)
     local root = mw.html.create("div")
    left:node(makeTitle("信息", "Info", "card_content-item-title--mt"))
        :addClass("ron-card ron-card--rarity-" .. rarity)
    left:node(makeHr())
        :attr("data-rarity", rarity)
     local infoGrid = left:tag("div"):addClass("card_content_info")
    -- 四项情报:JS 填充(data-card-field="info_grid")
    infoGrid:attr("data-card-field", "info_grid")


     -- 3. 满级属性(API 暂无,先留骨架)
     -- 全屏背景图,文件命名规则:卡面_卡面名.png
     left:node(makeTitle("等级 100", "Lv. Max", "card_content-item-title--mt"))
     root:tag("div"):addClass("card_fullscreen-img")
    left:node(makeHr())
        :wikitext("[[文件:卡面_" .. stylename .. ".png]]")
     left:tag("div")
     root:tag("div"):addClass("card_hero"):css("height", "100vh")
        :addClass("card_content_attributes")
        :attr("data-card-field", "attributes")
        :wikitext("—")


     -- 4. 映像(API 暂无,先留骨架)
     local layout = root:tag("div"):addClass("card_content-container")
    left:node(makeTitle("映像", "Reflection", "card_content-item-title--mt"))
        :tag("div"):addClass("card_content-background")
    left:node(makeHr())
        :tag("div"):addClass("card_content-inner")
    left:tag("div")
         :tag("div"):addClass("card_content card_content--layout")
        :addClass("card_content_reflection")
         :attr("data-card-field", "reflection")
        :wikitext("")


     -- 5. 认知(API 暂无,先留骨架)
     -- 左栏
     left:node(makeTitle("认知", "Cognition", "card_content-item-title--mt"))
     local leftCol = layout:tag("div"):addClass("card_content_left")
    left:node(makeHr())
    left:tag("div")
        :addClass("card_content_cognition")
        :attr("data-card-field", "cognition")
        :wikitext("—")


     -- ════════════════════════════════
     -- 卡名
     -- 右栏
     local nc = leftCol:tag("div"):addClass("card_content_name-container")
     -- ════════════════════════════════
    local nr = nc:tag("div"):addClass("card_content_name-row")
     local right = layout:tag("div"):addClass("card_content_right")
     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)


     -- ── 2-1. 战斗技能 ──
     -- 信息(稀有度 + 四维)
     right:node(makeTitle("战斗技能", "Tactical", "card_content-item-title--mt"))
     leftCol:node(makeTitle("信息", "Info"))
     right:node(makeHr("card_content-item-hr--mb12"))
     leftCol:node(makeHr())
    leftCol:node(makeInfoSection(card.intel, card.supply, card.execute, card.strategy, rarity))


     -- 普攻
     -- 等级 100 属性
     right:node(makeSkillCard("normal_attack"))
     leftCol:node(makeTitle("等级 100", "Lv. Max"))
     right:tag("div"):addClass("card_content_skill-gap")
    leftCol:node(makeHr())
    leftCol:node(makeAttrRow("生命", "—"))
    leftCol:node(makeAttrRow("攻击", "—"))
     leftCol:node(makeAttrRow("防御", "—"))
    leftCol:node(makeAttrRow("暴击率", "—"))
    leftCol:node(makeAttrRow("暴击伤害", "—"))


     -- 被动
     -- 映像
     right:node(makeSkillCard("passive"))
     leftCol:node(makeTitle("映像", "Reflection"))
     right:tag("div"):addClass("card_content_skill-gap")
    leftCol:node(makeHr())
    leftCol:node(makeAttrRow("固定攻击", "—"))
     leftCol:node(makeAttrRow("固定生命", "—"))
    leftCol:node(makeAttrRow("伤害加成", "—"))


     -- 终结技
     -- 认知
     right:node(makeSkillCard("ultimate"))
     leftCol:node(makeTitle("认知", "Cognition"))
    leftCol:node(makeHr())
    leftCol:tag("div"):addClass("card_content_attribute-item"):wikitext("(数据待补充)")


     -- ── 2-2. 觉醒 ──
     -- 右栏
     right:node(makeTitle("觉醒", "Feat", "card_content-item-title--mt"))
    local rightCol = layout:tag("div"):addClass("card_content_right")
     right:node(makeHr("card_content-item-hr--mb12"))
     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"))


     -- 觉醒列表,JS 根据 feats.stages 数组填充
     rightCol:node(makeTitle("觉醒", "Feat"))
    right:tag("ul")
    rightCol:node(makeHr("card_content-item-hr--mb12"))
        :addClass("card_content_feat-ul")
    rightCol:node(makeFeatList(featsStagesJson))
        :attr("data-card-field", "feats_list")


     -- ── 2-3. 小传 ──
     -- 小传
     local storySection = right:tag("div"):addClass("card_content_story")
     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", "card_content-item-title--mt"))
     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")
        :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()
         :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, stylename))


     -- 时间线,JS 根据 story 数据填充
     -- 邀约
    storyHeader:tag("div")
     rightCol:tag("div"):css("height", "12px"):css("width", "100%")
        :addClass("card_timeline card_content_story-timeline")
     rightCol:node(makeTitle("邀约", "Date"))
        :attr("data-card-field", "story_timeline")
     rightCol:node(makeHr("card_content-item-hr--mb12"))
        :tag("div"):addClass("card_timeline-line")
     rightCol:tag("div"):addClass("card_content_date")
 
        :tag("div"):addClass("card_content_date-track")
    -- ── 2-4. 邀约 ──
         :tag("ul"):addClass("card_content_date-list")
     right:tag("div"):css("height", "12px"):css("width", "100%")
         :tag("li"):addClass("card_content_date-item"):wikitext("(邀约数据待补充)")
     right:node(makeTitle("邀约", "Date", "card_content-item-title--mt"))
     right:node(makeHr("card_content-item-hr--mb12"))
 
    -- 邀约卡片轨道,JS 填充
     local dateWrap = right:tag("div"):addClass("card_content_date")
    dateWrap:tag("div")
        :addClass("card_content_date-track")
         :tag("ul")
            :addClass("card_content_date-list")
            :attr("data-card-field", "date_list")
 
    -- 邀约故事面板(点击卡片后展开)
    local dateStory = right:tag("div")
        :addClass("card_date_story")
        :attr("id", "date-story-panel")
         :attr("aria-live", "polite")
    dateStory:tag("div")
        :addClass("card_date_story-scene")
        :attr("id", "date-story-scene")
        :attr("data-card-field", "date_story_scene")
    dateStory:tag("div")
        :addClass("card_date_story-body")
        :attr("id", "date-story-body")
        :attr("data-card-field", "date_story_body")
 
    -- ── 加载提示(JS 完成后移除)──
    root:tag("div")
        :addClass("ron-card-loading")
        :wikitext("⏳ 加载中…")


    -- 输出 HTML + CSS 模板
     return tostring(root)
     return tostring(root) .. frame:expandTemplate{ title = "cardcss" }
end
end


return p
return p

2026年3月28日 (六) 22:00的最新版本

此模块的文档可以在模块:CardData/doc创建

-- Module:CardData
-- 用法:{{#invoke:CardData|render|复仇童谣}}

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 makeAttrRow(label, value)
    local item = mw.html.create("div"):addClass("card_content_attribute-item")
    local titleDiv = item:tag("div"):addClass("card_content_attribute-title")
    titleDiv:tag("div"):addClass("card_content_attribute-icon")
    titleDiv:tag("div"):wikitext(" " .. label)
    item:tag("div"):wikitext(value or "—")
    return item
end

-- 信息栏:稀有度 + 四维
local function makeInfoSection(intel, supply, execute, strategy, rarity)
    local infoDiv = mw.html.create("div"):addClass("card_content_info")

    local rarityItem = infoDiv:tag("div"):addClass("card_content_info-item")
    rarityItem:tag("div"):wikitext("稀有度")
    rarityItem:tag("div"):wikitext(rarityStars(rarity))

    local dims = {
        { label = "情报", value = intel    },
        { label = "物资", value = supply   },
        { label = "执行", value = execute  },
        { label = "策略", value = strategy },
    }
    for _, dim in ipairs(dims) do
        local item = infoDiv:tag("div"):addClass("card_content_info-item")
        item:tag("div"):wikitext(dim.label)
        local n = tonumber(dim.value)
        item:tag("div"):wikitext(n and ("※" .. n) or "—")
    end

    return infoDiv
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 maxCols = 0
    for _, row in ipairs(rows) do
        if type(row.levels) == "table" and #row.levels > maxCols then
            maxCols = #row.levels
        end
    end

    local DATA_COLS, gridClass
    if maxCols > 8 then
        DATA_COLS = 9
        gridClass = "card_content_skill-upgrade-grid card_content_skill-upgrade-grid--10"
    else
        DATA_COLS = 8
        gridClass = "card_content_skill-upgrade-grid"
    end

    local grid = wrap:tag("div"):addClass(gridClass)

    grid:tag("div")
    for i = 2, DATA_COLS + 1 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("→")

        local levels = (type(row.levels) == "table") and row.levels or {}
        for col = 1, DATA_COLS do
            local v = levels[col]
            local cell = grid:tag("div"):addClass("card_content_skill-upgrade-val")
            if v == nil or v == "" then
                cell:addClass("card_content_skill-upgrade-val--null"):wikitext("—")
            else
                cell:addClass("card_accent"):wikitext(tostring(v))
            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("欲火消耗 ")
        metaRow:tag("span"):addClass("card_content_skill-meta-val")
            :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 sw  = row:tag("div"):addClass("card_content_feat-stage-wrap")
        sw:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE")
        sw:tag("div"):addClass("card_content_feat-number")
            :wikitext(string.format("%02d", stage.stage or 0))
        local rd = row:tag("div")
        if stage.value and type(stage.value) == "table" then
            for _, v in ipairs(stage.value) do
                if v.description then
                    rd:tag("span"):addClass("card_accent"):wikitext(v.description)
                    rd:tag("br")
                end
            end
        end
        if stage.stat_boosts and type(stage.stat_boosts) == "table" then
            for _, boost in ipairs(stage.stat_boosts) do
                rd:wikitext((boost.type or "") .. "提升" .. (boost.value or ""))
            end
        end
        if stage.extra_name then
            rd:tag("span"):addClass("card_content_feat-stage-cn")
                :wikitext("「" .. stage.extra_name .. "」")
        end
    end
    return ul
end

local function makeStoryTimeline(stories, cardTitle)
    local timeline = mw.html.create("div")
        :addClass("card_timeline card_content_story-timeline")

    timeline:tag("div"):addClass("card_timeline-line")

    if not stories or type(stories) ~= "table" or #stories == 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(stories) do
        local item = timeline:tag("div"):addClass("card_timeline-item")
        if idx == #stories 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) .. " 解锁"))
        local titleDiv = item:tag("div"):addClass("card_timeline-title")
        titleDiv:tag("span"):addClass("card_timeline-numeral"):wikitext(romans[idx] or tostring(idx))
        titleDiv:tag("span"):addClass("card_timeline-chapter"):wikitext(cardTitle or "")
        titleDiv:tag("span"):addClass("card_timeline-label"):wikitext("/ Story")
        local bodyDiv = item:tag("div"):addClass("card_timeline-body")
        local text = chapter.text or ""
        if text ~= "" then
            text = text:gsub("\r\n", "\n")
            local paragraphs = mw.text.split(text, "\n+")
            for _, para in ipairs(paragraphs) do
                para = mw.text.trim(para)
                if para ~= "" then bodyDiv:tag("p"):wikitext(para) 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

    local encodedName = mw.uri.encode(name, "QUERY")
    local apiUrl = "https://data.saltedkiss.org/items/cards"
        .. "?fields=stylename,rarity,character.name,profession.name,desire.name"
        .. ",intel,supply,execute,strategy"
        .. ",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.text"
        .. "&filter[stylename][_eq]=" .. encodedName

    local rawResponse = frame:preprocess(
        '{{#get_web_data:url=' .. apiUrl
        .. '|format=text|data=responseText=__text}}'
        .. '{{#external_value:responseText}}'
    )
    rawResponse = mw.text.trim(rawResponse or "")

    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)

    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)

    -- 全屏背景图,文件命名规则:卡面_卡面名.png
    root:tag("div"):addClass("card_fullscreen-img")
        :wikitext("[[文件:卡面_" .. stylename .. ".png]]")
    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:node(makeInfoSection(card.intel, card.supply, card.execute, card.strategy, rarity))

    -- 等级 100 属性
    leftCol:node(makeTitle("等级 100", "Lv. Max"))
    leftCol:node(makeHr())
    leftCol:node(makeAttrRow("生命", "—"))
    leftCol:node(makeAttrRow("攻击", "—"))
    leftCol:node(makeAttrRow("防御", "—"))
    leftCol:node(makeAttrRow("暴击率", "—"))
    leftCol:node(makeAttrRow("暴击伤害", "—"))

    -- 映像
    leftCol:node(makeTitle("映像", "Reflection"))
    leftCol:node(makeHr())
    leftCol:node(makeAttrRow("固定攻击", "—"))
    leftCol:node(makeAttrRow("固定生命", "—"))
    leftCol:node(makeAttrRow("伤害加成", "—"))

    -- 认知
    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, stylename))

    -- 邀约
    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