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