跳转到内容

模組:SunXiu

本页使用了标题或全文手工转换
维基百科,自由的百科全书

require('Module:Module wikitext')._addText('{{D|G3|G21|见[[U:12З4567/Wenlongtian调查]]}}')
-- Module:SunXiu 太阳位置与二十八宿计算(自包含,无外部数据依赖)
local p = {}

local TERM_LONS = {0,15,30,45,60,75,90,105,120,135,150,165,180,195,210,225,240,255,270,285,300,315,330,345}
local TERM_NAMES = {
    [0]="春分",[15]="清明",[30]="谷雨",[45]="立夏",[60]="小满",[75]="芒种",
    [90]="夏至",[105]="小暑",[120]="大暑",[135]="立秋",[150]="处暑",[165]="白露",
    [180]="秋分",[195]="寒露",[210]="霜降",[225]="立冬",[240]="小雪",[255]="大雪",
    [270]="冬至",[285]="小寒",[300]="大寒",[315]="立春",[330]="雨水",[345]="惊蛰"
}
local TROP_SIGNS = {"白羊宫","金牛宫","双子宫","巨蟹宫","狮子宫","室女宫","天秤宫","天蝎宫","射手宫","摩羯宫","宝瓶宫","双鱼宫"}
local SID_SIGNS  = {"白羊座","金牛座","双子座","巨蟹座","狮子座","室女座","天秤座","天蝎座","射手座","摩羯座","宝瓶座","双鱼座"}
local IAU_CONST = {
    {name="白羊座", s=0, e=30},{name="金牛座", s=30, e=60},{name="双子座", s=60, e=90},
    {name="巨蟹座", s=90, e=120},{name="狮子座", s=120, e=150},{name="室女座", s=150, e=180},
    {name="天秤座", s=180, e=210},{name="天蝎座", s=210, e=240},{name="蛇夫座", s=240, e=270},
    {name="射手座", s=270, e=300},{name="摩羯座", s=300, e=330},{name="宝瓶座", s=330, e=360}
}
local XINGCI_NAMES = {"星纪","玄枵","娵訾","降娄","大梁","实沈","鹑首","鹑火","鹑尾","寿星","大火","析木"}
local CHEN_NAMES   = {"丑","子","亥","戌","酉","申","未","午","巳","辰","卯","寅"}
local XINGCI_START = 270
local XIU_SYSTEMS = {
    huainan = {
        name = "《淮南子·天文训》距度", origin = 0,
        stars = {{"角",12},{"亢",9},{"氐",15},{"房",5},{"心",5},{"尾",18},{"箕",11},
                 {"斗",26},{"牛",8},{"女",12},{"虚",10},{"危",17},{"室",16},{"壁",9},
                 {"奎",16},{"娄",12},{"胃",14},{"昴",11},{"毕",16},{"觜",2},{"参",9},
                 {"井",33},{"鬼",4},{"柳",15},{"星",7},{"张",18},{"翼",18},{"轸",17}}
    },
    dayan_ecl = {
        name = "大衍历·黄道宿度", origin = 0,
        stars = {{"角",12.5},{"亢",9.5},{"氐",16},{"房",5.5},{"心",5.5},{"尾",18.5},{"箕",10.5},
                 {"斗",24},{"牛",7.5},{"女",11.5},{"虚",9.5},{"危",16.5},{"室",17},{"壁",9.5},
                 {"奎",17},{"娄",12.5},{"胃",14.5},{"昴",10.5},{"毕",16.5},{"觜",1.5},{"参",9.5},
                 {"井",32},{"鬼",3.5},{"柳",14.5},{"星",7.5},{"张",17.5},{"翼",18.5},{"轸",16.5}}
    },
    dayan_equ = {
        name = "大衍历·赤道宿度", origin = 0,
        stars = {{"角",12},{"亢",9},{"氐",15},{"房",5},{"心",5},{"尾",18},{"箕",11},
                 {"斗",25},{"牛",7},{"女",11},{"虚",10},{"危",16},{"室",17},{"壁",9},
                 {"奎",16},{"娄",12},{"胃",14},{"昴",11},{"毕",16},{"觜",2},{"参",9},
                 {"井",33},{"鬼",4},{"柳",15},{"星",7},{"张",18},{"翼",18},{"轸",17}}
    },
    modern = {
        name = "现代真黄道宿界(含岁差)", origin = -28.0,
        stars = {{"角",11.3},{"亢",9.0},{"氐",14.4},{"房",5.2},{"心",6.1},{"尾",19.0},{"箕",10.2},
                 {"斗",24.0},{"牛",7.8},{"女",11.0},{"虚",9.5},{"危",16.2},{"室",17.0},{"壁",8.8},
                 {"奎",15.5},{"娄",11.8},{"胃",13.5},{"昴",10.2},{"毕",15.8},{"觜",1.8},{"参",9.2},
                 {"井",31.5},{"鬼",3.8},{"柳",14.2},{"星",7.2},{"张",17.2},{"翼",17.8},{"轸",16.8}}
    }
}

