574 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<script src="/static/js/chart.js"></script>
<!-- OpenLayers CSS and JS -->
<link rel="stylesheet" href="/static/css/ol.css">
<script src="/static/js/ol.js"></script>
<link rel="stylesheet" href="/static/css/tailwind.min.css">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
/* 自定义:百分比左右内边距(避免 JIT 依赖)*/
.px-7p {
padding-left: 7%;
padding-right: 7%;
}
/* 自定义:内容区宽度控制(避免 JIT 任意值类)*/
.content-narrow {
width: 86%;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
@media (max-width: 768px) {
.content-narrow {
width: 92%;
}
}
.header {
padding: 10px;
text-align: center;
border-bottom: 1px solid #ddd;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 15px;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
}
.control-row {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
}
.control-group {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
.station-input-group {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
#stationInput {
width: 120px;
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
}
select, input, button {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
button {
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.map-container {
height: 60vh;
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 4px;
position: relative;
transition: height 0.3s ease;
overflow: hidden;
}
.map-container.collapsed {
height: 20vh;
}
#map {
width: 100%;
height: 100%;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 5px;
}
.map-control-btn {
padding: 3px 8px;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
color: #666;
}
.map-control-btn:hover {
background-color: white;
}
.map-control-btn.active {
background-color: #007bff;
color: white;
}
.map-toggle-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 1001;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
color: white;
background-color: #007bff;
}
.station-info-title {
text-align: center;
margin-bottom: 15px;
padding: 10px;
font-size: 14px;
line-height: 1.6;
}
.chart-container {
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
background-color: #fff;
display: none;
}
.chart-container.show {
display: block;
animation: slideDown 0.3s ease;
}
.chart-wrapper {
height: 500px;
margin-bottom: 30px;
}
.table-container {
overflow-x: auto;
margin-top: 20px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
display: none;
}
.table-container.show {
display: block;
animation: slideDown 0.3s ease;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 12px 8px;
text-align: center;
}
th {
background-color: #f8f9fa;
font-weight: bold;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
tr:hover {
background-color: #f5f5f5;
}
.system-info {
background-color: #e9ecef;
padding: 10px;
margin-bottom: 20px;
border-radius: 4px;
font-size: 14px;
}
/* 设备列表样式 */
.device-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
}
.device-modal-content {
position: relative;
background-color: #fff;
height: auto;
max-height: 70vh;
width: 90%;
max-width: 720px;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
}
.device-list-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
position: relative;
flex-shrink: 0;
}
.device-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
position: relative;
}
.device-list-footer {
padding: 10px;
border-top: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
}
.pagination {
display: flex;
align-items: center;
gap: 10px;
font-size: 12px;
color: #666;
}
.pagination-btn {
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 3px;
background: #fff;
cursor: pointer;
font-size: 12px;
color: #666;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.device-item {
padding: 12px 20px;
border-bottom: 1px solid #f5f5f5;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
}
.device-item:hover {
background-color: #fafafa;
}
.device-item:last-child {
border-bottom: none;
}
.close-modal {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #f5f5f5;
cursor: pointer;
color: #666;
font-size: 18px;
line-height: 1;
}
.close-modal:hover {
background-color: #eee;
color: #333;
}
.error {
color: #dc3545;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
}
.station-marker {
width: 24px;
height: 24px;
background-color: #007bff;
border: 2px solid white;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.station-marker.offline {
background-color: #6c757d;
}
.station-label {
background-color: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #007bff;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
}
.control-group {
width: 100%;
}
select, input {
width: 100%;
}
.map-container {
height: 50vh;
}
}
</style>
</head>
<body x-data="{ showPastForecast: false, deviceModalOpen: false }" class="text-[14px] md:text-[15px]" x-init="window.addEventListener('close-device-modal', () => { deviceModalOpen = false })">
<div class="header p-2 text-center border-b border-gray-200">
<h1 class="text-2xl md:text-3xl font-semibold p-7">{{.Title}}</h1>
</div>
<!-- 设备列表 -->
<div id="deviceModal" class="device-modal" x-show="deviceModalOpen" x-transition.opacity @click.self="deviceModalOpen=false">
<div class="device-modal-content bg-white shadow-xl" x-transition.scale.duration.150ms>
<div class="device-list-header flex items-center justify-between border-b">
<div class="text-sm">设备列表</div>
<span class="close-modal" @click="deviceModalOpen=false">×</span>
</div>
<div id="deviceList" class="device-list">
<!-- 设备列表将通过JavaScript动态填充 -->
</div>
<div class="device-list-footer">
<div class="pagination">
<button class="pagination-btn" id="prevPage" :disabled="window.WeatherApp.currentPage <= 1" @click="window.WeatherApp.updateDeviceList(window.WeatherApp.currentPage - 1)">&lt; 上一页</button>
<span><span id="currentPage">1</span> 页,共 <span id="totalPages">1</span></span>
<button class="pagination-btn" id="nextPage" :disabled="window.WeatherApp.currentPage >= Math.ceil(window.WeatherApp.filteredDevices.length / window.WeatherApp.itemsPerPage)" @click="window.WeatherApp.updateDeviceList(window.WeatherApp.currentPage + 1)">下一页 &gt;</button>
</div>
</div>
</div>
</div>
<div class="container content-narrow py-5">
<!-- 系统信息 -->
<div class="system-info bg-gray-100 p-3 mb-5 rounded text-sm">
<strong>在线设备: </strong> <span id="onlineDevices">{{.OnlineDevices}}</span> 个 |
<strong>总设备: </strong> <a href="#" id="showDeviceList" class="text-blue-600 hover:text-blue-700 underline-offset-2" @click.prevent="deviceModalOpen = true; window.WeatherApp.updateDeviceList(1)"><span id="wh65lpCount">0</span></a>
</div>
<!-- 控制面板 -->
<div class="controls flex flex-col gap-4 mb-5 p-4 border rounded bg-white">
<div class="control-row flex items-center gap-4 flex-wrap">
<div class="station-input-group flex items-center gap-1">
<label for="stationInput" class="text-sm text-gray-600">站点编号:</label>
<input type="text" id="stationInput" placeholder="" class="w-32 px-2 py-1 border border-gray-300 rounded text-sm font-mono">
</div>
<div class="control-group">
<label for="mapType" class="text-sm text-gray-600">地图类型:</label>
<select id="mapType" onchange="switchLayer(this.value)" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="satellite">卫星图</option>
<option value="vector">矢量图</option>
<option value="terrain">地形图</option>
<option value="hybrid">混合地形图</option>
</select>
</div>
</div>
<div class="control-row flex items-center gap-4 flex-wrap">
<div class="control-group flex items-center gap-1">
<label for="interval" class="text-sm text-gray-600">数据粒度:</label>
<select id="interval" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="10min">10分钟</option>
<option value="30min">30分钟</option>
<option value="1hour" selected>1小时</option>
<option value="raw">原始(16s)</option>
</select>
</div>
<div class="control-group">
<label for="forecastProvider" class="text-sm text-gray-600">预报源:</label>
<select id="forecastProvider" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="">不显示预报</option>
<option value="open-meteo">Open-Meteo</option>
<option value="caiyun" selected>彩云</option>
</select>
</div>
<div class="control-group">
<label for="startDate" class="text-sm text-gray-600">开始时间:</label>
<input type="datetime-local" id="startDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
</div>
<div class="control-group">
<label for="endDate" class="text-sm text-gray-600">结束时间:</label>
<input type="datetime-local" id="endDate" class="px-2 py-1 border border-gray-300 rounded text-sm">
</div>
<div class="control-group">
<button onclick="queryHistoryData()" id="queryBtn" class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white px-3 py-1 rounded">查看历史数据</button>
</div>
</div>
</div>
<!-- 地图容器 -->
<div class="map-container border border-gray-200 rounded" id="mapContainer">
<div id="map"></div>
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" id="toggleMapBtn" onclick="toggleMap()">折叠地图</button>
</div>
<!-- 图表容器 -->
<div class="chart-container" id="chartContainer">
<div id="stationInfoTitle" class="station-info-title"></div>
<div class="chart-wrapper">
<canvas id="combinedChart"></canvas>
</div>
</div>
<!-- 数据表格 -->
<div class="table-container" id="tableContainer">
<div id="forecastToggleContainer" style="padding: 8px 12px; font-size: 12px; color: #666; display: none; display: flex; justify-content: center; align-items: center;">
<label style="display: flex; align-items: center; gap: 5px;">
<input type="checkbox" id="showPastForecast" style="vertical-align: middle;" x-model="showPastForecast" @change="window.WeatherTable.display(window.WeatherApp.cachedHistoryData, window.WeatherApp.cachedForecastData)">
显示历史预报
</label>
</div>
<table class="min-w-full text-sm text-center">
<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>
<script>
window.TIANDITU_KEY = '{{.TiandituKey}}';
</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>
</body>
</html>