update
This commit is contained in:
parent
f7fca2e0b9
commit
52548b4e4c
Binary file not shown.
@ -46,7 +46,7 @@ async def get_signals(ts_code: str):
|
||||
|
||||
@router.get("/{ts_code}/capital_flow")
|
||||
async def get_capital_flow(ts_code: str, days: int = 10):
|
||||
"""获取个股资金流向"""
|
||||
"""获取个股资金流向(含大/中/小单分拆)"""
|
||||
df = tushare_client.get_stock_moneyflow(ts_code, days=days)
|
||||
if df.empty:
|
||||
return []
|
||||
@ -61,6 +61,18 @@ async def get_capital_flow(ts_code: str, days: int = 10):
|
||||
"trade_date": row["trade_date"],
|
||||
"main_net_inflow": round(main_net, 2),
|
||||
"net_mf_amount": round(float(row.get("net_mf_amount", 0) or 0), 2),
|
||||
"elg_net": round(
|
||||
(row.get("buy_elg_amount", 0) or 0) - (row.get("sell_elg_amount", 0) or 0), 2
|
||||
),
|
||||
"lg_net": round(
|
||||
(row.get("buy_lg_amount", 0) or 0) - (row.get("sell_lg_amount", 0) or 0), 2
|
||||
),
|
||||
"md_net": round(
|
||||
(row.get("buy_md_amount", 0) or 0) - (row.get("sell_md_amount", 0) or 0), 2
|
||||
),
|
||||
"sm_net": round(
|
||||
(row.get("buy_sm_amount", 0) or 0) - (row.get("sell_sm_amount", 0) or 0), 2
|
||||
),
|
||||
})
|
||||
return records
|
||||
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
{
|
||||
"pages": {
|
||||
"/sectors/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/sectors/page.js"
|
||||
],
|
||||
"/layout": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
@ -20,6 +15,21 @@
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/recommendations/page.js"
|
||||
],
|
||||
"/stock/[code]/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/stock/[code]/page.js"
|
||||
],
|
||||
"/sectors/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/sectors/page.js"
|
||||
],
|
||||
"/_not-found/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/_not-found/page.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
2
frontend/.next/cache/.tsbuildinfo
vendored
2
frontend/.next/cache/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
@ -1 +1,20 @@
|
||||
{}
|
||||
{
|
||||
"components/capital-flow.tsx -> echarts": {
|
||||
"id": "components/capital-flow.tsx -> echarts",
|
||||
"files": [
|
||||
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
|
||||
]
|
||||
},
|
||||
"components/kline-chart.tsx -> echarts": {
|
||||
"id": "components/kline-chart.tsx -> echarts",
|
||||
"files": [
|
||||
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
|
||||
]
|
||||
},
|
||||
"components/score-radar.tsx -> echarts": {
|
||||
"id": "components/score-radar.tsx -> echarts",
|
||||
"files": [
|
||||
"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,7 @@
|
||||
{
|
||||
"/recommendations/page": "app/recommendations/page.js",
|
||||
"/page": "app/page.js",
|
||||
"/sectors/page": "app/sectors/page.js"
|
||||
"/_not-found/page": "app/_not-found/page.js",
|
||||
"/sectors/page": "app/sectors/page.js",
|
||||
"/recommendations/page": "app/recommendations/page.js",
|
||||
"/stock/[code]/page": "app/stock/[code]/page.js"
|
||||
}
|
||||
@ -1 +1 @@
|
||||
self.__REACT_LOADABLE_MANIFEST="{}"
|
||||
self.__REACT_LOADABLE_MANIFEST="{\"components/capital-flow.tsx -> echarts\":{\"id\":\"components/capital-flow.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/kline-chart.tsx -> echarts\":{\"id\":\"components/kline-chart.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]},\"components/score-radar.tsx -> echarts\":{\"id\":\"components/score-radar.tsx -> echarts\",\"files\":[\"static/chunks/_app-pages-browser_node_modules_echarts_index_js.js\"]}}"
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"node": {},
|
||||
"edge": {},
|
||||
"encryptionKey": "IAps6Nn+QS9GVH+sOr6laVRGfUJrD0aLxGy9A/+XkIs="
|
||||
"encryptionKey": "to26WE9kY05Cd2g+GH2pcshXDAS7bZksw3lMjIYHGFQ="
|
||||
}
|
||||
@ -125,7 +125,7 @@
|
||||
/******/
|
||||
/******/ /* webpack/runtime/getFullHash */
|
||||
/******/ (() => {
|
||||
/******/ __webpack_require__.h = () => ("a035f49818643978")
|
||||
/******/ __webpack_require__.h = () => ("5a8081291a3a3a33")
|
||||
/******/ })();
|
||||
/******/
|
||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||
|
||||
File diff suppressed because one or more lines are too long
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@ -11,6 +11,7 @@
|
||||
"echarts": "^5.5.0",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"next": "^14.2.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
@ -3706,6 +3707,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-themes": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
|
||||
@ -9,21 +9,22 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts-for-react": "^3.0.2"
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"next": "^14.2.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"typescript": "^5.0.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-next": "^14.2.0"
|
||||
"eslint-config-next": "^14.2.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { streamChat, type ChatMessage } from "@/lib/api";
|
||||
|
||||
interface DisplayMessage {
|
||||
@ -15,6 +16,7 @@ const QUICK_QUESTIONS = [
|
||||
];
|
||||
|
||||
export default function ChatPage() {
|
||||
const { theme } = useTheme();
|
||||
const [messages, setMessages] = useState<DisplayMessage[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [streaming, setStreaming] = useState(false);
|
||||
@ -87,7 +89,7 @@ export default function ChatPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto flex flex-col md:h-[calc(100dvh)]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-white/[0.04] bg-bg-primary/80 backdrop-blur-xl">
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-border-subtle bg-bg-primary/80 backdrop-blur-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-accent-cyan/30 to-accent-cyan/10 flex items-center justify-center border border-accent-cyan/20">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="text-accent-cyan/70">
|
||||
@ -104,7 +106,7 @@ export default function ChatPage() {
|
||||
{messages.length > 0 && (
|
||||
<button
|
||||
onClick={() => setMessages([])}
|
||||
className="text-xs text-text-muted hover:text-text-primary px-3 py-1.5 rounded-lg hover:bg-white/[0.04] transition-all duration-200"
|
||||
className="text-xs text-text-muted hover:text-text-primary px-3 py-1.5 rounded-lg hover:bg-surface-3 transition-all duration-200"
|
||||
>
|
||||
清空对话
|
||||
</button>
|
||||
@ -129,7 +131,7 @@ export default function ChatPage() {
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => sendMessage(q)}
|
||||
className="text-xs px-4 py-2.5 bg-white/[0.03] rounded-xl text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all duration-200 border border-white/[0.04] text-left"
|
||||
className="text-xs px-4 py-2.5 bg-surface-2 rounded-xl text-text-secondary hover:text-text-primary hover:bg-surface-4 transition-all duration-200 border border-border-subtle text-left"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
@ -153,7 +155,7 @@ export default function ChatPage() {
|
||||
{msg.role === "assistant" ? (
|
||||
msg.content ? (
|
||||
<div
|
||||
className="prose prose-invert prose-sm max-w-none [&_p]:my-1 [&_ul]:my-1 [&_li]:my-0.5 [&_strong]:text-orange-300"
|
||||
className={`prose ${theme !== "light" ? "prose-invert" : ""} prose-sm max-w-none [&_p]:my-1 [&_ul]:my-1 [&_li]:my-0.5 [&_strong]:${theme !== "light" ? "text-orange-300" : "text-orange-700"}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: formatMarkdown(msg.content),
|
||||
}}
|
||||
@ -186,7 +188,7 @@ export default function ChatPage() {
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="px-5 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] md:pb-3 border-t border-white/[0.04] bg-bg-primary/80 backdrop-blur-xl">
|
||||
<div className="px-5 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] md:pb-3 border-t border-border-subtle bg-bg-primary/80 backdrop-blur-xl">
|
||||
<div className="flex items-end gap-2">
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
@ -195,7 +197,7 @@ export default function ChatPage() {
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="输入问题..."
|
||||
rows={1}
|
||||
className="flex-1 bg-white/[0.03] rounded-xl px-4 py-2.5 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-accent-cyan/30 placeholder-text-muted/40 border border-white/[0.04] transition-all duration-200"
|
||||
className="flex-1 bg-surface-2 rounded-xl px-4 py-2.5 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-accent-cyan/30 placeholder-text-muted/40 border border-border-subtle transition-all duration-200"
|
||||
disabled={streaming}
|
||||
/>
|
||||
<button
|
||||
|
||||
@ -4,64 +4,180 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ========================================
|
||||
DARK THEME (default)
|
||||
======================================== */
|
||||
:root {
|
||||
/* Background */
|
||||
--bg-primary: #0a0a0c;
|
||||
--bg-secondary: #101013;
|
||||
--bg-card: #161619;
|
||||
--bg-elevated: #1e1e22;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #f5f5f7;
|
||||
--text-secondary: #9898a4;
|
||||
--text-muted: #58585f;
|
||||
|
||||
/* Accent */
|
||||
--accent-amber: #f59e0b;
|
||||
--accent-cyan: #22d3ee;
|
||||
|
||||
/* Financial */
|
||||
--color-up: #ff6b6b;
|
||||
--color-down: #34d399;
|
||||
--color-hot: #f59e0b;
|
||||
|
||||
/* Surface (replaces bg-white/[0.0x]) */
|
||||
--surface-1: rgba(255, 255, 255, 0.02);
|
||||
--surface-2: rgba(255, 255, 255, 0.03);
|
||||
--surface-3: rgba(255, 255, 255, 0.04);
|
||||
--surface-4: rgba(255, 255, 255, 0.06);
|
||||
|
||||
/* Border (replaces border-white/[0.0x]) */
|
||||
--border-subtle: rgba(255, 255, 255, 0.04);
|
||||
--border-default: rgba(255, 255, 255, 0.06);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
--shadow-glow: 0 0 20px rgba(245, 158, 11, 0.12);
|
||||
--shadow-glow-sm: 0 0 10px rgba(245, 158, 11, 0.08);
|
||||
|
||||
/* Glass */
|
||||
--glass-bg-from: rgba(22, 22, 25, 0.85);
|
||||
--glass-bg-to: rgba(10, 10, 12, 0.95);
|
||||
--glass-border: rgba(255, 255, 255, 0.05);
|
||||
--glass-inset: rgba(255, 255, 255, 0.03);
|
||||
--glass-hover-border: rgba(255, 255, 255, 0.10);
|
||||
--glass-hover-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
--glass-hover-inset: rgba(255, 255, 255, 0.05);
|
||||
--glass-sidebar-from: rgba(16, 16, 19, 0.97);
|
||||
--glass-sidebar-to: rgba(10, 10, 12, 0.99);
|
||||
--glass-sidebar-border: rgba(255, 255, 255, 0.04);
|
||||
|
||||
/* Effects */
|
||||
--noise-opacity: 0.018;
|
||||
--ambient-glow: rgba(245, 158, 11, 0.035);
|
||||
--shimmer-color: rgba(255, 255, 255, 0.04);
|
||||
--selection-bg: rgba(245, 158, 11, 0.25);
|
||||
|
||||
/* Scrollbar */
|
||||
--scrollbar-thumb: rgba(255, 255, 255, 0.10);
|
||||
--scrollbar-thumb-hover: rgba(255, 255, 255, 0.18);
|
||||
|
||||
/* Fonts */
|
||||
--font-display: 'Outfit', -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
--font-body: 'Outfit', -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
--font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
LIGHT THEME
|
||||
======================================== */
|
||||
:root.light {
|
||||
--bg-primary: #f5f6f8;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-card: #ffffff;
|
||||
--bg-elevated: #eef0f3;
|
||||
|
||||
--text-primary: #1a1a2e;
|
||||
--text-secondary: #4a4a5e;
|
||||
--text-muted: #8a8a9e;
|
||||
|
||||
--accent-amber: #d97706;
|
||||
--accent-cyan: #0891b2;
|
||||
|
||||
--color-up: #ef4444;
|
||||
--color-down: #22c55e;
|
||||
--color-hot: #d97706;
|
||||
|
||||
--surface-1: rgba(0, 0, 0, 0.02);
|
||||
--surface-2: rgba(0, 0, 0, 0.03);
|
||||
--surface-3: rgba(0, 0, 0, 0.05);
|
||||
--surface-4: rgba(0, 0, 0, 0.07);
|
||||
|
||||
--border-subtle: rgba(0, 0, 0, 0.07);
|
||||
--border-default: rgba(0, 0, 0, 0.10);
|
||||
|
||||
--shadow-card: 0 2px 12px rgba(0, 0, 0, 0.07);
|
||||
--shadow-glow: 0 0 16px rgba(217, 119, 6, 0.06);
|
||||
--shadow-glow-sm: 0 0 8px rgba(217, 119, 6, 0.04);
|
||||
|
||||
--glass-bg-from: rgba(255, 255, 255, 0.92);
|
||||
--glass-bg-to: rgba(255, 255, 255, 0.98);
|
||||
--glass-border: rgba(0, 0, 0, 0.08);
|
||||
--glass-inset: rgba(0, 0, 0, 0.02);
|
||||
--glass-hover-border: rgba(0, 0, 0, 0.14);
|
||||
--glass-hover-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
--glass-hover-inset: rgba(0, 0, 0, 0.03);
|
||||
--glass-sidebar-from: rgba(255, 255, 255, 0.97);
|
||||
--glass-sidebar-to: rgba(255, 255, 255, 0.99);
|
||||
--glass-sidebar-border: rgba(0, 0, 0, 0.07);
|
||||
|
||||
--noise-opacity: 0;
|
||||
--ambient-glow: transparent;
|
||||
--shimmer-color: rgba(0, 0, 0, 0.03);
|
||||
--selection-bg: rgba(217, 119, 6, 0.18);
|
||||
|
||||
--scrollbar-thumb: rgba(0, 0, 0, 0.12);
|
||||
--scrollbar-thumb-hover: rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
BASE
|
||||
======================================== */
|
||||
body {
|
||||
background-color: #0a0a0c;
|
||||
color: #f5f5f7;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-body);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Tabular numbers for financial data */
|
||||
.tabular-nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
/* Glass morphism card */
|
||||
/* ========================================
|
||||
GLASS MORPHISM (theme-aware)
|
||||
======================================== */
|
||||
@layer components {
|
||||
.glass-card {
|
||||
background: linear-gradient(135deg, rgba(22, 22, 25, 0.85) 0%, rgba(10, 10, 12, 0.95) 100%);
|
||||
background: linear-gradient(135deg, var(--glass-bg-from) 0%, var(--glass-bg-to) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
box-shadow: inset 0 1px 0 var(--glass-inset);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.10);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
border-color: var(--glass-hover-border);
|
||||
box-shadow: var(--glass-hover-shadow), inset 0 1px 0 var(--glass-hover-inset);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.glass-card-static {
|
||||
background: linear-gradient(135deg, rgba(22, 22, 25, 0.85) 0%, rgba(10, 10, 12, 0.95) 100%);
|
||||
background: linear-gradient(135deg, var(--glass-bg-from) 0%, var(--glass-bg-to) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
box-shadow: inset 0 1px 0 var(--glass-inset);
|
||||
}
|
||||
|
||||
.glass-sidebar {
|
||||
background: linear-gradient(180deg, rgba(16, 16, 19, 0.97) 0%, rgba(10, 10, 12, 0.99) 100%);
|
||||
background: linear-gradient(180deg, var(--glass-sidebar-from) 0%, var(--glass-sidebar-to) 100%);
|
||||
backdrop-filter: blur(30px);
|
||||
-webkit-backdrop-filter: blur(30px);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.04);
|
||||
border-right: 1px solid var(--glass-sidebar-border);
|
||||
}
|
||||
|
||||
.glow-accent {
|
||||
box-shadow: 0 0 20px rgba(245, 158, 11, 0.12), 0 0 60px rgba(245, 158, 11, 0.04);
|
||||
box-shadow: var(--shadow-glow), 0 0 60px rgba(245, 158, 11, 0.04);
|
||||
}
|
||||
|
||||
.gradient-border {
|
||||
@ -82,18 +198,22 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
EFFECTS (theme-aware)
|
||||
======================================== */
|
||||
|
||||
/* Noise texture overlay */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
opacity: 0.018;
|
||||
opacity: var(--noise-opacity);
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Ambient glow effect - top right (amber) */
|
||||
/* Ambient glow - top right (amber) */
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
@ -101,7 +221,7 @@ body::after {
|
||||
right: -200px;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(245, 158, 11, 0.035) 0%, transparent 70%);
|
||||
background: radial-gradient(circle, var(--ambient-glow) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
@ -114,14 +234,22 @@ body::after {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: var(--selection-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
ANIMATIONS
|
||||
======================================== */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
@ -162,7 +290,7 @@ body::after {
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.04), transparent);
|
||||
background: linear-gradient(90deg, transparent, var(--shimmer-color), transparent);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
@ -186,9 +314,3 @@ body::after {
|
||||
.score-bar-gradient-low {
|
||||
background: linear-gradient(90deg, #585860, #3a3a40);
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: rgba(245, 158, 11, 0.25);
|
||||
color: #f5f5f7;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { AuthProvider } from "@/hooks/use-auth";
|
||||
import { AuthGuard } from "@/components/auth-guard";
|
||||
import { UserMenu } from "@/components/user-menu";
|
||||
import { SidebarNav, MobileBottomNav } from "@/components/nav";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Dragon AI Agent",
|
||||
@ -23,49 +24,51 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" suppressHydrationWarning>
|
||||
<body className="min-h-screen bg-bg-primary text-text-primary font-display">
|
||||
<AuthProvider>
|
||||
<AuthGuard>
|
||||
{/* Desktop: sidebar + main */}
|
||||
<div className="flex min-h-screen">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:flex flex-col w-60 glass-sidebar fixed inset-y-0 left-0 z-40">
|
||||
{/* Brand */}
|
||||
<div className="px-6 pt-7 pb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-sm font-bold text-white shadow-glow-sm">
|
||||
D
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-sm font-semibold tracking-tight">Dragon AI Agent</h1>
|
||||
<p className="text-xs text-text-muted mt-0.5 font-light tracking-wide">资金驱动 · 四层漏斗模型</p>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||
<AuthProvider>
|
||||
<AuthGuard>
|
||||
{/* Desktop: sidebar + main */}
|
||||
<div className="flex min-h-screen">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:flex flex-col w-60 glass-sidebar fixed inset-y-0 left-0 z-40">
|
||||
{/* Brand */}
|
||||
<div className="px-6 pt-7 pb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-sm font-bold text-white shadow-glow-sm">
|
||||
D
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-sm font-semibold tracking-tight">Dragon AI Agent</h1>
|
||||
<p className="text-xs text-text-muted mt-0.5 font-light tracking-wide">资金驱动 · 四层漏斗模型</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mx-5 h-px bg-gradient-to-r from-transparent via-white/[0.06] to-transparent" />
|
||||
{/* Divider */}
|
||||
<div className="mx-5 h-px bg-gradient-to-r from-transparent via-border-default to-transparent" />
|
||||
|
||||
{/* Nav */}
|
||||
<SidebarNav />
|
||||
{/* Nav */}
|
||||
<SidebarNav />
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-5 border-t border-white/[0.04]">
|
||||
<UserMenu />
|
||||
</div>
|
||||
</aside>
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-5 border-t border-border-subtle">
|
||||
<UserMenu />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content area */}
|
||||
<main className="flex-1 md:ml-60 pb-16 md:pb-0 min-h-screen">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
{/* Main content area */}
|
||||
<main className="flex-1 md:ml-60 pb-16 md:pb-0 min-h-screen">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile bottom nav */}
|
||||
<MobileBottomNav />
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
{/* Mobile bottom nav */}
|
||||
<MobileBottomNav />
|
||||
</AuthGuard>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -34,7 +34,7 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg-primary">
|
||||
<div className="max-w-sm w-full mx-4 p-8 rounded-2xl bg-white/[0.02] border border-white/[0.06] backdrop-blur-sm">
|
||||
<div className="max-w-sm w-full mx-4 p-8 rounded-2xl bg-surface-1 border border-border-default backdrop-blur-sm">
|
||||
{/* Brand */}
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-base font-bold text-white shadow-glow-sm mb-4">
|
||||
@ -53,7 +53,7 @@ export default function LoginPage() {
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
className="w-full bg-surface-2 border border-border-default rounded-xl px-4 py-3 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
autoComplete="username"
|
||||
/>
|
||||
<input
|
||||
@ -61,7 +61,7 @@ export default function LoginPage() {
|
||||
placeholder="密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
className="w-full bg-surface-2 border border-border-default rounded-xl px-4 py-3 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
|
||||
@ -156,7 +156,7 @@ export default function RecommendationsPage() {
|
||||
className={`text-xs px-4 py-1.5 rounded-xl whitespace-nowrap transition-all duration-200 font-medium ${
|
||||
filter === key
|
||||
? "bg-gradient-to-r from-amber-500/25 to-amber-600/20 text-amber-400 border border-amber-500/15"
|
||||
: "bg-white/[0.03] text-text-muted hover:text-text-secondary hover:bg-white/[0.06] border border-transparent"
|
||||
: "bg-surface-2 text-text-muted hover:text-text-secondary hover:bg-surface-4 border border-transparent"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
@ -184,7 +184,7 @@ export default function RecommendationsPage() {
|
||||
{/* Date header */}
|
||||
<button
|
||||
onClick={() => toggleDay(group.date)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 glass-card-static hover:bg-white/[0.03] transition-colors group"
|
||||
className="w-full flex items-center gap-3 px-4 py-3 glass-card-static hover:bg-surface-2 transition-colors group"
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
|
||||
@ -17,7 +17,7 @@ function getStageInfo(stage: string) {
|
||||
case "end":
|
||||
return { label: "尾声", color: "text-red-400", bg: "bg-red-500/10 border-red-500/15" };
|
||||
default:
|
||||
return { label: "—", color: "text-text-muted", bg: "bg-white/[0.03] border-white/[0.06]" };
|
||||
return { label: "—", color: "text-text-muted", bg: "bg-surface-2 border-border-default" };
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ function LeadingStockTag({ stock }: { stock: LeadingStock }) {
|
||||
return (
|
||||
<a
|
||||
href={`/stock/${stock.ts_code}`}
|
||||
className="inline-flex items-center gap-1 text-[11px] px-2 py-1 rounded-lg bg-white/[0.03] border border-white/[0.06] hover:bg-white/[0.06] transition-colors"
|
||||
className="inline-flex items-center gap-1 text-[11px] px-2 py-1 rounded-lg bg-surface-2 border border-border-default hover:bg-surface-4 transition-colors"
|
||||
>
|
||||
<span className="text-text-secondary font-medium">{stock.name}</span>
|
||||
<span className={`font-mono tabular-nums ${isLimitUp ? "text-red-400 font-bold" : stock.pct_chg > 0 ? "text-red-400/80" : "text-emerald-400/80"}`}>
|
||||
@ -88,7 +88,7 @@ function SectorDetailCard({ sector, index }: { sector: SectorData; index: number
|
||||
? "bg-gradient-to-br from-slate-400/20 to-slate-500/15 text-slate-300 border border-slate-400/15"
|
||||
: index === 2
|
||||
? "bg-gradient-to-br from-amber-700/20 to-amber-800/15 text-amber-400 border border-amber-600/15"
|
||||
: "bg-white/[0.03] text-text-muted border border-white/[0.04]"
|
||||
: "bg-surface-2 text-text-muted border border-border-subtle"
|
||||
}`}>
|
||||
{index + 1}
|
||||
</span>
|
||||
@ -127,19 +127,19 @@ function SectorDetailCard({ sector, index }: { sector: SectorData; index: number
|
||||
|
||||
{/* Metrics row */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-3">
|
||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2">
|
||||
<div className="bg-surface-1 rounded-lg px-3 py-2">
|
||||
<div className="text-[10px] text-text-muted/50 mb-0.5">资金净流入</div>
|
||||
<div className={`text-xs font-mono tabular-nums font-semibold ${sector.capital_inflow > 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||||
{sector.capital_inflow > 0 ? "+" : ""}{formatNumber(sector.capital_inflow)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2">
|
||||
<div className="bg-surface-1 rounded-lg px-3 py-2">
|
||||
<div className="text-[10px] text-text-muted/50 mb-0.5">涨停股</div>
|
||||
<div className="text-xs font-mono tabular-nums font-semibold text-text-secondary">
|
||||
{displayLimitUp}<span className="text-text-muted/40"> 只</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2">
|
||||
<div className="bg-surface-1 rounded-lg px-3 py-2">
|
||||
<div className="text-[10px] text-text-muted/50 mb-0.5">热度评分</div>
|
||||
<div className={`text-xs font-mono tabular-nums font-semibold ${
|
||||
sector.heat_score >= 70 ? "text-amber-400" : sector.heat_score >= 50 ? "text-text-secondary" : "text-text-muted"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { fetchAPI } from "@/lib/api";
|
||||
import { getScoreColor, getSignalColor } from "@/lib/utils";
|
||||
import { getScoreColor } from "@/lib/utils";
|
||||
import KlineChart from "@/components/kline-chart";
|
||||
import CapitalFlowChart from "@/components/capital-flow";
|
||||
import ScoreRadar from "@/components/score-radar";
|
||||
@ -23,6 +23,10 @@ interface StockSignals {
|
||||
support_price: number | null;
|
||||
resist_price: number | null;
|
||||
stop_loss_price: number | null;
|
||||
rally_pct_5d: number;
|
||||
rally_pct_10d: number;
|
||||
distance_from_high: number;
|
||||
position_score: number;
|
||||
}
|
||||
|
||||
interface QuoteData {
|
||||
@ -36,8 +40,25 @@ interface QuoteData {
|
||||
pe: number | null;
|
||||
pb: number | null;
|
||||
circ_mv: number | null;
|
||||
total_mv: number | null;
|
||||
volume_ratio: number | null;
|
||||
high: number | null;
|
||||
low: number | null;
|
||||
open: number | null;
|
||||
pre_close: number | null;
|
||||
limit_up: number | null;
|
||||
limit_down: number | null;
|
||||
amplitude: number | null;
|
||||
}
|
||||
|
||||
interface FlowRecord {
|
||||
trade_date: string;
|
||||
main_net_inflow: number;
|
||||
net_mf_amount: number;
|
||||
elg_net: number;
|
||||
lg_net: number;
|
||||
md_net: number;
|
||||
sm_net: number;
|
||||
}
|
||||
|
||||
export default function StockDetailPage() {
|
||||
@ -48,29 +69,42 @@ export default function StockDetailPage() {
|
||||
const [signals, setSignals] = useState<StockSignals | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [kline, setKline] = useState<any[]>([]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [capitalFlow, setCapitalFlow] = useState<any[]>([]);
|
||||
const [capitalFlow, setCapitalFlow] = useState<FlowRecord[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!code) return;
|
||||
Promise.all([
|
||||
fetchAPI<QuoteData>(`/api/stocks/${code}/quote`).catch(() => null),
|
||||
fetchAPI<StockSignals>(`/api/stocks/${code}/signals`).catch(() => null),
|
||||
fetchAPI<unknown[]>(`/api/stocks/${code}/kline?days=60`).catch(() => []),
|
||||
fetchAPI<unknown[]>(`/api/stocks/${code}/capital_flow?days=10`).catch(() => []),
|
||||
fetchAPI<unknown[]>(`/api/stocks/${code}/kline?days=120`).catch(() => []),
|
||||
fetchAPI<FlowRecord[]>(`/api/stocks/${code}/capital_flow?days=10`).catch(() => []),
|
||||
]).then(([q, s, k, c]) => {
|
||||
setQuote(q);
|
||||
setSignals(s);
|
||||
setKline(k);
|
||||
setCapitalFlow(c);
|
||||
setCapitalFlow(c as FlowRecord[]);
|
||||
});
|
||||
}, [code]);
|
||||
|
||||
const latestFlow = capitalFlow.length > 0 ? capitalFlow[capitalFlow.length - 1] : null;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 md:px-8 pt-6 pb-20 md:pb-10 space-y-5">
|
||||
<div className="max-w-6xl mx-auto px-4 md:px-8 pt-6 pb-20 md:pb-10 space-y-4">
|
||||
{/* Back */}
|
||||
<a href="/" className="inline-flex items-center gap-1.5 text-xs text-text-muted hover:text-text-primary transition-colors animate-fade-in-up">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-text-muted hover:text-text-primary transition-colors animate-fade-in-up"
|
||||
>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||
</svg>
|
||||
返回
|
||||
@ -90,21 +124,91 @@ export default function StockDetailPage() {
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span
|
||||
className={`text-3xl font-bold font-mono tabular-nums tracking-tight ${
|
||||
quote.pct_chg > 0 ? "text-red-400" : quote.pct_chg < 0 ? "text-emerald-400" : "text-text-primary"
|
||||
quote.pct_chg > 0
|
||||
? "text-red-400"
|
||||
: quote.pct_chg < 0
|
||||
? "text-emerald-400"
|
||||
: "text-text-primary"
|
||||
}`}
|
||||
>
|
||||
{quote.price.toFixed(2)}
|
||||
</span>
|
||||
<span className={`text-sm font-mono tabular-nums font-medium ${quote.pct_chg > 0 ? "text-red-400" : "text-emerald-400"}`}>
|
||||
<span
|
||||
className={`text-sm font-mono tabular-nums font-medium ${
|
||||
quote.pct_chg > 0 ? "text-red-400" : "text-emerald-400"
|
||||
}`}
|
||||
>
|
||||
{quote.pct_chg > 0 ? "+" : ""}
|
||||
{quote.pct_chg.toFixed(2)}%
|
||||
</span>
|
||||
{quote.pre_close && (
|
||||
<span className="text-xs text-text-muted font-mono tabular-nums">
|
||||
昨收 {quote.pre_close.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3 mt-4">
|
||||
<QuoteStat label="换手率" value={`${quote.turnover_rate?.toFixed(2)}%`} />
|
||||
<QuoteStat label="市盈率" value={quote.pe?.toFixed(1) ?? "-"} />
|
||||
<QuoteStat label="市净率" value={quote.pb?.toFixed(2) ?? "-"} />
|
||||
<QuoteStat label="流通市值" value={quote.circ_mv ? `${quote.circ_mv.toFixed(0)}亿` : "-"} />
|
||||
|
||||
{/* OHLC row */}
|
||||
<div className="grid grid-cols-4 gap-2 mt-4">
|
||||
<MiniStat
|
||||
label="开盘"
|
||||
value={quote.open?.toFixed(2) ?? "-"}
|
||||
color={
|
||||
quote.open && quote.pre_close
|
||||
? quote.open >= quote.pre_close
|
||||
? "text-red-400"
|
||||
: "text-emerald-400"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<MiniStat
|
||||
label="最高"
|
||||
value={quote.high?.toFixed(2) ?? "-"}
|
||||
color="text-red-400"
|
||||
/>
|
||||
<MiniStat
|
||||
label="最低"
|
||||
value={quote.low?.toFixed(2) ?? "-"}
|
||||
color="text-emerald-400"
|
||||
/>
|
||||
<MiniStat
|
||||
label="振幅"
|
||||
value={quote.amplitude != null ? `${quote.amplitude.toFixed(2)}%` : "-"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Valuation row */}
|
||||
<div className="grid grid-cols-4 gap-2 mt-2">
|
||||
<MiniStat label="换手率" value={`${quote.turnover_rate?.toFixed(2)}%`} />
|
||||
<MiniStat label="市盈率" value={quote.pe?.toFixed(1) ?? "-"} />
|
||||
<MiniStat label="市净率" value={quote.pb?.toFixed(2) ?? "-"} />
|
||||
<MiniStat
|
||||
label="量比"
|
||||
value={quote.volume_ratio?.toFixed(2) ?? "-"}
|
||||
highlight={quote.volume_ratio != null && quote.volume_ratio > 2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Market cap row */}
|
||||
<div className="grid grid-cols-4 gap-2 mt-2">
|
||||
<MiniStat
|
||||
label="总市值"
|
||||
value={quote.total_mv ? `${formatBigNum(quote.total_mv)}亿` : "-"}
|
||||
/>
|
||||
<MiniStat
|
||||
label="流通市值"
|
||||
value={quote.circ_mv ? `${formatBigNum(quote.circ_mv)}亿` : "-"}
|
||||
/>
|
||||
<MiniStat
|
||||
label="涨停"
|
||||
value={quote.limit_up?.toFixed(2) ?? "-"}
|
||||
color="text-red-400"
|
||||
/>
|
||||
<MiniStat
|
||||
label="跌停"
|
||||
value={quote.limit_down?.toFixed(2) ?? "-"}
|
||||
color="text-emerald-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -117,13 +221,64 @@ export default function StockDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Position Safety + Capital Flow Breakdown */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 animate-fade-in-up delay-75">
|
||||
{/* Position Safety Card */}
|
||||
{signals && (
|
||||
<div className="glass-card-static p-5">
|
||||
<h2 className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-4">
|
||||
仓位安全评估
|
||||
</h2>
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Score ring */}
|
||||
<div className="relative w-24 h-24 flex-shrink-0">
|
||||
<svg viewBox="0 0 100 100" className="w-full h-full -rotate-90">
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="var(--surface-2)" strokeWidth="8" />
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42"
|
||||
fill="none"
|
||||
stroke={getPositionColor(signals.position_score)}
|
||||
strokeWidth="8"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${(signals.position_score / 100) * 264} 264`}
|
||||
className="transition-all duration-700"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className={`text-xl font-bold font-mono tabular-nums ${getPositionColor(signals.position_score)}`}>
|
||||
{Math.round(signals.position_score)}
|
||||
</span>
|
||||
<span className="text-[10px] text-text-muted">安全分</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Metrics */}
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
<PositionBar label="5日涨幅" value={signals.rally_pct_5d} />
|
||||
<PositionBar label="10日涨幅" value={signals.rally_pct_10d} />
|
||||
<PositionBar label="距高点" value={signals.distance_from_high} invert />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Capital Flow Breakdown */}
|
||||
{latestFlow && (
|
||||
<CapitalFlowBreakdown flow={latestFlow} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Technical signals */}
|
||||
{signals && (
|
||||
<div className="glass-card-static p-5 animate-fade-in-up delay-75">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xs font-semibold text-text-muted uppercase tracking-wider">技术面信号</h2>
|
||||
<h2 className="text-xs font-semibold text-text-muted uppercase tracking-wider">
|
||||
技术面信号
|
||||
</h2>
|
||||
<div className={`text-lg font-bold font-mono tabular-nums ${getScoreColor(signals.score)}`}>
|
||||
{signals.score}<span className="text-xs text-text-muted ml-0.5">分</span>
|
||||
{signals.score}
|
||||
<span className="text-xs text-text-muted ml-0.5">分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -138,24 +293,30 @@ export default function StockDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Price levels */}
|
||||
<div className="flex justify-between mt-4 pt-4 border-t border-white/[0.04] text-xs">
|
||||
<div className="flex justify-between mt-4 pt-4 border-t border-border-subtle text-xs">
|
||||
<div>
|
||||
<span className="text-text-muted">支撑位 </span>
|
||||
<span className="text-orange-400 font-mono tabular-nums">{signals.support_price ?? "-"}</span>
|
||||
<span className="text-orange-400 font-mono tabular-nums">
|
||||
{signals.support_price?.toFixed(2) ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">压力位 </span>
|
||||
<span className="text-red-400 font-mono tabular-nums">{signals.resist_price ?? "-"}</span>
|
||||
<span className="text-red-400 font-mono tabular-nums">
|
||||
{signals.resist_price?.toFixed(2) ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-text-muted">止损位 </span>
|
||||
<span className="text-emerald-400 font-mono tabular-nums">{signals.stop_loss_price ?? "-"}</span>
|
||||
<span className="text-emerald-400 font-mono tabular-nums">
|
||||
{signals.stop_loss_price?.toFixed(2) ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* K-line + Capital flow */}
|
||||
{/* K-line + Capital flow chart */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 animate-fade-in-up delay-150">
|
||||
{kline.length > 0 && <KlineChart data={kline} />}
|
||||
{capitalFlow.length > 0 && <CapitalFlowChart data={capitalFlow} />}
|
||||
@ -171,11 +332,124 @@ export default function StockDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function QuoteStat({ label, value }: { label: string; value: string }) {
|
||||
/* ── Helper components ── */
|
||||
|
||||
function MiniStat({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
highlight,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
color?: string;
|
||||
highlight?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
|
||||
<div className="text-xs text-text-muted mb-0.5">{label}</div>
|
||||
<div className="text-xs font-mono tabular-nums">{value}</div>
|
||||
<div className={`rounded-lg px-2.5 py-1.5 border ${highlight ? "border-amber-500/20 bg-amber-500/[0.04]" : "border-border-subtle bg-surface-1"}`}>
|
||||
<div className="text-[10px] text-text-muted leading-tight">{label}</div>
|
||||
<div className={`text-xs font-mono tabular-nums ${color ?? ""}`}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PositionBar({ label, value, invert = false }: { label: string; value: number; invert?: boolean }) {
|
||||
const absVal = Math.abs(value);
|
||||
const maxDisplay = 30;
|
||||
const pct = Math.min(absVal / maxDisplay, 1) * 100;
|
||||
const isPositive = value > 0;
|
||||
const showWarning = invert ? value < -20 : value > 20;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-text-muted">{label}</span>
|
||||
<span
|
||||
className={`font-mono tabular-nums ${
|
||||
showWarning
|
||||
? "text-amber-400"
|
||||
: isPositive
|
||||
? "text-red-400"
|
||||
: "text-emerald-400"
|
||||
}`}
|
||||
>
|
||||
{isPositive ? "+" : ""}
|
||||
{value.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-surface-2 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${
|
||||
showWarning
|
||||
? "bg-amber-400"
|
||||
: isPositive
|
||||
? "bg-red-400"
|
||||
: "bg-emerald-400"
|
||||
}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CapitalFlowBreakdown({ flow }: { flow: FlowRecord }) {
|
||||
const maxDisplay = Math.max(
|
||||
...[flow.elg_net, flow.lg_net, flow.md_net, flow.sm_net].map(Math.abs),
|
||||
1
|
||||
);
|
||||
return (
|
||||
<div className="glass-card-static p-5">
|
||||
<h2 className="text-xs font-semibold text-text-muted uppercase tracking-wider mb-4">
|
||||
今日资金流向
|
||||
</h2>
|
||||
<div className="space-y-2.5">
|
||||
<FlowBar label="特大单" value={flow.elg_net} max={maxDisplay} />
|
||||
<FlowBar label="大单" value={flow.lg_net} max={maxDisplay} />
|
||||
<FlowBar label="中单" value={flow.md_net} max={maxDisplay} />
|
||||
<FlowBar label="小单" value={flow.sm_net} max={maxDisplay} />
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-border-subtle flex items-center justify-between text-xs">
|
||||
<span className="text-text-muted">主力净流入</span>
|
||||
<span
|
||||
className={`font-mono tabular-nums font-semibold ${
|
||||
flow.main_net_inflow > 0 ? "text-red-400" : "text-emerald-400"
|
||||
}`}
|
||||
>
|
||||
{flow.main_net_inflow > 0 ? "+" : ""}
|
||||
{formatFlowAmount(flow.main_net_inflow)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FlowBar({ label, value, max }: { label: string; value: number; max: number }) {
|
||||
const absVal = Math.abs(value);
|
||||
const pct = (absVal / max) * 100;
|
||||
const isInflow = value > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs mb-1">
|
||||
<span className="text-text-muted">{label}</span>
|
||||
<span
|
||||
className={`font-mono tabular-nums ${isInflow ? "text-red-400" : "text-emerald-400"}`}
|
||||
>
|
||||
{isInflow ? "+" : ""}
|
||||
{formatFlowAmount(value)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 rounded-full bg-surface-2 overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${
|
||||
isInflow ? "bg-red-400" : "bg-emerald-400"
|
||||
}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -186,7 +460,7 @@ function SignalItem({ label, active, points }: { label: string; active: boolean;
|
||||
className={`flex items-center justify-between px-3 py-2 rounded-xl text-xs transition-all duration-200 ${
|
||||
active
|
||||
? "bg-red-500/[0.08] text-red-400 border border-red-500/10"
|
||||
: "bg-white/[0.02] text-text-muted border border-transparent"
|
||||
: "bg-surface-1 text-text-muted border border-transparent"
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{label}</span>
|
||||
@ -196,3 +470,22 @@ function SignalItem({ label, active, points }: { label: string; active: boolean;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Helper functions ── */
|
||||
|
||||
function getPositionColor(score: number) {
|
||||
if (score >= 70) return "#22c55e";
|
||||
if (score >= 40) return "#eab308";
|
||||
return "#ef4444";
|
||||
}
|
||||
|
||||
function formatBigNum(val: number): string {
|
||||
if (val >= 10000) return (val / 10000).toFixed(1) + "万";
|
||||
return val.toFixed(0);
|
||||
}
|
||||
|
||||
function formatFlowAmount(val: number): string {
|
||||
const absVal = Math.abs(val);
|
||||
if (absVal >= 10000) return (val / 10000).toFixed(1) + "亿";
|
||||
return val.toFixed(0) + "万";
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ export default function UsersPage() {
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
{/* Avatar */}
|
||||
<div className="w-9 h-9 rounded-full bg-white/[0.04] border border-white/[0.06] flex items-center justify-center text-sm font-medium text-text-secondary shrink-0">
|
||||
<div className="w-9 h-9 rounded-full bg-surface-3 border border-border-default flex items-center justify-center text-sm font-medium text-text-secondary shrink-0">
|
||||
{u.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
@ -155,7 +155,7 @@ export default function UsersPage() {
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded ${
|
||||
u.role === "admin"
|
||||
? "bg-amber-500/10 text-amber-400/80"
|
||||
: "bg-white/[0.04] text-text-muted"
|
||||
: "bg-surface-3 text-text-muted"
|
||||
}`}
|
||||
>
|
||||
{u.role}
|
||||
@ -179,7 +179,7 @@ export default function UsersPage() {
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => handleResetPassword(u.id)}
|
||||
className="px-3 py-1.5 rounded-lg text-xs text-text-secondary hover:text-text-primary bg-white/[0.03] hover:bg-white/[0.06] border border-white/[0.04] transition-all"
|
||||
className="px-3 py-1.5 rounded-lg text-xs text-text-secondary hover:text-text-primary bg-surface-2 hover:bg-surface-4 border border-border-subtle transition-all"
|
||||
>
|
||||
重置密码
|
||||
</button>
|
||||
@ -210,11 +210,11 @@ export default function UsersPage() {
|
||||
{showCreate && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowCreate(false)} />
|
||||
<div className="relative w-full max-w-sm mx-4 p-6 rounded-2xl bg-bg-card border border-white/[0.06] shadow-card">
|
||||
<div className="relative w-full max-w-sm mx-4 p-6 rounded-2xl bg-bg-card border border-border-default shadow-card">
|
||||
{createdResult ? (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-base font-semibold text-text-primary">用户创建成功</h3>
|
||||
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/[0.04] space-y-2">
|
||||
<div className="p-4 rounded-xl bg-surface-1 border border-border-subtle space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">用户名</span>
|
||||
<span className="text-text-primary font-medium">{createdResult.username}</span>
|
||||
@ -234,7 +234,7 @@ export default function UsersPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreate(false)}
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-white/[0.04] border border-white/[0.06] text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all"
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-surface-3 border border-border-default text-text-secondary hover:text-text-primary hover:bg-surface-4 transition-all"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
@ -249,12 +249,12 @@ export default function UsersPage() {
|
||||
placeholder="用户名"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
className="w-full bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
className="w-full bg-surface-2 border border-border-default rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
/>
|
||||
<select
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value)}
|
||||
className="w-full bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 appearance-none"
|
||||
className="w-full bg-surface-2 border border-border-default rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 appearance-none"
|
||||
>
|
||||
<option value="user" className="bg-bg-card text-text-primary">普通用户</option>
|
||||
<option value="admin" className="bg-bg-card text-text-primary">管理员</option>
|
||||
@ -264,7 +264,7 @@ export default function UsersPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreate(false)}
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-white/[0.04] border border-white/[0.06] text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all"
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-surface-3 border border-border-default text-text-secondary hover:text-text-primary hover:bg-surface-4 transition-all"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
@ -287,9 +287,9 @@ export default function UsersPage() {
|
||||
{resetResult && (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setResetResult(null)} />
|
||||
<div className="relative w-full max-w-sm mx-4 p-6 rounded-2xl bg-bg-card border border-white/[0.06] shadow-card space-y-4">
|
||||
<div className="relative w-full max-w-sm mx-4 p-6 rounded-2xl bg-bg-card border border-border-default shadow-card space-y-4">
|
||||
<h3 className="text-base font-semibold text-text-primary">密码已重置</h3>
|
||||
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/[0.04] space-y-2">
|
||||
<div className="p-4 rounded-xl bg-surface-1 border border-border-subtle space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-text-muted">用户名</span>
|
||||
<span className="text-text-primary font-medium">{resetResult.username}</span>
|
||||
@ -309,7 +309,7 @@ export default function UsersPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setResetResult(null)}
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-white/[0.04] border border-white/[0.06] text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all"
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-surface-3 border border-border-default text-text-secondary hover:text-text-primary hover:bg-surface-4 transition-all"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
interface FlowData {
|
||||
trade_date: string;
|
||||
@ -10,6 +11,7 @@ interface FlowData {
|
||||
|
||||
export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !data.length) return;
|
||||
@ -18,7 +20,13 @@ export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
||||
|
||||
import("echarts").then((ec) => {
|
||||
if (!chartRef.current) return;
|
||||
chart = ec.init(chartRef.current, "dark");
|
||||
const isDark = theme !== "light";
|
||||
chart = ec.init(chartRef.current, isDark ? "dark" : undefined);
|
||||
|
||||
const isLight = theme === "light";
|
||||
const axisLineColor = isLight ? "#d1d5db" : "#475569";
|
||||
const axisLabelColor = isLight ? "#6b7280" : "#94a3b8";
|
||||
const splitLineColor = isLight ? "#f3f4f6" : "#1e293b";
|
||||
|
||||
const dates = data.map((d) => d.trade_date);
|
||||
const values = data.map((d) => d.main_net_inflow);
|
||||
@ -36,15 +44,15 @@ export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: "#475569" } },
|
||||
axisLabel: { fontSize: 10, color: "#94a3b8", rotate: 30 },
|
||||
axisLine: { lineStyle: { color: axisLineColor } },
|
||||
axisLabel: { fontSize: 10, color: axisLabelColor, rotate: 30 },
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
splitLine: { lineStyle: { color: "#1e293b" } },
|
||||
splitLine: { lineStyle: { color: splitLineColor } },
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
color: "#94a3b8",
|
||||
color: axisLabelColor,
|
||||
formatter: (v: number) => (Math.abs(v) >= 10000 ? (v / 10000).toFixed(1) + "亿" : v + "万"),
|
||||
},
|
||||
},
|
||||
@ -67,7 +75,7 @@ export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
||||
return () => {
|
||||
chart?.dispose();
|
||||
};
|
||||
}, [data]);
|
||||
}, [data, theme]);
|
||||
|
||||
return (
|
||||
<div className="bg-bg-card rounded-xl p-4">
|
||||
|
||||
@ -66,7 +66,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={handleClose} />
|
||||
|
||||
{/* Dialog */}
|
||||
<div className="relative w-full max-w-sm mx-4 p-6 rounded-2xl bg-bg-card border border-white/[0.06] shadow-card">
|
||||
<div className="relative w-full max-w-sm mx-4 p-6 rounded-2xl bg-bg-card border border-border-default shadow-card">
|
||||
<h3 className="text-base font-semibold text-text-primary mb-5">修改密码</h3>
|
||||
|
||||
{success ? (
|
||||
@ -74,7 +74,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
||||
<p className="text-sm text-emerald-400">密码修改成功,下次登录请使用新密码。</p>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="w-full py-2.5 rounded-xl text-sm font-medium bg-white/[0.04] border border-white/[0.06] text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all"
|
||||
className="w-full py-2.5 rounded-xl text-sm font-medium bg-surface-3 border border-border-default text-text-secondary hover:text-text-primary hover:bg-surface-4 transition-all"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
@ -86,7 +86,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
||||
placeholder="旧密码"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
className="w-full bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
className="w-full bg-surface-2 border border-border-default rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<input
|
||||
@ -94,7 +94,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
||||
placeholder="新密码(至少 6 位)"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="w-full bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
className="w-full bg-surface-2 border border-border-default rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<input
|
||||
@ -102,7 +102,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
||||
placeholder="确认新密码"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
className="w-full bg-surface-2 border border-border-default rounded-xl px-4 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-amber-500/30 placeholder-text-muted/40"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
|
||||
@ -114,7 +114,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-white/[0.04] border border-white/[0.06] text-text-secondary hover:text-text-primary hover:bg-white/[0.06] transition-all"
|
||||
className="flex-1 py-2.5 rounded-xl text-sm font-medium bg-surface-3 border border-border-default text-text-secondary hover:text-text-primary hover:bg-surface-4 transition-all"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
interface KlineData {
|
||||
trade_date: string;
|
||||
@ -18,17 +19,22 @@ interface KlineData {
|
||||
|
||||
export default function KlineChart({ data }: { data: KlineData[] }) {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current || !data.length) return;
|
||||
|
||||
let echarts: typeof import("echarts") | null = null;
|
||||
let chart: ReturnType<typeof import("echarts")["init"]> | null = null;
|
||||
|
||||
import("echarts").then((ec) => {
|
||||
echarts = ec;
|
||||
if (!chartRef.current) return;
|
||||
chart = ec.init(chartRef.current, "dark");
|
||||
const isDark = theme !== "light";
|
||||
chart = ec.init(chartRef.current, isDark ? "dark" : undefined);
|
||||
|
||||
const isLight = theme === "light";
|
||||
const axisLineColor = isLight ? "#d1d5db" : "#475569";
|
||||
const axisLabelColor = isLight ? "#6b7280" : "#94a3b8";
|
||||
const splitLineColor = isLight ? "#f3f4f6" : "#1e293b";
|
||||
|
||||
const dates = data.map((d) => d.trade_date);
|
||||
const ohlc = data.map((d) => [d.open, d.close, d.low, d.high]);
|
||||
@ -51,29 +57,29 @@ export default function KlineChart({ data }: { data: KlineData[] }) {
|
||||
{
|
||||
type: "category",
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: "#475569" } },
|
||||
axisLabel: { fontSize: 10, color: "#94a3b8" },
|
||||
axisLine: { lineStyle: { color: axisLineColor } },
|
||||
axisLabel: { fontSize: 10, color: axisLabelColor },
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
gridIndex: 1,
|
||||
data: dates,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { lineStyle: { color: "#475569" } },
|
||||
axisLine: { lineStyle: { color: axisLineColor } },
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
scale: true,
|
||||
splitLine: { lineStyle: { color: "#1e293b" } },
|
||||
axisLabel: { fontSize: 10, color: "#94a3b8" },
|
||||
splitLine: { lineStyle: { color: splitLineColor } },
|
||||
axisLabel: { fontSize: 10, color: axisLabelColor },
|
||||
},
|
||||
{
|
||||
scale: true,
|
||||
gridIndex: 1,
|
||||
splitNumber: 2,
|
||||
splitLine: { lineStyle: { color: "#1e293b" } },
|
||||
axisLabel: { fontSize: 10, color: "#94a3b8" },
|
||||
splitLine: { lineStyle: { color: splitLineColor } },
|
||||
axisLabel: { fontSize: 10, color: axisLabelColor },
|
||||
},
|
||||
],
|
||||
series: [
|
||||
@ -143,7 +149,7 @@ export default function KlineChart({ data }: { data: KlineData[] }) {
|
||||
return () => {
|
||||
chart?.dispose();
|
||||
};
|
||||
}, [data]);
|
||||
}, [data, theme]);
|
||||
|
||||
return (
|
||||
<div className="bg-bg-card rounded-xl p-4">
|
||||
|
||||
@ -94,7 +94,7 @@ export default function MarketTemp({ data, indices }: MarketTempProps) {
|
||||
|
||||
{/* Middle row: Broken rate + MA20 */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
|
||||
<div className="bg-surface-1 rounded-lg px-3 py-2 border border-border-subtle">
|
||||
<div className="text-xs text-text-muted mb-0.5">炸板率</div>
|
||||
{hasBrokenRate ? (
|
||||
<div className="flex items-baseline gap-1">
|
||||
@ -109,7 +109,7 @@ export default function MarketTemp({ data, indices }: MarketTempProps) {
|
||||
<span className="text-sm text-text-muted/40">-</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
|
||||
<div className="bg-surface-1 rounded-lg px-3 py-2 border border-border-subtle">
|
||||
<div className="text-xs text-text-muted mb-0.5">上证均线</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${data.index_above_ma20 ? "bg-red-400" : "bg-emerald-400"}`} />
|
||||
@ -124,7 +124,7 @@ export default function MarketTemp({ data, indices }: MarketTempProps) {
|
||||
{indices && indices.length > 0 && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{indices.map((idx) => (
|
||||
<div key={idx.code} className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
|
||||
<div key={idx.code} className="bg-surface-1 rounded-lg px-3 py-2 border border-border-subtle">
|
||||
<div className="text-xs text-text-muted mb-0.5">
|
||||
{idx.name}
|
||||
{idx.realtime && <span className="text-emerald-400/60 ml-1">· 实时</span>}
|
||||
@ -145,7 +145,7 @@ export default function MarketTemp({ data, indices }: MarketTempProps) {
|
||||
|
||||
function StatCard({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
|
||||
<div className="bg-surface-1 rounded-lg px-3 py-2 border border-border-subtle">
|
||||
<div className="text-xs text-text-muted mb-0.5 font-medium">{label}</div>
|
||||
<div className="text-sm">{children}</div>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
function DashboardIcon() {
|
||||
return (
|
||||
@ -61,8 +62,8 @@ function SideNavItem({ href, icon, label }: { href: string; icon: React.ReactNod
|
||||
href={href}
|
||||
className={`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 ${
|
||||
isActive
|
||||
? "text-text-primary bg-white/[0.06]"
|
||||
: "text-text-secondary hover:text-text-primary hover:bg-white/[0.04]"
|
||||
? "text-text-primary bg-surface-4"
|
||||
: "text-text-secondary hover:text-text-primary hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<span className="text-base opacity-70">{icon}</span>
|
||||
@ -106,7 +107,7 @@ function MobileNavItem({ href, label, children }: { href: string; label: string;
|
||||
|
||||
export function MobileBottomNav() {
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 md:hidden z-50 bg-bg-secondary/95 backdrop-blur-xl border-t border-white/[0.04]">
|
||||
<nav className="fixed bottom-0 left-0 right-0 md:hidden z-50 bg-bg-secondary/95 backdrop-blur-xl border-t border-border-subtle">
|
||||
<div className="flex justify-around py-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
|
||||
<MobileNavItem href="/" label="总览">
|
||||
<DashboardIcon />
|
||||
@ -120,6 +121,9 @@ export function MobileBottomNav() {
|
||||
<MobileNavItem href="/chat" label="对话">
|
||||
<ChatIcon />
|
||||
</MobileNavItem>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
interface SignalsData {
|
||||
score: number;
|
||||
@ -15,6 +16,7 @@ interface SignalsData {
|
||||
|
||||
export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartRef.current) return;
|
||||
@ -23,7 +25,13 @@ export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
||||
|
||||
import("echarts").then((ec) => {
|
||||
if (!chartRef.current) return;
|
||||
chart = ec.init(chartRef.current, "dark");
|
||||
const isDark = theme !== "light";
|
||||
chart = ec.init(chartRef.current, isDark ? "dark" : undefined);
|
||||
|
||||
const isLight = theme === "light";
|
||||
const axisLineColor = isLight ? "#d1d5db" : "#475569";
|
||||
const axisLabelColor = isLight ? "#6b7280" : "#94a3b8";
|
||||
const splitLineColor = isLight ? "#e5e7eb" : "#334155";
|
||||
|
||||
const indicators = [
|
||||
{ name: "均线多头", max: 15 },
|
||||
@ -51,10 +59,10 @@ export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
||||
indicator: indicators,
|
||||
shape: "polygon",
|
||||
splitNumber: 3,
|
||||
axisName: { color: "#94a3b8", fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: "#334155" } },
|
||||
axisName: { color: axisLabelColor, fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: splitLineColor } },
|
||||
splitArea: { areaStyle: { color: ["transparent"] } },
|
||||
axisLine: { lineStyle: { color: "#475569" } },
|
||||
axisLine: { lineStyle: { color: axisLineColor } },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@ -80,7 +88,7 @@ export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
||||
return () => {
|
||||
chart?.dispose();
|
||||
};
|
||||
}, [signals]);
|
||||
}, [signals, theme]);
|
||||
|
||||
return (
|
||||
<div className="bg-bg-card rounded-xl p-4">
|
||||
|
||||
@ -66,7 +66,7 @@ export default function SectorHeatmap({ sectors }: { sectors: SectorData[] }) {
|
||||
? "bg-gradient-to-br from-slate-400/20 to-slate-500/15 text-slate-300 border border-slate-400/15"
|
||||
: index === 2
|
||||
? "bg-gradient-to-br from-amber-700/20 to-amber-800/15 text-amber-400 border border-amber-600/15"
|
||||
: "bg-white/[0.03] text-text-muted border border-white/[0.04]"
|
||||
: "bg-surface-2 text-text-muted border border-border-subtle"
|
||||
}`}>
|
||||
{index + 1}
|
||||
</span>
|
||||
@ -94,7 +94,7 @@ export default function SectorHeatmap({ sectors }: { sectors: SectorData[] }) {
|
||||
<span className={`font-mono tabular-nums text-xs font-semibold px-2 py-1 rounded-lg min-w-[36px] text-center ${
|
||||
isTop3
|
||||
? "bg-amber-500/20 text-amber-400 border border-amber-500/15"
|
||||
: "bg-white/[0.04] text-text-muted border border-white/[0.04]"
|
||||
: "bg-surface-3 text-text-muted border border-border-subtle"
|
||||
}`}>
|
||||
{s.heat_score.toFixed(0)}
|
||||
</span>
|
||||
|
||||
@ -66,7 +66,7 @@ export default function StockCard({ rec, showLLMLoading = false }: { rec: Recomm
|
||||
|
||||
{/* Price reference */}
|
||||
{rec.entry_price && (
|
||||
<div className="flex justify-between text-xs mb-3 bg-white/[0.03] rounded-xl px-4 py-2.5 border border-white/[0.04]">
|
||||
<div className="flex justify-between text-xs mb-3 bg-surface-2 rounded-xl px-4 py-2.5 border border-border-subtle">
|
||||
<div>
|
||||
<span className="text-text-muted">买入 </span>
|
||||
<span className="text-red-400 font-mono tabular-nums">{rec.entry_price}</span>
|
||||
@ -176,7 +176,7 @@ function MarkdownText({ text }: { text: string }) {
|
||||
|
||||
// 分隔线 --- 或 ***
|
||||
if (/^[-*_]{3,}$/.test(trimmed)) {
|
||||
return <div key={i} className="border-t border-white/[0.06] my-1.5" />;
|
||||
return <div key={i} className="border-t border-border-default my-1.5" />;
|
||||
}
|
||||
|
||||
// 无序列表 - 开头
|
||||
@ -215,7 +215,7 @@ function renderInlineFormat(text: string) {
|
||||
return parts.map((part, i) => {
|
||||
if (part.startsWith("`") && part.endsWith("`")) {
|
||||
return (
|
||||
<code key={i} className="bg-white/[0.08] px-1 py-0.5 rounded text-accent-cyan/90 font-mono text-[11px]">
|
||||
<code key={i} className="bg-surface-4 px-1 py-0.5 rounded text-accent-cyan/90 font-mono text-[11px]">
|
||||
{part.slice(1, -1)}
|
||||
</code>
|
||||
);
|
||||
@ -240,7 +240,7 @@ function ScoreBar({ label, value }: { label: string; value: number }) {
|
||||
<span className="font-medium">{label}</span>
|
||||
<span className="font-mono tabular-nums">{value.toFixed(0)}</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-white/[0.04] rounded-full overflow-hidden">
|
||||
<div className="h-1.5 bg-surface-3 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-700 ease-out ${gradientClass}`}
|
||||
style={{ width: `${width}%` }}
|
||||
|
||||
45
frontend/src/components/theme-toggle.tsx
Normal file
45
frontend/src/components/theme-toggle.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button className="w-8 h-8 rounded-lg bg-surface-3 border border-border-subtle" aria-label="Toggle theme" />
|
||||
);
|
||||
}
|
||||
|
||||
const isLight = theme === "light";
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(isLight ? "dark" : "light")}
|
||||
className="w-8 h-8 rounded-lg bg-surface-3 hover:bg-surface-4 border border-border-subtle flex items-center justify-center transition-all duration-200"
|
||||
aria-label={isLight ? "切换到深色模式" : "切换到浅色模式"}
|
||||
>
|
||||
{isLight ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-secondary">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-text-secondary">
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { ChangePasswordDialog } from "@/components/change-password-dialog";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
|
||||
export function UserMenu() {
|
||||
const { user, logout } = useAuth();
|
||||
@ -14,10 +15,13 @@ export function UserMenu() {
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-text-muted">{user.username}</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-400/80">
|
||||
{user.role}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-text-muted">{user.username}</span>
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-400/80">
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
@ -7,23 +8,33 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
up: "#ff6b6b",
|
||||
down: "#34d399",
|
||||
hot: "#f59e0b",
|
||||
up: "var(--color-up)",
|
||||
down: "var(--color-down)",
|
||||
hot: "var(--color-hot)",
|
||||
bg: {
|
||||
primary: "#0a0a0c",
|
||||
secondary: "#101013",
|
||||
card: "#161619",
|
||||
elevated: "#1e1e22",
|
||||
primary: "var(--bg-primary)",
|
||||
secondary: "var(--bg-secondary)",
|
||||
card: "var(--bg-card)",
|
||||
elevated: "var(--bg-elevated)",
|
||||
},
|
||||
text: {
|
||||
primary: "#f5f5f7",
|
||||
secondary: "#9898a4",
|
||||
muted: "#58585f",
|
||||
primary: "var(--text-primary)",
|
||||
secondary: "var(--text-secondary)",
|
||||
muted: "var(--text-muted)",
|
||||
},
|
||||
accent: {
|
||||
amber: "#f59e0b",
|
||||
cyan: "#22d3ee",
|
||||
amber: "var(--accent-amber)",
|
||||
cyan: "var(--accent-cyan)",
|
||||
},
|
||||
surface: {
|
||||
"1": "var(--surface-1)",
|
||||
"2": "var(--surface-2)",
|
||||
"3": "var(--surface-3)",
|
||||
"4": "var(--surface-4)",
|
||||
},
|
||||
border: {
|
||||
subtle: "var(--border-subtle)",
|
||||
default: "var(--border-default)",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
@ -35,9 +46,9 @@ module.exports = {
|
||||
"3xl": "20px",
|
||||
},
|
||||
boxShadow: {
|
||||
card: "0 4px 24px rgba(0, 0, 0, 0.3)",
|
||||
glow: "0 0 20px rgba(245, 158, 11, 0.12)",
|
||||
"glow-sm": "0 0 10px rgba(245, 158, 11, 0.08)",
|
||||
card: "var(--shadow-card)",
|
||||
glow: "var(--shadow-glow)",
|
||||
"glow-sm": "var(--shadow-glow-sm)",
|
||||
},
|
||||
animation: {
|
||||
"fade-in-up": "fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) both",
|
||||
|
||||
@ -1920,6 +1920,11 @@ natural-compare@^1.4.0:
|
||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next-themes@^0.4.6:
|
||||
version "0.4.6"
|
||||
resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz"
|
||||
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
|
||||
|
||||
next@^14.2.0:
|
||||
version "14.2.35"
|
||||
resolved "https://registry.npmjs.org/next/-/next-14.2.35.tgz"
|
||||
@ -2226,7 +2231,7 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
react-dom@^18.2.0, react-dom@^18.3.0:
|
||||
"react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", react-dom@^18.2.0, react-dom@^18.3.0:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
|
||||
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
||||
@ -2239,7 +2244,7 @@ react-is@^16.13.1:
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
"react@^15.0.0 || >=16.0.0", react@^18.2.0, react@^18.3.0, react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0":
|
||||
"react@^15.0.0 || >=16.0.0", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", react@^18.2.0, react@^18.3.0, react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0":
|
||||
version "18.3.1"
|
||||
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
|
||||
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user