commit
83a136092a
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>alphafuturespro</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@ -0,0 +1,16 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>new-project</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "new-project",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"antd": "^6.3.0",
|
||||
"lightweight-charts": "^5.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"vite": "^8.0.0-beta.13"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "^8.0.0-beta.13"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.jsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the Vite and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,68 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "new-project",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"antd": "^6.3.0",
|
||||
"lightweight-charts": "^5.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"vite": "^8.0.0-beta.13"
|
||||
},
|
||||
"overrides": {
|
||||
"vite": "^8.0.0-beta.13"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,64 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #f0f2f5;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* 主应用容器 */
|
||||
.App {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 通用标题样式 */
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 通用卡片样式 */
|
||||
.ant-card {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
/* 通用按钮样式 */
|
||||
.ant-btn {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 通用表单样式 */
|
||||
.ant-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 通用表格样式 */
|
||||
.ant-table {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './store';
|
||||
import MainLayout from './components/layout/MainLayout';
|
||||
import Dashboard from './pages/dashboard/Dashboard';
|
||||
import Detail from './pages/detail/Detail';
|
||||
import RiskControl from './pages/risk-control/RiskControl';
|
||||
import Config from './pages/config/Config';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<MainLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/detail" element={<Detail />} />
|
||||
<Route path="/risk-control" element={<RiskControl />} />
|
||||
<Route path="/config" element={<Config />} />
|
||||
</Routes>
|
||||
</MainLayout>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,69 @@
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
background: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-search {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dark-mode-toggle {
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.header-search {
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Layout, Menu, Button, Input, Avatar, Badge, Switch, ConfigProvider } from 'antd';
|
||||
import { SearchOutlined, BellOutlined, UserOutlined, MenuFoldOutlined, MenuUnfoldOutlined, HomeOutlined, BarChartOutlined, SafetyOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import './MainLayout.css';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
const { Search } = Input;
|
||||
|
||||
const MainLayout = ({ children }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed(!collapsed);
|
||||
};
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
setDarkMode(!darkMode);
|
||||
};
|
||||
|
||||
const getSelectedKey = () => {
|
||||
const path = location.pathname;
|
||||
if (path === '/') return '1';
|
||||
if (path === '/detail') return '2';
|
||||
if (path === '/risk-control') return '3';
|
||||
if (path === '/config') return '4';
|
||||
return '1';
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={darkMode ? { token: { colorScheme: 'dark' } } : {}}>
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Header className="header">
|
||||
<div className="header-left">
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={toggleCollapsed}
|
||||
style={{ marginRight: 16, color: '#fff' }}
|
||||
/>
|
||||
<h1 className="logo">AI期货分析系统</h1>
|
||||
</div>
|
||||
<div className="header-right">
|
||||
<Search
|
||||
placeholder="搜索品种"
|
||||
className="header-search"
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Badge count={3} style={{ marginLeft: 16 }}>
|
||||
<Button type="text" icon={<BellOutlined />} style={{ color: '#fff' }} />
|
||||
</Badge>
|
||||
<span className="dark-mode-toggle">
|
||||
<Switch checked={darkMode} onChange={toggleDarkMode} checkedChildren="暗" unCheckedChildren="亮" />
|
||||
</span>
|
||||
<Avatar icon={<UserOutlined />} style={{ marginLeft: 16 }} />
|
||||
</div>
|
||||
</Header>
|
||||
<Layout>
|
||||
<Sider width={200} theme="light" trigger={null} collapsible collapsed={collapsed}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[getSelectedKey()]}
|
||||
style={{ height: '100%', borderRight: 0 }}
|
||||
>
|
||||
<Menu.Item key="1" icon={<HomeOutlined />}>
|
||||
<Link to="/">Dashboard</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="2" icon={<BarChartOutlined />}>
|
||||
<Link to="/detail">详情分析</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="3" icon={<SafetyOutlined />}>
|
||||
<Link to="/risk-control">风控管理</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="4" icon={<SettingOutlined />}>
|
||||
<Link to="/config">配置管理</Link>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Sider>
|
||||
<Content className="content">
|
||||
<div className="content-inner">
|
||||
{children}
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
@ -0,0 +1,36 @@
|
||||
/* 全局样式 */
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 移除默认样式 */
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* 确保antd样式正确加载 */
|
||||
@import 'antd/dist/reset.css';
|
||||
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@ -0,0 +1,47 @@
|
||||
.config {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.config h2 {
|
||||
margin: 0 0 24px 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 参数设置项 */
|
||||
.param-item {
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.param-item label {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* 配置操作按钮 */
|
||||
.config-actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.config-actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-actions Button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,315 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Row, Col, Form, Input, Button, Select, Switch, Slider, Tag, Alert, Table } from 'antd';
|
||||
import { SettingOutlined, DatabaseOutlined, RobotOutlined, SlidersOutlined } from '@ant-design/icons';
|
||||
import './Config.css';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Item } = Form;
|
||||
|
||||
const Config = () => {
|
||||
const [form] = Form.useForm();
|
||||
|
||||
// 数据源配置
|
||||
const dataSources = [
|
||||
{ id: 1, name: 'Wind', status: 'online', responseTime: '120ms', priority: 1, enabled: true },
|
||||
{ id: 2, name: '同花顺', status: 'online', responseTime: '150ms', priority: 2, enabled: true },
|
||||
{ id: 3, name: '东方财富', status: 'online', responseTime: '180ms', priority: 3, enabled: true },
|
||||
{ id: 4, name: '新浪财经', status: 'offline', responseTime: '-', priority: 4, enabled: false }
|
||||
];
|
||||
|
||||
// AI模型配置
|
||||
const aiModels = [
|
||||
{ id: 1, name: 'DeepSeek', accuracy: '85%', responseTime: '250ms', enabled: true },
|
||||
{ id: 2, name: 'GPT-4', accuracy: '88%', responseTime: '350ms', enabled: false },
|
||||
{ id: 3, name: 'Claude', accuracy: '82%', responseTime: '200ms', enabled: false },
|
||||
{ id: 4, name: '自定义模型', accuracy: '78%', responseTime: '150ms', enabled: false }
|
||||
];
|
||||
|
||||
// 系统配置参数
|
||||
const systemParams = {
|
||||
refreshInterval: 30, // 秒
|
||||
alertThreshold: 70, // %
|
||||
maxPositionSize: 50, // %
|
||||
backtestDays: 90 // 天
|
||||
};
|
||||
|
||||
const [params, setParams] = useState(systemParams);
|
||||
|
||||
// 处理系统参数变更
|
||||
const handleParamChange = (key, value) => {
|
||||
setParams(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = (values) => {
|
||||
console.log('配置保存:', values);
|
||||
// 模拟保存操作
|
||||
Alert.success('配置已保存');
|
||||
};
|
||||
|
||||
// 获取数据源状态颜色
|
||||
const getDataSourceStatusColor = (status) => {
|
||||
return status === 'online' ? 'green' : 'red';
|
||||
};
|
||||
|
||||
// 获取数据源状态文本
|
||||
const getDataSourceStatusText = (status) => {
|
||||
return status === 'online' ? '在线' : '离线';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="config">
|
||||
<h2>配置管理</h2>
|
||||
|
||||
{/* 数据源配置 */}
|
||||
<Card
|
||||
title={<span><DatabaseOutlined /> 数据源配置</span>}
|
||||
className="config-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Table
|
||||
dataSource={dataSources}
|
||||
columns={[
|
||||
{
|
||||
title: '数据源',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status) => (
|
||||
<Tag color={getDataSourceStatusColor(status)}>
|
||||
{getDataSourceStatusText(status)}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'responseTime',
|
||||
key: 'responseTime'
|
||||
},
|
||||
{
|
||||
title: '优先级',
|
||||
dataIndex: 'priority',
|
||||
key: 'priority',
|
||||
render: (priority) => (
|
||||
<Select
|
||||
defaultValue={priority}
|
||||
style={{ width: 80 }}
|
||||
onChange={() => {}}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map(p => (
|
||||
<Option key={p} value={p}>{p}</Option>
|
||||
))}
|
||||
</Select>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '启用',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (enabled) => (
|
||||
<Switch checked={enabled} onChange={() => {}} />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: () => (
|
||||
<Button type="link">编辑</Button>
|
||||
)
|
||||
}
|
||||
]}
|
||||
rowKey="id"
|
||||
/>
|
||||
<Button type="primary" style={{ marginTop: 16 }}>
|
||||
添加数据源
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* AI模型配置 */}
|
||||
<Card
|
||||
title={<span><RobotOutlined /> AI模型配置</span>}
|
||||
className="config-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Table
|
||||
dataSource={aiModels}
|
||||
columns={[
|
||||
{
|
||||
title: '模型名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '准确率',
|
||||
dataIndex: 'accuracy',
|
||||
key: 'accuracy'
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'responseTime',
|
||||
key: 'responseTime'
|
||||
},
|
||||
{
|
||||
title: '默认模型',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (enabled) => (
|
||||
<Switch checked={enabled} onChange={() => {}} />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: () => (
|
||||
<Button type="link">配置</Button>
|
||||
)
|
||||
}
|
||||
]}
|
||||
rowKey="id"
|
||||
/>
|
||||
|
||||
{/* 模型参数调优 */}
|
||||
<Card title="模型参数调优" style={{ marginTop: 24 }}>
|
||||
<Form layout="vertical">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Item label="预测周期">
|
||||
<Select defaultValue="1D">
|
||||
<Option value="1H">1小时</Option>
|
||||
<Option value="4H">4小时</Option>
|
||||
<Option value="1D">1天</Option>
|
||||
<Option value="1W">1周</Option>
|
||||
</Select>
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="置信度阈值">
|
||||
<Input addonAfter="%" type="number" defaultValue={70} min={50} max={95} />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="历史数据长度">
|
||||
<Input addonAfter="天" type="number" defaultValue={90} min={30} max={365} />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button type="primary" style={{ marginTop: 8 }}>
|
||||
应用参数
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
{/* 系统配置 */}
|
||||
<Card
|
||||
title={<span><SlidersOutlined /> 系统配置</span>}
|
||||
className="config-card"
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="param-item">
|
||||
<label>数据刷新间隔: {params.refreshInterval}秒</label>
|
||||
<Slider
|
||||
min={10}
|
||||
max={300}
|
||||
step={10}
|
||||
value={params.refreshInterval}
|
||||
onChange={(value) => handleParamChange('refreshInterval', value)}
|
||||
marks={{
|
||||
10: '10s',
|
||||
300: '5m'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="param-item">
|
||||
<label>预警阈值: {params.alertThreshold}%</label>
|
||||
<Slider
|
||||
min={50}
|
||||
max={90}
|
||||
step={5}
|
||||
value={params.alertThreshold}
|
||||
onChange={(value) => handleParamChange('alertThreshold', value)}
|
||||
marks={{
|
||||
50: '50%',
|
||||
90: '90%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="param-item">
|
||||
<label>最大仓位比例: {params.maxPositionSize}%</label>
|
||||
<Slider
|
||||
min={10}
|
||||
max={100}
|
||||
step={5}
|
||||
value={params.maxPositionSize}
|
||||
onChange={(value) => handleParamChange('maxPositionSize', value)}
|
||||
marks={{
|
||||
10: '10%',
|
||||
100: '100%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="param-item">
|
||||
<label>回测天数: {params.backtestDays}天</label>
|
||||
<Slider
|
||||
min={30}
|
||||
max={365}
|
||||
step={30}
|
||||
value={params.backtestDays}
|
||||
onChange={(value) => handleParamChange('backtestDays', value)}
|
||||
marks={{
|
||||
30: '30天',
|
||||
365: '1年'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 其他系统设置 */}
|
||||
<Card title="其他设置" style={{ marginTop: 24 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Item label="自动复盘">
|
||||
<Switch defaultChecked />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="邮件通知">
|
||||
<Switch />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="短信通知">
|
||||
<Switch />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<div className="config-actions">
|
||||
<Button type="default" style={{ marginRight: 8 }}>
|
||||
恢复默认
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存配置
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Config;
|
||||
@ -0,0 +1,231 @@
|
||||
.dashboard {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
margin: 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.dashboard-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 16px 0;
|
||||
color: #262626;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dashboard-card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 市场热点 */
|
||||
.market-hotspots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hotspot-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hotspot-rank {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.hotspot-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hotspot-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.hotspot-change {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hotspot-winrate {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* 风险预警 */
|
||||
.risk-alerts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* AI研判 */
|
||||
.ai-analysis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.ai-overall {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ai-factors h4,
|
||||
.ai-recommendations h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.factor-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ai-recommendations ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ai-recommendations li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 品种卡片 */
|
||||
.future-card {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.future-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.future-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.future-name h4 {
|
||||
margin: 0 0 4px 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.future-change {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.future-price {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #262626;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.future-metrics {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.future-trends {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.trend-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.trend-period {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 加载容器 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dashboard-header-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.ai-overall {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.future-trends {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
.detail {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.detail-header h2 {
|
||||
margin: 8px 0 0 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 加载容器 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* 错误容器 */
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* 图表标题 */
|
||||
.chart-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-title h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* K线图表 */
|
||||
.kline-chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* 趋势卡片 */
|
||||
.trend-card {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trend-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.trend-header h4 {
|
||||
margin: 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.trend-status {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.trend-rsi {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 技术指标 */
|
||||
.indicator-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.indicator-label {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* 风险评估 */
|
||||
.risk-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.risk-label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.detail-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kline-chart {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.trend-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.indicator-item,
|
||||
.risk-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,377 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Card, Row, Col, Button, Select, Tag, Statistic, Alert, Spin } from 'antd';
|
||||
import { ArrowUpOutlined, ArrowDownOutlined, LineChartOutlined, BarChartOutlined, AlertOutlined, CalculatorOutlined } from '@ant-design/icons';
|
||||
import { fetchFutureDetail } from '../../store/futuresSlice';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { generateKlineData } from '../../utils/mockData';
|
||||
import './Detail.css';
|
||||
|
||||
// 导入TradingView Lightweight Charts
|
||||
import { createChart } from 'lightweight-charts';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const Detail = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const chartRef = useRef(null);
|
||||
const chartInstance = useRef(null);
|
||||
const { selectedFuture, loading } = useSelector(state => state.futures);
|
||||
const [timeframe, setTimeframe] = useState('1D');
|
||||
|
||||
// 解析URL参数获取品种信息
|
||||
const getQueryParams = () => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
return {
|
||||
code: params.get('code') || 'MA',
|
||||
name: params.get('name') || '甲醇'
|
||||
};
|
||||
};
|
||||
|
||||
const { code, name } = getQueryParams();
|
||||
|
||||
useEffect(() => {
|
||||
// 获取品种详情数据
|
||||
dispatch(fetchFutureDetail({ code, name }));
|
||||
}, [dispatch, code, name]);
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化K线图表
|
||||
if (chartRef.current && selectedFuture) {
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.destroy();
|
||||
}
|
||||
|
||||
const chart = createChart(chartRef.current, {
|
||||
width: chartRef.current.clientWidth,
|
||||
height: 400,
|
||||
layout: {
|
||||
backgroundColor: '#fff',
|
||||
textColor: '#262626'
|
||||
},
|
||||
grid: {
|
||||
vertLines: {
|
||||
color: '#f0f0f0'
|
||||
},
|
||||
horzLines: {
|
||||
color: '#f0f0f0'
|
||||
}
|
||||
},
|
||||
priceScale: {
|
||||
borderColor: '#f0f0f0'
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: '#f0f0f0',
|
||||
timeVisible: true,
|
||||
secondsVisible: false
|
||||
}
|
||||
});
|
||||
|
||||
// 添加K线系列
|
||||
const candlestickSeries = chart.addCandlestickSeries({
|
||||
upColor: '#52c41a',
|
||||
downColor: '#ff4d4f',
|
||||
borderUpColor: '#52c41a',
|
||||
borderDownColor: '#ff4d4f',
|
||||
wickUpColor: '#52c41a',
|
||||
wickDownColor: '#ff4d4f'
|
||||
});
|
||||
|
||||
// 生成K线数据
|
||||
const klineData = generateKlineData(30);
|
||||
candlestickSeries.setData(klineData);
|
||||
|
||||
// 添加成交量系列
|
||||
const volumeSeries = chart.addHistogramSeries({
|
||||
color: '#82ca9d',
|
||||
lineWidth: 1,
|
||||
priceScaleId: '',
|
||||
scaleMargins: {
|
||||
top: 0.8,
|
||||
bottom: 0
|
||||
}
|
||||
});
|
||||
|
||||
const volumeData = klineData.map(item => ({
|
||||
time: item.time,
|
||||
value: item.volume,
|
||||
color: item.close >= item.open ? '#52c41a' : '#ff4d4f'
|
||||
}));
|
||||
|
||||
volumeSeries.setData(volumeData);
|
||||
|
||||
// 缩放到合适的范围
|
||||
chart.timeScale().fitContent();
|
||||
|
||||
chartInstance.current = chart;
|
||||
|
||||
// 响应窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.resize(chartRef.current.clientWidth, 400);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (chartInstance.current) {
|
||||
chartInstance.current.destroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [selectedFuture]);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const getChangeColor = (changePercent) => {
|
||||
return changePercent >= 0 ? '#52c41a' : '#ff4d4f';
|
||||
};
|
||||
|
||||
const getChangeIcon = (changePercent) => {
|
||||
return changePercent >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />;
|
||||
};
|
||||
|
||||
const getTrendColor = (direction) => {
|
||||
if (direction === '看多') return '#52c41a';
|
||||
if (direction === '看空') return '#ff4d4f';
|
||||
return '#faad14';
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" tip="加载数据中..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selectedFuture) {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<Alert message="未找到品种数据" type="error" />
|
||||
<Button type="primary" onClick={handleBack} style={{ marginTop: 16 }}>
|
||||
返回主页
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="detail">
|
||||
{/* 页面头部 */}
|
||||
<div className="detail-header">
|
||||
<Button type="default" onClick={handleBack} style={{ marginBottom: 16 }}>
|
||||
返回主页
|
||||
</Button>
|
||||
<h2>{selectedFuture.fullName}</h2>
|
||||
</div>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<Card className="detail-card" style={{ marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="当前价格"
|
||||
value={selectedFuture.currentPrice}
|
||||
valueStyle={{ color: '#262626' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="涨跌幅"
|
||||
value={Math.abs(selectedFuture.changePercent)}
|
||||
suffix="%"
|
||||
valueStyle={{ color: getChangeColor(selectedFuture.changePercent) }}
|
||||
prefix={getChangeIcon(selectedFuture.changePercent)}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="胜率"
|
||||
value={selectedFuture.winRate}
|
||||
suffix="%"
|
||||
valueStyle={{ color: selectedFuture.winRate > 60 ? '#52c41a' : selectedFuture.winRate > 40 ? '#faad14' : '#ff4d4f' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="ATR"
|
||||
value={selectedFuture.atr}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="ADX"
|
||||
value={selectedFuture.adx}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="趋势状态"
|
||||
value={selectedFuture.adxStatus}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* K线图表 */}
|
||||
<Card
|
||||
title={
|
||||
<div className="chart-title">
|
||||
<LineChartOutlined /> K线图表
|
||||
<Select
|
||||
defaultValue="1D"
|
||||
style={{ width: 120, marginLeft: 16 }}
|
||||
onChange={setTimeframe}
|
||||
>
|
||||
<Option value="5MIN">5分钟</Option>
|
||||
<Option value="30MIN">30分钟</Option>
|
||||
<Option value="1H">1小时</Option>
|
||||
<Option value="1D">1天</Option>
|
||||
<Option value="1W">1周</Option>
|
||||
</Select>
|
||||
</div>
|
||||
}
|
||||
className="detail-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<div ref={chartRef} className="kline-chart"></div>
|
||||
</Card>
|
||||
|
||||
{/* 多周期趋势分析 */}
|
||||
<Card
|
||||
title={<span><BarChartOutlined /> 多周期趋势分析</span>}
|
||||
className="detail-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
{Object.entries(selectedFuture.trends).map(([period, trend]) => (
|
||||
<Col span={6} key={period}>
|
||||
<Card className="trend-card">
|
||||
<div className="trend-header">
|
||||
<h4>{period}</h4>
|
||||
<Tag color={getTrendColor(trend.direction)}>
|
||||
{trend.direction}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="trend-status">
|
||||
{trend.status}
|
||||
</div>
|
||||
<div className="trend-rsi">
|
||||
RSI: {trend.rsi}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 技术指标 */}
|
||||
<Card
|
||||
title="技术指标"
|
||||
className="detail-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={6}>
|
||||
<div className="indicator-item">
|
||||
<div className="indicator-label">MACD</div>
|
||||
<div className="indicator-value">{selectedFuture.indicators.macd}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="indicator-item">
|
||||
<div className="indicator-label">RSI</div>
|
||||
<div className="indicator-value">{selectedFuture.indicators.rsi}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="indicator-item">
|
||||
<div className="indicator-label">布林带</div>
|
||||
<div className="indicator-value">{selectedFuture.indicators.bollinger}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="indicator-item">
|
||||
<div className="indicator-label">KDJ</div>
|
||||
<div className="indicator-value">{selectedFuture.indicators.kdj}</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 交易建议 */}
|
||||
<Card
|
||||
title={<span><CalculatorOutlined /> 交易建议</span>}
|
||||
className="detail-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="入场价"
|
||||
value={selectedFuture.tradingAdvice.entry}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="止损价"
|
||||
value={selectedFuture.tradingAdvice.stopLoss}
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="目标价"
|
||||
value={selectedFuture.tradingAdvice.target}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 风险评估 */}
|
||||
<Card
|
||||
title={<span><AlertOutlined /> 风险评估</span>}
|
||||
className="detail-card"
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="risk-item">
|
||||
<div className="risk-label">风险等级</div>
|
||||
<Tag color={selectedFuture.riskLevel === '高' ? 'red' : selectedFuture.riskLevel === '中等' ? 'orange' : 'green'}>
|
||||
{selectedFuture.riskLevel}
|
||||
</Tag>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="risk-item">
|
||||
<div className="risk-label">波动率</div>
|
||||
<Tag color={selectedFuture.volatility === '高' ? 'red' : selectedFuture.volatility === '中等' ? 'orange' : 'green'}>
|
||||
{selectedFuture.volatility}
|
||||
</Tag>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Alert
|
||||
message="风险提示"
|
||||
description="期货交易具有高风险,请根据自身风险承受能力合理控制仓位,严格执行止损策略。"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Detail;
|
||||
@ -0,0 +1,74 @@
|
||||
.risk-control {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.risk-control h2 {
|
||||
margin: 0 0 24px 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.risk-card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 仓位计算结果 */
|
||||
.position-result {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 16px;
|
||||
padding: 24px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 风险偏好设置 */
|
||||
.risk-preference {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.risk-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.risk-slider label {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.risk-slider .ant-slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 风险监控指标 */
|
||||
.risk-metric-card {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.risk-level {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.position-result {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.risk-slider {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.risk-slider .ant-slider {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,275 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Row, Col, Form, Input, Button, Select, Slider, Tag, Alert, Statistic, Table } from 'antd';
|
||||
import { SafetyOutlined, CalculatorOutlined, CalendarOutlined, BarChartOutlined } from '@ant-design/icons';
|
||||
import './RiskControl.css';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Item } = Form;
|
||||
|
||||
const RiskControl = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [positionSize, setPositionSize] = useState(0);
|
||||
const [riskLevel, setRiskLevel] = useState(50);
|
||||
|
||||
// 止损策略选项
|
||||
const stopLossStrategies = [
|
||||
{ value: 'fixed', label: '固定止损' },
|
||||
{ value: 'moving', label: '移动止损' },
|
||||
{ value: 'percent', label: '百分比止损' },
|
||||
{ value: 'volatility', label: '波动率止损' }
|
||||
];
|
||||
|
||||
// 换月预警数据
|
||||
const rolloverAlerts = [
|
||||
{ id: 1, symbol: '螺纹钢-RB605', expiryDate: '2026-05-15', daysLeft: 15, urgency: '中等' },
|
||||
{ id: 2, symbol: '原油-SC606', expiryDate: '2026-06-20', daysLeft: 30, urgency: '低' },
|
||||
{ id: 3, symbol: '铜-CU604', expiryDate: '2026-04-20', daysLeft: 5, urgency: '高' },
|
||||
{ id: 4, symbol: '甲醇-MA605', expiryDate: '2026-05-10', daysLeft: 10, urgency: '中等' }
|
||||
];
|
||||
|
||||
// 风险监控数据
|
||||
const riskMetrics = [
|
||||
{ name: '风险价值(VaR)', value: '12,500', unit: '元', level: '中等' },
|
||||
{ name: '最大回撤', value: '8.5', unit: '%', level: '低' },
|
||||
{ name: '夏普比率', value: '1.2', unit: '', level: '高' },
|
||||
{ name: '仓位使用率', value: '65', unit: '%', level: '中等' }
|
||||
];
|
||||
|
||||
// 计算仓位
|
||||
const calculatePosition = (values) => {
|
||||
const { accountBalance, riskPerTrade, stopLossPercent } = values;
|
||||
const riskAmount = accountBalance * (riskPerTrade / 100);
|
||||
const position = riskAmount / (stopLossPercent / 100);
|
||||
setPositionSize(position);
|
||||
};
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = (values) => {
|
||||
calculatePosition(values);
|
||||
};
|
||||
|
||||
// 获取紧急程度颜色
|
||||
const getUrgencyColor = (urgency) => {
|
||||
if (urgency === '高') return 'red';
|
||||
if (urgency === '中等') return 'orange';
|
||||
return 'green';
|
||||
};
|
||||
|
||||
// 获取风险等级颜色
|
||||
const getRiskLevelColor = (level) => {
|
||||
if (level === '高') return 'red';
|
||||
if (level === '中等') return 'orange';
|
||||
return 'green';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="risk-control">
|
||||
<h2>风控管理</h2>
|
||||
|
||||
{/* 止损策略设置 */}
|
||||
<Card
|
||||
title={<span><SafetyOutlined /> 止损策略设置</span>}
|
||||
className="risk-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Item label="止损策略" name="stopLossStrategy" rules={[{ required: true }]}>
|
||||
<Select placeholder="选择止损策略">
|
||||
{stopLossStrategies.map(strategy => (
|
||||
<Option key={strategy.value} value={strategy.value}>
|
||||
{strategy.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="止损百分比" name="stopLossPercent" rules={[{ required: true }]}>
|
||||
<Input addonAfter="%" type="number" min={0.1} max={50} step={0.1} />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="止损点位" name="stopLossPrice">
|
||||
<Input addonBefore="¥" type="number" step={0.01} />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button type="primary" htmlType="submit" style={{ marginTop: 8 }}>
|
||||
应用止损策略
|
||||
</Button>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 仓位管理 */}
|
||||
<Card
|
||||
title={<span><CalculatorOutlined /> 仓位管理</span>}
|
||||
className="risk-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Item label="账户余额" name="accountBalance" rules={[{ required: true }]}>
|
||||
<Input addonBefore="¥" type="number" min={1000} step={1000} />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="每笔交易风险" name="riskPerTrade" rules={[{ required: true }]}>
|
||||
<Input addonAfter="%" type="number" min={0.1} max={20} step={0.1} />
|
||||
</Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Item label="止损百分比" name="stopLossPercent" rules={[{ required: true }]}>
|
||||
<Input addonAfter="%" type="number" min={0.1} max={50} step={0.1} />
|
||||
</Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button type="primary" htmlType="submit" style={{ marginBottom: 16 }}>
|
||||
计算仓位
|
||||
</Button>
|
||||
|
||||
{positionSize > 0 && (
|
||||
<Card className="position-result">
|
||||
<Statistic
|
||||
title="建议仓位大小"
|
||||
value={positionSize.toFixed(2)}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
<Statistic
|
||||
title="风险暴露"
|
||||
value={(positionSize * 0.05).toFixed(2)}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
/>
|
||||
<Statistic
|
||||
title="仓位比例"
|
||||
value={(positionSize / form.getFieldValue('accountBalance') * 100).toFixed(2)}
|
||||
suffix="%"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
{/* 风险偏好设置 */}
|
||||
<Card title="风险偏好设置" style={{ marginTop: 24 }}>
|
||||
<div className="risk-preference">
|
||||
<div className="risk-slider">
|
||||
<label>风险偏好: </label>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
value={riskLevel}
|
||||
onChange={setRiskLevel}
|
||||
marks={{
|
||||
0: '保守',
|
||||
50: '适中',
|
||||
100: '激进'
|
||||
}}
|
||||
/>
|
||||
<Tag color={riskLevel < 33 ? 'green' : riskLevel < 66 ? 'orange' : 'red'}>
|
||||
{riskLevel < 33 ? '保守' : riskLevel < 66 ? '适中' : '激进'}
|
||||
</Tag>
|
||||
</div>
|
||||
<Alert
|
||||
message="风险提示"
|
||||
description="风险偏好设置将影响系统给出的仓位建议和止损策略,请根据自身风险承受能力合理设置。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
|
||||
{/* 换月预警 */}
|
||||
<Card
|
||||
title={<span><CalendarOutlined /> 换月预警</span>}
|
||||
className="risk-card"
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<Table
|
||||
dataSource={rolloverAlerts}
|
||||
columns={[
|
||||
{
|
||||
title: '合约',
|
||||
dataIndex: 'symbol',
|
||||
key: 'symbol'
|
||||
},
|
||||
{
|
||||
title: '到期日',
|
||||
dataIndex: 'expiryDate',
|
||||
key: 'expiryDate'
|
||||
},
|
||||
{
|
||||
title: '剩余天数',
|
||||
dataIndex: 'daysLeft',
|
||||
key: 'daysLeft',
|
||||
render: (days) => (
|
||||
<Tag color={days < 7 ? 'red' : days < 15 ? 'orange' : 'green'}>
|
||||
{days} 天
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '紧急程度',
|
||||
dataIndex: 'urgency',
|
||||
key: 'urgency',
|
||||
render: (urgency) => (
|
||||
<Tag color={getUrgencyColor(urgency)}>
|
||||
{urgency}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: () => (
|
||||
<Button type="link">查看详情</Button>
|
||||
)
|
||||
}
|
||||
]}
|
||||
rowKey="id"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 风险监控 */}
|
||||
<Card
|
||||
title={<span><BarChartOutlined /> 风险监控</span>}
|
||||
className="risk-card"
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
{riskMetrics.map((metric, index) => (
|
||||
<Col span={6} key={index}>
|
||||
<Card className="risk-metric-card">
|
||||
<Statistic
|
||||
title={metric.name}
|
||||
value={metric.value}
|
||||
suffix={metric.unit}
|
||||
valueStyle={{ color: getRiskLevelColor(metric.level) }}
|
||||
/>
|
||||
<div className="risk-level">
|
||||
<Tag color={getRiskLevelColor(metric.level)}>
|
||||
{metric.level}风险
|
||||
</Tag>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<Alert
|
||||
message="风险监控提示"
|
||||
description="系统会实时监控您的交易风险,当风险指标超过预警阈值时,会及时发出预警通知。"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RiskControl;
|
||||
@ -0,0 +1,123 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { generateFuturesOverview, generateFutureData, riskAlerts, aiMarketAnalysis } from '../utils/mockData';
|
||||
|
||||
// 模拟异步获取期货概览数据
|
||||
export const fetchFuturesOverview = createAsyncThunk(
|
||||
'futures/fetchOverview',
|
||||
async () => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return generateFuturesOverview();
|
||||
}
|
||||
);
|
||||
|
||||
// 模拟异步获取单个期货详情
|
||||
export const fetchFutureDetail = createAsyncThunk(
|
||||
'futures/fetchDetail',
|
||||
async ({ code, name }) => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return generateFutureData(code, name);
|
||||
}
|
||||
);
|
||||
|
||||
// 模拟异步获取风险预警
|
||||
export const fetchRiskAlerts = createAsyncThunk(
|
||||
'futures/fetchRiskAlerts',
|
||||
async () => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return riskAlerts;
|
||||
}
|
||||
);
|
||||
|
||||
// 模拟异步获取AI市场分析
|
||||
export const fetchAIMarketAnalysis = createAsyncThunk(
|
||||
'futures/fetchAIMarketAnalysis',
|
||||
async () => {
|
||||
// 模拟API请求延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 400));
|
||||
return aiMarketAnalysis;
|
||||
}
|
||||
);
|
||||
|
||||
const futuresSlice = createSlice({
|
||||
name: 'futures',
|
||||
initialState: {
|
||||
overview: [],
|
||||
selectedFuture: null,
|
||||
riskAlerts: [],
|
||||
aiAnalysis: null,
|
||||
loading: false,
|
||||
error: null
|
||||
},
|
||||
reducers: {
|
||||
selectFuture: (state, action) => {
|
||||
state.selectedFuture = action.payload;
|
||||
},
|
||||
clearSelectedFuture: (state) => {
|
||||
state.selectedFuture = null;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// 处理fetchFuturesOverview
|
||||
.addCase(fetchFuturesOverview.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchFuturesOverview.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.overview = action.payload;
|
||||
})
|
||||
.addCase(fetchFuturesOverview.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message;
|
||||
})
|
||||
|
||||
// 处理fetchFutureDetail
|
||||
.addCase(fetchFutureDetail.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchFutureDetail.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.selectedFuture = action.payload;
|
||||
})
|
||||
.addCase(fetchFutureDetail.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message;
|
||||
})
|
||||
|
||||
// 处理fetchRiskAlerts
|
||||
.addCase(fetchRiskAlerts.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchRiskAlerts.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.riskAlerts = action.payload;
|
||||
})
|
||||
.addCase(fetchRiskAlerts.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message;
|
||||
})
|
||||
|
||||
// 处理fetchAIMarketAnalysis
|
||||
.addCase(fetchAIMarketAnalysis.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchAIMarketAnalysis.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.aiAnalysis = action.payload;
|
||||
})
|
||||
.addCase(fetchAIMarketAnalysis.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { selectFuture, clearSelectedFuture } = futuresSlice.actions;
|
||||
export default futuresSlice.reducer;
|
||||
@ -0,0 +1,10 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import futuresReducer from './futuresSlice';
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
futures: futuresReducer
|
||||
}
|
||||
});
|
||||
|
||||
export default store;
|
||||
@ -0,0 +1,211 @@
|
||||
// 模拟数据
|
||||
|
||||
// 期货品种列表
|
||||
export const futuresList = [
|
||||
// 金属类
|
||||
{ code: 'AU', name: '金', type: '金属' },
|
||||
{ code: 'AG', name: '银', type: '金属' },
|
||||
{ code: 'CU', name: '铜', type: '金属' },
|
||||
{ code: 'NI', name: '镍', type: '金属' },
|
||||
{ code: 'SN', name: '锡', type: '金属' },
|
||||
{ code: 'AL', name: '铝', type: '金属' },
|
||||
{ code: 'ZN', name: '锌', type: '金属' },
|
||||
|
||||
// 建材类
|
||||
{ code: 'FG', name: '玻璃', type: '建材' },
|
||||
{ code: 'SJS', name: '烧碱', type: '建材' },
|
||||
{ code: 'SCA', name: '纯碱', type: '建材' },
|
||||
{ code: 'JM', name: '焦煤', type: '建材' },
|
||||
{ code: 'RB', name: '螺纹钢', type: '建材' },
|
||||
{ code: 'ALO', name: '氧化铝', type: '建材' },
|
||||
|
||||
// 能源化工类
|
||||
{ code: 'MA', name: '甲醇', type: '能源化工' },
|
||||
{ code: 'PVC', name: 'PVC', type: '能源化工' },
|
||||
{ code: 'FU', name: '燃油', type: '能源化工' },
|
||||
{ code: 'SC', name: '原油', type: '能源化工' },
|
||||
{ code: 'L', name: '橡胶', type: '能源化工' },
|
||||
{ code: 'NR', name: '20号胶', type: '能源化工' },
|
||||
{ code: 'BU', name: '沥青', type: '能源化工' },
|
||||
{ code: 'LU', name: '低硫燃油', type: '能源化工' },
|
||||
|
||||
// 农产品类
|
||||
{ code: 'P', name: '棕榈油', type: '农产品' },
|
||||
|
||||
// 新能源类
|
||||
{ code: 'LC', name: '碳酸锂', type: '新能源' },
|
||||
{ code: 'SI', name: '工业硅', type: '新能源' },
|
||||
{ code: 'PGS', name: '多晶硅', type: '新能源' },
|
||||
|
||||
// 金融类
|
||||
{ code: 'IC', name: '中证500', type: '金融' },
|
||||
{ code: 'IM', name: '中证1000', type: '金融' },
|
||||
{ code: 'IH', name: '上证50', type: '金融' }
|
||||
];
|
||||
|
||||
// 生成随机数据的工具函数
|
||||
const generateRandomPrice = (base, volatility) => {
|
||||
return +(base + (Math.random() - 0.5) * 2 * volatility).toFixed(2);
|
||||
};
|
||||
|
||||
const generateRandomChange = () => {
|
||||
return +(Math.random() * 10 - 5).toFixed(2);
|
||||
};
|
||||
|
||||
const generateRandomWinRate = () => {
|
||||
return Math.floor(Math.random() * 50) + 30;
|
||||
};
|
||||
|
||||
const generateRandomATR = () => {
|
||||
return +(Math.random() * 5 + 0.5).toFixed(2);
|
||||
};
|
||||
|
||||
const generateRandomADX = () => {
|
||||
return Math.floor(Math.random() * 60) + 10;
|
||||
};
|
||||
|
||||
const getADXStatus = (adx) => {
|
||||
if (adx < 20) return '无趋势/震荡';
|
||||
if (adx < 40) return '弱趋势';
|
||||
return '强趋势';
|
||||
};
|
||||
|
||||
const getTrendDirection = () => {
|
||||
const directions = ['看多', '看空', '观望'];
|
||||
return directions[Math.floor(Math.random() * directions.length)];
|
||||
};
|
||||
|
||||
const getTrendStatus = (direction) => {
|
||||
if (direction === '看多') return '多头趋势';
|
||||
if (direction === '看空') return '空头趋势';
|
||||
return '震荡';
|
||||
};
|
||||
|
||||
const generateRandomRSI = () => {
|
||||
return Math.floor(Math.random() * 80) + 10;
|
||||
};
|
||||
|
||||
// 生成品种详细数据
|
||||
export const generateFutureData = (code, name) => {
|
||||
const currentPrice = generateRandomPrice(2000, 500);
|
||||
const changePercent = generateRandomChange();
|
||||
const atr = generateRandomATR();
|
||||
const adx = generateRandomADX();
|
||||
const winRate = generateRandomWinRate();
|
||||
|
||||
const trends = {
|
||||
'5MIN': {
|
||||
direction: getTrendDirection(),
|
||||
status: getTrendStatus(getTrendDirection()),
|
||||
rsi: generateRandomRSI()
|
||||
},
|
||||
'30MIN': {
|
||||
direction: getTrendDirection(),
|
||||
status: getTrendStatus(getTrendDirection()),
|
||||
rsi: generateRandomRSI()
|
||||
},
|
||||
'1HOUR': {
|
||||
direction: getTrendDirection(),
|
||||
status: getTrendStatus(getTrendDirection()),
|
||||
rsi: generateRandomRSI()
|
||||
},
|
||||
'1DAY': {
|
||||
direction: getTrendDirection(),
|
||||
status: getTrendStatus(getTrendDirection()),
|
||||
rsi: generateRandomRSI()
|
||||
}
|
||||
};
|
||||
|
||||
const indicators = {
|
||||
macd: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)],
|
||||
rsi: `${generateRandomRSI()}(中性)`,
|
||||
bollinger: ['触及上轨', '触及下轨', '中轨附近'][Math.floor(Math.random() * 3)],
|
||||
kdj: ['金叉向上', '死叉向下', '走平'][Math.floor(Math.random() * 3)]
|
||||
};
|
||||
|
||||
const entry = currentPrice;
|
||||
const stopLoss = entry * (1 - 0.02 * (Math.random() + 0.5));
|
||||
const target = entry * (1 + 0.03 * (Math.random() + 0.5));
|
||||
|
||||
return {
|
||||
code,
|
||||
name,
|
||||
fullName: `${name}-${code}605`,
|
||||
currentPrice,
|
||||
changePercent,
|
||||
atr,
|
||||
adx,
|
||||
adxStatus: getADXStatus(adx),
|
||||
winRate,
|
||||
trends,
|
||||
indicators,
|
||||
tradingAdvice: {
|
||||
entry: +entry.toFixed(2),
|
||||
stopLoss: +stopLoss.toFixed(2),
|
||||
target: +target.toFixed(2)
|
||||
},
|
||||
riskLevel: ['低', '中等', '高'][Math.floor(Math.random() * 3)],
|
||||
volatility: ['低', '中等', '高'][Math.floor(Math.random() * 3)]
|
||||
};
|
||||
};
|
||||
|
||||
// 生成多个品种的概览数据
|
||||
export const generateFuturesOverview = () => {
|
||||
return futuresList.map(item => {
|
||||
const data = generateFutureData(item.code, item.name);
|
||||
return {
|
||||
code: data.code,
|
||||
name: data.name,
|
||||
currentPrice: data.currentPrice,
|
||||
changePercent: data.changePercent,
|
||||
winRate: data.winRate,
|
||||
atr: data.atr,
|
||||
adx: data.adx,
|
||||
adxStatus: data.adxStatus,
|
||||
trends: data.trends
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 风险预警数据
|
||||
export const riskAlerts = [
|
||||
{ id: 1, title: '原油波动加剧', level: '高', message: '原油价格近期波动较大,建议控制仓位' },
|
||||
{ id: 2, title: '螺纹钢换月提醒', level: '中等', message: '螺纹钢主力合约即将换月,请注意移仓' },
|
||||
{ id: 3, title: '市场情绪偏空', level: '中等', message: '多数品种技术指标显示空头信号,建议谨慎操作' },
|
||||
{ id: 4, title: '铜库存下降', level: '低', message: '铜库存持续下降,可能影响价格走势' }
|
||||
];
|
||||
|
||||
// AI市场研判
|
||||
export const aiMarketAnalysis = {
|
||||
overallTrend: '震荡偏弱',
|
||||
keyFactors: ['原油价格波动', '宏观经济数据', '政策面变化'],
|
||||
recommendations: ['控制仓位', '关注原油走势', '做好止损'],
|
||||
confidence: 75
|
||||
};
|
||||
|
||||
// K线图模拟数据
|
||||
export const generateKlineData = (days = 30) => {
|
||||
const data = [];
|
||||
let price = 2000;
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const open = price;
|
||||
const high = open + Math.random() * 50;
|
||||
const low = open - Math.random() * 50;
|
||||
const close = low + Math.random() * (high - low);
|
||||
const volume = Math.floor(Math.random() * 100000) + 10000;
|
||||
|
||||
data.push({
|
||||
time: new Date(Date.now() - (days - i) * 24 * 60 * 60 * 1000).getTime() / 1000,
|
||||
open: +open.toFixed(2),
|
||||
high: +high.toFixed(2),
|
||||
low: +low.toFixed(2),
|
||||
close: +close.toFixed(2),
|
||||
volume
|
||||
});
|
||||
|
||||
price = close;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Loading…
Reference in new issue