419 lines
17 KiB
HTML
419 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>北斗气象站</title>
|
||
<script src="/static/js/chart.js"></script>
|
||
<link rel="stylesheet" href="/static/css/ol.css">
|
||
<script src="/static/js/ol.js"></script>
|
||
<style>
|
||
:root {
|
||
--color-bg: #0b1e39;
|
||
--color-fg: #e6eefc;
|
||
--color-fg-muted: #dbe7ff;
|
||
--panel-bg: rgba(255,255,255,0.04);
|
||
--panel-border: rgba(255,255,255,0.12);
|
||
--table-head-bg: rgba(20,45,80,0.98);
|
||
--table-head-fg: #e6f0ff;
|
||
--table-row-alt: rgba(255,255,255,0.03);
|
||
--orange-bg-1: rgba(255, 171, 64, 0.18);
|
||
--orange-bg-2: rgba(255, 171, 64, 0.10);
|
||
--orange-border: rgba(230, 120, 20, 0.6);
|
||
--orange-shadow-1: rgba(255, 159, 64, 0.35);
|
||
--orange-shadow-2: rgba(255, 159, 64, 0.10);
|
||
--orange-shadow-3: rgba(255, 159, 64, 0.25);
|
||
--orange-fg: #2b1900;
|
||
--orange-fg-sub: #4a2b00;
|
||
/* 间距与圆角(相对单位) */
|
||
--space-1: 0.25rem; /* 4px */
|
||
--space-2: 0.5rem; /* 8px */
|
||
--space-3: 0.75rem; /* 12px */
|
||
--space-4: 1rem; /* 16px */
|
||
--radius-1: 0.25rem; /* 4px */
|
||
--radius-2: 0.375rem; /* 6px */
|
||
}
|
||
html, body { height: 100%; }
|
||
html { font-size: clamp(14px, 0.95vw, 18px); }
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
background: var(--color-bg);
|
||
color: var(--color-fg);
|
||
font-family: Arial, sans-serif;
|
||
font-size: 1rem;
|
||
}
|
||
body.alert-mode { position: relative; }
|
||
body.alert-mode::before {
|
||
content: '';
|
||
position: fixed;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
background: radial-gradient(circle at 50% 50%,
|
||
rgba(255, 159, 64, 0.00) 45%,
|
||
rgba(255, 159, 64, 0.12) 70%,
|
||
rgba(255, 159, 64, 0.20) 100%);
|
||
opacity: 0.16;
|
||
animation: alert-vignette 3s ease-in-out infinite;
|
||
z-index: 1;
|
||
}
|
||
@keyframes alert-vignette {
|
||
0% { opacity: 0.12; }
|
||
50% { opacity: 0.25; }
|
||
100% { opacity: 0.12; }
|
||
}
|
||
|
||
.screen {
|
||
height: 100vh;
|
||
width: 100vw;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: var(--space-2);
|
||
box-sizing: border-box;
|
||
padding: var(--space-2);
|
||
}
|
||
|
||
.left, .right { min-width: 0; min-height: 0; }
|
||
|
||
.left {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
.chart-container {
|
||
flex: 1;
|
||
min-height: 0;
|
||
background: var(--panel-bg);
|
||
border: 1px solid var(--panel-border);
|
||
border-radius: var(--radius-1);
|
||
padding: var(--space-2);
|
||
padding-bottom: 2em; /* 额外底部留白,避免溢出 */
|
||
}
|
||
.chart-wrapper { height: 100%; }
|
||
#combinedChart { width: 100% !important; height: 100% !important; }
|
||
|
||
.station-info-title { font-size: 1rem; margin-bottom: var(--space-2); color: #d5e3ff; text-align: center; font-weight: 700; }
|
||
.accuracy-panel { font-size: 0.8125rem; color: #a9bfe6; text-align: right; margin-top: calc(-1 * var(--space-1)); }
|
||
.accuracy-panel .label { color: #90a7d4; margin-right: 4px; }
|
||
.accuracy-panel .value { color: #ffffff; font-weight: 700; }
|
||
|
||
.map-container {
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
background: var(--panel-bg);
|
||
border: 1px solid var(--panel-border);
|
||
border-radius: var(--radius-1);
|
||
overflow: hidden;
|
||
}
|
||
#map { width: 100%; height: 100%; }
|
||
|
||
.right { display: flex; flex-direction: column; gap: var(--space-2); }
|
||
|
||
.controls {
|
||
display: flex; align-items: center; flex-wrap: wrap;
|
||
gap: var(--space-2);
|
||
background: var(--panel-bg);
|
||
border: 1px solid var(--panel-border);
|
||
border-radius: var(--radius-1);
|
||
padding: calc(var(--space-1) + 0.125rem) var(--space-2);
|
||
color: var(--color-fg-muted);
|
||
}
|
||
.controls label { font-size: 0.8125rem; color: #bfd0ef; }
|
||
.controls input, .controls select, .controls button {
|
||
padding: var(--space-1) var(--space-2);
|
||
font-size: 0.8125rem;
|
||
border-radius: 0.1875rem;
|
||
border: 1px solid rgba(255,255,255,0.18);
|
||
background: rgba(0,0,0,0.25);
|
||
color: var(--color-fg);
|
||
}
|
||
.controls button { background: #1e5eff; border-color: #1e5eff; cursor: pointer; }
|
||
.controls button:hover { background: #184bd1; }
|
||
.controls select option { background: var(--color-bg); color: var(--color-fg); }
|
||
|
||
.table-container {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
background: rgba(0,0,0,0.25);
|
||
border: 1px solid var(--panel-border);
|
||
border-radius: var(--radius-1);
|
||
}
|
||
table { width: 100%; border-collapse: collapse; }
|
||
thead th {
|
||
position: sticky;
|
||
top: 0;
|
||
background: var(--table-head-bg);
|
||
color: var(--table-head-fg);
|
||
font-weight: 600;
|
||
padding: var(--space-3) var(--space-2);
|
||
border-bottom: 1px solid var(--panel-border);
|
||
font-size: 0.875rem;
|
||
}
|
||
tbody td {
|
||
padding: var(--space-3) var(--space-2);
|
||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||
color: #dfe9ff;
|
||
font-size: 0.8125rem;
|
||
text-align: center;
|
||
}
|
||
tbody tr:nth-child(even) { background: var(--table-row-alt); }
|
||
|
||
/* Summary 面板默认样式(深蓝主题内轻边框) */
|
||
.summary-panel {
|
||
background: var(--panel-bg);
|
||
border: 1px solid var(--panel-border);
|
||
border-radius: var(--radius-2);
|
||
margin: var(--space-2) 0;
|
||
padding: var(--space-3) var(--space-3);
|
||
color: #dfe9ff;
|
||
line-height: 1.6;
|
||
}
|
||
.summary-title { font-size: 0.875rem; color: #e6f0ff; font-weight: 600; }
|
||
.summary-sub { font-size: 0.8125rem; color: #bcd0ff; }
|
||
|
||
.summary-panel.alert-on {
|
||
background: linear-gradient(180deg, var(--orange-bg-1), var(--orange-bg-2));
|
||
border: 1px solid var(--orange-border);
|
||
box-shadow: 0 0.375rem 1.125rem var(--orange-shadow-1), 0 0 0 1px var(--orange-shadow-2) inset, 0 0 1.5rem var(--orange-shadow-3) inset;
|
||
color: var(--orange-fg);
|
||
}
|
||
.summary-panel.alert-on .summary-title {
|
||
color: #bd6718;
|
||
font-size: 1.7rem;
|
||
font-weight: 600;
|
||
padding: 0.1rem 0.35rem;
|
||
border-radius: var(--radius-1);
|
||
}
|
||
.summary-panel.alert-on .summary-sub {
|
||
color: #bcd0ff;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.summary-panel.alert-red {
|
||
background: linear-gradient(180deg, rgba(255, 107, 107, 0.22), rgba(255, 107, 107, 0.12));
|
||
border: 1px solid rgba(200, 40, 40, 0.6);
|
||
box-shadow: 0 0.375rem 1.125rem rgba(255, 107, 107, 0.35), 0 0 0 1px rgba(255,107,107,0.10) inset, 0 0 1.5rem rgba(255, 107, 107, 0.25) inset;
|
||
color: #3a0b0b;
|
||
}
|
||
.summary-panel.alert-red .summary-title {
|
||
color: #b81f1f;
|
||
font-size: 1.7rem;
|
||
font-weight: 600;
|
||
padding: 0.1rem 0.35rem;
|
||
border-radius: var(--radius-1);
|
||
}
|
||
.summary-panel.alert-red .summary-sub {
|
||
color: #bcd0ff;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.screen.alert-on {
|
||
box-shadow:
|
||
inset 0 0 20rem var(--orange-shadow-3),
|
||
inset 0 0 20rem var(--orange-shadow-1);
|
||
animation: screenGlowOrange 1.8s ease-in-out infinite;
|
||
}
|
||
@keyframes screenGlowOrange {
|
||
0%, 100% { box-shadow:
|
||
inset 0 0 20rem var(--orange-shadow-3),
|
||
inset 0 0 20rem var(--orange-shadow-1);
|
||
}
|
||
50% { box-shadow: none; }
|
||
}
|
||
.screen.alert-red {
|
||
box-shadow:
|
||
inset 0 0 20rem rgba(255, 107, 107, 0.25),
|
||
inset 0 0 20rem rgba(255, 107, 107, 0.35);
|
||
animation: screenGlowRed 1.8s ease-in-out infinite;
|
||
}
|
||
@keyframes screenGlowRed {
|
||
0%, 100% { box-shadow:
|
||
inset 0 0 20rem rgba(255, 107, 107, 0.25),
|
||
inset 0 0 20rem rgba(255, 107, 107, 0.35);
|
||
}
|
||
50% { box-shadow: none; }
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
* { animation: none !important; }
|
||
}
|
||
|
||
.summary-panel.alert-on { animation: panelGlowOrange 1.8s ease-in-out infinite; }
|
||
.summary-panel.alert-on .summary-title,
|
||
.summary-panel.alert-on .summary-sub { animation: textPulseOrange 1.8s ease-in-out infinite; }
|
||
@keyframes panelGlowOrange {
|
||
0%, 100% {
|
||
background: linear-gradient(180deg, var(--orange-bg-1), var(--orange-bg-2));
|
||
border-color: var(--orange-border);
|
||
box-shadow: 0 0.375rem 1.125rem var(--orange-shadow-1), 0 0 0 1px var(--orange-shadow-2) inset, 0 0 1.5rem var(--orange-shadow-3) inset;
|
||
}
|
||
50% {
|
||
background: var(--color-bg);
|
||
border-color: var(--panel-border);
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
@keyframes textPulseOrange {
|
||
0%, 100% { /* 使用各自定义色 */ }
|
||
50% { color: var(--color-fg); }
|
||
}
|
||
.summary-panel.alert-red { animation: panelGlowRed 1.8s ease-in-out infinite; }
|
||
.summary-panel.alert-red .summary-title,
|
||
.summary-panel.alert-red .summary-sub { animation: textPulseRed 1.8s ease-in-out infinite; }
|
||
@keyframes panelGlowRed {
|
||
0%, 100% {
|
||
background: linear-gradient(180deg, rgba(255, 107, 107, 0.22), rgba(255, 107, 107, 0.12));
|
||
border-color: rgba(200, 40, 40, 0.6);
|
||
box-shadow: 0 0.375rem 1.125rem rgba(255, 107, 107, 0.35), 0 0 0 1px rgba(255,107,107,0.10) inset, 0 0 1.5rem rgba(255, 107, 107, 0.25) inset;
|
||
}
|
||
50% {
|
||
background: var(--color-bg);
|
||
border-color: var(--panel-border);
|
||
box-shadow: none;
|
||
}
|
||
}
|
||
@keyframes textPulseRed {
|
||
0%, 100% { /* 使用各自定义色 */ }
|
||
50% { color: var(--color-fg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="screen">
|
||
<div class="left">
|
||
<div class="chart-container" id="chartContainer">
|
||
<div id="stationInfoTitle" class="station-info-title"></div>
|
||
<div id="accuracyPanel" class="accuracy-panel">
|
||
<span class="item"><span class="label">+1h</span><span id="accH1" class="value">--</span></span>
|
||
<span class="item"><span class="label">+2h</span><span id="accH2" class="value">--</span></span>
|
||
<span class="item"><span class="label">+3h</span><span id="accH3" class="value">--</span></span>
|
||
</div>
|
||
<div class="chart-wrapper">
|
||
<canvas id="combinedChart"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="map-container" id="mapContainer">
|
||
<div id="map"></div>
|
||
<div id="tileValueTooltip" style="position:absolute;pointer-events:none;z-index:1003;display:none;background:rgba(0,0,0,0.65);color:#fff;font-size:12px;padding:4px 6px;border-radius:4px;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="right">
|
||
<div class="controls">
|
||
<label for="stationInput">站点:</label>
|
||
<input type="text" id="stationInput" placeholder="十进制或HEX" style="width:7.5rem;">
|
||
<label for="interval">粒度:</label>
|
||
<select id="interval">
|
||
<option value="raw">原始(16s)</option>
|
||
<option value="10min">10分钟</option>
|
||
<option value="30min">30分钟</option>
|
||
<option value="1hour" selected>1小时</option>
|
||
</select>
|
||
<label for="forecastProvider">预报源:</label>
|
||
<select id="forecastProvider">
|
||
<option value="">不显示预报</option>
|
||
<option value="imdroid_mix" selected>V4</option>
|
||
<option value="open-meteo">V3</option>
|
||
<option value="caiyun">V2</option>
|
||
<option value="imdroid">V1</option>
|
||
</select>
|
||
<label for="startDate">开始:</label>
|
||
<input type="datetime-local" id="startDate">
|
||
<label for="endDate">结束:</label>
|
||
<input type="datetime-local" id="endDate">
|
||
<button onclick="queryHistoryData()">查询</button>
|
||
|
||
<label for="tileProduct">叠加:</label>
|
||
<select id="tileProduct">
|
||
<option value="none">不显示</option>
|
||
<option value="rain">1h 实际降雨</option>
|
||
<option value="radar" selected>水汽含量</option>
|
||
</select>
|
||
<button id="btnTilePrev">上一时次</button>
|
||
<span id="tileCountInfo">共0条,第0条</span>
|
||
<button id="btnTileNext">下一时次</button>
|
||
<label for="tileTimeSelect">时间:</label>
|
||
<select id="tileTimeSelect" style="min-width:12rem">
|
||
<option value="">请选择时间</option>
|
||
</select>
|
||
</div>
|
||
<div id="summaryPanel" class="summary-panel">
|
||
<div id="futureRainSummary" class="summary-title">未来1~3小时降雨 -- mm</div>
|
||
<div id="pastAccuracySummary" class="summary-sub">过去预报准确率 +1h: -- +2h: -- +3h: --</div>
|
||
</div>
|
||
<div class="table-container" id="tableContainer">
|
||
<table>
|
||
<thead>
|
||
<tr id="tableHeader">
|
||
<th>时间</th>
|
||
<th>温度 (°C)</th>
|
||
<th>湿度 (%)</th>
|
||
<th>气压 (hPa)</th>
|
||
<th>风速 (m/s)</th>
|
||
<th>风向 (°)</th>
|
||
<th>雨量 (mm)</th>
|
||
<th>光照 (lux)</th>
|
||
<th>紫外线</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
window.TIANDITU_KEY = '{{.TiandituKey}}';
|
||
</script>
|
||
<script>
|
||
if (window.Chart && Chart.defaults) {
|
||
Chart.defaults.color = '#d5e3ff';
|
||
Chart.defaults.borderColor = 'rgba(213,227,255,0.25)';
|
||
if (Chart.defaults.scale && Chart.defaults.scale.grid) {
|
||
Chart.defaults.scale.grid.color = 'rgba(213,227,255,0.12)';
|
||
Chart.defaults.scale.ticks = Chart.defaults.scale.ticks || {};
|
||
Chart.defaults.scale.ticks.color = '#cbd9ff';
|
||
}
|
||
}
|
||
</script>
|
||
<script defer src="/static/js/alpinejs.min.js"></script>
|
||
<script src="/static/js/utils.js"></script>
|
||
<script src="/static/js/weather-app.js"></script>
|
||
<script src="/static/js/weather-chart.js"></script>
|
||
<script src="/static/js/weather-table.js"></script>
|
||
<script src="/static/js/app.js"></script>
|
||
<script>
|
||
window.IS_BIGSCREEN = true;
|
||
// 大屏默认展示指定站点 RS485-0029C3(十进制 10691),并自动发起查询
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const input = document.getElementById('stationInput');
|
||
const titleEl = document.getElementById('stationInfoTitle');
|
||
if (input) {
|
||
input.value = '10691';
|
||
// 先设置默认标题
|
||
if (titleEl) {
|
||
titleEl.textContent = '第七台气象站(宾阳县细塘村东南约112米)';
|
||
}
|
||
const maxTries = 50; // ~10s 内重试
|
||
let tries = 0;
|
||
const timer = setInterval(() => {
|
||
tries += 1;
|
||
const stations = (window.WeatherMap && Array.isArray(window.WeatherMap.stations)) ? window.WeatherMap.stations : [];
|
||
const s = stations.find(x => x && String(x.decimal_id) === '10691');
|
||
if (s && titleEl) {
|
||
if (s.location) titleEl.textContent = s.location;
|
||
clearInterval(timer);
|
||
}
|
||
if (tries >= maxTries) clearInterval(timer);
|
||
}, 200);
|
||
setTimeout(() => {
|
||
window.dispatchEvent(new CustomEvent('query-history-data'));
|
||
}, 600);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|