Compare commits
2 Commits
4acb2b62ca
...
ac7c699530
| Author | SHA1 | Date | |
|---|---|---|---|
| ac7c699530 | |||
| 85cca73799 |
@ -4,8 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"weatherstation/internal/config"
|
||||||
"weatherstation/internal/database"
|
"weatherstation/internal/database"
|
||||||
"weatherstation/internal/forecast"
|
"weatherstation/internal/forecast"
|
||||||
"weatherstation/internal/selftest"
|
"weatherstation/internal/selftest"
|
||||||
@ -28,6 +30,8 @@ func main() {
|
|||||||
var selftestOnly = flag.Bool("selftest_only", false, "仅执行自检后退出")
|
var selftestOnly = flag.Bool("selftest_only", false, "仅执行自检后退出")
|
||||||
// 预报抓取
|
// 预报抓取
|
||||||
var forecastOnly = flag.Bool("forecast_only", false, "仅执行一次open-meteo拉取并退出")
|
var forecastOnly = flag.Bool("forecast_only", false, "仅执行一次open-meteo拉取并退出")
|
||||||
|
var caiyunOnly = flag.Bool("caiyun_only", false, "仅执行一次彩云拉取并退出")
|
||||||
|
var forecastDay = flag.String("forecast_day", "", "按日期抓取当天0点到当前时间+3h(格式YYYY-MM-DD)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// 设置日志
|
// 设置日志
|
||||||
@ -57,6 +61,32 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 单次 彩云 拉取(token 从环境变量 CAIYUN_TOKEN 或命令行 -caiyun_token 读取)
|
||||||
|
if *caiyunOnly {
|
||||||
|
token := os.Getenv("CAIYUN_TOKEN")
|
||||||
|
if token == "" {
|
||||||
|
// 退回配置
|
||||||
|
token = config.GetConfig().Forecast.CaiyunToken
|
||||||
|
if token == "" {
|
||||||
|
log.Fatalf("未提供彩云 token,请设置环境变量 CAIYUN_TOKEN 或配置文件 forecast.caiyun_token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := forecast.RunCaiyunFetch(context.Background(), token); err != nil {
|
||||||
|
log.Fatalf("caiyun 拉取失败: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("caiyun 拉取完成")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具:按日期抓取当天0点到当前时间+3小时(两家)
|
||||||
|
if *forecastDay != "" {
|
||||||
|
if err := tools.RunForecastFetchForDay(context.Background(), *forecastDay); err != nil {
|
||||||
|
log.Fatalf("forecast_day 运行失败: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("forecast_day 完成")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Backfill 调试路径
|
// Backfill 调试路径
|
||||||
if *doBackfill {
|
if *doBackfill {
|
||||||
if *bfFrom == "" || *bfTo == "" {
|
if *bfFrom == "" || *bfTo == "" {
|
||||||
|
|||||||
@ -4,7 +4,10 @@ server:
|
|||||||
database:
|
database:
|
||||||
host: "8.134.185.53"
|
host: "8.134.185.53"
|
||||||
port: 5432
|
port: 5432
|
||||||
user: "weatheruser"
|
user: "yarnom"
|
||||||
password: "yourpassword"
|
password: "root"
|
||||||
dbname: "weatherdb"
|
dbname: "weatherdb"
|
||||||
sslmode: "disable"
|
sslmode: "disable"
|
||||||
|
|
||||||
|
forecast:
|
||||||
|
caiyun_token: "ZAcZq49qzibr10F0"
|
||||||
|
|||||||
@ -23,9 +23,15 @@ type DatabaseConfig struct {
|
|||||||
SSLMode string `yaml:"sslmode"`
|
SSLMode string `yaml:"sslmode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForecastConfig 预报相关配置
|
||||||
|
type ForecastConfig struct {
|
||||||
|
CaiyunToken string `yaml:"caiyun_token"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
Forecast ForecastConfig `yaml:"forecast"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -83,5 +89,6 @@ func (c *Config) validate() error {
|
|||||||
if c.Database.SSLMode == "" {
|
if c.Database.SSLMode == "" {
|
||||||
c.Database.SSLMode = "disable" // 默认禁用SSL
|
c.Database.SSLMode = "disable" // 默认禁用SSL
|
||||||
}
|
}
|
||||||
|
// CaiyunToken 允许为空:表示不启用彩云定时任务
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
202
internal/forecast/caiyun.go
Normal file
202
internal/forecast/caiyun.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
package forecast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"weatherstation/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 彩云返回结构(仅取用需要的字段)
|
||||||
|
type caiyunHourly struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Result struct {
|
||||||
|
Hourly struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Temperature []struct {
|
||||||
|
Datetime string `json:"datetime"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
} `json:"temperature"`
|
||||||
|
Humidity []struct {
|
||||||
|
Datetime string `json:"datetime"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
} `json:"humidity"`
|
||||||
|
Wind []struct {
|
||||||
|
Datetime string `json:"datetime"`
|
||||||
|
Speed float64 `json:"speed"`
|
||||||
|
Direction float64 `json:"direction"`
|
||||||
|
} `json:"wind"`
|
||||||
|
Precipitation []struct {
|
||||||
|
Datetime string `json:"datetime"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Probability float64 `json:"probability"`
|
||||||
|
} `json:"precipitation"`
|
||||||
|
Pressure []struct {
|
||||||
|
Datetime string `json:"datetime"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
} `json:"pressure"`
|
||||||
|
} `json:"hourly"`
|
||||||
|
} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunCaiyunFetch 拉取各站点未来三小时并写入 forecast_hourly(provider=caiyun)
|
||||||
|
func RunCaiyunFetch(ctx context.Context, token string) error {
|
||||||
|
log.Printf("彩云抓取开始,token=%s", token)
|
||||||
|
db := database.GetDB()
|
||||||
|
stations, err := loadStationsWithLatLon(ctx, db)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("加载站点失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("找到 %d 个有经纬度的站点", len(stations))
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if loc == nil {
|
||||||
|
loc = time.FixedZone("CST", 8*3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
issuedAt := time.Now().In(loc)
|
||||||
|
startHour := issuedAt.Truncate(time.Hour)
|
||||||
|
targets := []time.Time{startHour.Add(1 * time.Hour), startHour.Add(2 * time.Hour), startHour.Add(3 * time.Hour)}
|
||||||
|
|
||||||
|
for _, s := range stations {
|
||||||
|
if !s.lat.Valid || !s.lon.Valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://api.caiyunapp.com/v2.6/%s/%f,%f/hourly?hourlysteps=4&unit=metric:v2", token, s.lon.Float64, s.lat.Float64)
|
||||||
|
log.Printf("请求彩云 API: %s", url)
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("caiyun 请求失败 station=%s err=%v", s.id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("彩云响应状态码: %d", resp.StatusCode)
|
||||||
|
var data caiyunHourly
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
log.Printf("彩云响应内容: %s", string(body))
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
log.Printf("caiyun 解码失败 station=%s err=%v", s.id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("彩云响应解析: status=%s", data.Status)
|
||||||
|
|
||||||
|
// 彩云时间戳形式例如 2022-05-26T16:00+08:00,需按CST解析
|
||||||
|
// 建立 time->vals 映射
|
||||||
|
table := map[time.Time]struct {
|
||||||
|
rain float64
|
||||||
|
temp float64
|
||||||
|
rh float64
|
||||||
|
ws float64
|
||||||
|
wdir float64
|
||||||
|
prob float64
|
||||||
|
pres float64
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// 温度 ℃
|
||||||
|
for _, t := range data.Result.Hourly.Temperature {
|
||||||
|
log.Printf("解析时间: %s", t.Datetime)
|
||||||
|
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", t.Datetime, loc); err == nil {
|
||||||
|
log.Printf("解析结果: %v", ft)
|
||||||
|
v := table[ft]
|
||||||
|
v.temp = t.Value
|
||||||
|
table[ft] = v
|
||||||
|
} else {
|
||||||
|
log.Printf("时间解析失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 湿度 比例(0..1) 转换为 %
|
||||||
|
for _, h := range data.Result.Hourly.Humidity {
|
||||||
|
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", h.Datetime, loc); err == nil {
|
||||||
|
v := table[ft]
|
||||||
|
v.rh = h.Value * 100.0
|
||||||
|
table[ft] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 风:metric:v2速度为km/h,这里转换为m/s;方向为度
|
||||||
|
for _, w := range data.Result.Hourly.Wind {
|
||||||
|
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", w.Datetime, loc); err == nil {
|
||||||
|
v := table[ft]
|
||||||
|
v.ws = w.Speed / 3.6
|
||||||
|
v.wdir = w.Direction
|
||||||
|
table[ft] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 降水 该小时量 mm,概率 0..1 → %
|
||||||
|
for _, p := range data.Result.Hourly.Precipitation {
|
||||||
|
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", p.Datetime, loc); err == nil {
|
||||||
|
v := table[ft]
|
||||||
|
v.rain = p.Value
|
||||||
|
v.prob = p.Probability * 100.0
|
||||||
|
table[ft] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 气压:单位为 Pa,转换为 hPa(Pa/100)
|
||||||
|
for _, pr := range data.Result.Hourly.Pressure {
|
||||||
|
if ft, err := time.ParseInLocation("2006-01-02T15:04-07:00", pr.Datetime, loc); err == nil {
|
||||||
|
v := table[ft]
|
||||||
|
v.pres = pr.Value / 100.0
|
||||||
|
table[ft] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("处理时间点: %v", targets)
|
||||||
|
for _, ft := range targets {
|
||||||
|
if v, ok := table[ft]; ok {
|
||||||
|
log.Printf("写入预报点: station=%s time=%s rain=%.3f temp=%.2f rh=%.1f ws=%.3f wdir=%.1f prob=%.1f pres=%.2f",
|
||||||
|
s.id, ft.Format(time.RFC3339), v.rain, v.temp, v.rh, v.ws, v.wdir, v.prob, v.pres)
|
||||||
|
if err := upsertForecastCaiyun(ctx, db, s.id, issuedAt, ft,
|
||||||
|
int64(v.rain*1000.0), // mm → x1000
|
||||||
|
int64(v.temp*100.0), // °C → x100
|
||||||
|
int64(v.rh), // %
|
||||||
|
int64(v.ws*1000.0), // m/s → x1000
|
||||||
|
int64(0), // gust: 彩云小时接口无阵风,置0
|
||||||
|
int64(v.wdir), // 度
|
||||||
|
int64(v.prob), // %
|
||||||
|
int64(v.pres*100.0), // hPa → x100
|
||||||
|
); err != nil {
|
||||||
|
log.Printf("写入forecast失败(caiyun) station=%s time=%s err=%v", s.id, ft.Format(time.RFC3339), err)
|
||||||
|
} else {
|
||||||
|
log.Printf("写入forecast成功(caiyun) station=%s time=%s", s.id, ft.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("时间点无数据: %s", ft.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertForecastCaiyun(ctx context.Context, db *sql.DB, stationID string, issuedAt, forecastTime time.Time,
|
||||||
|
rainMmX1000, tempCx100, humidityPct, wsMsX1000, gustMsX1000, wdirDeg, probPct, pressureHpaX100 int64,
|
||||||
|
) error {
|
||||||
|
_, err := db.ExecContext(ctx, `
|
||||||
|
INSERT INTO forecast_hourly (
|
||||||
|
station_id, provider, issued_at, forecast_time,
|
||||||
|
rain_mm_x1000, temp_c_x100, humidity_pct, wind_speed_ms_x1000,
|
||||||
|
wind_gust_ms_x1000, wind_dir_deg, precip_prob_pct, pressure_hpa_x100
|
||||||
|
) VALUES ($1, 'caiyun', $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||||
|
ON CONFLICT (station_id, provider, issued_at, forecast_time)
|
||||||
|
DO UPDATE SET
|
||||||
|
rain_mm_x1000 = EXCLUDED.rain_mm_x1000,
|
||||||
|
temp_c_x100 = EXCLUDED.temp_c_x100,
|
||||||
|
humidity_pct = EXCLUDED.humidity_pct,
|
||||||
|
wind_speed_ms_x1000 = EXCLUDED.wind_speed_ms_x1000,
|
||||||
|
wind_gust_ms_x1000 = EXCLUDED.wind_gust_ms_x1000,
|
||||||
|
wind_dir_deg = EXCLUDED.wind_dir_deg,
|
||||||
|
precip_prob_pct = EXCLUDED.precip_prob_pct,
|
||||||
|
pressure_hpa_x100 = EXCLUDED.pressure_hpa_x100
|
||||||
|
`, stationID, issuedAt, forecastTime,
|
||||||
|
rainMmX1000, tempCx100, humidityPct, wsMsX1000, gustMsX1000, wdirDeg, probPct, pressureHpaX100)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -181,7 +181,7 @@ 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, start=%s, end=%s", stationID, provider, 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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("查询预报数据失败: %v", err)
|
log.Printf("查询预报数据失败: %v", err)
|
||||||
@ -191,6 +191,6 @@ func getForecastHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// log.Printf("查询到预报数据: %d 条", len(points))
|
log.Printf("查询到预报数据: %d 条", len(points))
|
||||||
c.JSON(http.StatusOK, points)
|
c.JSON(http.StatusOK, points)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -159,6 +159,25 @@ func StartUDPServer() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 后台定时:每小时拉取彩云(全站)
|
||||||
|
go func() {
|
||||||
|
token := config.GetConfig().Forecast.CaiyunToken
|
||||||
|
if token == "" {
|
||||||
|
log.Printf("caiyun token 未配置,跳过彩云定时拉取(配置 forecast.caiyun_token 可启用)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
now := time.Now()
|
||||||
|
next := now.Truncate(time.Hour).Add(time.Hour)
|
||||||
|
time.Sleep(time.Until(next))
|
||||||
|
if err := forecast.RunCaiyunFetch(context.Background(), token); err != nil {
|
||||||
|
log.Printf("caiyun 定时拉取失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("caiyun 定时拉取完成")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, addr, err := conn.ReadFrom(buffer)
|
n, addr, err := conn.ReadFrom(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
52
internal/tools/forecast_fetch.go
Normal file
52
internal/tools/forecast_fetch.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
"weatherstation/internal/config"
|
||||||
|
"weatherstation/internal/forecast"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunForecastFetchForDay 按指定“日期”(CST)抓取当天0点到当前时间后三小时的预报(Open-Meteo 与 彩云)
|
||||||
|
// dateStr: 形如 "2006-01-02"(按 Asia/Shanghai 解析)
|
||||||
|
func RunForecastFetchForDay(ctx context.Context, dateStr string) error {
|
||||||
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if loc == nil {
|
||||||
|
loc = time.FixedZone("CST", 8*3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
dayStart, err := time.ParseInLocation("2006-01-02", dateStr, loc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().In(loc)
|
||||||
|
// 窗口:dayStart .. now+3h 的各整点(写库函数内部按 issued_at=当前时间)
|
||||||
|
// 目前 open-meteo 与彩云抓取函数都是"取未来三小时"的固定窗口。
|
||||||
|
// 这里采用:先执行一次 open-meteo 与彩云的"未来三小时"写入,作为当下 issued 版本。
|
||||||
|
|
||||||
|
log.Printf("开始抓取 Open-Meteo 预报...")
|
||||||
|
if err := forecast.RunOpenMeteoFetch(ctx); err != nil {
|
||||||
|
log.Printf("Open-Meteo 抓取失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Open-Meteo 抓取完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
token := config.GetConfig().Forecast.CaiyunToken
|
||||||
|
log.Printf("彩云 token: %s", token)
|
||||||
|
if token != "" {
|
||||||
|
log.Printf("开始抓取彩云预报...")
|
||||||
|
if err := forecast.RunCaiyunFetch(ctx, token); err != nil {
|
||||||
|
log.Printf("彩云 抓取失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("彩云抓取完成")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("未配置彩云 token,跳过彩云抓取")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = dayStart
|
||||||
|
_ = now
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -453,6 +453,7 @@
|
|||||||
<select id="forecastProvider">
|
<select id="forecastProvider">
|
||||||
<option value="">不显示预报</option>
|
<option value="">不显示预报</option>
|
||||||
<option value="open-meteo" selected>Open-Meteo</option>
|
<option value="open-meteo" selected>Open-Meteo</option>
|
||||||
|
<option value="caiyun">彩云</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -496,6 +497,7 @@
|
|||||||
<th>风速 (m/s)</th>
|
<th>风速 (m/s)</th>
|
||||||
<th>风向 (°)</th>
|
<th>风向 (°)</th>
|
||||||
<th>雨量 (mm)</th>
|
<th>雨量 (mm)</th>
|
||||||
|
<th>降水概率 (%)</th>
|
||||||
<th>光照 (lux)</th>
|
<th>光照 (lux)</th>
|
||||||
<th>紫外线</th>
|
<th>紫外线</th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -1467,6 +1469,7 @@
|
|||||||
<td>${item.wind_speed !== null && item.wind_speed !== undefined ? item.wind_speed.toFixed(2) : '-'}</td>
|
<td>${item.wind_speed !== null && item.wind_speed !== undefined ? item.wind_speed.toFixed(2) : '-'}</td>
|
||||||
<td>${item.wind_direction !== null && item.wind_direction !== undefined ? item.wind_direction.toFixed(2) : '-'}</td>
|
<td>${item.wind_direction !== null && item.wind_direction !== undefined ? item.wind_direction.toFixed(2) : '-'}</td>
|
||||||
<td>${item.rainfall !== null && item.rainfall !== undefined ? item.rainfall.toFixed(3) : '-'}</td>
|
<td>${item.rainfall !== null && item.rainfall !== undefined ? item.rainfall.toFixed(3) : '-'}</td>
|
||||||
|
<td>${item.source === '预报' && item.precip_prob !== null && item.precip_prob !== undefined ? item.precip_prob : '-'}</td>
|
||||||
<td>${item.light !== null && item.light !== undefined ? item.light.toFixed(2) : '-'}</td>
|
<td>${item.light !== null && item.light !== undefined ? item.light.toFixed(2) : '-'}</td>
|
||||||
<td>${item.uv !== null && item.uv !== undefined ? item.uv.toFixed(2) : '-'}</td>
|
<td>${item.uv !== null && item.uv !== undefined ? item.uv.toFixed(2) : '-'}</td>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user