feat: 调整前端页面。加入 Angular 框架,以期减少前端耦合
This commit is contained in:
parent
28ce15ce13
commit
dc03e83562
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/log/*
|
||||
/rain_data/*
|
||||
export_data/*
|
||||
/.gopath/*
|
||||
/tech/*
|
||||
4
core/frontend/.gitignore
vendored
Normal file
4
core/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.angular/
|
||||
|
||||
64
core/frontend/angular.json
Normal file
64
core/frontend/angular.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/angular-cli",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"weatherstation-ui": {
|
||||
"projectType": "application",
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/ui",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"baseHref": "/ui/"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "weatherstation-ui:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "weatherstation-ui:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "weatherstation-ui:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12103
core/frontend/package-lock.json
generated
Normal file
12103
core/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
core/frontend/package.json
Normal file
30
core/frontend/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "weatherstation-ui",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "ng serve",
|
||||
"build": "ng build --configuration production --base-href /ui/",
|
||||
"dev": "ng serve",
|
||||
"test": "ng test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.3.0",
|
||||
"@angular/common": "^17.3.0",
|
||||
"@angular/compiler": "^17.3.0",
|
||||
"@angular/core": "^17.3.0",
|
||||
"@angular/forms": "^17.3.0",
|
||||
"@angular/platform-browser": "^17.3.0",
|
||||
"@angular/platform-browser-dynamic": "^17.3.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.6.2",
|
||||
"zone.js": "^0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.0",
|
||||
"@angular/cli": "^17.3.0",
|
||||
"@angular/compiler-cli": "^17.3.0",
|
||||
"typescript": "~5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
4
core/frontend/src/app.component.html
Normal file
4
core/frontend/src/app.component.html
Normal file
@ -0,0 +1,4 @@
|
||||
<div style="font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Noto Sans, 'Apple Color Emoji', 'Segoe UI Emoji'; padding: 24px;">
|
||||
<h1>Hello World</h1>
|
||||
</div>
|
||||
|
||||
12
core/frontend/src/index.html
Normal file
12
core/frontend/src/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>WeatherStation UI</title>
|
||||
<base href="/ui/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
<app-root>Loading...</app-root>
|
||||
</body>
|
||||
</html>
|
||||
3
core/frontend/src/polyfills.ts
Normal file
3
core/frontend/src/polyfills.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Angular zone support (default change detection)
|
||||
import 'zone.js';
|
||||
|
||||
4
core/frontend/src/styles.css
Normal file
4
core/frontend/src/styles.css
Normal file
@ -0,0 +1,4 @@
|
||||
/* Global styles (optional) */
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Noto Sans; }
|
||||
|
||||
14
core/frontend/tsconfig.app.json
Normal file
14
core/frontend/tsconfig.app.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts",
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
27
core/frontend/tsconfig.json
Normal file
27
core/frontend/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./out-tsc",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom"
|
||||
],
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,10 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"st
|
||||
"strconv"
|
||||
"time"
|
||||
"os"
|
||||
"weatherstation/internal/config"
|
||||
"weatherstation/internal/database"
|
||||
"weatherstation/pkg/types"
|
||||
@ -27,6 +29,23 @@ func StartGinServer() error {
|
||||
// 静态文件服务
|
||||
r.Static("/static", "./static")
|
||||
|
||||
// 前端SPA(Angular)静态资源与路由回退
|
||||
// 构建产物目录:./core/frontend/dist/ui
|
||||
r.GET("/ui/*filepath", func(c *gin.Context) {
|
||||
// 物理文件优先,否则回退到 index.html(支持前端路由)
|
||||
requested := c.Param("filepath")
|
||||
if requested == "" || requested == "/" {
|
||||
c.File("./core/frontend/dist/ui/index.html")
|
||||
return
|
||||
}
|
||||
full := ".//core/frontend/dist/ui" + requested
|
||||
if _, err := os.Stat(full); err == nil {
|
||||
c.File(full)
|
||||
return
|
||||
}
|
||||
c.File("./core/frontend/dist/ui/index.html")
|
||||
})
|
||||
|
||||
// 路由设置
|
||||
r.GET("/", indexHandler)
|
||||
r.GET("/radar/nanning", radarNanningHandler)
|
||||
|
||||
@ -15,6 +15,10 @@ const WeatherMap = {
|
||||
this.initializeLayers();
|
||||
this.initializeMap(tiandituKey);
|
||||
this.setupEventListeners();
|
||||
this.setupTileControls();
|
||||
// 默认不显示(需用户选择),但准备好默认索引
|
||||
this.tileZ = 7; this.tileY = 40; this.tileX = 102;
|
||||
this.tileProduct = 'none';
|
||||
},
|
||||
|
||||
// 初始化图层
|
||||
@ -56,6 +60,8 @@ const WeatherMap = {
|
||||
layers.vector,
|
||||
layers.terrain,
|
||||
layers.hybrid,
|
||||
// 栅格瓦片叠加层(动态)
|
||||
this.createTileOverlayLayer(),
|
||||
this.clusterLayer,
|
||||
this.singleStationLayer
|
||||
],
|
||||
@ -73,6 +79,17 @@ const WeatherMap = {
|
||||
this.updateLayerVisibility(initialZoom);
|
||||
},
|
||||
|
||||
createTileOverlayLayer(){
|
||||
const layer = new ol.layer.Image({
|
||||
visible: true,
|
||||
opacity: 0.8,
|
||||
source: null,
|
||||
zIndex: 999
|
||||
});
|
||||
this.tileOverlayLayer = layer;
|
||||
return layer;
|
||||
},
|
||||
|
||||
// 创建地图图层
|
||||
createMapLayers(tiandituKey) {
|
||||
return {
|
||||
@ -138,6 +155,220 @@ const WeatherMap = {
|
||||
};
|
||||
},
|
||||
|
||||
// ---- 瓦片联动:控件与渲染 ----
|
||||
setupTileControls(){
|
||||
const prodSel = document.getElementById('tileProduct');
|
||||
const timeSel = document.getElementById('tileTimeSelect');
|
||||
const prevBtn = document.getElementById('btnTilePrev');
|
||||
const nextBtn = document.getElementById('btnTileNext');
|
||||
if (!prodSel || !timeSel || !prevBtn || !nextBtn) return;
|
||||
|
||||
prodSel.addEventListener('change', async ()=>{
|
||||
this.tileProduct = prodSel.value;
|
||||
// 切换产品:none 则隐藏图层
|
||||
if (this.tileProduct === 'none') {
|
||||
this.tileTimes = [];
|
||||
this.tileCurrentIdx = -1;
|
||||
if (this.tileOverlayLayer) this.tileOverlayLayer.setSource(null);
|
||||
this.updateTileCountInfo();
|
||||
return;
|
||||
}
|
||||
await this.reloadTileTimesAndShow();
|
||||
});
|
||||
const sEl = document.getElementById('startDate');
|
||||
const eEl = document.getElementById('endDate');
|
||||
const onRangeChange = async ()=>{
|
||||
if (this.tileProduct && this.tileProduct !== 'none') {
|
||||
await this.reloadTileTimesAndShow();
|
||||
}
|
||||
};
|
||||
if (sEl) sEl.addEventListener('change', onRangeChange);
|
||||
if (eEl) eEl.addEventListener('change', onRangeChange);
|
||||
timeSel.addEventListener('change', async ()=>{
|
||||
const dt = timeSel.value;
|
||||
if (!dt) return;
|
||||
const idx = (this.tileTimes||[]).indexOf(dt);
|
||||
if (idx >= 0) this.tileCurrentIdx = idx;
|
||||
await this.loadAndRenderTile(dt);
|
||||
this.updateTileCountInfo();
|
||||
});
|
||||
prevBtn.addEventListener('click', async ()=>{
|
||||
if (!this.tileTimes || this.tileTimes.length === 0) return;
|
||||
// times 为倒序:上一时次=更老 => idx+1
|
||||
if (this.tileCurrentIdx < this.tileTimes.length - 1) {
|
||||
this.tileCurrentIdx += 1;
|
||||
const dt = this.tileTimes[this.tileCurrentIdx];
|
||||
timeSel.value = dt;
|
||||
await this.loadAndRenderTile(dt);
|
||||
this.updateTileCountInfo();
|
||||
}
|
||||
});
|
||||
nextBtn.addEventListener('click', async ()=>{
|
||||
if (!this.tileTimes || this.tileTimes.length === 0) return;
|
||||
if (this.tileCurrentIdx > 0) {
|
||||
this.tileCurrentIdx -= 1;
|
||||
const dt = this.tileTimes[this.tileCurrentIdx];
|
||||
timeSel.value = dt;
|
||||
await this.loadAndRenderTile(dt);
|
||||
this.updateTileCountInfo();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async reloadTileTimesAndShow(){
|
||||
try{
|
||||
const times = await this.fetchTileTimes(this.tileProduct, this.tileZ, this.tileY, this.tileX);
|
||||
this.tileTimes = times || [];
|
||||
const timeSel = document.getElementById('tileTimeSelect');
|
||||
if (timeSel){
|
||||
timeSel.innerHTML = '';
|
||||
if (this.tileTimes.length === 0){
|
||||
const opt = document.createElement('option'); opt.value=''; opt.textContent='无可用时次'; timeSel.appendChild(opt);
|
||||
} else {
|
||||
this.tileTimes.forEach(dt=>{ const o=document.createElement('option'); o.value=dt; o.textContent=dt; timeSel.appendChild(o); });
|
||||
this.tileCurrentIdx = 0;
|
||||
timeSel.value = this.tileTimes[0];
|
||||
await this.loadAndRenderTile(this.tileTimes[0]);
|
||||
}
|
||||
this.updateTileCountInfo();
|
||||
}
|
||||
}catch(e){ console.error('加载瓦片时次失败', e); }
|
||||
},
|
||||
|
||||
updateTileCountInfo(){
|
||||
const el = document.getElementById('tileCountInfo');
|
||||
if (!el) return;
|
||||
const total = this.tileTimes ? this.tileTimes.length : 0;
|
||||
const idxDisp = (this.tileCurrentIdx != null && this.tileCurrentIdx >= 0) ? (this.tileCurrentIdx+1) : 0;
|
||||
el.textContent = `共${total}条,第${idxDisp}条`;
|
||||
const prevBtn = document.getElementById('btnTilePrev');
|
||||
const nextBtn = document.getElementById('btnTileNext');
|
||||
if (prevBtn) prevBtn.disabled = !(total>0 && this.tileCurrentIdx < total-1);
|
||||
if (nextBtn) nextBtn.disabled = !(total>0 && this.tileCurrentIdx > 0);
|
||||
},
|
||||
|
||||
async fetchTileTimes(product, z,y,x){
|
||||
const sEl = document.getElementById('startDate');
|
||||
const eEl = document.getElementById('endDate');
|
||||
let url;
|
||||
if (product==='rain') url = `/api/rain/times?z=${z}&y=${y}&x=${x}`; else url = `/api/radar/times?z=${z}&y=${y}&x=${x}`;
|
||||
if (sEl && eEl && sEl.value && eEl.value) {
|
||||
const from = sEl.value.replace('T',' ') + ':00';
|
||||
const to = eEl.value.replace('T',' ') + ':00';
|
||||
url += `&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
|
||||
} else {
|
||||
url += '&limit=60';
|
||||
}
|
||||
const r = await fetch(url);
|
||||
if(!r.ok) return [];
|
||||
const j = await r.json();
|
||||
return j.times || [];
|
||||
},
|
||||
|
||||
async loadAndRenderTile(dtStr){
|
||||
try{
|
||||
const z=this.tileZ,y=this.tileY,x=this.tileX;
|
||||
const url = this.tileProduct==='rain' ? `/api/rain/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}` : `/api/radar/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`;
|
||||
const r = await fetch(url);
|
||||
if(!r.ok){ console.warn('瓦片未找到', dtStr); return; }
|
||||
const t = await r.json();
|
||||
await this.renderTileOnMap(this.tileProduct, t);
|
||||
}catch(e){ console.error('加载/渲染瓦片失败', e); }
|
||||
},
|
||||
|
||||
renderTileOnMap(product, t){
|
||||
if(!t || !t.values) return;
|
||||
const w=t.width, h=t.height, resDeg=t.res_deg;
|
||||
const west=t.west, south=t.south, east=t.east, north=t.north;
|
||||
// 生成画布
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = w; canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.createImageData(w, h);
|
||||
// 色带
|
||||
let colorFunc;
|
||||
if (product==='rain'){
|
||||
const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity];
|
||||
const colors = [
|
||||
[255,255,255], [126,212,121], [110,200,109], [97,169,97], [81,148,76], [90,158,112],
|
||||
[143,194,254], [92,134,245], [66,87,240], [45,48,214], [26,15,166], [63,22,145], [191,70,148], [213,1,146], [213,1,146]
|
||||
];
|
||||
colorFunc = (mm)=>{
|
||||
if (mm===0) return [0,0,0,0]; // 0 值透明
|
||||
let idx=0; while(idx<edges.length-1 && !(mm>=edges[idx] && mm<edges[idx+1])) idx++;
|
||||
const c = colors[Math.min(idx, colors.length-1)]; return [c[0],c[1],c[2],220];
|
||||
};
|
||||
} else {
|
||||
// radar dBZ,0..75 映射 15 段
|
||||
const colors = [[0,0,255],[0,191,255],[0,255,255],[127,255,212],[124,252,0],[173,255,47],[255,255,0],[255,215,0],[255,165,0],[255,140,0],[255,69,0],[255,0,0],[220,20,60],[199,21,133],[139,0,139]];
|
||||
colorFunc = (dbz)=>{
|
||||
let v = Math.max(0, Math.min(75, dbz));
|
||||
if (v===0) return [0,0,0,0]; // 0 值透明
|
||||
let bin = Math.floor(v/5); if (bin>=colors.length) bin=colors.length-1;
|
||||
const c = colors[bin]; return [c[0],c[1],c[2],220];
|
||||
};
|
||||
}
|
||||
// API 行从南到北?我们按数组行序逐行,从上到下绘制需倒置行
|
||||
let p=0;
|
||||
for(let row=0; row<h; row++){
|
||||
const srcRow = t.values[row] || [];
|
||||
const dstRow = (h-1-row); // 翻转以匹配北向上
|
||||
for(let col=0; col<w; col++){
|
||||
const v = srcRow[col];
|
||||
let rgba;
|
||||
if (v==null){ rgba=[0,0,0,0]; }
|
||||
else { rgba = colorFunc(Number(v)); }
|
||||
const off = (dstRow*w + col)*4;
|
||||
imgData.data[off+0]=rgba[0];
|
||||
imgData.data[off+1]=rgba[1];
|
||||
imgData.data[off+2]=rgba[2];
|
||||
imgData.data[off+3]=rgba[3];
|
||||
}
|
||||
}
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
// 去除数值文本叠加(应用户要求)
|
||||
const dataURL = canvas.toDataURL('image/png');
|
||||
const extent4326 = [west, south, east, north];
|
||||
const proj = this.map.getView().getProjection();
|
||||
const extentProj = ol.proj.transformExtent(extent4326, 'EPSG:4326', proj);
|
||||
const src = new ol.source.ImageStatic({ url: dataURL, imageExtent: extentProj, projection: proj });
|
||||
this.tileOverlayLayer.setSource(src);
|
||||
// 保存最近一次用于拾取
|
||||
this.tileLast = { product, meta: { west, south, east, north, resDeg, width:w, height:h }, values: t.values };
|
||||
this.setupTileHover();
|
||||
},
|
||||
|
||||
setupTileHover(){
|
||||
if (this._hoverBound) return;
|
||||
const tip = document.getElementById('tileValueTooltip');
|
||||
if (!tip) return;
|
||||
this.map.on('pointermove', (evt)=>{
|
||||
try{
|
||||
if (!this.tileLast || !this.tileOverlayLayer || !this.tileOverlayLayer.getSource()) { tip.style.display='none'; return; }
|
||||
const coord = this.map.getEventCoordinate(evt.originalEvent);
|
||||
const lonlat = ol.proj.transform(coord, this.map.getView().getProjection(), 'EPSG:4326');
|
||||
const {west,south,east,north,resDeg} = this.tileLast.meta;
|
||||
const lon = lonlat[0], lat = lonlat[1];
|
||||
if (lon<west || lon>east || lat<south || lat>north) { tip.style.display='none'; return; }
|
||||
const col = Math.floor((lon - west)/resDeg);
|
||||
const row = Math.floor((lat - south)/resDeg);
|
||||
if (row<0 || row>=this.tileLast.meta.height || col<0 || col>=this.tileLast.meta.width) { tip.style.display='none'; return; }
|
||||
const v = this.tileLast.values[row]?.[col];
|
||||
if (v==null) { tip.style.display='none'; return; }
|
||||
const val = Number(v);
|
||||
// 构建文本
|
||||
const txt = this.tileLast.product==='rain' ? `${val.toFixed(1)} mm` : `${val.toFixed(1)} dBZ`;
|
||||
tip.textContent = txt;
|
||||
// 位置
|
||||
const pixel = this.map.getPixelFromCoordinate(coord);
|
||||
tip.style.left = (pixel[0]+10) + 'px';
|
||||
tip.style.top = (pixel[1]+10) + 'px';
|
||||
tip.style.display = 'block';
|
||||
}catch{ tip.style.display='none'; }
|
||||
});
|
||||
this._hoverBound = true;
|
||||
},
|
||||
|
||||
// 设置事件监听
|
||||
setupEventListeners() {
|
||||
// 监听缩放事件
|
||||
|
||||
@ -517,11 +517,35 @@
|
||||
<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 class="control-row flex items-center gap-3 flex-wrap">
|
||||
<div class="control-group">
|
||||
<label class="text-sm text-gray-600">地图产品:</label>
|
||||
<select id="tileProduct" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||
<option value="none" selected>不显示</option>
|
||||
<option value="rain">一小时降雨</option>
|
||||
<option value="radar">组合反射率</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<button id="btnTilePrev" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100">上一时次</button>
|
||||
<span id="tileCountInfo" class="text-xs text-gray-800">共0条,第0条</span>
|
||||
<button id="btnTileNext" class="px-2 py-1 text-sm border border-gray-400 rounded bg-white text-gray-800 hover:bg-gray-100">下一时次</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="text-sm text-gray-600">时间:</label>
|
||||
<select id="tileTimeSelect" class="px-2 py-1 border border-gray-300 rounded text-sm min-w-[220px]">
|
||||
<option value="">请选择时间</option>
|
||||
</select>
|
||||
</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 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 class="chart-container" id="chartContainer">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user