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