From 306af38fdc1ef2542f675edb13f3fc54b1f38035 Mon Sep 17 00:00:00 2001 From: cheney Date: Sat, 25 Apr 2026 19:48:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE-=E4=BA=BA?= =?UTF-8?q?=E6=B0=91=E5=B8=81=E6=B1=87=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/TimeRangePicker.js | 145 ++++++++++++++++++++++++++++++++ pages/api/exchange-rate.js | 9 +- pages/data.js | 33 +++++++- styles/globals.css | 151 ++++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 5 deletions(-) create mode 100644 components/TimeRangePicker.js diff --git a/components/TimeRangePicker.js b/components/TimeRangePicker.js new file mode 100644 index 0000000..ea38ca7 --- /dev/null +++ b/components/TimeRangePicker.js @@ -0,0 +1,145 @@ +import { useState, useEffect } from 'react' + +const TimeRangePicker = ({ + value = '1m', + onChange, + className = '' +}) => { + const [timeRange, setTimeRange] = useState(value) + const [startDate, setStartDate] = useState('') + const [endDate, setEndDate] = useState('') + const [showDropdown, setShowDropdown] = useState(false) + + // 计算日期范围 + const calculateDateRange = (range) => { + const end = new Date() + const start = new Date() + + if (range === '1m') { + start.setMonth(start.getMonth() - 1) + } else if (range === '3m') { + start.setMonth(start.getMonth() - 3) + } else if (range === '1y') { + start.setFullYear(start.getFullYear() - 1) + } + + const formatDate = (date) => { + return date.toISOString().split('T')[0] + } + + return { + start: formatDate(start), + end: formatDate(end) + } + } + + // 初始化时设置日期范围 + useEffect(() => { + const range = calculateDateRange(value) + setStartDate(range.start) + setEndDate(range.end) + }, [value]) + + // 处理时间范围变化 + const handleTimeRangeChange = (range) => { + const dateRange = calculateDateRange(range) + setStartDate(dateRange.start) + setEndDate(dateRange.end) + setTimeRange(range) + if (onChange) { + onChange(range, dateRange.start, dateRange.end) + } + setShowDropdown(false) + } + + // 处理自定义日期范围 + const handleCustomRangeApply = () => { + if (startDate && endDate) { + setTimeRange('custom') + if (onChange) { + onChange('custom', startDate, endDate) + } + setShowDropdown(false) + } + } + + // 处理开始日期变化 + const handleStartDateChange = (e) => { + setStartDate(e.target.value) + } + + // 处理结束日期变化 + const handleEndDateChange = (e) => { + setEndDate(e.target.value) + } + + return ( +
+ + {showDropdown && ( +
+
+

自定义范围

+
+ + + + +
+
+
+

快捷选项

+
    +
  • handleTimeRangeChange('1m')} + > + 近一个月 +
  • +
  • handleTimeRangeChange('3m')} + > + 近三个月 +
  • +
  • handleTimeRangeChange('1y')} + > + 近一年 +
  • +
+
+
+ )} +
+ ) +} + +export default TimeRangePicker diff --git a/pages/api/exchange-rate.js b/pages/api/exchange-rate.js index 3ecb5e1..32cba0f 100644 --- a/pages/api/exchange-rate.js +++ b/pages/api/exchange-rate.js @@ -98,8 +98,11 @@ export default async function handler(req, res) { error: error.message }); } finally { - if (pool) { - await pool.end(); - } + // 不关闭连接池,保持连接池打开以提高性能 + // 如果需要关闭连接池,应该在关闭后将 pool 变量设置为 null + // if (pool) { + // await pool.end(); + // pool = null; + // } } } diff --git a/pages/data.js b/pages/data.js index c169462..2d08de7 100644 --- a/pages/data.js +++ b/pages/data.js @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react' import Head from 'next/head' import Navbar from '../components/Navbar' import Footer from '../components/Footer' +import TimeRangePicker from '../components/TimeRangePicker' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts' const chinaBondData = [ @@ -162,18 +163,39 @@ export default function Data() { const [exchangeRateData, setExchangeRateData] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) + const [timeRange, setTimeRange] = useState('1m') // 1m: 近一个月, 3m: 近三个月, 1y: 近一年, custom: 自定义 + const [startDate, setStartDate] = useState('') + const [endDate, setEndDate] = useState('') useEffect(() => { if (selectedIndicator === 'central-bank-usd-cny-rate') { fetchExchangeRateData() } - }, [selectedIndicator]) + }, [selectedIndicator, timeRange, startDate, endDate]) + + const handleTimeRangeChange = (range, start, end) => { + setTimeRange(range) + setStartDate(start) + setEndDate(end) + } const fetchExchangeRateData = async () => { setLoading(true) setError(null) try { - const response = await fetch('/api/exchange-rate?limit=60') + let url = '/api/exchange-rate' + + if (timeRange === 'custom' && startDate && endDate) { + url += `?startDate=${startDate}&endDate=${endDate}` + } else if (timeRange === '1m') { + url += '?limit=30' + } else if (timeRange === '3m') { + url += '?limit=90' + } else if (timeRange === '1y') { + url += '?limit=365' + } + + const response = await fetch(url) const data = await response.json() if (data.success) { setExchangeRateData(data.data) @@ -566,6 +588,13 @@ export default function Data() {
{error}
) : ( <> +
+ +
+
diff --git a/styles/globals.css b/styles/globals.css index 27c607c..201e9fb 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -493,3 +493,154 @@ main { margin-bottom: 1rem; } +/* Time range selector */ +.chart-header { + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +.time-range-picker { + position: relative; + display: inline-block; +} + +.time-range-btn { + padding: 8px 16px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: #fff; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; + min-width: 150px; + justify-content: space-between; + transition: all 0.3s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.time-range-btn:hover { + border-color: #333; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.dropdown-arrow { + font-size: 12px; + color: #666; + transition: transform 0.3s ease; +} + +.time-range-dropdown { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 16px; + min-width: 300px; + z-index: 1000; +} + +.dropdown-section { + margin-bottom: 16px; +} + +.dropdown-section:last-child { + margin-bottom: 0; +} + +.dropdown-title { + font-size: 12px; + font-weight: 600; + color: #999; + margin-bottom: 8px; + text-transform: uppercase; +} + +.dropdown-options { + list-style: none; + padding: 0; + margin: 0; +} + +.dropdown-option { + padding: 8px 12px; + cursor: pointer; + border-radius: 4px; + transition: all 0.2s ease; + font-size: 14px; +} + +.dropdown-option:hover { + background-color: #f0f0f0; +} + +.dropdown-option.active { + background-color: #333; + color: #fff; +} + +.custom-date-range { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.custom-date-range input[type="date"] { + padding: 6px 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + min-width: 120px; +} + +.custom-date-range span { + font-size: 14px; + color: #666; + white-space: nowrap; +} + +.apply-btn { + padding: 6px 12px; + border: 1px solid #333; + border-radius: 4px; + background-color: #333; + color: #fff; + cursor: pointer; + font-size: 14px; + transition: all 0.3s ease; +} + +.apply-btn:hover { + background-color: #555; + border-color: #555; +} + +.apply-btn:disabled { + background-color: #f0f0f0; + color: #999; + border-color: #ddd; + cursor: not-allowed; +} + +/* Loading and error states */ +.loading { + text-align: center; + padding: 40px; + font-size: 16px; + color: #666; +} + +.error { + text-align: center; + padding: 40px; + font-size: 16px; + color: #e53935; +} +