增加功能
continuous-integration/drone Build is failing

This commit is contained in:
曾志威
2026-05-12 00:02:27 +08:00
parent 4b1ffb4c6a
commit 4a5ae4cd6c
8 changed files with 184 additions and 115 deletions
+16 -12
View File
@@ -18,10 +18,11 @@ func NewDashboardHandler(svc *service.DashboardService) *DashboardHandler {
return &DashboardHandler{svc: svc} return &DashboardHandler{svc: svc}
} }
// GetSummary 获取最新交易日的市场概览数据 // GetSummary 获取市场概览数据,支持可选 date 参数查询历史数据
// GET /api/dashboard/summary // GET /api/dashboard/summary?date=2024-01-15
func (h *DashboardHandler) GetSummary(c *gin.Context) { func (h *DashboardHandler) GetSummary(c *gin.Context) {
summary, err := h.svc.GetSummary() date := c.Query("date") // 空字符串表示查最新交易日
summary, err := h.svc.GetSummary(date)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -29,14 +30,15 @@ func (h *DashboardHandler) GetSummary(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": summary}) c.JSON(http.StatusOK, gin.H{"data": summary})
} }
// GetDailySummary 获取近 N 天的每日市场统计数据 // GetDailySummary 获取近 N 天的每日市场统计数据,支持可选 end_date 参数
// GET /api/dashboard/daily?days=30 // GET /api/dashboard/daily?days=30&end_date=2024-01-15
func (h *DashboardHandler) GetDailySummary(c *gin.Context) { func (h *DashboardHandler) GetDailySummary(c *gin.Context) {
days, _ := strconv.Atoi(c.DefaultQuery("days", "30")) days, _ := strconv.Atoi(c.DefaultQuery("days", "30"))
if days <= 0 || days > 365 { if days <= 0 || days > 365 {
days = 30 days = 30
} }
data, err := h.svc.GetDailySummary(days) endDate := c.Query("end_date")
data, err := h.svc.GetDailySummary(days, endDate)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -44,10 +46,11 @@ func (h *DashboardHandler) GetDailySummary(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": data}) c.JSON(http.StatusOK, gin.H{"data": data})
} }
// GetConsecutiveLimits 获取最新交易日的连板股票统计 // GetConsecutiveLimits 获取连板股票统计,支持可选 date 参数查询历史数据
// GET /api/dashboard/consecutive // GET /api/dashboard/consecutive?date=2024-01-15
func (h *DashboardHandler) GetConsecutiveLimits(c *gin.Context) { func (h *DashboardHandler) GetConsecutiveLimits(c *gin.Context) {
data, err := h.svc.GetConsecutiveLimits() date := c.Query("date")
data, err := h.svc.GetConsecutiveLimits(date)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -55,10 +58,11 @@ func (h *DashboardHandler) GetConsecutiveLimits(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": data}) c.JSON(http.StatusOK, gin.H{"data": data})
} }
// GetHigh100 获取百日新高股票列表 // GetHigh100 获取百日新高股票列表,支持可选 date 参数查询历史数据
// GET /api/dashboard/high100 // GET /api/dashboard/high100?date=2024-01-15
func (h *DashboardHandler) GetHigh100(c *gin.Context) { func (h *DashboardHandler) GetHigh100(c *gin.Context) {
data, err := h.svc.GetHigh100() date := c.Query("date")
data, err := h.svc.GetHigh100(date)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
+40 -15
View File
@@ -128,9 +128,17 @@ func (r *StockRepo) GetSummaryByDate(date string) (*model.DashboardSummary, erro
// GetHigh100 获取最新交易日创百日新高的股票列表 // GetHigh100 获取最新交易日创百日新高的股票列表
func (r *StockRepo) GetHigh100() ([]model.StockRank, error) { func (r *StockRepo) GetHigh100() ([]model.StockRank, error) {
date, err := r.getLatestDate() return r.GetHigh100ByDate("")
if err != nil { }
return nil, err
// GetHigh100ByDate 获取指定日期创百日新高的股票列表,date 为空时使用最新交易日
func (r *StockRepo) GetHigh100ByDate(date string) ([]model.StockRank, error) {
var err error
if date == "" {
date, err = r.getLatestDate()
if err != nil {
return nil, err
}
} }
var list []model.StockRank var list []model.StockRank
r.db.Raw(` r.db.Raw(`
@@ -154,10 +162,16 @@ func (r *StockRepo) GetHigh100() ([]model.StockRank, error) {
// GetDailySummary 获取近 N 天的每日市场统计 // GetDailySummary 获取近 N 天的每日市场统计
// 包含: 涨跌数量、成交额、涨停跌停数量、连板数量 // 包含: 涨跌数量、成交额、涨停跌停数量、连板数量
func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) { func (r *StockRepo) GetDailySummary(days int, endDate string) ([]model.DailySummary, error) {
latest, err := r.getLatestDate() var latest string
if err != nil { var err error
return nil, err if endDate != "" {
latest = endDate
} else {
latest, err = r.getLatestDate()
if err != nil {
return nil, err
}
} }
var result []model.DailySummary var result []model.DailySummary
@@ -175,10 +189,10 @@ func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) {
SUM(CASE WHEN pct_change >= 19.5 THEN 1 ELSE 0 END) as limit_up_20, SUM(CASE WHEN pct_change >= 19.5 THEN 1 ELSE 0 END) as limit_up_20,
SUM(CASE WHEN pct_change <= -19.5 THEN 1 ELSE 0 END) as limit_down_20 SUM(CASE WHEN pct_change <= -19.5 THEN 1 ELSE 0 END) as limit_down_20
FROM stock_daily FROM stock_daily
WHERE date >= DATE_SUB(?, INTERVAL ? DAY) WHERE date >= DATE_SUB(?, INTERVAL ? DAY) AND date <= ?
GROUP BY date GROUP BY date
ORDER BY date ASC ORDER BY date ASC
`, latest, days-1).Scan(&result) `, latest, days-1, latest).Scan(&result)
// 计算连板数量: 获取相邻交易日对,批量查询当日和前一日都涨停的股票数量 // 计算连板数量: 获取相邻交易日对,批量查询当日和前一日都涨停的股票数量
type datePair struct { type datePair struct {
@@ -188,10 +202,10 @@ func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) {
var pairs []datePair var pairs []datePair
r.db.Raw(` r.db.Raw(`
SELECT d1.date AS cur_date, MAX(d2.date) AS prev_date SELECT d1.date AS cur_date, MAX(d2.date) AS prev_date
FROM (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY)) d1 FROM (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY) AND date <= ?) d1
JOIN (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY)) d2 ON d2.date < d1.date JOIN (SELECT DISTINCT date FROM stock_daily WHERE date >= DATE_SUB(?, INTERVAL ? DAY) AND date <= ?) d2 ON d2.date < d1.date
GROUP BY d1.date GROUP BY d1.date
`, latest, days-1, latest, days).Scan(&pairs) `, latest, days-1, latest, latest, days, latest).Scan(&pairs)
type consecRow struct { type consecRow struct {
Date string `gorm:"column:date"` Date string `gorm:"column:date"`
@@ -229,9 +243,20 @@ func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) {
// GetConsecutiveLimits 获取最新交易日连板股票列表 // GetConsecutiveLimits 获取最新交易日连板股票列表
// 查询近 30 天的涨停记录,计算当日涨停股票的连续涨停天数 // 查询近 30 天的涨停记录,计算当日涨停股票的连续涨停天数
func (r *StockRepo) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) { func (r *StockRepo) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) {
latest, err := r.getLatestDate() return r.GetConsecutiveLimitsByDate("")
if err != nil { }
return nil, err
// GetConsecutiveLimitsByDate 获取指定日期的连板股票列表,date 为空时使用最新交易日
func (r *StockRepo) GetConsecutiveLimitsByDate(date string) ([]model.ConsecutiveLimit, error) {
var latest string
var err error
if date != "" {
latest = date
} else {
latest, err = r.getLatestDate()
if err != nil {
return nil, err
}
} }
if latest == "" { if latest == "" {
return nil, nil return nil, nil
+28 -11
View File
@@ -35,8 +35,13 @@ func NewDashboardService(repo *repository.StockRepo) *DashboardService {
const cacheTTL = 3 * time.Minute const cacheTTL = 3 * time.Minute
// GetSummary 获取最新交易日市场概览(涨跌数量、涨停跌停、百日新高等) // GetSummary 获取市场概览,date 为空时查最新交易日(走缓存),指定日期时跳过缓存直接查询
func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) { func (s *DashboardService) GetSummary(date string) (*model.DashboardSummary, error) {
// 指定历史日期:直接查询,不走缓存
if date != "" {
return s.repo.GetSummaryByDate(date)
}
s.mu.RLock() s.mu.RLock()
if s.summary != nil && time.Now().Before(s.summary.expires) { if s.summary != nil && time.Now().Before(s.summary.expires) {
data := s.summary.data data := s.summary.data
@@ -45,11 +50,11 @@ func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) {
} }
s.mu.RUnlock() s.mu.RUnlock()
date, err := s.repo.GetLatestTradeDate() latestDate, err := s.repo.GetLatestTradeDate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
summary, err := s.repo.GetSummaryByDate(date) summary, err := s.repo.GetSummaryByDate(latestDate)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -61,8 +66,12 @@ func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) {
return summary, nil return summary, nil
} }
// GetDailySummary 获取近 N 天每日市场统计 // GetDailySummary 获取近 N 天每日市场统计,endDate 为空时查到最新交易日(走缓存)
func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, error) { func (s *DashboardService) GetDailySummary(days int, endDate string) ([]model.DailySummary, error) {
if endDate != "" {
return s.repo.GetDailySummary(days, endDate)
}
s.mu.RLock() s.mu.RLock()
if e, ok := s.daily[days]; ok && time.Now().Before(e.expires) { if e, ok := s.daily[days]; ok && time.Now().Before(e.expires) {
data := e.data data := e.data
@@ -71,7 +80,7 @@ func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, erro
} }
s.mu.RUnlock() s.mu.RUnlock()
data, err := s.repo.GetDailySummary(days) data, err := s.repo.GetDailySummary(days, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -83,8 +92,12 @@ func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, erro
return data, nil return data, nil
} }
// GetConsecutiveLimits 获取最新交易日连板股票(连续多日涨停) // GetConsecutiveLimits 获取连板股票,date 为空时查最新交易日(走缓存),指定日期时跳过缓存
func (s *DashboardService) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) { func (s *DashboardService) GetConsecutiveLimits(date string) ([]model.ConsecutiveLimit, error) {
if date != "" {
return s.repo.GetConsecutiveLimitsByDate(date)
}
s.mu.RLock() s.mu.RLock()
if s.consec != nil && time.Now().Before(s.consec.expires) { if s.consec != nil && time.Now().Before(s.consec.expires) {
data := s.consec.data data := s.consec.data
@@ -105,8 +118,12 @@ func (s *DashboardService) GetConsecutiveLimits() ([]model.ConsecutiveLimit, err
return data, nil return data, nil
} }
// GetHigh100 获取最新交易日创百日新高的股票列表 // GetHigh100 获取百日新高股票,date 为空时查最新交易日(走缓存),指定日期时跳过缓存
func (s *DashboardService) GetHigh100() ([]model.StockRank, error) { func (s *DashboardService) GetHigh100(date string) ([]model.StockRank, error) {
if date != "" {
return s.repo.GetHigh100ByDate(date)
}
s.mu.RLock() s.mu.RLock()
if s.high100 != nil && time.Now().Before(s.high100.expires) { if s.high100 != nil && time.Now().Before(s.high100.expires) {
data := s.high100.data data := s.high100.data
+12 -12
View File
@@ -45,15 +45,15 @@ export interface DailySummary {
consecutive_limit_count: number; // 连板股票数量 consecutive_limit_count: number; // 连板股票数量
} }
/** 获取市场概览数据 */ /** 获取市场概览数据date 为空时返回最新交易日数据 */
export async function getDashboardSummary(): Promise<DashboardSummary> { export async function getDashboardSummary(date?: string): Promise<DashboardSummary> {
const res = await api.get('/dashboard/summary'); const res = await api.get('/dashboard/summary', { params: date ? { date } : undefined });
return res.data.data; return res.data.data;
} }
/** 获取近 N 天每日市场统计 */ /** 获取近 N 天每日市场统计,endDate 为空时截止到最新交易日 */
export async function getDailySummary(days = 30): Promise<DailySummary[]> { export async function getDailySummary(days = 30, endDate?: string): Promise<DailySummary[]> {
const res = await api.get('/dashboard/daily', { params: { days } }); const res = await api.get('/dashboard/daily', { params: { days, ...(endDate ? { end_date: endDate } : {}) } });
return res.data.data; return res.data.data;
} }
@@ -66,15 +66,15 @@ export interface ConsecutiveLimit {
pct_change: number; pct_change: number;
} }
/** 获取连板股票统计 */ /** 获取连板股票统计date 为空时返回最新交易日数据 */
export async function getConsecutiveLimits(): Promise<ConsecutiveLimit[]> { export async function getConsecutiveLimits(date?: string): Promise<ConsecutiveLimit[]> {
const res = await api.get('/dashboard/consecutive'); const res = await api.get('/dashboard/consecutive', { params: date ? { date } : undefined });
return res.data.data; return res.data.data;
} }
/** 获取百日新高股票列表 */ /** 获取百日新高股票列表date 为空时返回最新交易日数据 */
export async function getHigh100(): Promise<StockRank[]> { export async function getHigh100(date?: string): Promise<StockRank[]> {
const res = await api.get('/dashboard/high100'); const res = await api.get('/dashboard/high100', { params: date ? { date } : undefined });
return res.data.data; return res.data.data;
} }
+21 -17
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Card, Spin, Table, Tag, Typography } from 'antd'; import { Card, Spin, Table, Tag, Typography } from 'antd';
import { getConsecutiveLimits, type ConsecutiveLimit } from '../api/dashboard'; import { getConsecutiveLimits, type ConsecutiveLimit } from '../api/dashboard';
import DateNav from '../components/DateNav';
const { Title } = Typography; const { Title } = Typography;
@@ -39,20 +40,14 @@ const columns = [
export default function ConsecutivePage() { export default function ConsecutivePage() {
const [data, setData] = useState<ConsecutiveLimit[]>([]); const [data, setData] = useState<ConsecutiveLimit[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [date, setDate] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
getConsecutiveLimits() setLoading(true);
.then(setData) getConsecutiveLimits(date)
.then((res) => setData(res || []))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, [date]);
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
<Spin size="large" />
</div>
);
}
// Group by streak // Group by streak
const grouped = data.reduce<Record<number, ConsecutiveLimit[]>>((acc, item) => { const grouped = data.reduce<Record<number, ConsecutiveLimit[]>>((acc, item) => {
@@ -65,14 +60,23 @@ export default function ConsecutivePage() {
return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Card style={{ marginBottom: 16, textAlign: 'center' }}> <Card style={{ marginBottom: 16 }} size="small">
<Title level={4} style={{ margin: 0 }}> {loading ? (
<span style={{ color: '#cf1322' }}>{data.length}</span> <div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
<span style={{ color: '#722ed1', fontSize: 28 }}>{maxStreak}</span> <Spin size="large" />
</Title> </div>
) : (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
<Title level={4} style={{ margin: 0 }}>
<span style={{ color: '#cf1322' }}>{data.length}</span>
<span style={{ color: '#722ed1', fontSize: 28 }}>{maxStreak}</span>
</Title>
<DateNav value={date} onChange={setDate} />
</div>
)}
</Card> </Card>
{Object.keys(grouped) {!loading && Object.keys(grouped)
.sort((a, b) => Number(b) - Number(a)) .sort((a, b) => Number(b) - Number(a))
.map((streak) => ( .map((streak) => (
<Card <Card
+13 -5
View File
@@ -5,6 +5,7 @@ import {
type DashboardSummary, type DashboardSummary,
} from '../api/dashboard'; } from '../api/dashboard';
import StatCards from '../components/StatCard'; import StatCards from '../components/StatCard';
import DateNav from '../components/DateNav';
const columns = [ const columns = [
{ title: '代码', dataIndex: 'code', key: 'code', width: 80 }, { title: '代码', dataIndex: 'code', key: 'code', width: 80 },
@@ -35,12 +36,14 @@ const columns = [
export default function Dashboard() { export default function Dashboard() {
const [summary, setSummary] = useState<DashboardSummary | null>(null); const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [date, setDate] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
getDashboardSummary() setLoading(true);
.then(setSummary) getDashboardSummary(date)
.then((res) => setSummary(res || null))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, [date]);
if (loading) { if (loading) {
return ( return (
@@ -54,9 +57,14 @@ export default function Dashboard() {
return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<StatCards data={summary} /> <Card style={{ marginBottom: 16 }} size="small">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
<DateNav value={date} onChange={setDate} />
</div>
<StatCards data={summary} />
</Card>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 24 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<Card title={`涨停 (${summary.limit_up_list.length}只)`} size="small"> <Card title={`涨停 (${summary.limit_up_list.length}只)`} size="small">
<Table <Table
columns={columns} columns={columns}
+35 -29
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Card, Spin, Table, Tag, Typography } from 'antd'; import { Card, Spin, Table, Tag, Typography } from 'antd';
import { getHigh100, type StockRank } from '../api/dashboard'; import { getHigh100, type StockRank } from '../api/dashboard';
import DateNav from '../components/DateNav';
const { Title } = Typography; const { Title } = Typography;
@@ -35,45 +36,50 @@ const columns = [
export default function High100Page() { export default function High100Page() {
const [data, setData] = useState<StockRank[]>([]); const [data, setData] = useState<StockRank[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [date, setDate] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
getHigh100() setLoading(true);
.then(setData) getHigh100(date)
.then((res) => setData(res || []))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, [date]);
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
<Spin size="large" />
</div>
);
}
const limitUp = data.filter((s) => s.pct_change >= 9.5).length; const limitUp = data.filter((s) => s.pct_change >= 9.5).length;
return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Card style={{ marginBottom: 16, textAlign: 'center' }}> <Card style={{ marginBottom: 16 }} size="small">
<Title level={4} style={{ margin: 0 }}> {loading ? (
150 <span style={{ color: '#cf1322', fontSize: 28 }}>{data.length}</span> <div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
{limitUp > 0 && ( <Spin size="large" />
<span style={{ color: '#722ed1', marginLeft: 16 }}> </div>
{limitUp} ) : (
</span> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
)} <Title level={4} style={{ margin: 0 }}>
</Title> 150 <span style={{ color: '#cf1322', fontSize: 28 }}>{data.length}</span>
{limitUp > 0 && (
<span style={{ color: '#722ed1', marginLeft: 16 }}>
{limitUp}
</span>
)}
</Title>
<DateNav value={date} onChange={setDate} />
</div>
)}
</Card> </Card>
<Card size="small"> {!loading && (
<Table <Card size="small">
columns={columns} <Table
dataSource={data} columns={columns}
rowKey="code" dataSource={data}
pagination={{ pageSize: 100, showSizeChanger: false, size: 'small' }} rowKey="code"
size="small" pagination={{ pageSize: 100, showSizeChanger: false, size: 'small' }}
/> size="small"
</Card> />
</Card>
)}
</div> </div>
); );
} }
+19 -14
View File
@@ -2,35 +2,40 @@ import { useEffect, useState } from 'react';
import { Card, Segmented, Spin } from 'antd'; import { Card, Segmented, Spin } from 'antd';
import { getDailySummary, type DailySummary } from '../api/dashboard'; import { getDailySummary, type DailySummary } from '../api/dashboard';
import DailyChart from '../components/DailyChart'; import DailyChart from '../components/DailyChart';
import DateNav from '../components/DateNav';
export default function TrendPage() { export default function TrendPage() {
const [daily, setDaily] = useState<DailySummary[]>([]); const [daily, setDaily] = useState<DailySummary[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [days, setDays] = useState<number>(30); const [days, setDays] = useState<number>(30);
const [endDate, setEndDate] = useState<string | undefined>(undefined);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
getDailySummary(days) getDailySummary(days, endDate)
.then(setDaily) .then((res) => setDaily(res || []))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [days]); }, [days, endDate]);
return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<Card <Card
size="small"
extra={ extra={
<Segmented <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
value={days} <DateNav value={endDate} onChange={setEndDate} />
onChange={(v) => setDays(v as number)} <Segmented
options={[ value={days}
{ label: '7天', value: 7 }, onChange={(v) => setDays(v as number)}
{ label: '30天', value: 30 }, options={[
{ label: '90天', value: 90 }, { label: '7天', value: 7 },
{ label: '180天', value: 180 }, { label: '30天', value: 30 },
]} { label: '90天', value: 90 },
/> { label: '180天', value: 180 },
]}
/>
</div>
} }
> >
{loading ? ( {loading ? (