fix: 优化前端体验
This commit is contained in:
parent
34b7bf3ff2
commit
4a73a41a6c
@ -21,7 +21,7 @@
|
||||
<!-- 第一行:站点编号 / 地图类型 / 预报源 / 图例 -->
|
||||
<div class="flex flex-wrap items-center gap-3 mb-3">
|
||||
<label class="text-sm text-gray-600">站点编号</label>
|
||||
<input class="px-2 py-1 border rounded w-32 font-mono text-sm" [(ngModel)]="decimalId" placeholder="十进制ID" />
|
||||
<input class="px-2 py-1 border rounded w-32 font-mono text-sm" [(ngModel)]="decimalId" placeholder="十六进制(如 29CA)" />
|
||||
|
||||
<label class="text-sm text-gray-600">地图类型</label>
|
||||
<select class="px-2 py-1 border rounded text-sm" [(ngModel)]="mapType" (change)="switchLayer(mapType)">
|
||||
|
||||
@ -30,8 +30,8 @@ export class ApiService {
|
||||
try { const r = await fetch('/api/stations'); return await r.json(); } catch { return []; }
|
||||
}
|
||||
|
||||
async getHistory(decimalId: string, from: string, to: string, interval: string): Promise<WeatherPoint[]> {
|
||||
const params = new URLSearchParams({ decimal_id: decimalId, start_time: from, end_time: to, interval });
|
||||
async getHistory(hexId: string, from: string, to: string, interval: string): Promise<WeatherPoint[]> {
|
||||
const params = new URLSearchParams({ hex_id: hexId, start_time: from, end_time: to, interval });
|
||||
const r = await fetch(`/api/data?${params.toString()}`);
|
||||
if (!r.ok) return [];
|
||||
return await r.json();
|
||||
@ -44,4 +44,3 @@ export class ApiService {
|
||||
return await r.json();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,11 +7,10 @@
|
||||
<div class="device-list" style="flex:1;overflow:auto;">
|
||||
<button class="device-item" *ngFor="let s of pageItems" (click)="choose(s)" style="padding:10px 16px;border-bottom:1px solid #f5f5f5;display:flex;justify-content:space-between;align-items:center;width:100%;text-align:left;">
|
||||
<div>
|
||||
<div class="text-sm font-mono">{{s.station_id}}</div>
|
||||
<div class="text-xs text-gray-500">十进制: {{decId(s)}}</div>
|
||||
<div class="text-xs text-gray-500" *ngIf="s.location">位置: {{s.location}}</div>
|
||||
<div class="text-sm" *ngIf="s.location">{{s.location}}</div>
|
||||
<div class="text-xs text-gray-500">设备编号:{{s.station_id}}</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600">{{s.device_type}}</div>
|
||||
<div class="text-xs" [class.text-green-600]="isOnline(s)" [class.text-gray-500]="!isOnline(s)">{{ isOnline(s) ? '在线' : '离线' }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="device-list-footer" style="padding:10px;border-top:1px solid #eee;display:flex;justify-content:space-between;align-items:center;">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
type Station = { station_id: string; device_type?: string; name?: string; station_alias?: string; location?: string };
|
||||
type Station = { station_id: string; device_type?: string; name?: string; station_alias?: string; location?: string; last_update?: string };
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
@ -12,7 +12,7 @@ type Station = { station_id: string; device_type?: string; name?: string; statio
|
||||
export class HeaderComponent {
|
||||
@Input() onlineDevices = 0;
|
||||
@Input() stations: Station[] = [];
|
||||
@Output() selectStation = new EventEmitter<{ stationId: string, decimalId: string }>();
|
||||
@Output() selectStation = new EventEmitter<{ stationId: string, hex: string }>();
|
||||
|
||||
modalOpen = false;
|
||||
currentPage = 1;
|
||||
@ -30,17 +30,23 @@ export class HeaderComponent {
|
||||
prev() { if (this.currentPage > 1) this.currentPage--; }
|
||||
next() { if (this.currentPage < this.totalPages) this.currentPage++; }
|
||||
|
||||
decId(s: Station): string {
|
||||
hexSuffix(s: Station): string {
|
||||
const id = s.station_id || '';
|
||||
if (id.length <= 6) return '';
|
||||
const hex = id.slice(-6);
|
||||
const n = parseInt(hex, 16);
|
||||
return isNaN(n) ? '' : String(n);
|
||||
if (id.length <= 6) return id.toUpperCase();
|
||||
return id.slice(-6).toUpperCase();
|
||||
}
|
||||
|
||||
isOnline(s: Station): boolean {
|
||||
const lu = (s as any).last_update as string | undefined;
|
||||
if (!lu) return false;
|
||||
const t = new Date(lu.replace(' ', 'T')).getTime();
|
||||
if (isNaN(t)) return false;
|
||||
return Date.now() - t <= 5 * 60 * 1000;
|
||||
}
|
||||
|
||||
choose(s: Station) {
|
||||
const dec = this.decId(s);
|
||||
if (dec) this.selectStation.emit({ stationId: s.station_id, decimalId: dec });
|
||||
const hex = this.hexSuffix(s);
|
||||
if (hex) this.selectStation.emit({ stationId: s.station_id, hex });
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,9 +190,15 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
image: new ol.style.Circle({ radius, fill: new ol.style.Fill({ color: 'rgba(0,123,255,0.8)' }), stroke: new ol.style.Stroke({ color: '#fff', width: 2 }) }),
|
||||
text: new ol.style.Text({ text: String(size), fill: new ol.style.Fill({ color: '#fff' }), font: `bold ${fontSize}px Arial`, offsetY: 1 })
|
||||
});
|
||||
} else {
|
||||
const f0 = features[0];
|
||||
const last = f0?.get('lastUpdate');
|
||||
const online = last ? (new Date(last).getTime() > Date.now() - 5*60*1000) : false;
|
||||
const color = online ? 'rgba(0,123,255,0.8)' : 'rgba(108,117,125,0.8)';
|
||||
return new ol.style.Style({
|
||||
image: new ol.style.Circle({ radius: 6, fill: new ol.style.Fill({ color }), stroke: new ol.style.Stroke({ color: '#fff', width: 2 }) })
|
||||
});
|
||||
}
|
||||
const f0 = features[0];
|
||||
return this.createStationStyle(f0);
|
||||
}
|
||||
return this.createStationStyle(features[0]);
|
||||
}
|
||||
@ -214,17 +220,21 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
let f = features[0];
|
||||
const subs = f.get('features');
|
||||
if (subs && subs.length>0) {
|
||||
// zoom in on cluster instead of selecting
|
||||
const view = this.map.getView();
|
||||
const z = view.getZoom();
|
||||
view.animate({ zoom: z + 1, center: evt.coordinate, duration: 200 });
|
||||
const ex = olAny.extent.createEmpty();
|
||||
for (const sf of subs) {
|
||||
olAny.extent.extend(ex, sf.getGeometry().getExtent());
|
||||
}
|
||||
view.fit(ex, { duration: 300, maxZoom: 14, padding: [40,40,40,40] });
|
||||
return;
|
||||
}
|
||||
const sid = f.get('stationId');
|
||||
const loc = f.get('location') || '';
|
||||
if (!sid) return;
|
||||
const hex = String(sid).slice(-6); const dec = parseInt(hex, 16);
|
||||
if (!isNaN(dec)) { this.decimalId = String(dec); await this.query(); this.scrollToChart(); }
|
||||
const hex = String(sid).slice(-6).toUpperCase();
|
||||
this.decimalId = hex;
|
||||
await this.query();
|
||||
this.scrollToChart();
|
||||
});
|
||||
this.map.on('pointermove', (evt:any) => {
|
||||
const features = this.map.getFeaturesAtPixel(evt.pixel, { layerFilter: (l:any)=> l===this.stationLayer || l===this.clusterLayer });
|
||||
@ -319,17 +329,17 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
await this.loadTileTimes(this.tileProduct);
|
||||
}
|
||||
|
||||
private makeStationId(dec: string): string | null {
|
||||
const n = Number(dec);
|
||||
if (!isFinite(n)) return null;
|
||||
const hex = Math.trunc(n).toString(16).toUpperCase().padStart(6,'0');
|
||||
private makeStationIdFromHex(hexRaw: string): string | null {
|
||||
if (!hexRaw) return null;
|
||||
const hex = String(hexRaw).toUpperCase().replace(/[^0-9A-F]/g, '').padStart(6, '0').slice(-6);
|
||||
if (!hex) return null;
|
||||
return `RS485-${hex}`;
|
||||
}
|
||||
|
||||
async query() {
|
||||
const dec = this.decimalId.trim();
|
||||
if (!dec) return;
|
||||
const sid = this.makeStationId(dec);
|
||||
const sid = this.makeStationIdFromHex(dec);
|
||||
if (!sid) return;
|
||||
const toFmt = (s: string) => s.replace('T',' ') + ':00';
|
||||
const from = toFmt(this.start);
|
||||
@ -357,8 +367,8 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||
this.scrollToChart();
|
||||
}
|
||||
|
||||
onSelectStation(ev: { stationId: string, decimalId: string }) {
|
||||
this.decimalId = ev.decimalId;
|
||||
onSelectStation(ev: { stationId: string, hex: string }) {
|
||||
this.decimalId = ev.hex;
|
||||
this.query();
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"weatherstation/core/internal/data"
|
||||
)
|
||||
@ -40,18 +41,34 @@ func handleStations(c *gin.Context) {
|
||||
}
|
||||
|
||||
func handleData(c *gin.Context) {
|
||||
dec := c.Query("decimal_id")
|
||||
idParam := c.Query("hex_id")
|
||||
startTime := c.Query("start_time")
|
||||
endTime := c.Query("end_time")
|
||||
interval := c.DefaultQuery("interval", "1hour")
|
||||
|
||||
decimalNum, err := strconv.ParseInt(dec, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid decimal_id"})
|
||||
if idParam == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing hex_id"})
|
||||
return
|
||||
}
|
||||
hexID := fmt.Sprintf("%06X", decimalNum)
|
||||
stationID := fmt.Sprintf("RS485-%s", hexID)
|
||||
upper := strings.ToUpper(idParam)
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(upper); i++ {
|
||||
ch := upper[i]
|
||||
if (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') {
|
||||
b.WriteByte(ch)
|
||||
}
|
||||
}
|
||||
hex := b.String()
|
||||
if hex == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hex_id"})
|
||||
return
|
||||
}
|
||||
if len(hex) < 6 {
|
||||
hex = strings.Repeat("0", 6-len(hex)) + hex
|
||||
} else if len(hex) > 6 {
|
||||
hex = hex[len(hex)-6:]
|
||||
}
|
||||
stationID := fmt.Sprintf("RS485-%s", hex)
|
||||
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||
if loc == nil {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user