import { useEffect, useMemo, useState } from 'react' import { Card, DotLoading, Empty, ErrorBlock } from 'antd-mobile' import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts' import { getOilForecast, getOilHistory, getOilSchedule } from '../../api/oil.js' // 油价页 (新): // - 上: 下次调价预测 (无 WTI / 无柱状图) // - 中: 浙江省最近半年价格走势 (3 油品折线, 一张图) // - 下: 调价日历 (按年切换) // 数据源: // - 预测/日历: apizero 实时 (后端代理) // - 走势: DB oil_price_history 表 (cron 每天 04:00 抓浙江 3 油品入库) const FUEL_LABELS = { '92': '92# 汽油', '95': '95# 汽油', '0': '0# 柴油', } // 3 油品各自配色 (与原柱状图色调保持一致) const FUEL_COLORS = { '92': '#1677ff', '95': '#52c41a', '0': '#888', } function n(v, d = 2) { return Number.isFinite(Number(v)) ? Number(v).toFixed(d) : '-' } // 调价预测 (保留, 但不显示 WTI / 布伦特) function ForecastPanel() { const [forecast, setForecast] = useState(null) const [loading, setLoading] = useState(true) const [err, setErr] = useState('') useEffect(() => { let cancelled = false setLoading(true) setErr('') getOilForecast() .then((d) => { if (!cancelled) setForecast(d) }) .catch((e) => !cancelled && setErr(e.message || '加载失败')) .finally(() => !cancelled && setLoading(false)) return () => { cancelled = true } }, []) if (loading) return
加载中...
if (err) return const p = forecast?.prediction return (
下次调价预测
调价日 {forecast?.next_adjust_date || '-'} 剩 {forecast?.days_remaining ?? '-'} 天
方向 {p?.direction_emoji || ''} {p?.direction || '-'} 置信度 {p?.confidence || '-'}
预计幅度 {p?.estimated_change_per_ton != null ? `${p.estimated_change_per_ton > 0 ? '+' : ''}${p.estimated_change_per_ton} 元/吨` : '-'} {p?.estimated_change_per_liter != null && ( ({p.estimated_change_per_liter > 0 ? '+' : ''}{p.estimated_change_per_liter} 元/升) )}
{p?.analysis && (
{p.analysis}
)}
) } // 浙江省最近半年价格走势 function HistoryPanel() { const [data, setData] = useState([]) const [loading, setLoading] = useState(true) const [err, setErr] = useState('') useEffect(() => { let cancelled = false setLoading(true) setErr('') getOilHistory({ days: 180 }) .then((d) => { if (!cancelled) setData(d.items || []) }) .catch((e) => !cancelled && setErr(e.message || '加载失败')) .finally(() => !cancelled && setLoading(false)) return () => { cancelled = true } }, []) // 整形: rows = [{date, fuel, price}] -> 3 油品并排 // pivot: [{date, 92, 95, 0}] const chartData = useMemo(() => { const m = new Map() for (const r of data) { if (!m.has(r.date)) m.set(r.date, { date: r.date }) m.get(r.date)[r.fuel] = r.price } return Array.from(m.values()).sort((a, b) => a.date.localeCompare(b.date)) }, [data]) // 3 油品都有数据? const hasData = chartData.length > 0 return (
浙江省最近半年价格走势
{loading &&
加载中...
} {err && } {!loading && !err && !hasData && ( )} {!loading && !err && hasData && ( <>
`${Number(v).toFixed(3)} 元/升`} /> {Object.keys(FUEL_LABELS).map((k) => ( ))}
共 {chartData.length} 个数据点 (调价次日入库)
)}
) } // 调价日历 (不变) function SchedulePanel() { const [year, setYear] = useState(new Date().getFullYear()) const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [err, setErr] = useState('') useEffect(() => { let cancelled = false setLoading(true) setErr('') getOilSchedule(year) .then((d) => { if (!cancelled) setData(d) }) .catch((e) => !cancelled && setErr(e.message || '加载失败')) .finally(() => !cancelled && setLoading(false)) return () => { cancelled = true } }, [year]) const byMonth = useMemo(() => { const m = {} if (!data?.schedule) return m for (const it of data.schedule) { const mm = it.date.slice(0, 7) if (!m[mm]) m[mm] = [] m[mm].push(it) } return m }, [data]) return (
调价日历
{[2025, 2026].map((y) => ( ))}
{data?._cached &&
本次来自本地缓存, 不消耗 apizero 配额
} {loading &&
加载中...
} {err && } {!loading && !err && data && ( <>
{data.year} 年共 {data.total_adjustments} 个调价窗口 {data.next_adjust_date && ( 下一个: {data.next_adjust_date} (剩 {data.days_remaining} 天) )}
{Object.keys(byMonth).sort().map((m) => (
{m}
{byMonth[m].map((it) => (
{it.date.slice(8)}
{it.status}
))}
))}
)}
) } // 油价页入口: 预测 + 走势 + 日历 export default function OilPage() { return (
) }