欢迎来到《帝国时代Mobile》WIKI频道!


本WIKI开放编辑权限,欢迎收藏起来防止迷路,也希望有爱的小伙伴和我们一起把WIKI做大做强~!
编辑置顶公告WIKI编辑帮助BWIKI反馈

全站通知:

模块:EventCalendar

来自帝国时代MWIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索

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

local p = {}

-- ============================================================================
--  辅助逻辑函数
-- ============================================================================
local function parseDate(dateStr)
    if not dateStr then return os.time() end
    local p = "(%d+)-(%d+)-(%d+)"
    local y, m, d = dateStr:match(p)
    if not y then return os.time() end
    return os.time({year=y, month=m, day=d})
end

local function formatDate(timestamp)
    return os.date("%m/%d", timestamp)
end

local function getChineseWeekName(timestamp)
    local wday = os.date("*t", timestamp).wday
    local names = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"}
    return names[wday]
end

local function isEventActive(evt, checkTime)
    if not evt then return false end
    local checkDate = os.date("*t", checkTime)
    checkTime = os.time({year=checkDate.year, month=checkDate.month, day=checkDate.day})
    local startTime = parseDate(evt.start)
    if evt.type == 'special' then
        local endTime = parseDate(evt['end'])
        return checkTime >= startTime and checkTime <= endTime
    elseif evt.type == 'cycle' then
        if checkTime < startTime then return false end
        local duration = tonumber(evt.duration) or 1
        local interval = tonumber(evt.interval) or 0
        local cycleLen = duration + interval
        if cycleLen <= 0 then return false end
        local diffDays = math.floor(os.difftime(checkTime, startTime) / 86400)
        return (diffDays % cycleLen) < duration
    end
    return false
end

