删除股票相关功能,仅保留用户体系

This commit is contained in:
2026-07-04 14:29:15 +08:00
parent b1776a1c2f
commit 024320e082
16 changed files with 7 additions and 1837 deletions
+2 -4
View File
@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'
import { NavLink, Outlet, useNavigate } from 'react-router-dom'
import {
LayoutDashboard,
Users,
User,
LogOut,
@@ -46,7 +45,6 @@ export function Layout() {
}
const navItems = [
{ to: '/', label: '看板', icon: LayoutDashboard },
...(user && canManageUsers(user.role) ? [{ to: '/users', label: '用户管理', icon: Users }] : []),
{ to: '/profile', label: '个人中心', icon: User },
]
@@ -70,7 +68,7 @@ export function Layout() {
A
</div>
<div className="font-mono font-bold text-[13px] tracking-[0.06em] text-foreground leading-tight">
<div>A股</div>
<div></div>
<div></div>
</div>
</div>
@@ -133,7 +131,7 @@ export function Layout() {
<div className="lg:hidden flex items-center justify-between px-4 py-3 border-b border-border bg-surface shrink-0">
<div className="flex items-center gap-2.5">
<div className="h-6 w-6 rounded-md flex items-center justify-center text-white text-xs font-bold" style={{ background: BRAND }}>A</div>
<span className="text-sm font-semibold">A股工作台</span>
<span className="text-sm font-semibold"></span>
</div>
<button onClick={() => setMobileOpen(!mobileOpen)} className="p-2 rounded-btn hover:bg-elevated">
<Menu className="h-5 w-5" />
-283
View File
@@ -1,283 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import {
BarChart3,
TrendingUp,
TrendingDown,
Minus,
RefreshCw,
Loader2,
AlertCircle,
Calendar,
Activity,
} from 'lucide-react'
import { api, MarketOverview, SyncJob, StockRow } from '@/lib/api'
import { canManageUsers } from '@/lib/auth'
import { cn } from '@/lib/cn'
interface StockOverviewProps {
role: 'system_admin' | 'admin' | 'user'
}
export function StockOverview({ role }: StockOverviewProps) {
const [overview, setOverview] = useState<MarketOverview | null>(null)
const [job, setJob] = useState<SyncJob | null>(null)
const [loading, setLoading] = useState(true)
const [syncing, setSyncing] = useState(false)
const [error, setError] = useState('')
const pollRef = useRef<number | null>(null)
const canSync = canManageUsers(role)
const fetchAll = async () => {
setLoading(true)
setError('')
try {
const [o, j] = await Promise.all([api.stockOverview(), api.stockSyncStatus()])
setOverview(o)
setJob(j)
} catch (err: any) {
setError(err?.message || '加载市场数据失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchAll()
return () => {
if (pollRef.current) {
window.clearInterval(pollRef.current)
}
}
}, [])
const startPolling = () => {
if (pollRef.current) {
window.clearInterval(pollRef.current)
}
pollRef.current = window.setInterval(async () => {
try {
const j = await api.stockSyncStatus()
setJob(j)
if (!j || j.status !== 'running') {
if (pollRef.current) {
window.clearInterval(pollRef.current)
pollRef.current = null
}
setSyncing(false)
await fetchAll()
if (j?.status === 'failed') {
setError(j.error_message || '同步失败')
}
}
} catch (err: any) {
if (pollRef.current) {
window.clearInterval(pollRef.current)
pollRef.current = null
}
setSyncing(false)
setError(err?.message || '轮询同步状态失败')
}
}, 2000)
}
const handleSync = async () => {
setSyncing(true)
setError('')
try {
await api.triggerStockSync()
startPolling()
} catch (err: any) {
setSyncing(false)
setError(err?.message || '同步失败')
}
}
if (loading) {
return (
<div className="rounded-card border border-border bg-surface p-6 flex items-center justify-center gap-2 text-sm text-muted">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
)
}
if (!overview?.latest_trade_date) {
return (
<div className="rounded-card border border-border bg-surface p-6 text-center">
<div className="text-sm text-secondary"></div>
{canSync && (
<button
onClick={handleSync}
disabled={syncing}
className="mt-3 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn bg-accent text-white text-xs font-medium hover:bg-accent/90 disabled:opacity-60"
>
{syncing ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
</button>
)}
</div>
)
}
const { counts, avg_change_pct, indices, top_gainers, top_losers, turnover_leaders } = overview
return (
<div className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3 rounded-card border border-border bg-surface/80 px-4 py-3">
<div className="flex items-center gap-2">
<BarChart3 className="h-4 w-4 text-accent" />
<h2 className="text-base font-semibold text-foreground"></h2>
<span className="text-xs text-muted flex items-center gap-1">
<Calendar className="h-3 w-3" />
{overview.latest_trade_date}
</span>
</div>
<div className="flex items-center gap-3">
{job && (
<span className="text-xs text-muted">
{formatSyncStatus(job)}
</span>
)}
{canSync && (
<button
onClick={handleSync}
disabled={syncing}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn bg-accent text-white text-xs font-medium hover:bg-accent/90 disabled:opacity-60 transition-colors"
>
{syncing ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
</button>
)}
</div>
</div>
{error && (
<div className="flex items-start gap-2 rounded-btn border border-danger/30 bg-danger/10 px-3 py-2.5 text-xs text-danger">
<AlertCircle className="h-3.5 w-3.5 mt-px shrink-0" />
<span>{error}</span>
</div>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<KpiCard icon={<TrendingUp className="h-4 w-4 text-bull" />} label="上涨" value={counts.up} />
<KpiCard icon={<TrendingDown className="h-4 w-4 text-bear" />} label="下跌" value={counts.down} />
<KpiCard icon={<Minus className="h-4 w-4 text-muted" />} label="平盘" value={counts.flat} />
<KpiCard
icon={<Activity className="h-4 w-4 text-accent" />}
label="平均涨跌"
value={`${avg_change_pct >= 0 ? '+' : ''}${(avg_change_pct * 100).toFixed(2)}%`}
valueClass={avg_change_pct >= 0 ? 'text-bull' : 'text-bear'}
/>
</div>
{indices.length > 0 && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{indices.map((item) => (
<IndexCard key={item.symbol} item={item} />
))}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<StockList title="涨幅榜" rows={top_gainers} valueKey="change_pct" />
<StockList title="跌幅榜" rows={top_losers} valueKey="change_pct" />
<StockList title="成交额榜" rows={turnover_leaders} valueKey="amount" />
</div>
</div>
)
}
function KpiCard({
icon,
label,
value,
valueClass,
}: {
icon: React.ReactNode
label: string
value: React.ReactNode
valueClass?: string
}) {
return (
<div className="rounded-card border border-border bg-surface p-3 flex items-center gap-3">
<div className="p-1.5 rounded-btn bg-elevated">{icon}</div>
<div>
<div className={cn('text-lg font-bold text-foreground', valueClass)}>{value}</div>
<div className="text-xs text-secondary">{label}</div>
</div>
</div>
)
}
function IndexCard({ item }: { item: StockRow }) {
const positive = item.change_pct >= 0
return (
<div className="rounded-card border border-border bg-surface p-3">
<div className="text-xs text-secondary truncate">{item.name || item.symbol}</div>
<div className={cn('text-base font-semibold mt-1', positive ? 'text-bull' : 'text-bear')}>
{item.close.toFixed(2)}
</div>
<div className={cn('text-xs font-medium', positive ? 'text-bull' : 'text-bear')}>
{positive ? '+' : ''}{(item.change_pct * 100).toFixed(2)}%
</div>
</div>
)
}
function StockList({
title,
rows,
valueKey,
}: {
title: string
rows: StockRow[]
valueKey: 'change_pct' | 'amount'
}) {
return (
<div className="rounded-card border border-border bg-surface p-3">
<h3 className="text-xs font-semibold text-secondary uppercase tracking-wider mb-2">{title}</h3>
<div className="space-y-1">
{rows.length === 0 ? (
<div className="text-xs text-muted py-2"></div>
) : (
rows.map((item, idx) => {
const val = valueKey === 'amount' ? (item.amount ?? 0) / 1e8 : item.change_pct * 100
const isPositive = valueKey === 'amount' ? true : item.change_pct >= 0
return (
<div key={item.symbol} className="flex items-center justify-between text-sm py-1">
<div className="flex items-center gap-2 min-w-0">
<span className="text-[10px] text-muted w-4">{idx + 1}</span>
<span className="text-foreground font-medium truncate">{item.name || item.symbol}</span>
</div>
<div className={cn('text-xs font-mono shrink-0', valueKey === 'change_pct' ? (isPositive ? 'text-bull' : 'text-bear') : 'text-foreground')}>
{valueKey === 'amount'
? `${val.toFixed(2)}亿`
: `${isPositive ? '+' : ''}${val.toFixed(2)}%`}
</div>
</div>
)
})
)}
</div>
</div>
)
}
function formatSyncStatus(job: SyncJob): string {
if (!job || !job.id) return '状态未知'
if (job.status === 'running') return '同步中…'
const ts = job.finished_at || job.started_at
let timeStr = '时间未知'
if (ts && ts !== '0001-01-01T00:00:00Z') {
const d = new Date(ts)
if (!isNaN(d.getTime())) {
timeStr = d.toLocaleString()
}
}
const statusText = job.status === 'success' ? '成功' : '失败'
if (job.status === 'failed' && job.error_message) {
return `${statusText} · ${timeStr} · ${job.error_message} · ${job.id.slice(0, 8)}`
}
return `${statusText} · ${timeStr} · ${job.id.slice(0, 8)}`
}
-41
View File
@@ -28,40 +28,6 @@ export interface AuthResponse {
user: UserInfo
}
export interface StockRow {
symbol: string
name: string
close: number
change_pct: number
amount?: number
}
export interface MarketOverview {
latest_trade_date: string
counts: {
up: number
down: number
flat: number
total: number
}
avg_change_pct: number
indices: StockRow[]
top_gainers: StockRow[]
top_losers: StockRow[]
turnover_leaders: StockRow[]
}
export interface SyncJob {
id: string
job_date: string
status: 'running' | 'success' | 'failed'
records_count: number
started_at: string
finished_at?: string
error_message?: string
trigger_by: string
}
export function getToken(): string | null {
try {
return localStorage.getItem('access_token')
@@ -141,11 +107,4 @@ export const api = {
request<{ message: string }>(`/api/admin/users/${id}`, { method: 'DELETE' }),
listRoles: () => request<Role[]>('/api/admin/roles'),
stockOverview: () => request<MarketOverview>('/api/stocks/overview'),
stockSyncStatus: () => request<SyncJob | null>('/api/stocks/sync/status'),
triggerStockSync: () =>
request<{ job: SyncJob }>('/api/admin/stocks/sync', { method: 'POST' }),
}
-64
View File
@@ -1,64 +0,0 @@
import { LayoutDashboard, ShieldCheck } from 'lucide-react'
import { getStoredUser, roleLabel } from '@/lib/auth'
import { StockOverview } from '@/components/StockOverview'
export function Dashboard() {
const user = getStoredUser()
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<div className="p-2 rounded-btn bg-accent/10 text-accent">
<LayoutDashboard className="h-5 w-5" />
</div>
<div>
<h1 className="text-xl font-bold text-foreground"></h1>
<p className="text-sm text-secondary">{user?.username || '—'}</p>
</div>
</div>
<StockOverview role={user?.role || 'user'} />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card
icon={<ShieldCheck className="h-5 w-5 text-accent" />}
title="当前身份"
value={user ? roleLabel(user.role) : '—'}
desc={user ? `用户:${user.username}` : '加载中…'}
/>
</div>
<div className="rounded-card border border-border bg-surface p-5">
<h2 className="text-sm font-semibold text-foreground mb-3"></h2>
<ul className="space-y-2 text-sm text-secondary">
<li><span className="text-accent font-medium"></span></li>
<li><span className="text-accent font-medium"></span></li>
<li><span className="text-accent font-medium"></span>访</li>
</ul>
</div>
</div>
)
}
function Card({
icon,
title,
value,
desc,
}: {
icon: React.ReactNode
title: string
value: string
desc: string
}) {
return (
<div className="rounded-card border border-border bg-surface p-4 transition hover:border-accent/30">
<div className="flex items-center gap-2 mb-3">
{icon}
<span className="text-xs font-medium text-secondary uppercase tracking-wider">{title}</span>
</div>
<div className="text-lg font-bold text-foreground">{value}</div>
<div className="mt-1 text-xs text-muted">{desc}</div>
</div>
)
}
+1 -2
View File
@@ -2,7 +2,6 @@ import { createBrowserRouter, Navigate } from 'react-router-dom'
import { Layout } from './components/Layout'
import { AuthGuard } from './components/AuthGuard'
import { Login } from './pages/Login'
import { Dashboard } from './pages/Dashboard'
import { Users } from './pages/Users'
import { Profile } from './pages/Profile'
@@ -16,7 +15,7 @@ export const router = createBrowserRouter([
</AuthGuard>
),
children: [
{ index: true, element: <Dashboard /> },
{ index: true, element: <Navigate to="/profile" replace /> },
{
path: 'users',
element: (