feat: AI分析单页展示 + 主题切换 + 移动端适配

- AI分析页改为直接展示最新报告,历史存数据库
- 新增浅色/深色/系统自动主题切换
- 市场看板、AI分析页适配主题变量
- 移动端表格可滑动、按钮自动换行
- Mermaid图表支持
- Markdown表格渲染支持(remark-gfm)
This commit is contained in:
Sakurasan
2026-09-01 12:33:27 +08:00
parent 912a224b6b
commit 6478e0d403
16 changed files with 614 additions and 407 deletions
+4
View File
@@ -1,6 +1,7 @@
import * as React from 'react'
import { Outlet, createRootRoute } from '@tanstack/react-router'
import { Toaster } from 'sonner'
import { ThemeToggle } from '../components/ThemeToggle'
export const Route = createRootRoute({
component: RootComponent,
@@ -11,6 +12,9 @@ function RootComponent() {
<React.Fragment>
<Outlet />
<Toaster position="top-center" richColors />
<div className="fixed bottom-4 right-4 z-50">
<ThemeToggle />
</div>
</React.Fragment>
)
}
-138
View File
@@ -1,138 +0,0 @@
import * as React from "react";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ArrowLeft, BrainCircuit, Clock, Loader2, AlertCircle, RefreshCw, Wrench } from "lucide-react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import Markdown from "react-markdown";
import { Mermaid } from "../components/Mermaid";
import { fetchAiReport, regenerateAiReport } from "../lib/ai-analysis-api";
import { Button } from "../components/ui/button";
import { Card, CardContent } from "../components/ui/card";
import { toast } from "sonner";
export const Route = createFileRoute("/ai-analysis/$id")({
component: AiReportDetailPage,
});
function AiReportDetailPage() {
const { id } = Route.useParams();
const queryClient = useQueryClient();
const reportId = Number(id);
const { data: report, isLoading, isError, refetch } = useQuery({
queryKey: ["ai-report", reportId],
queryFn: () => fetchAiReport(reportId),
staleTime: 60_000,
retry: false,
});
const regenerateMutation = useMutation({
mutationFn: () => regenerateAiReport(reportId),
onSuccess: () => {
toast.success("重新生成完成");
queryClient.invalidateQueries({ queryKey: ["ai-report", reportId] });
queryClient.invalidateQueries({ queryKey: ["ai-reports"] });
},
onError: (err: Error) => {
toast.error(err.message || "重新生成失败");
},
});
return (
<div className="min-h-screen bg-[#0d1117] text-[#e6edf3]">
<div className="max-w-4xl mx-auto px-4 py-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Link to="/ai-analysis" className="text-[#8b949e] hover:text-[#e6edf3] transition-colors">
<ArrowLeft className="h-5 w-5" />
</Link>
<h1 className="text-xl md:text-2xl font-bold flex items-center gap-2">
<BrainCircuit className="h-5 w-5 text-[#58a6ff]" />
{report?.title || "AI 分析报告"}
</h1>
</div>
<Button
size="sm"
variant="outline"
onClick={() => regenerateMutation.mutate()}
disabled={regenerateMutation.isPending}
className="gap-1.5"
>
{regenerateMutation.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
重新生成
</Button>
</div>
{/* Content */}
{isLoading ? (
<div className="flex flex-col items-center gap-3 py-20">
<Loader2 className="h-6 w-6 animate-spin text-[#58a6ff]" />
<p className="text-sm text-[#8b949e]">加载中...</p>
</div>
) : isError ? (
<div className="flex flex-col items-center gap-3 py-20">
<AlertCircle className="h-6 w-6 text-[#f85149]" />
<p className="text-sm text-[#8b949e]">报告加载失败</p>
<button onClick={() => refetch()} className="text-xs text-[#58a6ff] hover:underline">
点击重试
</button>
</div>
) : report ? (
<>
{/* Meta Info */}
<Card className="bg-[#161b22] border-[#30363d] mb-6">
<CardContent className="p-4">
<div className="flex flex-wrap items-center gap-4 text-sm text-[#8b949e]">
<span className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5" />
{report.created_at}
</span>
<span>{report.model}</span>
<span>{report.tokens_used?.toLocaleString()} tokens</span>
{report.toolsUsed && report.toolsUsed.length > 0 && (
<span className="flex items-center gap-1.5">
<Wrench className="h-3.5 w-3.5" />
{report.toolsUsed.length} 个工具
</span>
)}
</div>
</CardContent>
</Card>
{/* Report Content */}
<Card className="bg-[#161b22] border-[#30363d]">
<CardContent className="p-4 md:p-6 ai-report-content">
<Markdown
components={{
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
if (match && match[1] === "mermaid") {
return <Mermaid chart={String(children).replace(/\n$/, "")} />;
}
return (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{report.content}
</Markdown>
</CardContent>
</Card>
{/* Disclaimer */}
<p className="text-xs text-[#6e7681] text-center mt-6">
本报告由 AI 生成,仅供参考,不构成投资建议
</p>
</>
) : null}
</div>
</div>
);
}
-103
View File
@@ -1,103 +0,0 @@
import * as React from "react";
import { Link, createFileRoute } from "@tanstack/react-router";
import { Clock, Zap, Loader2, AlertCircle, BrainCircuit } from "lucide-react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { fetchAiReports, triggerAiAnalysis } from "../lib/ai-analysis-api";
import { Button } from "../components/ui/button";
import { Card, CardContent } from "../components/ui/card";
import { toast } from "sonner";
export const Route = createFileRoute("/ai-analysis/_index")({
component: AiAnalysisIndex,
});
function AiAnalysisIndex() {
const queryClient = useQueryClient();
const { data: reports, isLoading, isError, refetch } = useQuery({
queryKey: ["ai-reports"],
queryFn: fetchAiReports,
staleTime: 30_000,
retry: false,
});
const triggerMutation = useMutation({
mutationFn: triggerAiAnalysis,
onSuccess: () => {
toast.success("AI 分析已开始,请稍候...");
queryClient.invalidateQueries({ queryKey: ["ai-reports"] });
},
onError: (err: Error) => {
toast.error(err.message || "触发失败");
},
});
return (
<>
<div className="flex justify-end mb-6">
<Button
size="sm"
onClick={() => triggerMutation.mutate()}
disabled={triggerMutation.isPending}
className="gap-1.5"
>
{triggerMutation.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Zap className="h-3.5 w-3.5" />
)}
立即分析
</Button>
</div>
{isLoading ? (
<div className="flex flex-col items-center gap-3 py-20">
<Loader2 className="h-6 w-6 animate-spin text-[#58a6ff]" />
<p className="text-sm text-[#8b949e]">加载中...</p>
</div>
) : isError ? (
<div className="flex flex-col items-center gap-3 py-20">
<AlertCircle className="h-6 w-6 text-[#f85149]" />
<p className="text-sm text-[#8b949e]">数据加载失败</p>
<button onClick={() => refetch()} className="text-xs text-[#58a6ff] hover:underline">
点击重试
</button>
</div>
) : reports && reports.length > 0 ? (
<div className="space-y-4">
{reports.map((report) => (
<Card key={report.id} className="bg-[#161b22] border-[#30363d] hover:border-[#58a6ff]/50 transition-colors">
<CardContent className="p-4 md:p-5">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-[#e6edf3] mb-1">{report.title}</h3>
{report.summary && (
<p className="text-sm text-[#8b949e] line-clamp-2 mb-2">{report.summary}</p>
)}
<div className="flex flex-wrap items-center gap-3 text-xs text-[#8b949e]">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{report.created_at}
</span>
<span>{report.model}</span>
<span>{report.tokens_used?.toLocaleString()} tokens</span>
</div>
</div>
<Link to="/ai-analysis/$id" params={{ id: String(report.id) }} className="text-xs text-[#58a6ff] shrink-0 hover:underline">
查看 →
</Link>
</div>
</CardContent>
</Card>
))}
</div>
) : (
<div className="flex flex-col items-center gap-3 py-20">
<BrainCircuit className="h-10 w-10 text-[#30363d]" />
<p className="text-sm text-[#8b949e]">暂无分析报告</p>
<p className="text-xs text-[#6e7681]">点击"立即分析"生成今日报告</p>
</div>
)}
</>
);
}
+128 -16
View File
@@ -1,28 +1,140 @@
import * as React from "react";
import { Link, Outlet, createFileRoute } from "@tanstack/react-router";
import { ArrowLeft, BrainCircuit } from "lucide-react";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ArrowLeft, Clock, Loader2, AlertCircle, RefreshCw, Wrench, Zap, Calendar } from "lucide-react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Mermaid } from "../components/Mermaid";
import { fetchAiLatestReport, triggerAiAnalysis, fetchAiReport } from "../lib/ai-analysis-api";
import { Button } from "../components/ui/button";
import { Card, CardContent } from "../components/ui/card";
import { toast } from "sonner";
export const Route = createFileRoute("/ai-analysis")({
component: AiAnalysisLayout,
component: AiAnalysisPage,
});
function AiAnalysisLayout() {
function AiAnalysisPage() {
const queryClient = useQueryClient();
const { data: report, isLoading, isError, refetch } = useQuery({
queryKey: ["ai-report-latest"],
queryFn: fetchAiLatestReport,
retry: false,
});
const triggerMutation = useMutation({
mutationFn: triggerAiAnalysis,
onSuccess: () => {
toast.success("AI 分析已开始,请稍候...");
queryClient.invalidateQueries({ queryKey: ["ai-report-latest"] });
},
onError: (err: Error) => {
toast.error(err.message || "触发失败");
},
});
return (
<div className="min-h-screen bg-[#0d1117] text-[#e6edf3]">
<div className="max-w-4xl mx-auto px-4 py-6">
<div className="min-h-screen bg-background text-foreground">
<div className="max-w-4xl mx-auto px-3 sm:px-4 py-4 sm:py-6">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<Link to="/" className="text-[#8b949e] hover:text-[#e6edf3] transition-colors">
<ArrowLeft className="h-5 w-5" />
</Link>
<h1 className="text-xl md:text-2xl font-bold flex items-center gap-2">
<BrainCircuit className="h-5 w-5 text-[#58a6ff]" />
AI 市场分析
</h1>
<div className="flex items-center justify-between mb-4 sm:mb-6">
<div className="flex items-center gap-2 sm:gap-3">
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors">
<ArrowLeft className="h-5 w-5" />
</Link>
<h1 className="text-lg sm:text-xl md:text-2xl font-bold flex items-center gap-2">
<Calendar className="h-5 w-5 text-primary" />
AI 收盘分析
</h1>
</div>
<Button
size="sm"
onClick={() => triggerMutation.mutate()}
disabled={triggerMutation.isPending}
className="gap-1.5 text-xs sm:text-sm"
>
{triggerMutation.isPending ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Zap className="h-3.5 w-3.5" />
)}
<span className="hidden sm:inline">立即分析</span>
<span className="sm:hidden">分析</span>
</Button>
</div>
{/* Child routes render here */}
<Outlet />
{/* Content */}
{isLoading ? (
<div className="flex flex-col items-center gap-3 py-20">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">加载中...</p>
</div>
) : isError || !report ? (
<div className="flex flex-col items-center gap-3 py-20">
<AlertCircle className="h-6 w-6 text-destructive" />
<p className="text-sm text-muted-foreground">暂无分析报告</p>
<p className="text-xs text-muted-foreground/60">点击"立即分析"生成今日报告</p>
</div>
) : (
<>
{/* Meta Info */}
<Card className="bg-card border-border mb-3 sm:mb-4">
<CardContent className="p-3 sm:p-4">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 sm:gap-4 text-xs sm:text-sm text-muted-foreground">
<span className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{report.created_at}</span>
</span>
<span className="truncate">{report.model}</span>
<span>{report.tokens_used?.toLocaleString()} tokens</span>
{report.toolsUsed && report.toolsUsed.length > 0 && (
<span className="flex items-center gap-1.5">
<Wrench className="h-3.5 w-3.5 shrink-0" />
{report.toolsUsed.length} 个工具
</span>
)}
</div>
</CardContent>
</Card>
{/* Report Content */}
<Card className="bg-card border-border">
<CardContent className="p-3 sm:p-4 md:p-6 ai-report-content">
<Markdown
remarkPlugins={[remarkGfm]}
components={{
table({ children, ...props }) {
return (
<div className="overflow-x-auto -mx-3 sm:mx-0 px-3 sm:px-0">
<table {...props}>{children}</table>
</div>
);
},
code({ className, children, ...props }) {
const match = /language-(\w+)/.exec(className || "");
if (match && match[1] === "mermaid") {
return <Mermaid chart={String(children).replace(/\n$/, "")} />;
}
return (
<code className={className} {...props}>
{children}
</code>
);
},
}}
>
{report.content}
</Markdown>
</CardContent>
</Card>
{/* Disclaimer */}
<p className="text-xs text-muted-foreground/60 text-center mt-4 sm:mt-6">
本报告由 AI 生成,仅供参考,不构成投资建议
</p>
</>
)}
</div>
</div>
);
+55 -55
View File
@@ -40,17 +40,17 @@ function DashboardPage() {
}, [autoRefresh, refetch]);
return (
<div className="min-h-screen bg-[#0d1117] text-[#e6edf3]">
<div className="min-h-screen bg-background text-foreground">
{/* ── 顶栏 ── */}
<header className="sticky top-0 z-10 bg-[#0d1117]/95 backdrop-blur border-b border-[#21262d]">
<header className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b border-border">
<div className="max-w-[1400px] mx-auto px-4 h-12 flex items-center justify-between">
<div className="flex items-center gap-3">
<Link to="/" className="text-[#8b949e] hover:text-[#e6edf3] transition-colors">
<Link to="/" className="text-muted-foreground hover:text-foreground transition-colors">
<ArrowLeft className="h-5 w-5" />
</Link>
<h1 className="text-base font-semibold">市场看板</h1>
</div>
<div className="flex items-center gap-3 text-xs text-[#8b949e]">
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{data?.updateTime && (
<span>行情时间 {data.updateTime}</span>
)}
@@ -59,13 +59,13 @@ function DashboardPage() {
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
className="w-3 h-3 rounded border-[#30363d] bg-[#161b22] accent-[#58a6ff]"
className="w-3 h-3 rounded border-border bg-muted accent-primary"
/>
自动刷新
</label>
<button
onClick={() => refetch()}
className="text-[#8b949e] hover:text-[#e6edf3] transition-colors"
className="text-muted-foreground hover:text-foreground transition-colors"
title="刷新"
>
<RefreshCw className={`h-4 w-4 ${isFetching ? "animate-spin" : ""}`} />
@@ -80,7 +80,7 @@ function DashboardPage() {
<DashboardSkeleton />
) : isError ? (
<div className="flex flex-col items-center gap-3 py-20">
<p className="text-sm text-[#8b949e]">数据加载失败</p>
<p className="text-sm text-muted-foreground">数据加载失败</p>
<button onClick={() => refetch()} className="text-xs text-[#58a6ff] hover:underline">
点击重试
</button>
@@ -139,10 +139,10 @@ function IndicesRow({ indices }: { indices: MarketIndex[] }) {
function IndexCard({ index }: { index: MarketIndex }) {
const isUp = index.changePct >= 0;
return (
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-3">
<div className="bg-card border border-border rounded-lg p-3">
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-[#8b949e] truncate">{index.name}</span>
<span className="text-[10px] text-[#484f58] tabular-nums">{index.code.replace(/\.(SH|SZ)$/, "")}</span>
<span className="text-xs text-muted-foreground truncate">{index.name}</span>
<span className="text-[10px] text-muted-foreground/50 tabular-nums">{index.code.replace(/\.(SH|SZ)$/, "")}</span>
</div>
<div className={`text-xl font-bold tabular-nums ${isUp ? "text-[#f85149]" : "text-[#3fb950]"}`}>
{index.price.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@@ -172,12 +172,12 @@ function AuctionSignalBar({ stats }: { stats: MarketDashboardData["marketStats"]
"#d29922";
return (
<div className="bg-[#161b22] border border-[#21262d] rounded-lg px-4 py-2 flex items-center gap-3">
<div className="bg-card border border-border rounded-lg px-4 py-2 flex items-center gap-3">
<Target className="h-4 w-4 shrink-0" style={{ color }} />
<span className="text-xs text-[#8b949e]">竞价信号</span>
<span className="text-xs text-muted-foreground">竞价信号</span>
<span className="text-sm font-semibold" style={{ color }}>{signal}</span>
{stats.auction?.date && (
<span className="text-[10px] text-[#484f58] ml-auto">{stats.auction.date}</span>
<span className="text-[10px] text-muted-foreground/50 ml-auto">{stats.auction.date}</span>
)}
</div>
);
@@ -195,10 +195,10 @@ function MarketTemperatureSection({ stats }: { stats: MarketDashboardData["marke
temp.score >= 20 ? "#3fb950" : "#58a6ff";
return (
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4">
<div className="bg-card border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Thermometer className="h-4 w-4 text-[#8b949e]" />
<Thermometer className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">市场温度</span>
</div>
<div className="flex items-baseline gap-2">
@@ -223,7 +223,7 @@ function MarketTemperatureSection({ stats }: { stats: MarketDashboardData["marke
{/* 温度因子明细 */}
{temp.factors && (
<div className="mt-3 pt-2 border-t border-[#21262d] flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-[#484f58]">
<div className="mt-3 pt-2 border-t border-border flex flex-wrap gap-x-4 gap-y-1 text-[10px] text-muted-foreground/50">
<span>涨跌 {temp.factors.advanceScore > 0 ? "+" : ""}{temp.factors.advanceScore}</span>
<span>中位 {temp.factors.medianScore > 0 ? "+" : ""}{temp.factors.medianScore}</span>
<span>强弱 {temp.factors.strongScore > 0 ? "+" : ""}{temp.factors.strongScore}</span>
@@ -239,7 +239,7 @@ function MarketTemperatureSection({ stats }: { stats: MarketDashboardData["marke
function StatItem({ label, value, valueColor }: { label: string; value: string; valueColor?: string }) {
return (
<div>
<div className="text-[10px] text-[#484f58] mb-0.5">{label}</div>
<div className="text-[10px] text-muted-foreground/50 mb-0.5">{label}</div>
<div className="text-sm font-medium tabular-nums" style={valueColor ? { color: valueColor } : undefined}>
{value}
</div>
@@ -259,13 +259,13 @@ function AdvanceDeclineBar({ stats }: { stats: MarketDashboardData["marketStats"
const downPct = (stats.downCount / total) * 100;
return (
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4">
<div className="bg-card border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<BarChart3 className="h-4 w-4 text-[#8b949e]" />
<BarChart3 className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">涨跌家数分布</span>
</div>
<span className="text-[10px] text-[#484f58]">
<span className="text-[10px] text-muted-foreground/50">
涨停 {stats.limitUp} 炸板 {stats.limitBreak} 跌停 {stats.limitDown} 共 {total} 只
</span>
</div>
@@ -279,17 +279,17 @@ function AdvanceDeclineBar({ stats }: { stats: MarketDashboardData["marketStats"
<div className="flex items-center gap-4 text-xs">
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-[#f85149]" />
<span className="text-[#8b949e]">上涨</span>
<span className="text-muted-foreground">上涨</span>
<span className="font-medium tabular-nums">{stats.upCount}</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-[#484f58]" />
<span className="text-[#8b949e]">平盘</span>
<span className="text-muted-foreground">平盘</span>
<span className="font-medium tabular-nums">{stats.flatCount}</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-[#3fb950]" />
<span className="text-[#8b949e]">下跌</span>
<span className="text-muted-foreground">下跌</span>
<span className="font-medium tabular-nums">{stats.downCount}</span>
</div>
</div>
@@ -304,10 +304,10 @@ function SectorStrengthList({ sectors }: { sectors: SectorStrengthItem[] }) {
if (sectors.length === 0) return null;
return (
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4">
<div className="bg-card border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Zap className="h-4 w-4 text-[#8b949e]" />
<Zap className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">行业强度榜 TOP{sectors.length}</span>
</div>
</div>
@@ -328,7 +328,7 @@ function SectorRow({ sector }: { sector: SectorStrengthItem }) {
return (
<div className="flex items-center gap-3">
<span className="text-xs w-20 shrink-0 truncate" title={sector.name}>{sector.name}</span>
<div className="flex-1 h-4 bg-[#0d1117] rounded-sm overflow-hidden relative">
<div className="flex-1 h-4 bg-background rounded-sm overflow-hidden relative">
<div
className="h-full rounded-sm transition-all duration-500"
style={{
@@ -340,12 +340,12 @@ function SectorRow({ sector }: { sector: SectorStrengthItem }) {
/>
</div>
<div className="flex items-center gap-2 shrink-0 text-[10px] tabular-nums w-44 justify-end">
<span className="text-[#8b949e]">强度 {sector.strength}</span>
<span className="text-muted-foreground">强度 {sector.strength}</span>
<span className={isUp ? "text-[#f85149]" : "text-[#3fb950]"}>
{isUp ? "+" : ""}{sector.changePct.toFixed(2)}%
</span>
<span className="text-[#8b949e]">宽度 {sector.breadthPct}%</span>
<span className="text-[#8b949e]">强 {sector.strongCount}</span>
<span className="text-muted-foreground">宽度 {sector.breadthPct}%</span>
<span className="text-muted-foreground">强 {sector.strongCount}</span>
</div>
</div>
);
@@ -358,10 +358,10 @@ function ConceptStrengthList({ concepts }: { concepts: MarketDashboardData["conc
if (concepts.length === 0) return null;
return (
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4">
<div className="bg-card border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-[#8b949e]" />
<TrendingUp className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">概念板块热度 TOP{concepts.length}</span>
</div>
</div>
@@ -395,15 +395,15 @@ function ConceptStrengthList({ concepts }: { concepts: MarketDashboardData["conc
============================================================ */
function EventSidebar({ events }: { events: MarketDashboardData["events"] }) {
return (
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4">
<div className="bg-card border border-border rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Newspaper className="h-4 w-4 text-[#8b949e]" />
<Newspaper className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">事件情报</span>
</div>
<div className="space-y-3">
{events.length === 0 ? (
<p className="text-xs text-[#484f58]">暂无事件</p>
<p className="text-xs text-muted-foreground/50">暂无事件</p>
) : (
events.map((evt, i) => (
<EventItem key={i} event={evt} />
@@ -421,20 +421,20 @@ function EventItem({ event }: { event: MarketDashboardData["events"][0] }) {
event.type === "hot" ? "text-[#d29922] bg-[#d29922]/10 border-[#d29922]/30" :
event.type === "skyrocket" ? "text-[#f778ba] bg-[#f778ba]/10 border-[#f778ba]/30" :
event.type === "dragon_tiger" ? "text-[#a371f7] bg-[#a371f7]/10 border-[#a371f7]/30" :
event.type === "limit_break" ? "text-[#8b949e] bg-[#8b949e]/10 border-[#8b949e]/30" :
event.type === "limit_break" ? "text-muted-foreground bg-[#8b949e]/10 border-[#8b949e]/30" :
event.type === "anomaly" ? "text-[#58a6ff] bg-[#58a6ff]/10 border-[#58a6ff]/30" :
"text-[#8b949e] bg-[#8b949e]/10 border-[#8b949e]/30";
"text-muted-foreground bg-[#8b949e]/10 border-[#8b949e]/30";
return (
<div className="border-l-2 border-[#21262d] pl-3">
<div className="border-l-2 border-border pl-3">
<div className="flex items-center gap-2 mb-0.5">
<span className={`text-[9px] font-medium px-1 py-0.5 rounded border ${labelColor}`}>
{event.label}
</span>
</div>
<p className="text-xs text-[#e6edf3] leading-relaxed">{event.name}</p>
<p className="text-xs text-foreground leading-relaxed">{event.name}</p>
{event.detail && (
<p className="text-[10px] text-[#484f58] mt-0.5">{event.detail}</p>
<p className="text-[10px] text-muted-foreground/50 mt-0.5">{event.detail}</p>
)}
</div>
);
@@ -448,35 +448,35 @@ function DashboardSkeleton() {
<div className="space-y-4">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="bg-[#161b22] border border-[#21262d] rounded-lg p-3 animate-pulse">
<div className="h-3 w-16 bg-[#21262d] rounded mb-2" />
<div className="h-6 w-24 bg-[#21262d] rounded mb-1" />
<div className="h-3 w-20 bg-[#21262d] rounded" />
<div key={i} className="bg-card border border-border rounded-lg p-3 animate-pulse">
<div className="h-3 w-16 bg-muted rounded mb-2" />
<div className="h-6 w-24 bg-muted rounded mb-1" />
<div className="h-3 w-20 bg-muted rounded" />
</div>
))}
</div>
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4 animate-pulse">
<div className="h-5 w-24 bg-[#21262d] rounded mb-4" />
<div className="bg-card border border-border rounded-lg p-4 animate-pulse">
<div className="h-5 w-24 bg-muted rounded mb-4" />
<div className="grid grid-cols-3 gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i}>
<div className="h-2 w-16 bg-[#21262d] rounded mb-1" />
<div className="h-4 w-12 bg-[#21262d] rounded" />
<div className="h-2 w-16 bg-muted rounded mb-1" />
<div className="h-4 w-12 bg-muted rounded" />
</div>
))}
</div>
</div>
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4 animate-pulse">
<div className="h-5 w-32 bg-[#21262d] rounded mb-3" />
<div className="h-5 w-full bg-[#21262d] rounded-full" />
<div className="bg-card border border-border rounded-lg p-4 animate-pulse">
<div className="h-5 w-32 bg-muted rounded mb-3" />
<div className="h-5 w-full bg-muted rounded-full" />
</div>
<div className="bg-[#161b22] border border-[#21262d] rounded-lg p-4 animate-pulse">
<div className="h-5 w-40 bg-[#21262d] rounded mb-3" />
<div className="bg-card border border-border rounded-lg p-4 animate-pulse">
<div className="h-5 w-40 bg-muted rounded mb-3" />
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 mb-2">
<div className="h-3 w-16 bg-[#21262d] rounded" />
<div className="flex-1 h-4 bg-[#21262d] rounded" />
<div className="h-3 w-24 bg-[#21262d] rounded" />
<div className="h-3 w-16 bg-muted rounded" />
<div className="flex-1 h-4 bg-muted rounded" />
<div className="h-3 w-24 bg-muted rounded" />
</div>
))}
</div>
+1 -1
View File
@@ -211,7 +211,7 @@ function Index() {
A股走势追踪
</h1>
<p className="text-sm md:text-base text-muted-foreground">创建股票集合,分享历史走势</p>
<div className="mt-3 flex items-center justify-center gap-2">
<div className="mt-3 flex flex-wrap items-center justify-center gap-2">
<Link to="/dashboard">
<Button variant="outline" size="sm" className="gap-1.5 text-xs">
<BarChart3 className="h-3.5 w-3.5" />