-- ============================================================================
--  主函数
-- ============================================================================
function p.main(frame)
    -- 1. 数据准备
    local offsetWeek = tonumber(frame.args.week_offset) or 0
    local maxHeight = frame.args.height
    
    local success, rawData = pcall(mw.loadData, 'Module:EventCalendar/Data')
    if not success then rawData = { events={}, schedule={} } end

    local now = os.time()
    local nowTable = os.date("*t", now)
    local todayStart = os.time({year=nowTable.year, month=nowTable.month, day=nowTable.day})

    -- 视图锚点:前2后4
    local startDate = todayStart - (2 * 86400) + (offsetWeek * 7 * 86400)
    local weekDates = {}
    for i = 0, 6 do
        table.insert(weekDates, startDate + (i * 86400))
    end
    local viewCenterDate = weekDates[3] 

    -- 2. 数据计算与排序
    local displayList = {}
    for _, item in ipairs(rawData.schedule) do
        local meta = rawData.events[item.eventId]
        if meta then
            local isActiveInWeek = false
            local sIdx, eIdx = -1, -1
            for i = 1, 7 do
                if isEventActive(item, weekDates[i]) then
                    if not isActiveInWeek then isActiveInWeek = true; sIdx = i end
                    eIdx = i
                end
            end
            if isActiveInWeek then
                local isExpired = eIdx < 3
                table.insert(displayList, {
                    id = item.eventId,
                    priority = meta.priority or 0,
                    name = meta.name,
                    link = meta.link,
                    icon = meta.icon,
                    sched = item,
                    sIdx = sIdx,
                    eIdx = eIdx,
                    isExpired = isExpired
                })
            end
        end
    end

    table.sort(displayList, function(a, b)
        if a.isExpired ~= b.isExpired then return not a.isExpired end
        if a.sIdx ~= b.sIdx then return a.sIdx < b.sIdx end
        if a.priority ~= b.priority then return a.priority > b.priority end
        return a.id < b.id 
    end)

    -- 3. 主题配色
    local theme = {
        bg_container = 'rgba(40, 44, 60, 0.95)', 
        border_outer = '1px solid rgba(244, 218, 136, 0.8)', 
        border_col   = '1px solid rgba(244, 218, 136, 0.15)', 
        border_header= '1px solid rgba(244, 218, 136, 0.3)', 
        text_title   = '#ffdf91',
        text_tips    = 'rgba(255, 223, 145, 0.6)', 
        text_white   = 'rgba(255, 255, 255, 0.9)',
        bg_title     = 'rgba(0, 0, 0, 0.25)',
        bg_header    = 'rgba(40, 44, 60, 0.95)', -- 不透明背景
        
        today_bg     = 'rgba(220, 179, 101, 0.15)',
        today_shadow = 'inset 0 0 15px rgba(220, 179, 101, 0.1)',
        today_line   = '#ffdf91',
        bar_bg       = 'linear-gradient(to bottom, rgba(68, 80, 110, 0.95), rgba(48, 59, 85, 1))',
        bar_border   = '1px solid rgba(244, 218, 136, 0.5)',
        bar_text     = '#ffdf91',
        expired_bg     = 'linear-gradient(to bottom, rgba(45, 50, 60, 0.9), rgba(35, 40, 50, 0.9))',
        expired_border = '1px solid rgba(244, 218, 136, 0.15)',
        expired_text   = 'rgba(255, 255, 255, 0.4)'
    }

    -- 4. 结构构建
    local outerBox = mw.html.create('div')
        :css({
            ['font-family'] = '"Microsoft YaHei", Arial, sans-serif',
            ['background'] = theme.bg_container,
            ['border'] = theme.border_outer,
            ['border-radius'] = '6px',
            ['box-shadow'] = '0 5px 15px rgba(0, 0, 0, 0.6)',
            ['backdrop-filter'] = 'blur(5px)',
            ['color'] = theme.text_white,
            ['max-width'] = '800px',
            ['margin'] = '10px auto', 
            ['overflow'] = 'hidden',
            ['display'] = 'flex',
            ['flex-direction'] = 'column',
            ['box-sizing'] = 'border-box'
        })

    -- 4.1 标题栏
    local titleBar = outerBox:tag('div'):css({
        ['display'] = 'flex',
        ['justify-content'] = 'space-between',
        ['align-items'] = 'center',
        ['padding'] = '10px 15px',
        ['background'] = theme.bg_title,
        ['border-bottom'] = theme.border_header,
        ['flex-shrink'] = '0'
    })
    titleBar:tag('div'):css({['font-size']='16px',['font-weight']='bold',['color']=theme.text_title,['text-shadow']='0 1px 2px rgba(0,0,0,0.8)',['display']='flex',['align-items']='center',['gap']='6px'}):wikitext('✦ 活动日历')
    titleBar:tag('div'):css({['font-size']='12px',['color']=theme.text_tips,['font-family']='Arial, sans-serif'}):wikitext(os.date("%Y / %m", viewCenterDate)) 

    -- 4.2 滚动视口 (使用原生滚动条)
    local scrollWindow = outerBox:tag('div')
        :css({
            ['position'] = 'relative',
            ['overflow-y'] = 'auto',
            ['overflow-x'] = 'hidden',
            ['width'] = '100%'
        })
    
    if maxHeight and maxHeight ~= "" then
        scrollWindow:css('max-height', maxHeight)
    else
        scrollWindow:css('min-height', 'auto') 
    end

    -- 4.3 内部大容器
    local innerWrapper = scrollWindow:tag('div'):css({
        ['position'] = 'relative',
        ['width'] = '100%',
        ['box-sizing'] = 'border-box'
    })

    -- 层1: 背景网格 (修复错位逻辑:自动避开滚动条)
    local bgGrid = innerWrapper:tag('div'):css({
        ['position'] = 'absolute',
        ['top'] = '0', 
        ['left'] = '0', 
        ['right'] = '0', 
        ['bottom'] = '0',
        ['display'] = 'grid',
        ['grid-template-columns'] = 'repeat(7, 1fr)',
        ['z-index'] = '0',
        ['pointer-events'] = 'none',
        ['box-sizing'] = 'border-box'
    })
    for i = 1, 7 do
        local cell = bgGrid:tag('div'):css({['border-right']=theme.border_col, ['box-sizing']='border-box'})
        if os.date("%Y%m%d", weekDates[i]) == os.date("%Y%m%d", now) then cell:css('background', 'rgba(220, 179, 101, 0.05)') end
        if i == 7 then cell:css('border-right', 'none') end
    end

    -- 层2: 表头 (Sticky)
    local header = innerWrapper:tag('div')
        :css({
            ['display'] = 'grid',
            ['grid-template-columns'] = 'repeat(7, 1fr)',
            ['background'] = theme.bg_header,
            ['color'] = theme.text_title,
            ['text-align'] = 'center',
            ['font-size'] = '14px',
            ['border-bottom'] = theme.border_col, 
            ['gap'] = '0',
            ['position'] = 'sticky',
            ['top'] = '0',
            ['z-index'] = '10',
            ['width'] = '100%',
            ['box-sizing'] = 'border-box'
        })
    
    for i = 0, 6 do
        local d = weekDates[i+1]
        local isToday = os.date("%Y%m%d", d) == os.date("%Y%m%d", now)
        local col = header:tag('div'):css({['padding']='8px 0', ['border-right']=theme.border_col, ['position']='relative', ['box-sizing']='border-box'})
        if i == 6 then col:css('border-right', 'none') end
        
        if isToday then 
            col:css({['background']=theme.today_bg,['box-shadow']=theme.today_shadow})
            col:tag('div'):css({['position']='absolute', ['bottom']='0', ['left']='0', ['width']='100%', ['height']='2px', ['background']=theme.today_line})
        end
        col:tag('div'):wikitext(getChineseWeekName(d))
        col:tag('div'):css({['font-size']='12px', ['opacity']='0.7', ['margin-top']='2px'}):wikitext(formatDate(d))
    end

    -- 层3: 活动内容
    local contentLayer = innerWrapper:tag('div'):css({
        ['position'] = 'relative', 
        ['z-index'] = '1', 
        ['padding'] = '10px 0',
        ['width'] = '100%',
        ['box-sizing'] = 'border-box'
    })

    if #displayList > 0 then
        for _, evt in ipairs(displayList) do
            local row = contentLayer:tag('div'):css({['display']='grid', ['grid-template-columns']='repeat(7, 1fr)', ['margin']='6px 0', ['padding']='0', ['gap']='0', ['width']='100%'})
            local posWrapper = row:tag('div'):addClass('plainlinks'):css({['grid-column']=evt.sIdx .. ' / span ' .. (evt.eIdx - evt.sIdx + 1), ['margin']='0 2px'})
            
            local currentBg = evt.isExpired and theme.expired_bg or theme.bar_bg
            local currentBorder = evt.isExpired and theme.expired_border or theme.bar_border
            local currentText = evt.isExpired and theme.expired_text or theme.bar_text
            
            local styleStr = table.concat({
                'background:' .. currentBg,
                'border:' .. currentBorder,
                'color:' .. currentText,
                'font-size: 13px',
                'padding: 5px 8px',
                'border-radius: 4px',
                'box-shadow: 0 2px 5px rgba(0,0,0,0.4)',
                'white-space: nowrap',
                'overflow: hidden',
                'display: flex', 
                'align-items: center',
                'width: auto', 
                'height: 100%',
                'box-sizing: border-box',
                'cursor: default'
            }, ';')
            
            local hasLink = (evt.link and evt.link ~= "")
            if hasLink and not evt.isExpired then styleStr = styleStr .. ';cursor:pointer' end
            
            if evt.sIdx == 1 and isEventActive(evt.sched, weekDates[1] - 86400) then styleStr = styleStr .. ';border-top-left-radius:0;border-bottom-left-radius:0;border-left:none' end
            if evt.eIdx == 7 and isEventActive(evt.sched, weekDates[7] + 86400) then styleStr = styleStr .. ';border-top-right-radius:0;border-bottom-right-radius:0;border-right:none' end

            local contentTxt = evt.name
            if hasLink then contentTxt = '<span style="font-weight:bold;">' .. evt.name .. '</span>' end
            if evt.icon and evt.icon ~= '' then 
                contentTxt = '[[File:' .. evt.icon .. '|18px|link=]] ' .. contentTxt 
            end
            
            -- 保留了 class 接口,但删除了 CSS 注入代码。
            -- 悬浮特效:如果你以后在 Common.css 里加了 .aoem-hover-anim,这里会自动生效。
            -- 如果不加,就是无特效,但不报错。
            if hasLink and not evt.isExpired then
                posWrapper:wikitext('[[' .. evt.link .. '|<span class="aoem-hover-anim" style="' .. styleStr .. '">' .. contentTxt .. '</span>]]')
            elseif hasLink then
                posWrapper:wikitext('[[' .. evt.link .. '|<span style="' .. styleStr .. '">' .. contentTxt .. '</span>]]')
            else
                posWrapper:tag('div'):attr('style', styleStr):wikitext(contentTxt)
            end
        end
    else
        contentLayer:tag('div'):css({['text-align']='center', ['color']='rgba(255,255,255,0.3)', ['padding']='30px', ['font-style']='italic'}):wikitext('暂无活动')
    end

    return tostring(outerBox)
end

return p