feat: 新增3h预报
This commit is contained in:
parent
d4fb3f5986
commit
701292c54b
@ -263,27 +263,29 @@ func buildWeatherDataQuery(interval string) string {
|
|||||||
ORDER BY g.time_group`
|
ORDER BY g.time_group`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetForecastData 获取指定站点的预报数据(优先最新issued_at)
|
// GetForecastData 获取指定站点的预报数据(支持返回每个forecast_time的多版本issued_at)
|
||||||
func GetForecastData(db *sql.DB, stationID string, startTime, endTime time.Time, provider string) ([]types.ForecastPoint, error) {
|
func GetForecastData(db *sql.DB, stationID string, startTime, endTime time.Time, provider string, versions int) ([]types.ForecastPoint, error) {
|
||||||
var query string
|
var query string
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
|
|
||||||
|
if versions <= 0 {
|
||||||
|
versions = 1
|
||||||
|
}
|
||||||
|
|
||||||
if provider != "" {
|
if provider != "" {
|
||||||
// 指定预报提供商
|
|
||||||
if provider == "open-meteo" {
|
if provider == "open-meteo" {
|
||||||
// 合并实时与历史,优先实时的最新issued_at
|
// 合并实时与历史,按 issued_at 降序为每个 forecast_time 取前 N 个版本
|
||||||
query = `
|
query = `
|
||||||
WITH latest_forecasts AS (
|
WITH ranked AS (
|
||||||
SELECT DISTINCT ON (forecast_time)
|
SELECT
|
||||||
station_id, provider, issued_at, forecast_time,
|
station_id, provider, issued_at, forecast_time,
|
||||||
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
||||||
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100
|
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY forecast_time ORDER BY issued_at DESC) AS rn,
|
||||||
|
CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours
|
||||||
FROM forecast_hourly
|
FROM forecast_hourly
|
||||||
WHERE station_id = $1 AND provider IN ('open-meteo','open-meteo_historical')
|
WHERE station_id = $1 AND provider IN ('open-meteo','open-meteo_historical')
|
||||||
AND forecast_time BETWEEN $2 AND $3
|
AND forecast_time BETWEEN $2 AND $3
|
||||||
ORDER BY forecast_time,
|
|
||||||
CASE WHEN provider='open-meteo' THEN 0 ELSE 1 END,
|
|
||||||
issued_at DESC
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
||||||
@ -296,49 +298,24 @@ func GetForecastData(db *sql.DB, stationID string, startTime, endTime time.Time,
|
|||||||
wind_dir_deg as wind_direction,
|
wind_dir_deg as wind_direction,
|
||||||
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
||||||
precip_prob_pct as precip_prob,
|
precip_prob_pct as precip_prob,
|
||||||
uv_index as uv
|
uv_index as uv,
|
||||||
FROM latest_forecasts
|
lead_hours
|
||||||
ORDER BY forecast_time`
|
FROM ranked
|
||||||
args = []interface{}{stationID, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07")}
|
WHERE rn <= $4
|
||||||
|
ORDER BY forecast_time, issued_at DESC`
|
||||||
// 调试日志
|
args = []interface{}{stationID, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07"), versions}
|
||||||
log.Printf("执行open-meteo合并查询: stationID=%s, start=%s, end=%s",
|
|
||||||
stationID, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07"))
|
|
||||||
|
|
||||||
// 检查是否有历史数据
|
|
||||||
var histCount int
|
|
||||||
err := db.QueryRow(`SELECT COUNT(*) FROM forecast_hourly
|
|
||||||
WHERE station_id = $1 AND provider = 'open-meteo_historical'
|
|
||||||
AND forecast_time BETWEEN $2 AND $3`,
|
|
||||||
stationID, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07")).Scan(&histCount)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("查询历史数据计数失败: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("时间范围内历史数据计数: %d 条", histCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有实时数据
|
|
||||||
var rtCount int
|
|
||||||
err = db.QueryRow(`SELECT COUNT(*) FROM forecast_hourly
|
|
||||||
WHERE station_id = $1 AND provider = 'open-meteo'
|
|
||||||
AND forecast_time BETWEEN $2 AND $3`,
|
|
||||||
stationID, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07")).Scan(&rtCount)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("查询实时数据计数失败: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("时间范围内实时数据计数: %d 条", rtCount)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
query = `
|
query = `
|
||||||
WITH latest_forecasts AS (
|
WITH ranked AS (
|
||||||
SELECT DISTINCT ON (forecast_time)
|
SELECT
|
||||||
station_id, provider, issued_at, forecast_time,
|
station_id, provider, issued_at, forecast_time,
|
||||||
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
||||||
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100
|
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY forecast_time ORDER BY issued_at DESC) AS rn,
|
||||||
|
CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours
|
||||||
FROM forecast_hourly
|
FROM forecast_hourly
|
||||||
WHERE station_id = $1 AND provider = $2
|
WHERE station_id = $1 AND provider = $2
|
||||||
AND forecast_time BETWEEN $3 AND $4
|
AND forecast_time BETWEEN $3 AND $4
|
||||||
ORDER BY forecast_time, issued_at DESC
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
||||||
@ -351,22 +328,25 @@ func GetForecastData(db *sql.DB, stationID string, startTime, endTime time.Time,
|
|||||||
wind_dir_deg as wind_direction,
|
wind_dir_deg as wind_direction,
|
||||||
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
||||||
precip_prob_pct as precip_prob,
|
precip_prob_pct as precip_prob,
|
||||||
uv_index as uv
|
uv_index as uv,
|
||||||
FROM latest_forecasts
|
lead_hours
|
||||||
ORDER BY forecast_time`
|
FROM ranked
|
||||||
args = []interface{}{stationID, provider, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07")}
|
WHERE rn <= $5
|
||||||
|
ORDER BY forecast_time, issued_at DESC`
|
||||||
|
args = []interface{}{stationID, provider, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07"), versions}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 不指定预报提供商,取所有
|
// 不指定预报提供商:对每个 provider,forecast_time 返回前 N 个 issued_at 版本
|
||||||
query = `
|
query = `
|
||||||
WITH latest_forecasts AS (
|
WITH ranked AS (
|
||||||
SELECT DISTINCT ON (provider, forecast_time)
|
SELECT
|
||||||
station_id, provider, issued_at, forecast_time,
|
station_id, provider, issued_at, forecast_time,
|
||||||
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
temp_c_x100, humidity_pct, wind_speed_ms_x1000, wind_gust_ms_x1000,
|
||||||
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100
|
wind_dir_deg, rain_mm_x1000, precip_prob_pct, uv_index, pressure_hpa_x100,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY provider, forecast_time ORDER BY issued_at DESC) AS rn,
|
||||||
|
CEIL(EXTRACT(EPOCH FROM (forecast_time - issued_at)) / 3600.0)::int AS lead_hours
|
||||||
FROM forecast_hourly
|
FROM forecast_hourly
|
||||||
WHERE station_id = $1 AND forecast_time BETWEEN $2 AND $3
|
WHERE station_id = $1 AND forecast_time BETWEEN $2 AND $3
|
||||||
ORDER BY provider, forecast_time, issued_at DESC
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
to_char(forecast_time, 'YYYY-MM-DD HH24:MI:SS') as date_time,
|
||||||
@ -379,13 +359,14 @@ func GetForecastData(db *sql.DB, stationID string, startTime, endTime time.Time,
|
|||||||
wind_dir_deg as wind_direction,
|
wind_dir_deg as wind_direction,
|
||||||
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
ROUND(rain_mm_x1000::numeric / 1000.0, 3) as rainfall,
|
||||||
precip_prob_pct as precip_prob,
|
precip_prob_pct as precip_prob,
|
||||||
uv_index as uv
|
uv_index as uv,
|
||||||
FROM latest_forecasts
|
lead_hours
|
||||||
ORDER BY forecast_time, provider`
|
FROM ranked
|
||||||
args = []interface{}{stationID, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07")}
|
WHERE rn <= $4
|
||||||
|
ORDER BY forecast_time, provider, issued_at DESC`
|
||||||
|
args = []interface{}{stationID, startTime.Format("2006-01-02 15:04:05-07"), endTime.Format("2006-01-02 15:04:05-07"), versions}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询
|
|
||||||
rows, err := db.Query(query, args...)
|
rows, err := db.Query(query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("查询预报数据失败: %v", err)
|
return nil, fmt.Errorf("查询预报数据失败: %v", err)
|
||||||
@ -407,6 +388,7 @@ func GetForecastData(db *sql.DB, stationID string, startTime, endTime time.Time,
|
|||||||
&point.Rainfall,
|
&point.Rainfall,
|
||||||
&point.PrecipProb,
|
&point.PrecipProb,
|
||||||
&point.UV,
|
&point.UV,
|
||||||
|
&point.LeadHours,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("数据扫描错误: %v", err)
|
log.Printf("数据扫描错误: %v", err)
|
||||||
|
|||||||
@ -150,6 +150,11 @@ func getForecastHandler(c *gin.Context) {
|
|||||||
startTime := c.Query("from")
|
startTime := c.Query("from")
|
||||||
endTime := c.Query("to")
|
endTime := c.Query("to")
|
||||||
provider := c.Query("provider")
|
provider := c.Query("provider")
|
||||||
|
versionsStr := c.DefaultQuery("versions", "1")
|
||||||
|
versions, _ := strconv.Atoi(versionsStr)
|
||||||
|
if versions <= 0 {
|
||||||
|
versions = 1
|
||||||
|
}
|
||||||
|
|
||||||
if stationID == "" {
|
if stationID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少station_id参数"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少station_id参数"})
|
||||||
@ -186,8 +191,8 @@ func getForecastHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取预报数据
|
// 获取预报数据
|
||||||
log.Printf("查询预报数据: stationID=%s, provider=%s, start=%s, end=%s", stationID, provider, start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"))
|
log.Printf("查询预报数据: stationID=%s, provider=%s, versions=%d, start=%s, end=%s", stationID, provider, versions, start.Format("2006-01-02 15:04:05"), end.Format("2006-01-02 15:04:05"))
|
||||||
points, err := database.GetForecastData(database.GetDB(), stationID, start, end, provider)
|
points, err := database.GetForecastData(database.GetDB(), stationID, start, end, provider, versions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("查询预报数据失败: %v", err)
|
log.Printf("查询预报数据失败: %v", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
@ -197,11 +202,5 @@ func getForecastHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("查询到预报数据: %d 条", len(points))
|
log.Printf("查询到预报数据: %d 条", len(points))
|
||||||
// 调试:打印前几条记录
|
|
||||||
for i, p := range points {
|
|
||||||
if i < 5 {
|
|
||||||
log.Printf("预报数据 #%d: time=%s, provider=%s, issued=%s", i, p.DateTime, p.Provider, p.IssuedAt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, points)
|
c.JSON(http.StatusOK, points)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,4 +55,5 @@ type ForecastPoint struct {
|
|||||||
PrecipProb *float64 `json:"precip_prob"`
|
PrecipProb *float64 `json:"precip_prob"`
|
||||||
UV *float64 `json:"uv"`
|
UV *float64 `json:"uv"`
|
||||||
Source string `json:"source"` // "forecast"
|
Source string `json:"source"` // "forecast"
|
||||||
|
LeadHours int `json:"lead_hours"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -197,7 +197,8 @@ const WeatherApp = {
|
|||||||
station_id: stationID,
|
station_id: stationID,
|
||||||
from: startTime.replace('T', ' ') + ':00',
|
from: startTime.replace('T', ' ') + ':00',
|
||||||
to: endTime.replace('T', ' ') + ':00',
|
to: endTime.replace('T', ' ') + ':00',
|
||||||
provider: forecastProvider
|
provider: forecastProvider,
|
||||||
|
versions: '3'
|
||||||
});
|
});
|
||||||
const forecastResponse = await fetch(`/api/forecast?${forecastParams}`);
|
const forecastResponse = await fetch(`/api/forecast?${forecastParams}`);
|
||||||
if (forecastResponse.ok) {
|
if (forecastResponse.ok) {
|
||||||
|
|||||||
@ -13,13 +13,13 @@ const WeatherChart = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并历史数据和预报数据的时间轴
|
// 合并历史数据和预报数据的时间轴(按 date_time 对齐)
|
||||||
const allLabels = [...new Set([
|
const allLabels = [...new Set([
|
||||||
...historyData.map(item => item.date_time),
|
...historyData.map(item => item.date_time),
|
||||||
...forecastData.map(item => item.date_time)
|
...forecastData.map(item => item.date_time)
|
||||||
])].sort();
|
])].sort();
|
||||||
|
|
||||||
// 准备历史数据
|
// 历史数据
|
||||||
const historyTemperatures = allLabels.map(label => {
|
const historyTemperatures = allLabels.map(label => {
|
||||||
const item = historyData.find(d => d.date_time === label);
|
const item = historyData.find(d => d.date_time === label);
|
||||||
return item ? item.temperature : null;
|
return item ? item.temperature : null;
|
||||||
@ -37,24 +37,48 @@ const WeatherChart = {
|
|||||||
return item && item.rain_total !== undefined ? item.rain_total : null;
|
return item && item.rain_total !== undefined ? item.rain_total : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 准备预报数据
|
// 预报:按同一 forecast_time 的不同 lead_hours 组织(仅用 0..3h)
|
||||||
const forecastTemperatures = allLabels.map(label => {
|
const byTime = new Map();
|
||||||
const item = forecastData.find(d => d.date_time === label);
|
forecastData.forEach(fp => {
|
||||||
return item && item.temperature !== null ? item.temperature : null;
|
if (!byTime.has(fp.date_time)) byTime.set(fp.date_time, {});
|
||||||
});
|
const bucket = byTime.get(fp.date_time);
|
||||||
const forecastHumidities = allLabels.map(label => {
|
const h = typeof fp.lead_hours === 'number' ? fp.lead_hours : null;
|
||||||
const item = forecastData.find(d => d.date_time === label);
|
if (h !== null && h >= 0 && h <= 3) {
|
||||||
return item && item.humidity !== null ? item.humidity : null;
|
bucket[h] = fp;
|
||||||
});
|
}
|
||||||
const forecastRainfalls = allLabels.map(label => {
|
|
||||||
const item = forecastData.find(d => d.date_time === label);
|
|
||||||
return item && item.rainfall !== null ? item.rainfall : null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getRainAtLead = (label, lead) => {
|
||||||
|
const b = byTime.get(label);
|
||||||
|
if (!b || !b[lead]) return null;
|
||||||
|
return b[lead].rainfall != null ? b[lead].rainfall : null;
|
||||||
|
};
|
||||||
|
const getTempAtLead0 = (label) => {
|
||||||
|
const b = byTime.get(label);
|
||||||
|
if (!b || !b[0]) return null;
|
||||||
|
return b[0].temperature != null ? b[0].temperature : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const forecastRainfallsH0 = allLabels.map(label => getRainAtLead(label, 0));
|
||||||
|
const forecastRainfallsH1 = allLabels.map(label => getRainAtLead(label, 1));
|
||||||
|
const forecastRainfallsH2 = allLabels.map(label => getRainAtLead(label, 2));
|
||||||
|
const forecastRainfallsH3 = allLabels.map(label => getRainAtLead(label, 3));
|
||||||
|
const pickNearestTemp = (label) => {
|
||||||
|
// 近似优先级:0h > 1h > 2h > 3h
|
||||||
|
const b = byTime.get(label);
|
||||||
|
if (!b) return null;
|
||||||
|
if (b[0] && b[0].temperature != null) return b[0].temperature;
|
||||||
|
if (b[1] && b[1].temperature != null) return b[1].temperature;
|
||||||
|
if (b[2] && b[2].temperature != null) return b[2].temperature;
|
||||||
|
if (b[3] && b[3].temperature != null) return b[3].temperature;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const forecastTemperaturesNearest = allLabels.map(label => pickNearestTemp(label));
|
||||||
|
|
||||||
// 销毁旧图表
|
// 销毁旧图表
|
||||||
if (this.chart) this.chart.destroy();
|
if (this.chart) this.chart.destroy();
|
||||||
|
|
||||||
// 创建数据集
|
// 数据集
|
||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: '温度 (°C) - 实测',
|
label: '温度 (°C) - 实测',
|
||||||
@ -96,39 +120,25 @@ const WeatherChart = {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 添加预报数据集
|
|
||||||
if (forecastData.length > 0) {
|
if (forecastData.length > 0) {
|
||||||
|
// 雨量 仅显示 -1h/-2h/-3h
|
||||||
datasets.push(
|
datasets.push(
|
||||||
{
|
{ label: '雨量 (mm) - 预报 (-1h)', data: forecastRainfallsH1, type: 'bar', backgroundColor: 'rgba(255, 99, 71, 0.55)', borderColor: 'rgb(255, 99, 71)', yAxisID: 'y-rainfall' },
|
||||||
|
{ label: '雨量 (mm) - 预报 (-2h)', data: forecastRainfallsH2, type: 'bar', backgroundColor: 'rgba(255, 205, 86, 0.55)', borderColor: 'rgb(255, 205, 86)', yAxisID: 'y-rainfall' },
|
||||||
|
{ label: '雨量 (mm) - 预报 (-3h)', data: forecastRainfallsH3, type: 'bar', backgroundColor: 'rgba(76, 175, 80, 0.55)', borderColor: 'rgb(76, 175, 80)', yAxisID: 'y-rainfall' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 温度 取最接近的预报(0h>1h>2h>3h)
|
||||||
|
datasets.push({
|
||||||
label: '温度 (°C) - 预报',
|
label: '温度 (°C) - 预报',
|
||||||
data: forecastTemperatures,
|
data: forecastTemperaturesNearest,
|
||||||
borderColor: 'rgb(255, 165, 0)',
|
borderColor: 'rgb(255, 159, 64)',
|
||||||
backgroundColor: 'rgba(255, 165, 0, 0.1)',
|
backgroundColor: 'rgba(255, 159, 64, 0.1)',
|
||||||
borderDash: [5, 5],
|
borderDash: [5, 5],
|
||||||
yAxisID: 'y-temperature',
|
yAxisID: 'y-temperature',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
spanGaps: false,
|
spanGaps: false
|
||||||
},
|
});
|
||||||
{
|
|
||||||
label: '湿度 (%) - 预报',
|
|
||||||
data: forecastHumidities,
|
|
||||||
borderColor: 'rgba(255, 165, 0, 0.8)',
|
|
||||||
backgroundColor: 'rgba(255, 165, 0, 0.05)',
|
|
||||||
borderDash: [5, 5],
|
|
||||||
yAxisID: 'y-humidity',
|
|
||||||
tension: 0.4,
|
|
||||||
spanGaps: false,
|
|
||||||
hidden: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '雨量 (mm) - 预报',
|
|
||||||
data: forecastRainfalls,
|
|
||||||
type: 'bar',
|
|
||||||
backgroundColor: 'rgba(255, 165, 0, 0.4)',
|
|
||||||
borderColor: 'rgb(255, 165, 0)',
|
|
||||||
yAxisID: 'y-rainfall'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建组合图表
|
// 创建组合图表
|
||||||
@ -142,47 +152,11 @@ const WeatherChart = {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: {
|
interaction: { mode: 'index', intersect: false },
|
||||||
mode: 'index',
|
|
||||||
intersect: false,
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
'y-temperature': {
|
'y-temperature': { type: 'linear', display: true, position: 'left', title: { display: true, text: '温度 (°C)' } },
|
||||||
type: 'linear',
|
'y-humidity': { type: 'linear', display: true, position: 'right', title: { display: true, text: '湿度 (%)' }, grid: { drawOnChartArea: false }, min: 0, max: 100 },
|
||||||
display: true,
|
'y-rainfall': { type: 'linear', display: true, position: 'right', title: { display: true, text: '雨量 (mm)' }, grid: { drawOnChartArea: false }, beginAtZero: true }
|
||||||
position: 'left',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '温度 (°C)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'y-humidity': {
|
|
||||||
type: 'linear',
|
|
||||||
display: true,
|
|
||||||
position: 'right',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '湿度 (%)'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
drawOnChartArea: false
|
|
||||||
},
|
|
||||||
min: 0,
|
|
||||||
max: 100
|
|
||||||
},
|
|
||||||
'y-rainfall': {
|
|
||||||
type: 'linear',
|
|
||||||
display: true,
|
|
||||||
position: 'right',
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: '雨量 (mm)'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
drawOnChartArea: false
|
|
||||||
},
|
|
||||||
beginAtZero: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,10 +37,10 @@ const WeatherTable = {
|
|||||||
thead.appendChild(th);
|
thead.appendChild(th);
|
||||||
});
|
});
|
||||||
if (hasForecast) {
|
if (hasForecast) {
|
||||||
const th = document.createElement('th');
|
const th0 = document.createElement('th');
|
||||||
th.textContent = '降水概率 (%)';
|
th0.textContent = '降水概率 (%)';
|
||||||
th.className = 'bg-gray-50 font-semibold';
|
th0.className = 'bg-gray-50 font-semibold';
|
||||||
thead.appendChild(th);
|
thead.appendChild(th0);
|
||||||
}
|
}
|
||||||
const remainingHeaders = ['光照 (lux)', '紫外线'];
|
const remainingHeaders = ['光照 (lux)', '紫外线'];
|
||||||
remainingHeaders.forEach(text => {
|
remainingHeaders.forEach(text => {
|
||||||
@ -75,8 +75,16 @@ const WeatherTable = {
|
|||||||
row.style.backgroundColor = 'rgba(255, 165, 0, 0.08)';
|
row.style.backgroundColor = 'rgba(255, 165, 0, 0.08)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const issuedBadge = (() => {
|
||||||
|
if (item.source !== '预报') return '';
|
||||||
|
const lead = (typeof item.lead_hours === 'number' && item.lead_hours >= 0) ? ` -${item.lead_hours}h` : '';
|
||||||
|
const issued = item.issued_at ? new Date(item.issued_at.replace(' ', 'T')) : null;
|
||||||
|
const issuedStr = issued ? `${String(issued.getHours()).padStart(2,'0')}:${String(issued.getMinutes()).padStart(2,'0')}` : '-';
|
||||||
|
return `<span style="font-size: 12px; color: #6b7280;">(发布: ${issuedStr}${lead})</span>`;
|
||||||
|
})();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
`<td>${item.date_time}${hasForecast ? ` <span style="font-size: 12px; color: ${item.source === '预报' ? '#ff8c00' : '#28a745'};">[${item.source}]</span>` : ''}</td>`,
|
`<td>${item.date_time}${hasForecast ? ` <span style="font-size: 12px; color: ${item.source === '预报' ? '#ff8c00' : '#28a745'};">[${item.source}]</span>` : ''}${item.source === '预报' ? `<br>${issuedBadge}` : ''}</td>`,
|
||||||
`<td>${fmt2(item.temperature)}</td>`,
|
`<td>${fmt2(item.temperature)}</td>`,
|
||||||
`<td>${fmt2(item.humidity)}</td>`,
|
`<td>${fmt2(item.humidity)}</td>`,
|
||||||
`<td>${fmt2(item.pressure)}</td>`,
|
`<td>${fmt2(item.pressure)}</td>`,
|
||||||
@ -86,7 +94,9 @@ const WeatherTable = {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (hasForecast) {
|
if (hasForecast) {
|
||||||
columns.push(`<td>${item.source === '预报' && item.precip_prob !== null && item.precip_prob !== undefined ? item.precip_prob : '-'}</td>`);
|
columns.push(
|
||||||
|
`${item.source === '预报' && item.precip_prob !== null && item.precip_prob !== undefined ? `<td>${item.precip_prob}</td>` : '<td>-</td>'}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
columns.push(
|
columns.push(
|
||||||
|
|||||||
@ -500,6 +500,7 @@
|
|||||||
<option value="">不显示预报</option>
|
<option value="">不显示预报</option>
|
||||||
<option value="open-meteo">Open-Meteo</option>
|
<option value="open-meteo">Open-Meteo</option>
|
||||||
<option value="caiyun" selected>彩云</option>
|
<option value="caiyun" selected>彩云</option>
|
||||||
|
<option value="imdroid">英卓</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user