feat: 大屏幕的页面

This commit is contained in:
yarnom 2025-11-03 15:45:49 +08:00
parent abb45a13ec
commit ca914355ba
6 changed files with 316 additions and 61 deletions

View File

@ -8,7 +8,14 @@ import (
func main() {
server.SetupLogger()
// 同时在配置端口与 8888 端口各启动一个 Gin 服务
go func() {
if err := server.StartGinServer(); err != nil {
log.Fatalf("service-api failed: %v", err)
log.Fatalf("service-api (config port) failed: %v", err)
}
}()
// Bigscreen 专用:在 10008 端口将根路径指向大屏页面
if err := server.StartBigscreenServerOn(10008); err != nil {
log.Fatalf("service-api (10008 bigscreen) failed: %v", err)
}
}

View File

@ -2,10 +2,10 @@ server:
udp_port: 10006
database:
host: "8.134.185.53"
port: 5432
host: "127.0.0.1"
port: 15432
user: "yarnom"
password: "root"
password: "fKvVq6SHjLD2bl"
dbname: "weatherdb"
sslmode: "disable"
@ -36,9 +36,9 @@ radar:
x: 104
mysql:
host: "127.0.0.1"
host: "8.134.185.53"
port: 3306
user: "remote"
password: "your_password"
password: "root"
dbname: "rtk_data"
params: "parseTime=true&loc=Asia%2FShanghai"

View File

@ -15,8 +15,8 @@ import (
"github.com/gin-gonic/gin"
)
// StartGinServer 启动Gin Web服务器
func StartGinServer() error {
// newGinEngine 统一创建和配置 Gin 引擎(模板/静态/路由)
func newGinEngine() *gin.Engine {
// 设置Gin模式
gin.SetMode(gin.ReleaseMode)
@ -109,6 +109,7 @@ func StartGinServer() error {
// 路由设置
r.GET("/", indexHandler)
r.GET("/bigscreen", bigscreenHandler)
r.GET("/radar/nanning", radarNanningHandler)
r.GET("/radar/guangzhou", radarGuangzhouHandler)
r.GET("/radar/panyu", radarPanyuHandler)
@ -140,18 +141,150 @@ func StartGinServer() error {
api.GET("/rain/times", rainTileTimesHandler)
api.GET("/rain/tiles_at", rainTilesAtHandler)
}
return r
}
// StartGinServer 启动Gin Web服务器配置端口
func StartGinServer() error {
r := newGinEngine()
// 获取配置的Web端口
port := config.GetConfig().Server.WebPort
if port == 0 {
port = 10003 // 默认端口
}
// 启动服务器
fmt.Printf("Gin Web服务器启动监听端口 %d...\n", port)
return r.Run(fmt.Sprintf(":%d", port))
}
// StartGinServerOn 在指定端口启动一个独立的 Gin 实例
func StartGinServerOn(port int) error {
r := newGinEngine()
if port <= 0 {
port = 10008
}
fmt.Printf("Gin Web服务器启动监听端口 %d...\n", port)
return r.Run(fmt.Sprintf(":%d", port))
}
// StartBigscreenServerOn 在指定端口启动以大屏为根路径的 Gin 实例(/ -> bigscreen
func StartBigscreenServerOn(port int) error {
// 独立构建一个以大屏为根路径的引擎,避免与普通引擎重复注册根路由
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
exe, _ := os.Executable()
exeDir := filepath.Dir(exe)
// 模板
candidatesTpl := []string{
filepath.Join(exeDir, "templates", "*"),
filepath.Join(exeDir, "..", "templates", "*"),
filepath.Join("templates", "*"),
filepath.Join("..", "templates", "*"),
}
var tplGlob string
for _, c := range candidatesTpl {
base := filepath.Dir(c)
if st, err := os.Stat(base); err == nil && st.IsDir() {
tplGlob = c
break
}
}
if tplGlob == "" {
tplGlob = filepath.Join("templates", "*")
}
r.LoadHTMLGlob(tplGlob)
// 静态资源
candidatesStatic := []string{
filepath.Join(exeDir, "static"),
filepath.Join(exeDir, "..", "static"),
"./static",
"../static",
}
staticDir := candidatesStatic[0]
for _, c := range candidatesStatic {
if st, err := os.Stat(c); err == nil && st.IsDir() {
staticDir = c
break
}
}
r.Static("/static", staticDir)
// SPA 资源
r.GET("/ui/*filepath", func(c *gin.Context) {
requested := c.Param("filepath")
if requested == "" || requested == "/" {
candidates := []string{
filepath.Join(exeDir, "core/frontend/dist/ui/index.html"),
filepath.Join(exeDir, "..", "core/frontend/dist/ui/index.html"),
"./core/frontend/dist/ui/index.html",
"../core/frontend/dist/ui/index.html",
}
for _, p := range candidates {
if _, err := os.Stat(p); err == nil {
c.File(p)
return
}
}
c.String(http.StatusNotFound, "ui not found")
return
}
baseDirCandidates := []string{
filepath.Join(exeDir, "core/frontend/dist/ui"),
filepath.Join(exeDir, "..", "core/frontend/dist/ui"),
"./core/frontend/dist/ui",
"../core/frontend/dist/ui",
}
baseDir := baseDirCandidates[0]
for _, d := range baseDirCandidates {
if st, err := os.Stat(d); err == nil && st.IsDir() {
baseDir = d
break
}
}
full := baseDir + requested
if _, err := os.Stat(full); err == nil {
c.File(full)
return
}
c.File(filepath.Join(baseDir, "index.html"))
})
// 根路径 -> 大屏
r.GET("/", bigscreenHandler)
// API 路由
api := r.Group("/api")
{
api.GET("/system/status", systemStatusHandler)
api.GET("/stations", getStationsHandler)
api.GET("/data", getDataHandler)
api.GET("/forecast", getForecastHandler)
api.GET("/radar/latest", latestRadarTileHandler)
api.GET("/radar/at", radarTileAtHandler)
api.GET("/radar/nearest", nearestRadarTileHandler)
api.GET("/radar/times", radarTileTimesHandler)
api.GET("/radar/tiles_at", radarTilesAtHandler)
api.GET("/radar/weather_latest", latestRadarWeatherHandler)
api.GET("/radar/weather_at", radarWeatherAtHandler)
api.GET("/radar/weather_aliases", radarWeatherAliasesHandler)
api.GET("/radar/aliases", radarConfigAliasesHandler)
api.GET("/radar/weather_nearest", radarWeatherNearestHandler)
api.GET("/rain/latest", latestRainTileHandler)
api.GET("/rain/at", rainTileAtHandler)
api.GET("/rain/nearest", nearestRainTileHandler)
api.GET("/rain/times", rainTileTimesHandler)
api.GET("/rain/tiles_at", rainTilesAtHandler)
}
if port <= 0 {
port = 10008
}
fmt.Printf("Gin Bigscreen 服务器启动,监听端口 %d...\n", port)
return r.Run(fmt.Sprintf(":%d", port))
}
// indexHandler 处理主页请求
func indexHandler(c *gin.Context) {
data := types.PageData{
@ -163,6 +296,17 @@ func indexHandler(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", data)
}
// bigscreenHandler 大屏演示页
func bigscreenHandler(c *gin.Context) {
data := types.PageData{
Title: "英卓大屏",
ServerTime: time.Now().Format("2006-01-02 15:04:05"),
OnlineDevices: database.GetOnlineDevicesCount(database.GetDB()),
TiandituKey: "0c260b8a094a4e0bc507808812cefdac",
}
c.HTML(http.StatusOK, "bigscreen.html", data)
}
// radarNanningHandler 南宁雷达站占位页
func radarNanningHandler(c *gin.Context) {
data := types.PageData{

View File

@ -12,6 +12,29 @@ const WeatherApp = {
setInterval(() => this.updateOnlineDevices(), 30000);
this.bindUI();
window.addEventListener('query-history-data', () => this.queryHistoryData());
// 大屏每10分钟刷新一次雷达/雨量瓦片时次并更新显示
if (window.IS_BIGSCREEN) {
// 防抖锁,避免重复请求
this._autoRefreshing = false;
setInterval(async () => {
try {
// 刷新雷达/雨量瓦片时次
if (WeatherMap && typeof WeatherMap.reloadTileTimesAndShow === 'function') {
const prodSel = document.getElementById('tileProduct');
const prod = prodSel ? prodSel.value : WeatherMap.tileProduct;
if (prod && prod !== 'none') {
WeatherMap.reloadTileTimesAndShow();
}
}
// 刷新图表/表格(基于当前控件选择)
const sid = (document.getElementById('stationInput')?.value || '').trim();
if (!this._autoRefreshing && sid) {
this._autoRefreshing = true;
try { await this.queryHistoryData(); } finally { this._autoRefreshing = false; }
}
} catch (e) { console.warn('自动刷新失败', e); }
}, 10 * 60 * 1000);
}
},
bindUI() {
@ -144,10 +167,10 @@ const WeatherApp = {
return;
}
const startTime = document.getElementById('startDate').value;
const endTime = document.getElementById('endDate').value;
const interval = document.getElementById('interval').value;
const forecastProvider = document.getElementById('forecastProvider').value;
const startTime = document.getElementById('startDate')?.value || '';
const endTime = document.getElementById('endDate')?.value || '';
const interval = document.getElementById('interval')?.value || '1hour';
const forecastProvider = document.getElementById('forecastProvider')?.value || '';
if (!startTime || !endTime) {
alert('请选择开始和结束时间');
return;
@ -212,17 +235,9 @@ const WeatherApp = {
const station = (WeatherMap.stations || []).find(s => s.decimal_id == decimalId);
const stationInfoTitle = document.getElementById('stationInfoTitle');
if (stationInfoTitle) {
if (station) {
stationInfoTitle.innerHTML = `
<strong>
${station.location || '未知位置'} ·
编号 ${decimalId} ·
坐标 ${station.latitude ? station.latitude.toFixed(6) : '未知'}, ${station.longitude ? station.longitude.toFixed(6) : '未知'}
</strong>
`;
} else {
stationInfoTitle.innerHTML = `编号 ${decimalId}`;
}
// 仅展示站点位置location无有效值时保持现有文本不变
const loc = station?.location || '';
if (loc) stationInfoTitle.textContent = loc;
}
if (!WeatherMap.isMapCollapsed) WeatherMap.toggleMap();
@ -242,6 +257,9 @@ const WeatherApp = {
if (legendMode) {
WeatherChart.applyLegendMode(legendMode.value);
}
// 更新大屏摘要区域(未来降雨与过去准确率)
this.updateSummaryPanel(historyData, forecastData);
} catch (error) {
console.error('查询数据失败:', error);
alert('查询数据失败: ' + error.message);
@ -254,3 +272,56 @@ window.WeatherApp = WeatherApp;
document.addEventListener('DOMContentLoaded', () => {
WeatherApp.init();
});
// 扩展:用于大屏摘要展示
WeatherApp.updateSummaryPanel = function(historyData, forecastData){
try{
const elFuture = document.getElementById('futureRainSummary');
const elAcc = document.getElementById('pastAccuracySummary');
if (!elFuture && !elAcc) return;
// --- 未来1~3小时降雨 ---
const fmt = (n)=> (n==null||isNaN(Number(n))) ? '--' : Number(n).toFixed(1);
const pad2 = (n)=> String(n).padStart(2,'0');
const fmtDT = (d)=> `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:00:00`;
const now = new Date();
const ceilHour = (d)=>{ const t = new Date(d); if (t.getMinutes()||t.getSeconds()||t.getMilliseconds()) t.setHours(t.getHours()+1); t.setMinutes(0,0,0); return t; };
const t1 = ceilHour(now), t2 = new Date(t1.getTime()+3600*1000), t3 = new Date(t1.getTime()+2*3600*1000);
const pickBestAt = (dtStr)=>{
const cand = (forecastData||[]).filter(x=>x.date_time===dtStr && x.rainfall!=null);
if (cand.length===0) return null;
// 选择最小 lead_hours 的记录
cand.sort((a,b)=> (a.lead_hours??99)-(b.lead_hours??99));
return cand[0].rainfall;
};
const r1 = pickBestAt(fmtDT(t1));
const r2 = pickBestAt(fmtDT(t2));
const r3 = pickBestAt(fmtDT(t3));
const futureSum = [r1,r2,r3].reduce((s,v)=> s + (v!=null?Number(v):0), 0);
if (elFuture) elFuture.textContent = `未来1~3小时降雨 ${fmt(futureSum)} 毫米`;
// --- 过去预报准确率(按小时分档 [0,5), [5,10), [10,∞)---
const bucketOf = (mm)=>{
if (mm==null || isNaN(Number(mm))) return null;
const v = Math.max(0, Number(mm));
if (v < 5) return 0; if (v < 10) return 1; return 2;
};
const rainActual = new Map();
(historyData||[]).forEach(it=>{ if (it && it.date_time) rainActual.set(it.date_time, it.rainfall); });
const tally = (lead)=>{
let correct=0,total=0;
(forecastData||[]).forEach(f=>{
if (f.lead_hours!==lead) return;
const a = rainActual.get(f.date_time);
if (a==null) return;
const ba=bucketOf(a), bf=bucketOf(f.rainfall);
if (ba==null || bf==null) return;
total += 1; if (ba===bf) correct += 1;
});
return {correct,total};
};
const rH1 = tally(1), rH2 = tally(2), rH3 = tally(3);
const pct = (r)=> r.total>0 ? `${(r.correct/r.total*100).toFixed(1)}%` : '--';
if (elAcc) elAcc.textContent = `过去预报准确率 +1h: ${pct(rH1)} +2h: ${pct(rH2)} +3h: ${pct(rH3)}`;
}catch(e){ console.warn('更新摘要失败', e); }
}

View File

@ -704,11 +704,11 @@ const WeatherMap = {
const toggleBtn = document.getElementById('toggleMapBtn');
if (this.isMapCollapsed) {
mapContainer.classList.add('collapsed');
toggleBtn.textContent = '展开地图';
mapContainer?.classList.add('collapsed');
if (toggleBtn) toggleBtn.textContent = '展开地图';
} else {
mapContainer.classList.remove('collapsed');
toggleBtn.textContent = '折叠地图';
mapContainer?.classList.remove('collapsed');
if (toggleBtn) toggleBtn.textContent = '折叠地图';
}
setTimeout(() => {
@ -720,15 +720,23 @@ const WeatherMap = {
async loadStations() {
try {
const response = await fetch('/api/stations');
this.stations = await response.json();
if (!response.ok) {
console.warn('获取站点列表失败HTTP', response.status);
this.stations = [];
} else {
const data = await response.json();
this.stations = Array.isArray(data) ? data : [];
}
// 更新WH65LP设备数量
const wh65lpDevices = this.stations.filter(station => station.device_type === 'WH65LP');
document.getElementById('wh65lpCount').textContent = wh65lpDevices.length;
const wh65lpDevices = (this.stations || []).filter(station => station.device_type === 'WH65LP');
const cntEl = document.getElementById('wh65lpCount');
if (cntEl) cntEl.textContent = wh65lpDevices.length;
this.displayStationsOnMap();
} catch (error) {
console.error('加载站点失败:', error);
this.stations = [];
}
},

View File

@ -281,6 +281,8 @@ const WeatherChart = {
}
const ctx = document.getElementById('combinedChart').getContext('2d');
const totalLabels = allLabels.length;
const tickStep = Math.max(1, Math.ceil(totalLabels / 10)); // 约10个刻度
const chartConfig = {
type: 'line',
data: {
@ -303,6 +305,29 @@ const WeatherChart = {
}
},
scales: {
x: {
type: 'category',
ticks: {
autoSkip: false,
maxRotation: 0,
minRotation: 0,
callback: function(value, index) {
// 仅显示每 tickStep 个刻度,格式 MM-DD HH
if (index % tickStep !== 0) return '';
const labels = this.chart?.data?.labels || [];
const raw = labels[index] || '';
// 原始格式: YYYY-MM-DD HH:MM:SS
if (typeof raw === 'string' && raw.length >= 13) {
return raw.substring(5, 13);
}
return raw;
}
},
grid: {
display: true,
drawOnChartArea: true
}
},
'y-temperature': {
type: 'linear',
display: true,