local function round(v, n)
    local pow = 10^(n or 2)
    return math.floor(v*pow + 0.5) / pow
end
local function mod360(x) return x - math.floor(x/360)*360 end
local function mod365(x) return x - math.floor(x/365.25)*365.25 end
local function fmtDays(days)
    local sec = math.abs(days) * 86400
    local d = math.floor(sec / 86400)
    local r = sec % 86400
    local h = math.floor(r / 3600)
    local m = math.floor((r % 3600) / 60)
    return string.format("%d天 %d时%02d分", d, h, m)
end

local function jdFromDate(y,m,d,h)
    h = h or 12
    if m <= 2 then y=y-1; m=m+12 end
    local a = math.floor(y/100)
    local b = 2 - a + math.floor(a/4)
    return math.floor(365.25*(y+4716)) + math.floor(30.6001*(m+1)) + d + b - 1524.5 + h/24
end
local function dateFromJd(jd)
    local z = math.floor(jd+0.5)
    local f = (jd+0.5)-z
    local a = z
    if z >= 2299161 then
        local alpha = math.floor((z-1867216.25)/36524.25)
        a = z+1+alpha-math.floor(alpha/4)
    end
    local b = a+1524
    local c = math.floor((b-122.1)/365.25)
    local dd = math.floor(365.25*c)
    local e = math.floor((b-dd)/30.6001)
    local day = b-dd-math.floor(30.6001*e)+f
    local month = e<14 and (e-1) or (e-13)
    local year = month>2 and (c-4716) or (c-4715)
    local hh = math.floor((day-math.floor(day))*24)
    local mi = math.floor(((day-math.floor(day))*24-hh)*60)
    return year, month, math.floor(day), hh, mi
end

local function calcNutation(jd)
    local T = (jd-2451545.0)/36525.0
    local D = 297.85036+445267.11148*T-0.0019142*T*T+T^3/189474
    local M = 357.52772+35999.05034*T-0.0001603*T*T-T^3/300000
    local Mp= 134.96298+477198.867398*T+0.0086972*T*T+T^3/56250
    local F = 93.27191+483202.017538*T-0.0036825*T*T+T^3/327270
    local Om= 125.04452-1934.136261*T+0.0020708*T*T+T^3/450000
    local rd=math.rad; local s=math.sin; local c=math.cos
    local dD=rd(D); local dM=rd(M); local dMp=rd(Mp); local dF=rd(F); local dOm=rd(Om)
    local dp = -17.1996*s(dOm) -1.3187*s(2*(dF-dOm+dMp)) -0.2274*s(2*dF) +0.2062*s(2*dOm)
           +0.1426*s(2*(dF+dMp)) +0.0712*s(2*(dF-dOm)) -0.0517*s(2*dMp) -0.038*s(2*(dF+dMp-dOm))
           -0.0304*s(2*(dF+dMp+dOm)) +0.0153*s(2*(dF-dOm-dMp)) -0.0127*s(2*(dF+dM)) -0.0103*s(2*(dF-dM))
    local de = 9.2025*c(dOm) +0.5736*c(2*(dF-dOm+dMp)) +0.0977*c(2*dF) -0.0895*c(2*dOm)
           +0.0224*c(2*(dF+dMp)) -0.0209*c(2*(dF-dOm)) -0.0095*c(2*dMp) +0.0078*c(2*(dF+dMp-dOm))
           +0.0068*c(2*(dF+dMp+dOm))
    return dp, de
end
local function meanObliquity(jd)
    local T=(jd-2451545.0)/36525.0
    return 23.439291-0.0130042*T-1.64e-7*T*T+5.04e-7*T*T*T
end

