SHA256
+14
-8
@@ -28,18 +28,24 @@ func main() {
|
||||
}
|
||||
fmt.Println("Database connected successfully")
|
||||
|
||||
// 异步创建常用索引,避免首次查询慢
|
||||
// 异步创建常用索引,已存在则跳过
|
||||
go func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
indexes := []string{
|
||||
"ALTER TABLE stock_daily ADD INDEX idx_sd_date (date)",
|
||||
"ALTER TABLE stock_daily ADD INDEX idx_sd_code_date (code, date)",
|
||||
"ALTER TABLE stock_daily ADD INDEX idx_sd_date_pct (date, pct_change)",
|
||||
"ALTER TABLE stock_daily ADD INDEX idx_sd_code_date_close (code, date, close)",
|
||||
indexes := []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{"idx_sd_date", "ALTER TABLE stock_daily ADD INDEX idx_sd_date (date)"},
|
||||
{"idx_sd_code_date", "ALTER TABLE stock_daily ADD INDEX idx_sd_code_date (code, date)"},
|
||||
{"idx_sd_date_pct", "ALTER TABLE stock_daily ADD INDEX idx_sd_date_pct (date, pct_change)"},
|
||||
}
|
||||
for _, idx := range indexes {
|
||||
if err := db.Exec(idx).Error; err == nil {
|
||||
fmt.Printf("index created: %s\n", idx)
|
||||
var cnt int64
|
||||
db.Raw("SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'stock_daily' AND index_name = ?", idx.name).Scan(&cnt)
|
||||
if cnt == 0 {
|
||||
if err := db.Exec(idx.sql).Error; err == nil {
|
||||
fmt.Printf("index created: %s\n", idx.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -120,5 +120,5 @@ export default function IndexKlineChart({ data, title = '上证指数' }: Props)
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [data, title]);
|
||||
|
||||
return <div ref={ref} style={{ height: 400 }} />;
|
||||
return <div ref={ref} style={{ width: '100%', height: 'calc(100vh - 240px)', minHeight: 400 }} />;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,14 @@ export default function OverlayKlineChart({ series }: Props) {
|
||||
}
|
||||
const baseDates = [...dateSet].sort();
|
||||
|
||||
// 日期交集为空时无法绘制,清空图表退出
|
||||
if (baseDates.length === 0) {
|
||||
if (chartRef.current) {
|
||||
chartRef.current.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 2. 建立日期→数据的快速查找映射 ──
|
||||
const dateMaps = series.map((s) => new Map(s.data.map((d) => [d.date.slice(0, 10), d])));
|
||||
|
||||
|
||||
@@ -64,11 +64,7 @@ export default function ConsecutivePage() {
|
||||
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>
|
||||
|
||||
<div style={{ width: '100%' }}>
|
||||
<Card style={{ marginBottom: 16, textAlign: 'center' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
今日涨停 <span style={{ color: '#cf1322' }}>{data.length}</span> 只,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Table, Tag, Typography } from 'antd';
|
||||
import { Card, Spin, Table, Tag } from 'antd';
|
||||
import {
|
||||
getDashboardSummary,
|
||||
type DashboardSummary,
|
||||
} from '../api/dashboard';
|
||||
import StatCards from '../components/StatCard';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const columns = [
|
||||
{ title: '代码', dataIndex: 'code', key: 'code', width: 80 },
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 100 },
|
||||
@@ -55,11 +53,7 @@ export default function Dashboard() {
|
||||
if (!summary) return <div>加载失败</div>;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
A股市场概览
|
||||
</Title>
|
||||
|
||||
<div style={{ width: '100%' }}>
|
||||
<StatCards data={summary} />
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 24 }}>
|
||||
|
||||
@@ -53,11 +53,7 @@ export default function High100Page() {
|
||||
const limitUp = data.filter((s) => s.pct_change >= 9.5).length;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 800, margin: '0 auto' }}>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
百日新高
|
||||
</Title>
|
||||
|
||||
<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> 只
|
||||
|
||||
+47
-52
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* 指数行情页面
|
||||
* 左侧显示指数列表(上证指数、深证成指等),右侧显示选中指数的 K 线图
|
||||
* 左侧显示指数列表,右侧显示选中指数的 K 线图,无左右留白
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, List, Spin, Typography } from 'antd';
|
||||
import { getIndexKline, type IndexKline } from '../api/dashboard';
|
||||
import IndexKlineChart from '../components/IndexKlineChart';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Text } = Typography;
|
||||
|
||||
/** 支持的指数列表 */
|
||||
const INDICES = [
|
||||
@@ -35,58 +35,53 @@ export default function IndexPage() {
|
||||
const latest = kline.length > 0 ? kline[kline.length - 1] : null;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: 24 }}>
|
||||
指数行情
|
||||
</Title>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '160px 1fr', gap: 16 }}>
|
||||
{/* 左侧指数列表 */}
|
||||
<Card size="small" style={{ padding: 0 }}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={INDICES}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setSelected(item.code)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
background: item.code === selected ? '#e6f7ff' : undefined,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Text strong={item.code === selected}>{item.name}</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 右侧 K 线图 */}
|
||||
<Card
|
||||
title={INDICES.find((i) => i.code === selected)?.name}
|
||||
<div style={{ height: '100%', display: 'flex', gap: 8 }}>
|
||||
{/* 左侧指数列表 */}
|
||||
<Card size="small" style={{ padding: 0, flexShrink: 0, width: 120 }}>
|
||||
<List
|
||||
size="small"
|
||||
>
|
||||
{/* 最新收盘价和涨跌幅 */}
|
||||
{latest && (
|
||||
<div style={{ marginBottom: 8, fontSize: 16 }}>
|
||||
<Text strong style={{ fontSize: 22 }}>{latest.close.toFixed(2)}</Text>
|
||||
<Text
|
||||
style={{ marginLeft: 12, color: latest.pct_change >= 0 ? '#cf1322' : '#3f8600' }}
|
||||
>
|
||||
{latest.pct_change >= 0 ? '+' : ''}{latest.pct_change.toFixed(2)}%
|
||||
</Text>
|
||||
</div>
|
||||
dataSource={INDICES}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
onClick={() => setSelected(item.code)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
padding: '8px 12px',
|
||||
background: item.code === selected ? '#e6f7ff' : undefined,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Text strong={item.code === selected}>{item.name}</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<IndexKlineChart data={kline} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 右侧 K 线图 */}
|
||||
<Card
|
||||
title={INDICES.find((i) => i.code === selected)?.name}
|
||||
size="small"
|
||||
style={{ flex: 1, overflow: 'hidden' }}
|
||||
>
|
||||
{/* 最新收盘价和涨跌幅 */}
|
||||
{latest && (
|
||||
<div style={{ marginBottom: 8, fontSize: 16 }}>
|
||||
<Text strong style={{ fontSize: 22 }}>{latest.close.toFixed(2)}</Text>
|
||||
<Text
|
||||
style={{ marginLeft: 12, color: latest.pct_change >= 0 ? '#cf1322' : '#3f8600' }}
|
||||
>
|
||||
{latest.pct_change >= 0 ? '+' : ''}{latest.pct_change.toFixed(2)}%
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<IndexKlineChart data={kline} />
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function OverlayPage() {
|
||||
|
||||
try {
|
||||
const res = await getStockKline(trimmed);
|
||||
if (!res.data || res.data.length === 0) {
|
||||
if (!res || !res.data || res.data.length === 0) {
|
||||
// 无数据时移除该条目
|
||||
setStocks((prev) => prev.filter((s) => s.code !== trimmed));
|
||||
message.error(trimmed + ' 无数据');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, Segmented, Spin, Typography } from 'antd';
|
||||
import { Card, Segmented, Spin } 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[]>([]);
|
||||
@@ -18,10 +17,7 @@ export default function TrendPage() {
|
||||
}, [days]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1200, margin: '0 auto' }}>
|
||||
<Title level={2} style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
每日趋势
|
||||
</Title>
|
||||
<div style={{ width: '100%' }}>
|
||||
|
||||
<Card
|
||||
extra={
|
||||
|
||||
Reference in New Issue
Block a user