SHA256
+18
@@ -0,0 +1,18 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
# docker 打包
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
registry: https://registry.cn-hangzhou.aliyuncs.com
|
||||
repo: registry.cn-hangzhou.aliyuncs.com/hub_z/ashareview
|
||||
tags:
|
||||
- latest
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+9
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+21
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ClaudeCodeTabState">
|
||||
<option name="tabSessions">
|
||||
<map>
|
||||
<entry key="0">
|
||||
<value>
|
||||
<TabSessionState>
|
||||
<option name="provider" value="claude" />
|
||||
<option name="sessionId" value="2695427f-f87e-45f6-80a4-bd1cdbcb850a" />
|
||||
<option name="cwd" value="$PROJECT_DIR$" />
|
||||
<option name="model" value="claude-opus-4-7[1m]" />
|
||||
<option name="permissionMode" value="bypassPermissions" />
|
||||
<option name="reasoningEffort" value="high" />
|
||||
</TabSessionState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/AShareView.iml" filepath="$PROJECT_DIR$/.idea/AShareView.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# ---- 前端构建 ----
|
||||
FROM node:20-alpine AS web-builder
|
||||
WORKDIR /app/web
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY web/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ---- 后端构建 ----
|
||||
FROM golang:1.24-alpine AS go-builder
|
||||
WORKDIR /app/server
|
||||
COPY server/go.mod server/go.sum ./
|
||||
RUN go mod download
|
||||
COPY server/ ./
|
||||
COPY --from=web-builder /app/web/dist ./static/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o ashareview .
|
||||
|
||||
# ---- 运行镜像 ----
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
WORKDIR /app
|
||||
COPY --from=go-builder /app/server/ashareview .
|
||||
COPY --from=go-builder /app/server/static ./static/
|
||||
COPY server/config.yaml ./config.yaml
|
||||
EXPOSE 8080
|
||||
CMD ["./ashareview"]
|
||||
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
ashareview:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/hub_z/ashareview:latest
|
||||
container_name: ashareview
|
||||
restart: always
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./server/config.yaml:/app/config.yaml
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
+40
-21
@@ -9,32 +9,51 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
db, err := sql.Open("mysql", "root:ttx2011@tcp(db.freeicu.top:32000)/ashare?charset=utf8mb4")
|
||||
db, err := sql.Open("mysql", "root:ttx2011@tcp(db.freeicu.top:32000)/ashare?charset=utf8mb4&parseTime=True&loc=Local")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Direct query to check limit counts
|
||||
var lu10, ld10, lu20, ld20 int
|
||||
// Test 1: dates from trading_day
|
||||
var dates []string
|
||||
rows, _ := db.Query("SELECT DATE(date) as d FROM trading_day ORDER BY date DESC LIMIT 3")
|
||||
for rows.Next() {
|
||||
var d string
|
||||
rows.Scan(&d)
|
||||
dates = append(dates, d)
|
||||
fmt.Printf("date: [%s]\n", d)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
if len(dates) == 0 {
|
||||
fmt.Println("NO DATES!")
|
||||
return
|
||||
}
|
||||
latest := dates[0]
|
||||
fmt.Printf("latest: [%s]\n", latest)
|
||||
|
||||
// Test 2: today's limit-up count
|
||||
var cnt int
|
||||
db.QueryRow("SELECT COUNT(*) FROM stock_daily WHERE date = ? AND pct_change >= 9.5", latest).Scan(&cnt)
|
||||
fmt.Printf("limit-up count on %s: %d\n", latest, cnt)
|
||||
|
||||
// Test 3: with JOIN
|
||||
var cnt2 int
|
||||
db.QueryRow(`
|
||||
SELECT
|
||||
SUM(CASE WHEN pct_change >= 9.5 AND pct_change < 19.5 THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN pct_change <= -9.5 AND pct_change > -19.5 THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN pct_change >= 19.5 THEN 1 ELSE 0 END),
|
||||
SUM(CASE WHEN pct_change <= -19.5 THEN 1 ELSE 0 END)
|
||||
FROM stock_daily WHERE date = '2026-05-08'
|
||||
`).Scan(&lu10, &ld10, &lu20, &ld20)
|
||||
fmt.Printf("Direct SQL: limit_up_10=%d, limit_down_10=%d, limit_up_20=%d, limit_down_20=%d\n", lu10, ld10, lu20, ld20)
|
||||
SELECT COUNT(*)
|
||||
FROM stock_daily sd
|
||||
LEFT JOIN stock_info si ON sd.code = si.code
|
||||
WHERE sd.date = ? AND sd.pct_change >= 9.5
|
||||
`, latest).Scan(&cnt2)
|
||||
fmt.Printf("limit-up with JOIN: %d\n", cnt2)
|
||||
|
||||
// Count with broader threshold
|
||||
var cnt int
|
||||
db.QueryRow(`SELECT COUNT(*) FROM stock_daily WHERE date = '2026-05-08' AND pct_change >= 9.0`).Scan(&cnt)
|
||||
fmt.Printf("pct >= 9.0: %d\n", cnt)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM stock_daily WHERE date = '2026-05-08' AND pct_change >= 19.0`).Scan(&cnt)
|
||||
fmt.Printf("pct >= 19.0: %d\n", cnt)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM stock_daily WHERE date = '2026-05-08' AND pct_change <= -9.0`).Scan(&cnt)
|
||||
fmt.Printf("pct <= -9.0: %d\n", cnt)
|
||||
db.QueryRow(`SELECT COUNT(*) FROM stock_daily WHERE date = '2026-05-08' AND pct_change <= -19.0`).Scan(&cnt)
|
||||
fmt.Printf("pct <= -19.0: %d\n", cnt)
|
||||
// Test 4: limit-up rows
|
||||
rows2, _ := db.Query("SELECT code, DATE(date) as d FROM stock_daily WHERE pct_change >= 9.5 AND date >= ? LIMIT 5", dates[len(dates)-1])
|
||||
for rows2.Next() {
|
||||
var code, d string
|
||||
rows2.Scan(&code, &d)
|
||||
fmt.Printf(" limitRow: code=%s date=[%s]\n", code, d)
|
||||
}
|
||||
rows2.Close()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# MySQL 数据库连接配置
|
||||
mysql:
|
||||
host: "db.freeicu.top"
|
||||
port: 32000
|
||||
user: "root"
|
||||
password: "ttx2011"
|
||||
database: "ashare"
|
||||
charset: "utf8mb4"
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
port: "8080"
|
||||
+61
-14
@@ -3,39 +3,86 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type DBConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
DBName string
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
DBName string `yaml:"database"`
|
||||
Charset string `yaml:"charset"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string `yaml:"port"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DB DBConfig
|
||||
Server struct {
|
||||
Port string
|
||||
}
|
||||
DB DBConfig `yaml:"mysql"`
|
||||
Server ServerConfig `yaml:"server"`
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
cfg := &Config{}
|
||||
|
||||
cfg.DB.Host = getEnv("DB_HOST", "db.freeicu.top")
|
||||
cfg.DB.Port = getEnv("DB_PORT", "32000")
|
||||
data, err := os.ReadFile("config.yaml")
|
||||
if err != nil {
|
||||
fmt.Println("config.yaml not found, using env defaults")
|
||||
cfg.DB.Host = getEnv("DB_HOST", "127.0.0.1")
|
||||
cfg.DB.Port = getEnv("DB_PORT", "3306")
|
||||
cfg.DB.User = getEnv("DB_USER", "root")
|
||||
cfg.DB.Password = getEnv("DB_PASSWORD", "ttx2011")
|
||||
cfg.DB.Password = getEnv("DB_PASSWORD", "")
|
||||
cfg.DB.DBName = getEnv("DB_NAME", "ashare")
|
||||
cfg.DB.Charset = "utf8mb4"
|
||||
cfg.Server.Port = getEnv("SERVER_PORT", "8080")
|
||||
return cfg
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
fmt.Printf("parse config.yaml failed: %v, using env defaults\n", err)
|
||||
cfg.DB.Host = getEnv("DB_HOST", "127.0.0.1")
|
||||
cfg.DB.Port = getEnv("DB_PORT", "3306")
|
||||
cfg.DB.User = getEnv("DB_USER", "root")
|
||||
cfg.DB.Password = getEnv("DB_PASSWORD", "")
|
||||
cfg.DB.DBName = getEnv("DB_NAME", "ashare")
|
||||
cfg.DB.Charset = "utf8mb4"
|
||||
cfg.Server.Port = getEnv("SERVER_PORT", "8080")
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Environment variables override config file
|
||||
if v := os.Getenv("DB_HOST"); v != "" {
|
||||
cfg.DB.Host = v
|
||||
}
|
||||
if v := os.Getenv("DB_PORT"); v != "" {
|
||||
cfg.DB.Port = v
|
||||
}
|
||||
if v := os.Getenv("DB_USER"); v != "" {
|
||||
cfg.DB.User = v
|
||||
}
|
||||
if v := os.Getenv("DB_PASSWORD"); v != "" {
|
||||
cfg.DB.Password = v
|
||||
}
|
||||
if v := os.Getenv("DB_NAME"); v != "" {
|
||||
cfg.DB.DBName = v
|
||||
}
|
||||
if v := os.Getenv("SERVER_PORT"); v != "" {
|
||||
cfg.Server.Port = v
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c *DBConfig) DSN() string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
c.User, c.Password, c.Host, c.Port, c.DBName)
|
||||
charset := c.Charset
|
||||
if charset == "" {
|
||||
charset = "utf8mb4"
|
||||
}
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
|
||||
c.User, c.Password, c.Host, c.Port, c.DBName, charset)
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
|
||||
+2
-1
@@ -4,6 +4,8 @@ go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
@@ -19,7 +21,6 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
|
||||
@@ -42,6 +42,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
@@ -59,6 +63,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -92,6 +98,8 @@ golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -37,3 +37,12 @@ func (h *DashboardHandler) GetDailySummary(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": data})
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) GetConsecutiveLimits(c *gin.Context) {
|
||||
data, err := h.svc.GetConsecutiveLimits()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": data})
|
||||
}
|
||||
|
||||
+11
-2
@@ -58,6 +58,15 @@ type DashboardSummary struct {
|
||||
FlatCount int64 `json:"flat_count"`
|
||||
TotalTurnover float64 `json:"total_turnover"`
|
||||
AvgPctChange float64 `json:"avg_pct_change"`
|
||||
TopGainers []StockRank `json:"top_gainers"`
|
||||
TopLosers []StockRank `json:"top_losers"`
|
||||
LimitUpList []StockRank `json:"limit_up_list"`
|
||||
LimitDownList []StockRank `json:"limit_down_list"`
|
||||
High100List []StockRank `json:"high_100_list"`
|
||||
}
|
||||
|
||||
type ConsecutiveLimit struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Streak int `json:"streak"`
|
||||
Close float64 `json:"close"`
|
||||
PctChange float64 `json:"pct_change"`
|
||||
}
|
||||
|
||||
@@ -57,18 +57,32 @@ func (r *StockRepo) GetSummaryByDate(date string) (*model.DashboardSummary, erro
|
||||
r.db.Table("stock_daily as sd").
|
||||
Select("sd.code, si.name, sd.pct_change, sd.close").
|
||||
Joins("LEFT JOIN stock_info si ON sd.code = si.code").
|
||||
Where("sd.date = ? AND sd.pct_change IS NOT NULL", date).
|
||||
Where("sd.date = ? AND sd.pct_change >= 9.5", date).
|
||||
Order("sd.pct_change DESC").
|
||||
Limit(5).
|
||||
Find(&summary.TopGainers)
|
||||
Find(&summary.LimitUpList)
|
||||
|
||||
r.db.Table("stock_daily as sd").
|
||||
Select("sd.code, si.name, sd.pct_change, sd.close").
|
||||
Joins("LEFT JOIN stock_info si ON sd.code = si.code").
|
||||
Where("sd.date = ? AND sd.pct_change IS NOT NULL", date).
|
||||
Where("sd.date = ? AND sd.pct_change <= -9.5", date).
|
||||
Order("sd.pct_change ASC").
|
||||
Limit(5).
|
||||
Find(&summary.TopLosers)
|
||||
Find(&summary.LimitDownList)
|
||||
|
||||
r.db.Raw(`
|
||||
SELECT sd.code, si.name, sd.pct_change, sd.close
|
||||
FROM stock_daily sd
|
||||
LEFT JOIN stock_info si ON sd.code = si.code
|
||||
WHERE sd.date = ?
|
||||
AND sd.close = (
|
||||
SELECT MAX(sd2.close)
|
||||
FROM stock_daily sd2
|
||||
WHERE sd2.code = sd.code
|
||||
AND sd2.date <= sd.date
|
||||
AND sd2.date >= DATE_SUB(sd.date, INTERVAL 150 DAY)
|
||||
)
|
||||
AND sd.close > 0
|
||||
ORDER BY sd.pct_change DESC
|
||||
`, date).Scan(&summary.High100List)
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
@@ -97,3 +111,85 @@ func (r *StockRepo) GetDailySummary(days int) ([]model.DailySummary, error) {
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *StockRepo) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) {
|
||||
// Get latest trade date from stock_daily
|
||||
type dateResult struct {
|
||||
D string `gorm:"column:d"`
|
||||
}
|
||||
var dr dateResult
|
||||
r.db.Raw("SELECT MAX(DATE(date)) as d FROM stock_daily").Scan(&dr)
|
||||
latest := dr.D
|
||||
if latest == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get recent trading dates up to latest
|
||||
var dates []string
|
||||
r.db.Raw(`SELECT DATE(date) as d FROM stock_daily WHERE date <= ? GROUP BY date ORDER BY date DESC LIMIT 20`, latest).Scan(&dates)
|
||||
if len(dates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
earliest := dates[len(dates)-1]
|
||||
|
||||
// Get all limit-up records for recent dates
|
||||
type limitRow struct {
|
||||
Code string
|
||||
D string
|
||||
}
|
||||
var rows []limitRow
|
||||
r.db.Raw(`SELECT code, DATE(date) as d FROM stock_daily WHERE pct_change >= 9.5 AND date >= ?`, earliest).Scan(&rows)
|
||||
|
||||
// Build set of (code, date) for limit-up
|
||||
limitSet := make(map[string]bool)
|
||||
for _, row := range rows {
|
||||
limitSet[row.Code+"_"+row.D] = true
|
||||
}
|
||||
|
||||
// Get today's limit-up stocks with info
|
||||
type stockInfo struct {
|
||||
Code string `gorm:"column:code"`
|
||||
Name string `gorm:"column:name"`
|
||||
Close float64 `gorm:"column:close"`
|
||||
PctChange float64 `gorm:"column:pct_change"`
|
||||
}
|
||||
var todayLimit []stockInfo
|
||||
r.db.Raw(`
|
||||
SELECT sd.code, si.name, sd.close, sd.pct_change
|
||||
FROM stock_daily sd
|
||||
LEFT JOIN stock_info si ON sd.code = si.code
|
||||
WHERE sd.date = ? AND sd.pct_change >= 9.5
|
||||
ORDER BY sd.pct_change DESC
|
||||
`, latest).Scan(&todayLimit)
|
||||
|
||||
// For each stock, count consecutive limit-up days backwards from latest
|
||||
var result []model.ConsecutiveLimit
|
||||
for _, s := range todayLimit {
|
||||
streak := 0
|
||||
for _, d := range dates {
|
||||
if limitSet[s.Code+"_"+d] {
|
||||
streak++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, model.ConsecutiveLimit{
|
||||
Code: s.Code,
|
||||
Name: s.Name,
|
||||
Streak: streak,
|
||||
Close: s.Close,
|
||||
PctChange: s.PctChange,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by streak desc
|
||||
for i := 0; i < len(result); i++ {
|
||||
for j := i + 1; j < len(result); j++ {
|
||||
if result[j].Streak > result[i].Streak || (result[j].Streak == result[i].Streak && result[j].PctChange > result[i].PctChange) {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func Setup(r *gin.Engine, dh *handler.DashboardHandler) {
|
||||
{
|
||||
api.GET("/dashboard/summary", dh.GetSummary)
|
||||
api.GET("/dashboard/daily", dh.GetDailySummary)
|
||||
api.GET("/dashboard/consecutive", dh.GetConsecutiveLimits)
|
||||
}
|
||||
|
||||
// Serve frontend static files
|
||||
|
||||
@@ -24,3 +24,7 @@ func (s *DashboardService) GetSummary() (*model.DashboardSummary, error) {
|
||||
func (s *DashboardService) GetDailySummary(days int) ([]model.DailySummary, error) {
|
||||
return s.repo.GetDailySummary(days)
|
||||
}
|
||||
|
||||
func (s *DashboardService) GetConsecutiveLimits() ([]model.ConsecutiveLimit, error) {
|
||||
return s.repo.GetConsecutiveLimits()
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>web</title>
|
||||
<script type="module" crossorigin src="/assets/index-B76CqnWZ.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-hDKBor3F.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DGNrK5qb.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+5
-1
@@ -5,14 +5,17 @@ import zhCN from 'antd/locale/zh_CN';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
LineChartOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import TrendPage from './pages/TrendPage';
|
||||
import ConsecutivePage from './pages/ConsecutivePage';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
|
||||
const menuItems = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: '市场概览' },
|
||||
{ key: '/consecutive', icon: <ThunderboltOutlined />, label: '连板高度' },
|
||||
{ key: '/trend', icon: <LineChartOutlined />, label: '每日趋势' },
|
||||
];
|
||||
|
||||
@@ -35,9 +38,10 @@ function AppLayout() {
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
</Header>
|
||||
<Content style={{ padding: '24px', background: '#f5f5f5' }}>
|
||||
<Content style={{ padding: 24, background: '#f5f5f5' }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/consecutive" element={<ConsecutivePage />} />
|
||||
<Route path="/trend" element={<TrendPage />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
|
||||
@@ -18,8 +18,9 @@ export interface DashboardSummary {
|
||||
flat_count: number;
|
||||
total_turnover: number;
|
||||
avg_pct_change: number;
|
||||
top_gainers: StockRank[];
|
||||
top_losers: StockRank[];
|
||||
limit_up_list: StockRank[];
|
||||
limit_down_list: StockRank[];
|
||||
high_100_list: StockRank[];
|
||||
}
|
||||
|
||||
export interface DailySummary {
|
||||
@@ -45,3 +46,16 @@ export async function getDailySummary(days = 30): Promise<DailySummary[]> {
|
||||
const res = await api.get('/dashboard/daily', { params: { days } });
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
export interface ConsecutiveLimit {
|
||||
code: string;
|
||||
name: string;
|
||||
streak: number;
|
||||
close: number;
|
||||
pct_change: number;
|
||||
}
|
||||
|
||||
export async function getConsecutiveLimits(): Promise<ConsecutiveLimit[]> {
|
||||
const res = await api.get('/dashboard/consecutive');
|
||||
return res.data.data;
|
||||
}
|
||||
|
||||
@@ -22,43 +22,35 @@ export default function DailyChart({ data }: Props) {
|
||||
}
|
||||
const chart = chartRef.current;
|
||||
|
||||
const dates = data.map((d) => d.date);
|
||||
const dates = data.map((d) => d.date.slice(0, 10));
|
||||
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' },
|
||||
},
|
||||
legend: [
|
||||
{
|
||||
legend: {
|
||||
top: 0,
|
||||
data: ['上涨数', '下跌数', '平均涨跌幅(%)'],
|
||||
data: ['上涨数', '下跌数', '成交额(亿)', '涨停10%', '跌停10%', '涨停20%', '跌停20%'],
|
||||
},
|
||||
{
|
||||
top: 25,
|
||||
data: ['涨停(10%)', '跌停(10%)', '涨停(20%)', '跌停(20%)'],
|
||||
},
|
||||
],
|
||||
grid: [
|
||||
{ left: 60, right: 20, top: 70, height: '25%' },
|
||||
{ left: 60, right: 20, top: '48%', height: '20%' },
|
||||
{ left: 60, right: 20, top: '76%', height: '18%' },
|
||||
{ left: 60, right: 70, top: 60, height: '30%' },
|
||||
{ left: 60, right: 70, top: '50%', height: '44%' },
|
||||
],
|
||||
xAxis: [
|
||||
{ type: 'category', data: dates, gridIndex: 0, boundaryGap: false },
|
||||
{ type: 'category', data: dates, gridIndex: 1, boundaryGap: false, show: false },
|
||||
{ type: 'category', data: dates, gridIndex: 2, boundaryGap: false },
|
||||
{ type: 'category', data: dates, gridIndex: 1, boundaryGap: false },
|
||||
],
|
||||
yAxis: [
|
||||
{ type: 'value', name: '股票数', gridIndex: 0 },
|
||||
{ type: 'value', name: '家', gridIndex: 1 },
|
||||
{ type: 'value', name: '亿', gridIndex: 2 },
|
||||
{ type: 'value', name: '亿', gridIndex: 0, position: 'right', splitLine: { show: false } },
|
||||
{ type: 'value', name: '10%家', gridIndex: 1, position: 'left' },
|
||||
{ type: 'value', name: '20%家', gridIndex: 1, position: 'right', splitLine: { show: false } },
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0, 1, 2], start: 0, end: 100 },
|
||||
{ type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 },
|
||||
],
|
||||
series: [
|
||||
// 涨跌概览
|
||||
{
|
||||
name: '上涨数',
|
||||
type: 'line',
|
||||
@@ -79,69 +71,61 @@ export default function DailyChart({ data }: Props) {
|
||||
lineStyle: { width: 2 },
|
||||
symbol: 'none',
|
||||
},
|
||||
{
|
||||
name: '平均涨跌幅(%)',
|
||||
type: 'line',
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 0,
|
||||
data: data.map((d) => +d.avg_pct_change.toFixed(2)),
|
||||
itemStyle: { color: '#1677ff' },
|
||||
lineStyle: { width: 2, type: 'dashed' },
|
||||
symbol: 'none',
|
||||
},
|
||||
// 涨停跌停
|
||||
{
|
||||
name: '涨停(10%)',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
stack: 'limit',
|
||||
data: data.map((d) => d.limit_up_10),
|
||||
itemStyle: { color: '#ff4d4f' },
|
||||
barMaxWidth: 12,
|
||||
},
|
||||
{
|
||||
name: '涨停(20%)',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
stack: 'limit',
|
||||
data: data.map((d) => d.limit_up_20),
|
||||
itemStyle: { color: '#ff7875' },
|
||||
barMaxWidth: 12,
|
||||
},
|
||||
{
|
||||
name: '跌停(10%)',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
stack: 'limit_down',
|
||||
data: data.map((d) => -d.limit_down_10),
|
||||
itemStyle: { color: '#52c41a' },
|
||||
barMaxWidth: 12,
|
||||
},
|
||||
{
|
||||
name: '跌停(20%)',
|
||||
type: 'bar',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
stack: 'limit_down',
|
||||
data: data.map((d) => -d.limit_down_20),
|
||||
itemStyle: { color: '#95de64' },
|
||||
barMaxWidth: 12,
|
||||
},
|
||||
// 成交额
|
||||
{
|
||||
name: '成交额(亿)',
|
||||
type: 'line',
|
||||
xAxisIndex: 2,
|
||||
yAxisIndex: 2,
|
||||
xAxisIndex: 0,
|
||||
yAxisIndex: 1,
|
||||
data: data.map((d) => fmtYi(d.total_turnover)),
|
||||
areaStyle: { color: 'rgba(22,119,255,0.15)' },
|
||||
itemStyle: { color: '#1677ff' },
|
||||
itemStyle: { color: 'rgba(114,46,209,0.6)' },
|
||||
lineStyle: { width: 2 },
|
||||
areaStyle: { color: 'rgba(114,46,209,0.15)' },
|
||||
symbol: 'none',
|
||||
},
|
||||
{
|
||||
name: '涨停10%',
|
||||
type: 'line',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 2,
|
||||
data: data.map((d) => d.limit_up_10),
|
||||
itemStyle: { color: '#ff4d4f' },
|
||||
lineStyle: { width: 2 },
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
},
|
||||
{
|
||||
name: '跌停10%',
|
||||
type: 'line',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 2,
|
||||
data: data.map((d) => -d.limit_down_10),
|
||||
itemStyle: { color: '#52c41a' },
|
||||
lineStyle: { width: 2 },
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
},
|
||||
{
|
||||
name: '涨停20%',
|
||||
type: 'line',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 3,
|
||||
data: data.map((d) => d.limit_up_20),
|
||||
itemStyle: { color: '#ff7875' },
|
||||
lineStyle: { width: 2, type: 'dashed' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
},
|
||||
{
|
||||
name: '跌停20%',
|
||||
type: 'line',
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 3,
|
||||
data: data.map((d) => -d.limit_down_20),
|
||||
itemStyle: { color: '#95de64' },
|
||||
lineStyle: { width: 2, type: 'dashed' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
},
|
||||
],
|
||||
}, true);
|
||||
|
||||
@@ -150,5 +134,5 @@ export default function DailyChart({ data }: Props) {
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [data]);
|
||||
|
||||
return <div ref={ref} style={{ height: 640 }} />;
|
||||
return <div ref={ref} style={{ height: 600 }} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Table, Tag, Typography } from 'antd';
|
||||
import { getConsecutiveLimits, type ConsecutiveLimit } from '../api/dashboard';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '连板',
|
||||
dataIndex: 'streak',
|
||||
key: 'streak',
|
||||
width: 70,
|
||||
render: (v: number) => (
|
||||
<Tag color={v >= 5 ? '#722ed1' : v >= 3 ? '#cf1322' : '#fa541c'} style={{ fontWeight: 'bold', fontSize: 14 }}>
|
||||
{v}连板
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '代码', dataIndex: 'code', key: 'code', width: 80 },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 100 },
|
||||
{
|
||||
title: '最新价',
|
||||
dataIndex: 'close',
|
||||
key: 'close',
|
||||
width: 80,
|
||||
render: (v: number) => v?.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '涨跌幅',
|
||||
dataIndex: 'pct_change',
|
||||
key: 'pct_change',
|
||||
width: 90,
|
||||
render: (v: number) => (
|
||||
<Tag color="red">{v >= 0 ? '+' : ''}{v?.toFixed(2)}%</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function ConsecutivePage() {
|
||||
const [data, setData] = useState<ConsecutiveLimit[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getConsecutiveLimits()
|
||||
.then(setData)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Group by streak
|
||||
const grouped = data.reduce<Record<number, ConsecutiveLimit[]>>((acc, item) => {
|
||||
if (!acc[item.streak]) acc[item.streak] = [];
|
||||
acc[item.streak].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const maxStreak = data.length > 0 ? data[0].streak : 0;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
连板高度
|
||||
</Title>
|
||||
|
||||
<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>
|
||||
|
||||
{Object.keys(grouped)
|
||||
.sort((a, b) => Number(b) - Number(a))
|
||||
.map((streak) => (
|
||||
<Card
|
||||
key={streak}
|
||||
title={`${streak}连板 (${grouped[Number(streak)].length}只)`}
|
||||
size="small"
|
||||
style={{ marginBottom: 12 }}
|
||||
>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={grouped[Number(streak)]}
|
||||
rowKey="code"
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+32
-16
@@ -8,26 +8,29 @@ import StatCards from '../components/StatCard';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const rankColumns = [
|
||||
{ title: '代码', dataIndex: 'code', key: 'code', width: 90 },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 110 },
|
||||
const columns = [
|
||||
{ title: '代码', dataIndex: 'code', key: 'code', width: 80 },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 100 },
|
||||
{
|
||||
title: '最新价',
|
||||
dataIndex: 'close',
|
||||
key: 'close',
|
||||
width: 90,
|
||||
width: 80,
|
||||
render: (v: number) => v?.toFixed(2),
|
||||
},
|
||||
{
|
||||
title: '涨跌幅',
|
||||
dataIndex: 'pct_change',
|
||||
key: 'pct_change',
|
||||
width: 100,
|
||||
render: (v: number) => (
|
||||
<Tag color={v >= 0 ? 'red' : 'green'}>
|
||||
width: 90,
|
||||
render: (v: number) => {
|
||||
const is20 = Math.abs(v) >= 19.5;
|
||||
return (
|
||||
<Tag color={v >= 0 ? 'red' : 'green'} style={is20 ? { fontWeight: 'bold' } : undefined}>
|
||||
{v >= 0 ? '+' : ''}{v?.toFixed(2)}%
|
||||
</Tag>
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -60,25 +63,38 @@ export default function Dashboard() {
|
||||
<StatCards data={summary} />
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 24 }}>
|
||||
<Card title="涨幅前5" size="small">
|
||||
<Card title={`涨停 (${summary.limit_up_list.length}只)`} size="small">
|
||||
<Table
|
||||
columns={rankColumns}
|
||||
dataSource={summary.top_gainers}
|
||||
columns={columns}
|
||||
dataSource={summary.limit_up_list}
|
||||
rowKey="code"
|
||||
pagination={false}
|
||||
pagination={{ pageSize: 50, size: 'small', showSizeChanger: false }}
|
||||
size="small"
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
</Card>
|
||||
<Card title="跌幅前5" size="small">
|
||||
<Card title={`跌停 (${summary.limit_down_list.length}只)`} size="small">
|
||||
<Table
|
||||
columns={rankColumns}
|
||||
dataSource={summary.top_losers}
|
||||
columns={columns}
|
||||
dataSource={summary.limit_down_list}
|
||||
rowKey="code"
|
||||
pagination={false}
|
||||
pagination={{ pageSize: 50, size: 'small', showSizeChanger: false }}
|
||||
size="small"
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card title={`百日新高 (${summary.high_100_list.length}只)`} size="small" style={{ marginTop: 16 }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={summary.high_100_list}
|
||||
rowKey="code"
|
||||
pagination={{ pageSize: 50, size: 'small', showSizeChanger: false }}
|
||||
size="small"
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Segmented, Spin, Typography } from 'antd';
|
||||
import { getDailySummary, type DailySummary } from '../api/dashboard';
|
||||
import DailyChart from '../components/DailyChart';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function TrendPage() {
|
||||
const [daily, setDaily] = useState<DailySummary[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [days, setDays] = useState<number>(30);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
getDailySummary(days)
|
||||
.then(setDaily)
|
||||
.finally(() => setLoading(false));
|
||||
}, [days]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
每日趋势
|
||||
</Title>
|
||||
|
||||
<Card
|
||||
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 },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 60 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<DailyChart data={daily} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user