SHA256
+16
-12
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user