local function sunPos(jd)
    local T=(jd-2451545.0)/36525.0; local T2=T*T
    local L0=mod360(280.46646+36000.76983*T+0.0003032*T2)
    local M=mod360(357.52911+35999.05029*T-0.0001537*T2)
    local Mr=math.rad(M)
    local C=(1.914602-0.004817*T-0.000014*T2)*math.sin(Mr)
          +(0.019993-0.000101*T)*math.sin(2*Mr)+0.000289*math.sin(3*Mr)
    local lon=mod360(L0+C)
    local R=1.000001018*(1-0.016708634*math.cos(Mr)-0.000042037*math.cos(2*Mr)-0.000000126*math.cos(3*Mr))
    local kappa=20.49552
    local lonApp=lon+(kappa/3600)/R
    local dp,_=calcNutation(jd)
    lonApp=mod360(lonApp+dp/3600)
    local eps=meanObliquity(jd); local epsRad=math.rad(eps)
    local lonRad=math.rad(lonApp)
    local atan2 = math.atan2 or function(y,x) return math.atan(y, x) end
    local ra=atan2(math.sin(lonRad)*math.cos(epsRad), math.cos(lonRad))
    local dec=math.asin(math.sin(lonRad)*math.sin(epsRad))
    local raDeg=mod360(math.deg(ra)); local raHour=raDeg/15
    local y=math.tan(epsRad/2)^2
    local sin2L0=math.sin(2*math.rad(L0)); local sinM=math.sin(Mr)
    local cos2L0=math.cos(2*math.rad(L0)); local cosM=math.cos(Mr)
    local eqTime=4*(y*sin2L0-2 * 0.0167*sinM+0.0433*sin2L0*cosM-0.0325*cos2L0*sinM-0.125*cos2L0)
    if eqTime>20 then eqTime=eqTime-720 end
    if eqTime<-20 then eqTime=eqTime+720 end
    local radius=959.633/R
    return {lonApp=lonApp, dec=math.deg(dec), ra=raHour, raDeg=raDeg, distAu=R,
            eqTime=eqTime, epsTrue=eps, radiusArcsec=radius, lat=0}
end

local function sunLonQuick(jd)
    local n=jd-2451545.0
    local L=mod360(280.460+0.9856474*n)
    local g=mod360(357.528+0.9856003*n); local gr=math.rad(g)
    return mod360(L+1.915*math.sin(gr)+0.020*math.sin(2*gr))
end

local function refineTerm(jdGuess, targetLon)
    local lo=jdGuess-5; local hi=jdGuess+5
    for _=1,40 do
        local mid=(lo+hi)/2
        local diff=sunLonQuick(mid)-targetLon
        diff=mod360(diff+180)-180
        if diff>0 then hi=mid else lo=mid end
    end
    return (lo+hi)/2
end

local function findTermNear(targetLon, jdAround)
    local lo=jdAround-10; local hi=jdAround+10
    for _=1,50 do
        local mid=(lo+hi)/2
        local diff=sunLonQuick(mid)-targetLon
        diff=mod360(diff+180)-180
        if diff>0 then hi=mid else lo=mid end
    end
    return (lo+hi)/2
end

local function findTermBracket(targetLon, jdStart)
    local lo=jdStart; local hi=jdStart+400
    for _=1,50 do
        local mid=(lo+hi)/2
        local diff=sunLonQuick(mid)-targetLon
        diff=mod360(diff+180)-180
        if diff>0 then hi=mid else lo=mid end
    end
    return (lo+hi)/2
end

local function getTermBracket(jd)
    local candidates={}
    for _,lon in ipairs(TERM_LONS) do
        local guess=jd+(lon/15-12)*15.218
        local lo=guess-200; local hi=guess+200
        for _=1,60 do
            local mid=(lo+hi)/2
            local diff=sunLonQuick(mid)-lon
            diff=mod360(diff+180)-180
            if diff>0 then hi=mid else lo=mid end
        end
        local tjd=(lo+hi)/2
        table.insert(candidates,{lon=lon, name=TERM_NAMES[lon], jd=tjd})
    end
    local prevTerm, nextTerm
    for _,c in ipairs(candidates) do
        if c.jd <= jd then
            if not prevTerm or c.jd > prevTerm.jd then prevTerm=c end
        end
        if c.jd > jd then
            if not nextTerm or c.jd < nextTerm.jd then nextTerm=c end
        end
    end
    if not prevTerm then
        prevTerm=candidates[1]; for _,c in ipairs(candidates) do
            if c.jd > prevTerm.jd then prevTerm=c end end
    end
    if not nextTerm then
        nextTerm=candidates[1]; for _,c in ipairs(candidates) do
            if c.jd < nextTerm.jd then nextTerm=c end end
    end
    return prevTerm, nextTerm, candidates
