refactor(theme): 主题判定移到服务端,前端加载时获取
- 后端新增 GET /api/theme(公开):启动后按部署地定位 + NOAA 计算当天 日出/日落 + 判定当前主题,按日期与坐标缓存;极昼/极夜返回 theme=null - 删除 /api/location 路由,前端不再拉坐标 - 前端 useTheme:页面加载时拉 /api/theme 应用服务端主题与时刻表, 离线回退本地 NOAA(含 RA 象限修正);手动覆盖/自动开关逻辑不变
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 主题 API —— GET /api/theme(公开,无需登录:登录页加载时也要获取主题)
|
||||
* 返回服务端判定的主题与当天日出/日落时刻,前端据此应用主题并安排自动切换
|
||||
*/
|
||||
import express from 'express'
|
||||
import { getThemeInfo } from '../services/theme.js'
|
||||
|
||||
export const themeRouter = express.Router()
|
||||
|
||||
themeRouter.get('/theme', async (req, res) => {
|
||||
try {
|
||||
res.json(await getThemeInfo())
|
||||
} catch {
|
||||
res.json({ theme: null, sunrise: null, sunset: null, lat: 31.23, lng: 121.47, src: 'fallback' })
|
||||
}
|
||||
})
|
||||
+3
-3
@@ -32,7 +32,7 @@ import { syncRouter } from './routes/sync.js'
|
||||
import { toplistRouter } from './routes/toplist.js'
|
||||
import { cleanRouter } from './routes/clean.js'
|
||||
import { logsRouter } from './routes/logs.js'
|
||||
import { locationRouter } from './routes/location.js'
|
||||
import { themeRouter } from './routes/theme.js'
|
||||
import { apiNotFound, errorHandler } from './http.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
@@ -41,7 +41,8 @@ app.use(express.json({ limit: '1mb' }))
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000)
|
||||
|
||||
// —— 路由装配:登录/登出公开,其余 /api/* 需鉴权 ——
|
||||
// —— 路由装配:登录/登出/主题公开,其余 /api/* 需鉴权 ——
|
||||
app.use('/api', themeRouter) // 公开:登录页加载时也需要主题
|
||||
app.use('/api', authRouter)
|
||||
app.use('/api', authMiddleware)
|
||||
app.use('/api', thresholdsRouter)
|
||||
@@ -51,7 +52,6 @@ app.use('/api', syncRouter)
|
||||
app.use('/api', toplistRouter)
|
||||
app.use('/api', cleanRouter)
|
||||
app.use('/api', logsRouter)
|
||||
app.use('/api', locationRouter)
|
||||
// —— /api 未匹配任何路由 → JSON 404(Express 默认对非 GET 返回 HTML) ——
|
||||
app.use('/api', apiNotFound)
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 主题信息 —— 服务端定位 + 计算日出/日落 + 判定当前主题
|
||||
* 前端页面加载时调用 GET /api/theme 获取,失败时才回退前端本地算法
|
||||
* 逻辑:部署地坐标(getDeployLocation)→ NOAA 算法算当天日出/日落 →
|
||||
* 当前时刻在日出前/日落后为暗色,日出至日落为亮色;极昼极夜返回 theme=null(不自动切)
|
||||
* 按日期 + 坐标内存缓存,避免每次请求都重复计算
|
||||
*/
|
||||
import { getDeployLocation } from './location.js'
|
||||
|
||||
const ZENITH = 90.833
|
||||
const toRad = d => (d * Math.PI) / 180
|
||||
const toDeg = r => (r * 180) / Math.PI
|
||||
const dayOfYear = d => Math.floor((d - new Date(d.getFullYear(), 0, 0)) / 86400000)
|
||||
|
||||
/** NOAA 日出日落:返回当天服务器本地时间的日出/日落 Date;极昼/极夜返回 null */
|
||||
function sunTimes(date, lat, lng) {
|
||||
const ymd = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const N = dayOfYear(date)
|
||||
const lngHour = lng / 15
|
||||
const calc = hour => {
|
||||
const t = N + (hour - lngHour) / 24
|
||||
const M = 0.9856 * t - 3.289
|
||||
const L = M + 1.916 * Math.sin(toRad(M)) + 0.02 * Math.sin(toRad(2 * M)) + 282.634
|
||||
// 右赤经:atan 主值范围 [-90,90],需把 RA 校正到与太阳黄经 λ 同象限(否则日出/日落会颠倒 ~12h)
|
||||
let RA = toDeg(Math.atan(0.91764 * Math.tan(toRad(L))))
|
||||
RA = ((RA % 360) + 360) % 360
|
||||
const lam = ((L % 360) + 360) % 360
|
||||
while (Math.floor(RA / 90) % 4 !== Math.floor(lam / 90) % 4) RA += 180
|
||||
RA = (RA % 360) / 15
|
||||
const sinDec = 0.39782 * Math.sin(toRad(L))
|
||||
const cosDec = Math.cos(Math.asin(sinDec))
|
||||
const cosH = (Math.cos(toRad(ZENITH)) - sinDec * Math.sin(toRad(lat))) / (cosDec * Math.cos(toRad(lat)))
|
||||
if (cosH > 1 || cosH < -1) return null
|
||||
const H = hour < 12 ? (360 - toDeg(Math.acos(cosH))) / 15 : toDeg(Math.acos(cosH)) / 15
|
||||
const ut = (H + RA - 0.0653 * t - 6.622 - lngHour + 24) % 24
|
||||
return new Date(Date.UTC(ymd.getFullYear(), ymd.getMonth(), ymd.getDate(), Math.floor(ut), Math.round((ut % 1) * 60)))
|
||||
}
|
||||
return { sunrise: calc(6), sunset: calc(18) }
|
||||
}
|
||||
|
||||
let cache = null // { dateKey, lat, lng, theme, sunrise, sunset }
|
||||
|
||||
export async function getThemeInfo() {
|
||||
const loc = await getDeployLocation()
|
||||
const now = new Date()
|
||||
const dateKey = now.toDateString() // 服务器本地日期(TZ=Asia/Shanghai)
|
||||
if (cache && cache.dateKey === dateKey && cache.lat === loc.lat && cache.lng === loc.lng) return cache
|
||||
const s = sunTimes(now, loc.lat, loc.lng)
|
||||
let theme = null
|
||||
if (s.sunrise && s.sunset) {
|
||||
theme = now < s.sunrise || now >= s.sunset ? 'dark' : 'bright'
|
||||
}
|
||||
cache = {
|
||||
dateKey, lat: loc.lat, lng: loc.lng, src: loc.src,
|
||||
theme,
|
||||
sunrise: s.sunrise ? s.sunrise.toISOString() : null,
|
||||
sunset: s.sunset ? s.sunset.toISOString() : null,
|
||||
}
|
||||
return cache
|
||||
}
|
||||
Reference in New Issue
Block a user