管理员增加数据同步功能,支持初始化股票列表
This commit is contained in:
@@ -3,6 +3,7 @@ import { NavLink, Outlet, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Users,
|
||||
User,
|
||||
Database,
|
||||
LogOut,
|
||||
Moon,
|
||||
Sun,
|
||||
@@ -46,6 +47,7 @@ export function Layout() {
|
||||
|
||||
const navItems = [
|
||||
...(user && canManageUsers(user.role) ? [{ to: '/users', label: '用户管理', icon: Users }] : []),
|
||||
...(user && canManageUsers(user.role) ? [{ to: '/data-sync', label: '数据同步', icon: Database }] : []),
|
||||
{ to: '/profile', label: '个人中心', icon: User },
|
||||
]
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ export interface AuthResponse {
|
||||
user: UserInfo
|
||||
}
|
||||
|
||||
export interface InitStocksResponse {
|
||||
success: boolean
|
||||
data: {
|
||||
count: number
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export function getToken(): string | null {
|
||||
try {
|
||||
return localStorage.getItem('access_token')
|
||||
@@ -107,4 +115,7 @@ export const api = {
|
||||
request<{ message: string }>(`/api/admin/users/${id}`, { method: 'DELETE' }),
|
||||
|
||||
listRoles: () => request<Role[]>('/api/admin/roles'),
|
||||
|
||||
initStocks: () =>
|
||||
request<InitStocksResponse>('/api/admin/data-sync/init-stocks', { method: 'POST' }),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useState } from 'react'
|
||||
import { Database, RefreshCw, CheckCircle, AlertCircle } from 'lucide-react'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
export function DataSync() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState<{ count: number; message: string } | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleInitStocks = async () => {
|
||||
if (!confirm('确定要从 Tushare 初始化股票列表?这会清空现有股票数据。')) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError('')
|
||||
setResult(null)
|
||||
try {
|
||||
const res = await api.initStocks()
|
||||
setResult(res.data)
|
||||
} catch (err: any) {
|
||||
setError(err?.message || '初始化失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-btn bg-accent/10 text-accent">
|
||||
<Database className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground">数据同步</h1>
|
||||
<p className="text-sm text-secondary">从 Tushare 拉取基础数据</p>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="flex items-start gap-2 rounded-btn border border-success/30 bg-success/10 px-3 py-2.5 text-xs text-success">
|
||||
<CheckCircle className="h-3.5 w-3.5 mt-px shrink-0" />
|
||||
<span>{result.message},共 {result.count} 条记录</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-card border border-border bg-surface p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-foreground">初始化股票列表</h2>
|
||||
<p className="text-xs text-secondary mt-1">调用 Tushare stock_basic 接口,获取全部 A 股基础信息</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleInitStocks}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-btn bg-accent text-white text-sm font-medium hover:bg-accent/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
{loading ? '同步中...' : '开始同步'}
|
||||
</button>
|
||||
</div>
|
||||
</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>• 需要配置 TUSHARE_TOKEN 环境变量</li>
|
||||
<li>• 每次同步会清空 stocks 表并重新写入</li>
|
||||
<li>• 仅获取基础字段:代码、名称、交易所、行业等</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { AuthGuard } from './components/AuthGuard'
|
||||
import { Login } from './pages/Login'
|
||||
import { Users } from './pages/Users'
|
||||
import { Profile } from './pages/Profile'
|
||||
import { DataSync } from './pages/DataSync'
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{ path: '/login', element: <Login /> },
|
||||
@@ -24,6 +25,14 @@ export const router = createBrowserRouter([
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'data-sync',
|
||||
element: (
|
||||
<AuthGuard allowedRoles={['admin', 'system_admin']} requireAuth>
|
||||
<DataSync />
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
element: <Profile />,
|
||||
|
||||
Reference in New Issue
Block a user