end

local function findStar(pos, sys)
    local acc=0
    for i,s in ipairs(sys.stars) do
        local w=s[2]
        if pos>=acc and pos<acc+w then return s[1],acc,w,i end
        acc=acc+w
    end
    local last=sys.stars[#sys.stars]
    return last[1], 365.25-last[2], last[2], #sys.stars
end

local function sunInXiu(lon, sysKey)
    local sys=XIU_SYSTEMS[sysKey] or XIU_SYSTEMS.huainan
    local pos=mod365(lon-sys.origin)
    if pos<0 then pos=pos+365.25 end
    local name,startDeg,_,_=findStar(pos,sys)
    local inDeg=pos-startDeg
    return name, inDeg, sys.name
end

local function getZodiac(lon)
    local lonNorm=mod360(lon)
    local tropIdx=math.floor(lonNorm/30)+1
    local tropIn=lonNorm-(tropIdx-1)*30
    local tropDays=(30-tropIn)/0.985647
    local ayanamsa=23.0
    local sidLon=mod360(lonNorm-ayanamsa)
    local sidIdx=math.floor(sidLon/30)+1
    local sidIn=sidLon-(sidIdx-1)*30
    local iauName="双鱼座"; local iauIn=lonNorm-330
    for _,c in ipairs(IAU_CONST) do
        if lonNorm>=c.s and lonNorm<c.e then iauName=c.name; iauIn=lonNorm-c.s; break end
    end
    local xcLon=mod360(lonNorm-XINGCI_START)
    local xcIdx=math.floor(xcLon/30)+1
    local xcName=XINGCI_NAMES[xcIdx]; local chen=CHEN_NAMES[xcIdx]
    local xcIn=xcLon-(xcIdx-1)*30
    return {
        trop_sign=TROP_SIGNS[tropIdx], trop_sign_cn=TROP_SIGNS[tropIdx], trop_in_deg=tropIn, trop_days_to_next=tropDays,
        sidereal_sign=SID_SIGNS[sidIdx], sidereal_ayanamsa=ayanamsa, sidereal_in_deg=sidIn,
        iau_constellation=iauName, iau_in_deg=iauIn, iau_note="注:IAU星座边界非均匀,此为近似",
        xing_ci=xcName, chen=chen, xingci_in_deg=xcIn
    }
end

local function compute(args)
    -- === 获取当前北京时间(UTC+8)并转换为UT用于天文计算 ===
    local utc = os.date("!*t")               -- 当前UTC时间表
    local utc_sec = utc.hour * 3600 + utc.min * 60 + utc.sec  -- UTC当天秒数
    local bj_sec = utc_sec + 8 * 3600        -- 北京时间当天秒数
    local bj_day_offset = math.floor(bj_sec / 86400)
    local bj_h = (bj_sec % 86400) / 3600     -- 北京时间小时(含小数,用于显示)
    local bj_day = utc.day + bj_day_offset
    local bj_month = utc.month
    local bj_year = utc.year
    -- 处理跨日/月/年
    while bj_day > 28 do
        local days_in_month = os.date("*t", os.time({year=bj_year, month=bj_month, day=1}) - 86400).day
        if bj_day <= days_in_month then break end
        bj_day = bj_day - days_in_month
        bj_month = bj_month + 1
        if bj_month > 12 then bj_month = 1; bj_year = bj_year + 1 end
    end
    while bj_day < 1 do
        bj_month = bj_month - 1
        if bj_month < 1 then bj_month = 12; bj_year = bj_year - 1 end
        local days_in_prev = os.date("*t", os.time({year=bj_year, month=bj_month, day=1}) - 86400).day
        bj_day = bj_day + days_in_prev
    end

    -- 应用用户传入的参数(覆盖默认的北京时间)
    local y = tonumber(args.year)  or bj_year
    local m = tonumber(args.month) or bj_month
    local d = tonumber(args.day)   or bj_day
    local bj_h_display = tonumber(args.hour) or math.floor(bj_h)  -- 显示用整点小时
    local sysKey = args.system or "huainan"

    -- 将北京时间转换为世界时(UT)
    local ut_h = bj_h_display - 8
    local ut_day = d
    if ut_h < 0 then ut_h = ut_h + 24; ut_day = d - 1 end
    if ut_h >= 24 then ut_h = ut_h - 24; ut_day = d + 1 end
    -- 处理跨月(简化:若ut_day超出范围则借用os.date重新计算,但此处只需保证jdFromDate能正确处理)
    -- 实际上jdFromDate可以处理负数和大于月份天数的日期,但为了严谨,我们简单处理:
    if ut_day < 1 then
        local prev_month = m - 1
        local prev_year = y
        if prev_month < 1 then prev_month = 12; prev_year = y - 1 end
        local days_in_prev = os.date("*t", os.time({year=prev_year, month=prev_month, day=1}) - 86400).day
        ut_day = ut_day + days_in_prev
        m = prev_month
        y = prev_year
    elseif ut_day > 28 then
        local days_in_this = os.date("*t", os.time({year=y, month=m, day=1}) - 86400).day
        if ut_day > days_in_this then
            ut_day = ut_day - days_in_this
            m = m + 1
            if m > 12 then m = 1; y = y + 1 end
        end
    end

    local jd = jdFromDate(y, m, ut_day, ut_h)
    local jdTT = jd + 69.184/86400
    -- === 时间处理结束 ===

    local sun = sunPos(jdTT)
    local lonApp = sun.lonApp; local dec = sun.dec

    local prev, nxt = getTermBracket(jd)
    local prevTerm, prevTermJd = prev.name, prev.jd
    local termLon = prev.lon
    local segNames={
        ["春分"]="春分后",["清明"]="清明后",["谷雨"]="谷雨后",["立夏"]="立夏后",
        ["小满"]="小满后",["芒种"]="芒种后",["夏至"]="夏至后",["小暑"]="小暑后",
        ["大暑"]="大暑后",["立秋"]="立秋后",["处暑"]="处暑后",["白露"]="白露后",
        ["秋分"]="秋分后",["寒露"]="寒露后",["霜降"]="霜降后",["立冬"]="立冬后",
        ["小雪"]="小雪后",["大雪"]="大雪后",["冬至"]="冬至后",["小寒"]="小寒后",
        ["大寒"]="大寒后",["立春"]="立春后",["雨水"]="雨水后",["惊蛰"]="惊蛰后"
    }
    local curTerm = segNames[prev.name] or (prev.name.."后")
    local nextTerm, nextTermJd = nxt.name, nxt.jd
    local daysAfter = jd - prev.jd
    local daysToNext = nxt.jd - jd
    if daysAfter < 0 then daysAfter = daysAfter + 365.25 end
    if daysToNext < 0 then daysToNext = daysToNext + 365.25 end
    local daysAfterHM = fmtDays(daysAfter)
    local function formatJd(jdVal)
        local jdLocal=jdVal+8/24
        local yy,mm,dd,hh,mi=dateFromJd(jdLocal)
        return string.format("%04d-%02d-%02d %02d:%02d",yy,mm,dd,hh,mi)
    end
    local prevTermTime = formatJd(prevTermJd)
    local nextTermTime = formatJd(nextTermJd)

    local xiuname, xiuindeg, xiusysname = sunInXiu(lonApp, sysKey)
    local quji=90-dec

    local zod=getZodiac(lonApp)

    return {
        jd=jd, jd_tt=jdTT, T=(jdTT-2451545.0)/36525.0,
        lon=sun.lonApp, lon_app=sun.lonApp, lat=sun.lat,
        ra=sun.ra, ra_deg=sun.raDeg, dec=sun.dec,
        dist_au=sun.distAu, eq_time=sun.eqTime,
        eps_true=sun.epsTrue, radius_arcsec=sun.radiusArcsec,
        prev_term=prevTerm, prev_term_jd=prevTermJd, prev_term_time=prevTermTime,
        cur_term=curTerm, days_after_term=daysAfter, days_after_term_hm=daysAfterHM,
        next_term=nextTerm, next_term_jd=nextTermJd, next_term_time=nextTermTime,
        days_to_next_term=daysToNext, term_lon=termLon,
        trop_sign=zod.trop_sign, trop_sign_cn=zod.trop_sign_cn,
        trop_in_deg=zod.trop_in_deg, trop_days_to_next=zod.trop_days_to_next,
        sidereal_sign=zod.sidereal_sign, sidereal_ayanamsa=zod.sidereal_ayanamsa,
        sidereal_in_deg=zod.sidereal_in_deg,
        iau_constellation=zod.iau_constellation, iau_in_deg=zod.iau_in_deg, iau_note=zod.iau_note,
        xing_ci=zod.xing_ci, chen=zod.chen, xingci_in_deg=zod.xingci_in_deg,
        sun_xiu=xiuname, sun_xiu_in_deg=xiuindeg, sun_xiu_quji=quji,
        sun_xiu_system=sysKey, sun_xiu_sysname=xiusysname, 
        -- 存储北京时间(显示用)
        year = y, month = m, day = d, hour = bj_h
    }
end

local function renderBox(d)
    local out = {}
    table.insert(out, '<div style="width:300px;float:right;clear:right;margin:0 0 1em 1em;'..
        'border:1px solid #a2a9b1;border-radius:10px;overflow:hidden;font-family:sans-serif;'..
        'box-shadow:0 2px 6px rgba(0,0,0,.12);background:#fff;font-size:13px;line-height:1.5">')

    -- d.hour 已经是北京时间,无需再转换
    local timeStr = string.format("%04d-%02d-%02d %02d:00 UTC+8", d.year, d.month, d.day, d.hour)
    table.insert(out, string.format(
        '<div style="background:linear-gradient(90deg,#e68a2e,#f0b400);color:#fff;padding:10px 12px;'..
        'font-weight:bold;text-align:center;font-size:15px">太阳推算 · %s</div>', timeStr))

    table.insert(out, string.format(
        '<div style="padding:12px;text-align:center;background:#fff8e6;border-bottom:1px solid #eaecf0">'..
        '<div style="font-size:18px;font-weight:bold;color:#8a5500">太阳在【%s】宿</div>'..
        '<div style="margin-top:4px">入宿距度 <b>%.2f°</b> · 去极度 <b>%.2f°</b></div>'..
        '<div style="font-size:11px;color:#887">体系:%s</div></div>',
        d.sun_xiu, d.sun_xiu_in_deg, d.sun_xiu_quji, d.sun_xiu_sysname))

    local function row(label, val, bg)
        bg = bg or "#fafcff"
        return '<tr style="background:' .. bg .. ';"><td style="width:96px;padding:6px 8px;font-weight:bold;background:#f1f4f6;border-right:1px solid #eaecf0;border-bottom:1px solid #eaecf0;">' .. label .. '</td><td style="padding:6px 8px;border-bottom:1px solid #eaecf0;">' .. val .. '</td></tr>'
    end

    table.insert(out, '<table style="width:100%;border-collapse:collapse;">')

    table.insert(out, row("计算参数",
        string.format("JD(UT) %.4f · JD(TT) %.4f<br/>T=%.6f",
        d.jd, d.jd_tt, d.T)))

    table.insert(out, row("岁差/章动",
        string.format("真黄赤交角 %.4f°<br/>章动 Δψ=%.3f″ Δε=%.3f″",
        d.eps_true, 0, 0), "#f6f8fa"))

    table.insert(out, row("太阳坐标",
        string.format("视黄经 %.4f° · 赤纬 %.4f°<br/>赤经 %.4fʰ",
        d.lon_app, d.dec, d.ra)))

    table.insert(out, row("几何位置",
        string.format("日地距 %.6f AU<br/>均时差 %.2f min · 视半径 %.1f″",
        d.dist_au, d.eq_time, d.radius_arcsec), "#f6f8fa"))

    table.insert(out, row("节气状态",
        string.format("当前:%s(%s)<br/>下一节气:%s(%s)",
        d.cur_term, d.days_after_term_hm, d.next_term, d.next_term_time)))

    table.insert(out, row("回归黄道宫",
        string.format("%s · 入度 %.2f°<br/>距下一宫约 %.1f 天",
        d.trop_sign, d.trop_in_deg, d.trop_days_to_next), "#f6f8fa"))

    table.insert(out, row("恒星黄道宫",
        string.format("%s · 入度 %.2f°<br/>岁差偏移 %.2f°",
        d.sidereal_sign, d.sidereal_in_deg, d.sidereal_ayanamsa)))

    table.insert(out, row("真实星座(IAU)",
        string.format("%s · 入度 %.2f°<br/><small>%s</small>",
        d.iau_constellation, d.iau_in_deg, d.iau_note), "#f6f8fa"))

    table.insert(out, row("中国十二次",
        string.format("%s · 辰在%s · 入度 %.2f°",
        d.xing_ci, d.chen, d.xingci_in_deg)))

    table.insert(out, '</table>')
    table.insert(out, '</div>')
    return table.concat(out)
end

-- 公开接口
function p.get(frame) 
    local d = compute(frame.args)
    local key = frame.args.param or frame.args[1]
    if key and d[key] ~= nil then
        if type(d[key]) == "number" then return round(d[key], 5) end
        return tostring(d[key])
    end
    return "未知参数:" .. tostring(key)
end

function p.getJD(frame) return tostring(compute(frame.args).jd) end
function p.infoBox(frame) return renderBox(compute(frame.args)) end

-- 带节气信息的简洁文本
function p.short(frame) 
    local d = compute(frame.args)
    return string.format("太阳在【%s】宿,距度 %.2f°,去极度 %.2f°;当前节气:%s(%s);下一节气:%s(%.2f天);黄道宫:%s;十二次:%s",
        d.sun_xiu, d.sun_xiu_in_deg, d.sun_xiu_quji, d.cur_term, d.days_after_term_hm,
        d.next_term, d.days_to_next_term, d.trop_sign, d.xing_ci)
end

-- 不带节气信息的极简文本(供 SunJQ 等模板使用)
function p.brief(frame)
    local d = compute(frame.args)
    return string.format("太阳在【%s】宿,距度 %.2f°,去极度 %.2f°;黄道宫:%s;十二次:%s",
        d.sun_xiu, d.sun_xiu_in_deg, d.sun_xiu_quji, d.trop_sign, d.xing_ci)
end

function p.solarTermTable(frame)
    local year=tonumber(frame.args.year) or tonumber(os.date("%Y"))
    local out={'{| class="wikitable"\n! 序号 !! 节气 !! 黄经 !! 日期时间 (UTC+8)'}
    for _,lon in ipairs(TERM_LONS) do
        local anchor=jdFromDate(year,12,21,0)
        local offset=(lon-270)/15
        local guess=anchor+offset*15.218
        local tjd=refineTerm(guess,lon)
        local jdLocal=tjd+8/24
        local yy,mm,dd,hh,mi=dateFromJd(jdLocal)
        local ds=string.format("%04d-%02d-%02d %02d:%02d",yy,mm,dd,hh,mi)
        table.insert(out,string.format("\n|-\n| %d || %s || %.0f° || %s",(lon/15)+1,TERM_NAMES[lon],lon,ds))
    end
    table.insert(out,"\n|}")
    return table.concat(out)
end

function p.zodiac(frame)
    local d=compute(frame.args)
    local out={'{| class="wikitable"\n! 体系 !! 名称 !! 入度 !! 备注'}
    table.insert(out,string.format("\n|-\n| 回归黄道宫 || %s || %.2f° || 春分点为0°",d.trop_sign,d.trop_in_deg))
    table.insert(out,string.format("\n|-\n| 恒星黄道宫 || %s || %.2f° || 岁差偏移 %.2f°",d.sidereal_sign,d.sidereal_in_deg,d.sidereal_ayanamsa))
    table.insert(out,string.format("\n|-\n| IAU真实星座 || %s || %.2f° || %s",d.iau_constellation,d.iau_in_deg,d.iau_note))
    table.insert(out,string.format("\n|-\n| 中国十二次 || %s(辰在%s) || %.2f° || 起于星纪",d.xing_ci,d.chen,d.xingci_in_deg))
    table.insert(out,string.format("\n|-\n| 二十八宿日躔 || %s || %.2f° || 体系:%s",d.sun_xiu,d.sun_xiu_in_deg,d.sun_xiu_sysname))
    table.insert(out,"\n|}")
    return table.concat(out)
end

-- 供 {{节气状态}} 模板一次性取完整文案,避免多次 #invoke 时间快照不一致
function p.termStatus(frame)
    local d = compute(frame.args)
    local prevHM = d.days_after_term_hm           -- 已经是 "N天 H时MM分"
    local toNext = math.floor(d.days_to_next_term + 0.5)  -- 四舍五入,与显示对齐
    return string.format("%s%s,距离%s还有%d天",
        d.cur_term, prevHM, d.next_term, toNext)
end

return p