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")
|
@router.get("/{ts_code}/capital_flow")
|
||||||
async def get_capital_flow(ts_code: str, days: int = 10):
|
async def get_capital_flow(ts_code: str, days: int = 10):
|
||||||
"""获取个股资金流向"""
|
"""获取个股资金流向(含大/中/小单分拆)"""
|
||||||
df = tushare_client.get_stock_moneyflow(ts_code, days=days)
|
df = tushare_client.get_stock_moneyflow(ts_code, days=days)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return []
|
return []
|
||||||
@ -61,6 +61,18 @@ async def get_capital_flow(ts_code: str, days: int = 10):
|
|||||||
"trade_date": row["trade_date"],
|
"trade_date": row["trade_date"],
|
||||||
"main_net_inflow": round(main_net, 2),
|
"main_net_inflow": round(main_net, 2),
|
||||||
"net_mf_amount": round(float(row.get("net_mf_amount", 0) or 0), 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
|
return records
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
{
|
{
|
||||||
"pages": {
|
"pages": {
|
||||||
"/sectors/page": [
|
|
||||||
"static/chunks/webpack.js",
|
|
||||||
"static/chunks/main-app.js",
|
|
||||||
"static/chunks/app/sectors/page.js"
|
|
||||||
],
|
|
||||||
"/layout": [
|
"/layout": [
|
||||||
"static/chunks/webpack.js",
|
"static/chunks/webpack.js",
|
||||||
"static/chunks/main-app.js",
|
"static/chunks/main-app.js",
|
||||||
@ -20,6 +15,21 @@
|
|||||||
"static/chunks/webpack.js",
|
"static/chunks/webpack.js",
|
||||||
"static/chunks/main-app.js",
|
"static/chunks/main-app.js",
|
||||||
"static/chunks/app/recommendations/page.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",
|
"/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": {},
|
"node": {},
|
||||||
"edge": {},
|
"edge": {},
|
||||||
"encryptionKey": "IAps6Nn+QS9GVH+sOr6laVRGfUJrD0aLxGy9A/+XkIs="
|
"encryptionKey": "to26WE9kY05Cd2g+GH2pcshXDAS7bZksw3lMjIYHGFQ="
|
||||||
}
|
}
|
||||||
@ -125,7 +125,7 @@
|
|||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/getFullHash */
|
/******/ /* webpack/runtime/getFullHash */
|
||||||
/******/ (() => {
|
/******/ (() => {
|
||||||
/******/ __webpack_require__.h = () => ("a035f49818643978")
|
/******/ __webpack_require__.h = () => ("5a8081291a3a3a33")
|
||||||
/******/ })();
|
/******/ })();
|
||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
/******/ /* 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": "^5.5.0",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
"next": "^14.2.0",
|
"next": "^14.2.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18.3.0",
|
"react": "^18.3.0",
|
||||||
"react-dom": "^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": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
|
|||||||
@ -9,21 +9,22 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^14.2.0",
|
|
||||||
"react": "^18.3.0",
|
|
||||||
"react-dom": "^18.3.0",
|
|
||||||
"echarts": "^5.5.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": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/react": "^18.3.0",
|
"@types/react": "^18.3.0",
|
||||||
"@types/react-dom": "^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",
|
"autoprefixer": "^10.4.0",
|
||||||
"eslint": "^8.0.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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import { streamChat, type ChatMessage } from "@/lib/api";
|
import { streamChat, type ChatMessage } from "@/lib/api";
|
||||||
|
|
||||||
interface DisplayMessage {
|
interface DisplayMessage {
|
||||||
@ -15,6 +16,7 @@ const QUICK_QUESTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
|
const { theme } = useTheme();
|
||||||
const [messages, setMessages] = useState<DisplayMessage[]>([]);
|
const [messages, setMessages] = useState<DisplayMessage[]>([]);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [streaming, setStreaming] = useState(false);
|
const [streaming, setStreaming] = useState(false);
|
||||||
@ -87,7 +89,7 @@ export default function ChatPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-3xl mx-auto flex flex-col md:h-[calc(100dvh)]">
|
<div className="max-w-3xl mx-auto flex flex-col md:h-[calc(100dvh)]">
|
||||||
{/* Header */}
|
{/* 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="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">
|
<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">
|
<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 && (
|
{messages.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setMessages([])}
|
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>
|
</button>
|
||||||
@ -129,7 +131,7 @@ export default function ChatPage() {
|
|||||||
<button
|
<button
|
||||||
key={q}
|
key={q}
|
||||||
onClick={() => sendMessage(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}
|
{q}
|
||||||
</button>
|
</button>
|
||||||
@ -153,7 +155,7 @@ export default function ChatPage() {
|
|||||||
{msg.role === "assistant" ? (
|
{msg.role === "assistant" ? (
|
||||||
msg.content ? (
|
msg.content ? (
|
||||||
<div
|
<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={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: formatMarkdown(msg.content),
|
__html: formatMarkdown(msg.content),
|
||||||
}}
|
}}
|
||||||
@ -186,7 +188,7 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input */}
|
{/* 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">
|
<div className="flex items-end gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@ -195,7 +197,7 @@ export default function ChatPage() {
|
|||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="输入问题..."
|
placeholder="输入问题..."
|
||||||
rows={1}
|
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}
|
disabled={streaming}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -4,64 +4,180 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
DARK THEME (default)
|
||||||
|
======================================== */
|
||||||
:root {
|
: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-up: #ff6b6b;
|
||||||
--color-down: #34d399;
|
--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-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-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;
|
--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 {
|
body {
|
||||||
background-color: #0a0a0c;
|
background-color: var(--bg-primary);
|
||||||
color: #f5f5f7;
|
color: var(--text-primary);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabular numbers for financial data */
|
|
||||||
.tabular-nums {
|
.tabular-nums {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
font-feature-settings: 'tnum';
|
font-feature-settings: 'tnum';
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glass morphism card */
|
/* ========================================
|
||||||
|
GLASS MORPHISM (theme-aware)
|
||||||
|
======================================== */
|
||||||
@layer components {
|
@layer components {
|
||||||
.glass-card {
|
.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);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-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;
|
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);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card:hover {
|
.glass-card:hover {
|
||||||
border-color: rgba(255, 255, 255, 0.10);
|
border-color: var(--glass-hover-border);
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
box-shadow: var(--glass-hover-shadow), inset 0 1px 0 var(--glass-hover-inset);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card-static {
|
.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);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-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;
|
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 {
|
.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);
|
backdrop-filter: blur(30px);
|
||||||
-webkit-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 {
|
.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 {
|
.gradient-border {
|
||||||
@ -82,18 +198,22 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
EFFECTS (theme-aware)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
/* Noise texture overlay */
|
/* Noise texture overlay */
|
||||||
body::before {
|
body::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
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");
|
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;
|
pointer-events: none;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ambient glow effect - top right (amber) */
|
/* Ambient glow - top right (amber) */
|
||||||
body::after {
|
body::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -101,7 +221,7 @@ body::after {
|
|||||||
right: -200px;
|
right: -200px;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
height: 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;
|
pointer-events: none;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
@ -114,14 +234,22 @@ body::after {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(255, 255, 255, 0.10);
|
background: var(--scrollbar-thumb);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-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 {
|
@keyframes fadeInUp {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@ -162,7 +290,7 @@ body::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.animate-shimmer {
|
.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%;
|
background-size: 200% 100%;
|
||||||
animation: shimmer 2s infinite;
|
animation: shimmer 2s infinite;
|
||||||
}
|
}
|
||||||
@ -186,9 +314,3 @@ body::after {
|
|||||||
.score-bar-gradient-low {
|
.score-bar-gradient-low {
|
||||||
background: linear-gradient(90deg, #585860, #3a3a40);
|
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 { AuthGuard } from "@/components/auth-guard";
|
||||||
import { UserMenu } from "@/components/user-menu";
|
import { UserMenu } from "@/components/user-menu";
|
||||||
import { SidebarNav, MobileBottomNav } from "@/components/nav";
|
import { SidebarNav, MobileBottomNav } from "@/components/nav";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Dragon AI Agent",
|
title: "Dragon AI Agent",
|
||||||
@ -23,49 +24,51 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" suppressHydrationWarning>
|
||||||
<body className="min-h-screen bg-bg-primary text-text-primary font-display">
|
<body className="min-h-screen bg-bg-primary text-text-primary font-display">
|
||||||
<AuthProvider>
|
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||||
<AuthGuard>
|
<AuthProvider>
|
||||||
{/* Desktop: sidebar + main */}
|
<AuthGuard>
|
||||||
<div className="flex min-h-screen">
|
{/* Desktop: sidebar + main */}
|
||||||
{/* Desktop sidebar */}
|
<div className="flex min-h-screen">
|
||||||
<aside className="hidden md:flex flex-col w-60 glass-sidebar fixed inset-y-0 left-0 z-40">
|
{/* Desktop sidebar */}
|
||||||
{/* Brand */}
|
<aside className="hidden md:flex flex-col w-60 glass-sidebar fixed inset-y-0 left-0 z-40">
|
||||||
<div className="px-6 pt-7 pb-5">
|
{/* Brand */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="px-6 pt-7 pb-5">
|
||||||
<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">
|
<div className="flex items-center gap-3">
|
||||||
D
|
<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">
|
||||||
</div>
|
D
|
||||||
<div>
|
</div>
|
||||||
<h1 className="text-sm font-semibold tracking-tight">Dragon AI Agent</h1>
|
<div>
|
||||||
<p className="text-xs text-text-muted mt-0.5 font-light tracking-wide">资金驱动 · 四层漏斗模型</p>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="mx-5 h-px bg-gradient-to-r from-transparent via-white/[0.06] to-transparent" />
|
<div className="mx-5 h-px bg-gradient-to-r from-transparent via-border-default to-transparent" />
|
||||||
|
|
||||||
{/* Nav */}
|
{/* Nav */}
|
||||||
<SidebarNav />
|
<SidebarNav />
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="px-6 py-5 border-t border-white/[0.04]">
|
<div className="px-6 py-5 border-t border-border-subtle">
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<main className="flex-1 md:ml-60 pb-16 md:pb-0 min-h-screen">
|
<main className="flex-1 md:ml-60 pb-16 md:pb-0 min-h-screen">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile bottom nav */}
|
{/* Mobile bottom nav */}
|
||||||
<MobileBottomNav />
|
<MobileBottomNav />
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-bg-primary">
|
<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 */}
|
{/* Brand */}
|
||||||
<div className="flex flex-col items-center mb-8">
|
<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">
|
<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="用户名"
|
placeholder="用户名"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
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"
|
autoComplete="username"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@ -61,7 +61,7 @@ export default function LoginPage() {
|
|||||||
placeholder="密码"
|
placeholder="密码"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
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"
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
<button
|
<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 ${
|
className={`text-xs px-4 py-1.5 rounded-xl whitespace-nowrap transition-all duration-200 font-medium ${
|
||||||
filter === key
|
filter === key
|
||||||
? "bg-gradient-to-r from-amber-500/25 to-amber-600/20 text-amber-400 border border-amber-500/15"
|
? "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}
|
{label}
|
||||||
@ -184,7 +184,7 @@ export default function RecommendationsPage() {
|
|||||||
{/* Date header */}
|
{/* Date header */}
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleDay(group.date)}
|
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
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
|
|||||||
@ -17,7 +17,7 @@ function getStageInfo(stage: string) {
|
|||||||
case "end":
|
case "end":
|
||||||
return { label: "尾声", color: "text-red-400", bg: "bg-red-500/10 border-red-500/15" };
|
return { label: "尾声", color: "text-red-400", bg: "bg-red-500/10 border-red-500/15" };
|
||||||
default:
|
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 (
|
return (
|
||||||
<a
|
<a
|
||||||
href={`/stock/${stock.ts_code}`}
|
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="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"}`}>
|
<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"
|
? "bg-gradient-to-br from-slate-400/20 to-slate-500/15 text-slate-300 border border-slate-400/15"
|
||||||
: index === 2
|
: index === 2
|
||||||
? "bg-gradient-to-br from-amber-700/20 to-amber-800/15 text-amber-400 border border-amber-600/15"
|
? "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}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
@ -127,19 +127,19 @@ function SectorDetailCard({ sector, index }: { sector: SectorData; index: number
|
|||||||
|
|
||||||
{/* Metrics row */}
|
{/* Metrics row */}
|
||||||
<div className="grid grid-cols-3 gap-3 mb-3">
|
<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-[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"}`}>
|
<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)}
|
{sector.capital_inflow > 0 ? "+" : ""}{formatNumber(sector.capital_inflow)}
|
||||||
</div>
|
</div>
|
||||||
</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-[10px] text-text-muted/50 mb-0.5">涨停股</div>
|
||||||
<div className="text-xs font-mono tabular-nums font-semibold text-text-secondary">
|
<div className="text-xs font-mono tabular-nums font-semibold text-text-secondary">
|
||||||
{displayLimitUp}<span className="text-text-muted/40"> 只</span>
|
{displayLimitUp}<span className="text-text-muted/40"> 只</span>
|
||||||
</div>
|
</div>
|
||||||
</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-[10px] text-text-muted/50 mb-0.5">热度评分</div>
|
||||||
<div className={`text-xs font-mono tabular-nums font-semibold ${
|
<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"
|
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 { useEffect, useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { fetchAPI } from "@/lib/api";
|
import { fetchAPI } from "@/lib/api";
|
||||||
import { getScoreColor, getSignalColor } from "@/lib/utils";
|
import { getScoreColor } from "@/lib/utils";
|
||||||
import KlineChart from "@/components/kline-chart";
|
import KlineChart from "@/components/kline-chart";
|
||||||
import CapitalFlowChart from "@/components/capital-flow";
|
import CapitalFlowChart from "@/components/capital-flow";
|
||||||
import ScoreRadar from "@/components/score-radar";
|
import ScoreRadar from "@/components/score-radar";
|
||||||
@ -23,6 +23,10 @@ interface StockSignals {
|
|||||||
support_price: number | null;
|
support_price: number | null;
|
||||||
resist_price: number | null;
|
resist_price: number | null;
|
||||||
stop_loss_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 {
|
interface QuoteData {
|
||||||
@ -36,8 +40,25 @@ interface QuoteData {
|
|||||||
pe: number | null;
|
pe: number | null;
|
||||||
pb: number | null;
|
pb: number | null;
|
||||||
circ_mv: number | null;
|
circ_mv: number | null;
|
||||||
|
total_mv: number | null;
|
||||||
|
volume_ratio: number | null;
|
||||||
high: number | null;
|
high: number | null;
|
||||||
low: 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() {
|
export default function StockDetailPage() {
|
||||||
@ -48,29 +69,42 @@ export default function StockDetailPage() {
|
|||||||
const [signals, setSignals] = useState<StockSignals | null>(null);
|
const [signals, setSignals] = useState<StockSignals | null>(null);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [kline, setKline] = useState<any[]>([]);
|
const [kline, setKline] = useState<any[]>([]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const [capitalFlow, setCapitalFlow] = useState<FlowRecord[]>([]);
|
||||||
const [capitalFlow, setCapitalFlow] = useState<any[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!code) return;
|
if (!code) return;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetchAPI<QuoteData>(`/api/stocks/${code}/quote`).catch(() => null),
|
fetchAPI<QuoteData>(`/api/stocks/${code}/quote`).catch(() => null),
|
||||||
fetchAPI<StockSignals>(`/api/stocks/${code}/signals`).catch(() => null),
|
fetchAPI<StockSignals>(`/api/stocks/${code}/signals`).catch(() => null),
|
||||||
fetchAPI<unknown[]>(`/api/stocks/${code}/kline?days=60`).catch(() => []),
|
fetchAPI<unknown[]>(`/api/stocks/${code}/kline?days=120`).catch(() => []),
|
||||||
fetchAPI<unknown[]>(`/api/stocks/${code}/capital_flow?days=10`).catch(() => []),
|
fetchAPI<FlowRecord[]>(`/api/stocks/${code}/capital_flow?days=10`).catch(() => []),
|
||||||
]).then(([q, s, k, c]) => {
|
]).then(([q, s, k, c]) => {
|
||||||
setQuote(q);
|
setQuote(q);
|
||||||
setSignals(s);
|
setSignals(s);
|
||||||
setKline(k);
|
setKline(k);
|
||||||
setCapitalFlow(c);
|
setCapitalFlow(c as FlowRecord[]);
|
||||||
});
|
});
|
||||||
}, [code]);
|
}, [code]);
|
||||||
|
|
||||||
|
const latestFlow = capitalFlow.length > 0 ? capitalFlow[capitalFlow.length - 1] : null;
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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">
|
<a
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
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" />
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
返回
|
返回
|
||||||
@ -90,21 +124,91 @@ export default function StockDetailPage() {
|
|||||||
<div className="flex items-baseline gap-3">
|
<div className="flex items-baseline gap-3">
|
||||||
<span
|
<span
|
||||||
className={`text-3xl font-bold font-mono tabular-nums tracking-tight ${
|
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)}
|
{quote.price.toFixed(2)}
|
||||||
</span>
|
</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 > 0 ? "+" : ""}
|
||||||
{quote.pct_chg.toFixed(2)}%
|
{quote.pct_chg.toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
|
{quote.pre_close && (
|
||||||
|
<span className="text-xs text-text-muted font-mono tabular-nums">
|
||||||
|
昨收 {quote.pre_close.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-3 mt-4">
|
|
||||||
<QuoteStat label="换手率" value={`${quote.turnover_rate?.toFixed(2)}%`} />
|
{/* OHLC row */}
|
||||||
<QuoteStat label="市盈率" value={quote.pe?.toFixed(1) ?? "-"} />
|
<div className="grid grid-cols-4 gap-2 mt-4">
|
||||||
<QuoteStat label="市净率" value={quote.pb?.toFixed(2) ?? "-"} />
|
<MiniStat
|
||||||
<QuoteStat label="流通市值" value={quote.circ_mv ? `${quote.circ_mv.toFixed(0)}亿` : "-"} />
|
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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -117,13 +221,64 @@ export default function StockDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Technical signals */}
|
||||||
{signals && (
|
{signals && (
|
||||||
<div className="glass-card-static p-5 animate-fade-in-up delay-75">
|
<div className="glass-card-static p-5 animate-fade-in-up delay-75">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<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)}`}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -138,24 +293,30 @@ export default function StockDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price levels */}
|
{/* 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>
|
<div>
|
||||||
<span className="text-text-muted">支撑位 </span>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-text-muted">压力位 </span>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-text-muted">止损位 </span>
|
<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>
|
</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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 animate-fade-in-up delay-150">
|
||||||
{kline.length > 0 && <KlineChart data={kline} />}
|
{kline.length > 0 && <KlineChart data={kline} />}
|
||||||
{capitalFlow.length > 0 && <CapitalFlowChart data={capitalFlow} />}
|
{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 (
|
return (
|
||||||
<div className="bg-white/[0.02] rounded-lg px-3 py-2 border border-white/[0.03]">
|
<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-xs text-text-muted mb-0.5">{label}</div>
|
<div className="text-[10px] text-text-muted leading-tight">{label}</div>
|
||||||
<div className="text-xs font-mono tabular-nums">{value}</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>
|
</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 ${
|
className={`flex items-center justify-between px-3 py-2 rounded-xl text-xs transition-all duration-200 ${
|
||||||
active
|
active
|
||||||
? "bg-red-500/[0.08] text-red-400 border border-red-500/10"
|
? "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>
|
<span className="font-medium">{label}</span>
|
||||||
@ -196,3 +470,22 @@ function SignalItem({ label, active, points }: { label: string; active: boolean;
|
|||||||
</div>
|
</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">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
{/* Avatar */}
|
{/* 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()}
|
{u.username.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@ -155,7 +155,7 @@ export default function UsersPage() {
|
|||||||
className={`text-[10px] px-1.5 py-0.5 rounded ${
|
className={`text-[10px] px-1.5 py-0.5 rounded ${
|
||||||
u.role === "admin"
|
u.role === "admin"
|
||||||
? "bg-amber-500/10 text-amber-400/80"
|
? "bg-amber-500/10 text-amber-400/80"
|
||||||
: "bg-white/[0.04] text-text-muted"
|
: "bg-surface-3 text-text-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{u.role}
|
{u.role}
|
||||||
@ -179,7 +179,7 @@ export default function UsersPage() {
|
|||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleResetPassword(u.id)}
|
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>
|
</button>
|
||||||
@ -210,11 +210,11 @@ export default function UsersPage() {
|
|||||||
{showCreate && (
|
{showCreate && (
|
||||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
<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="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 ? (
|
{createdResult ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-base font-semibold text-text-primary">用户创建成功</h3>
|
<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">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-text-muted">用户名</span>
|
<span className="text-text-muted">用户名</span>
|
||||||
<span className="text-text-primary font-medium">{createdResult.username}</span>
|
<span className="text-text-primary font-medium">{createdResult.username}</span>
|
||||||
@ -234,7 +234,7 @@ export default function UsersPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreate(false)}
|
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>
|
</button>
|
||||||
@ -249,12 +249,12 @@ export default function UsersPage() {
|
|||||||
placeholder="用户名"
|
placeholder="用户名"
|
||||||
value={newUsername}
|
value={newUsername}
|
||||||
onChange={(e) => setNewUsername(e.target.value)}
|
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
|
<select
|
||||||
value={newRole}
|
value={newRole}
|
||||||
onChange={(e) => setNewRole(e.target.value)}
|
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="user" className="bg-bg-card text-text-primary">普通用户</option>
|
||||||
<option value="admin" 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowCreate(false)}
|
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>
|
</button>
|
||||||
@ -287,9 +287,9 @@ export default function UsersPage() {
|
|||||||
{resetResult && (
|
{resetResult && (
|
||||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
<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="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>
|
<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">
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-text-muted">用户名</span>
|
<span className="text-text-muted">用户名</span>
|
||||||
<span className="text-text-primary font-medium">{resetResult.username}</span>
|
<span className="text-text-primary font-medium">{resetResult.username}</span>
|
||||||
@ -309,7 +309,7 @@ export default function UsersPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setResetResult(null)}
|
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>
|
</button>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
interface FlowData {
|
interface FlowData {
|
||||||
trade_date: string;
|
trade_date: string;
|
||||||
@ -10,6 +11,7 @@ interface FlowData {
|
|||||||
|
|
||||||
export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
||||||
const chartRef = useRef<HTMLDivElement>(null);
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartRef.current || !data.length) return;
|
if (!chartRef.current || !data.length) return;
|
||||||
@ -18,7 +20,13 @@ export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
|||||||
|
|
||||||
import("echarts").then((ec) => {
|
import("echarts").then((ec) => {
|
||||||
if (!chartRef.current) return;
|
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 dates = data.map((d) => d.trade_date);
|
||||||
const values = data.map((d) => d.main_net_inflow);
|
const values = data.map((d) => d.main_net_inflow);
|
||||||
@ -36,15 +44,15 @@ export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
data: dates,
|
data: dates,
|
||||||
axisLine: { lineStyle: { color: "#475569" } },
|
axisLine: { lineStyle: { color: axisLineColor } },
|
||||||
axisLabel: { fontSize: 10, color: "#94a3b8", rotate: 30 },
|
axisLabel: { fontSize: 10, color: axisLabelColor, rotate: 30 },
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
splitLine: { lineStyle: { color: "#1e293b" } },
|
splitLine: { lineStyle: { color: splitLineColor } },
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: "#94a3b8",
|
color: axisLabelColor,
|
||||||
formatter: (v: number) => (Math.abs(v) >= 10000 ? (v / 10000).toFixed(1) + "亿" : v + "万"),
|
formatter: (v: number) => (Math.abs(v) >= 10000 ? (v / 10000).toFixed(1) + "亿" : v + "万"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -67,7 +75,7 @@ export default function CapitalFlowChart({ data }: { data: FlowData[] }) {
|
|||||||
return () => {
|
return () => {
|
||||||
chart?.dispose();
|
chart?.dispose();
|
||||||
};
|
};
|
||||||
}, [data]);
|
}, [data, theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bg-card rounded-xl p-4">
|
<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} />
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={handleClose} />
|
||||||
|
|
||||||
{/* Dialog */}
|
{/* 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>
|
<h3 className="text-base font-semibold text-text-primary mb-5">修改密码</h3>
|
||||||
|
|
||||||
{success ? (
|
{success ? (
|
||||||
@ -74,7 +74,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
|||||||
<p className="text-sm text-emerald-400">密码修改成功,下次登录请使用新密码。</p>
|
<p className="text-sm text-emerald-400">密码修改成功,下次登录请使用新密码。</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
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>
|
</button>
|
||||||
@ -86,7 +86,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
|||||||
placeholder="旧密码"
|
placeholder="旧密码"
|
||||||
value={oldPassword}
|
value={oldPassword}
|
||||||
onChange={(e) => setOldPassword(e.target.value)}
|
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"
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@ -94,7 +94,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
|||||||
placeholder="新密码(至少 6 位)"
|
placeholder="新密码(至少 6 位)"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
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"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@ -102,7 +102,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
|||||||
placeholder="确认新密码"
|
placeholder="确认新密码"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
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"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ export function ChangePasswordDialog({ open, onClose }: ChangePasswordDialogProp
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClose}
|
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>
|
</button>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
interface KlineData {
|
interface KlineData {
|
||||||
trade_date: string;
|
trade_date: string;
|
||||||
@ -18,17 +19,22 @@ interface KlineData {
|
|||||||
|
|
||||||
export default function KlineChart({ data }: { data: KlineData[] }) {
|
export default function KlineChart({ data }: { data: KlineData[] }) {
|
||||||
const chartRef = useRef<HTMLDivElement>(null);
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartRef.current || !data.length) return;
|
if (!chartRef.current || !data.length) return;
|
||||||
|
|
||||||
let echarts: typeof import("echarts") | null = null;
|
|
||||||
let chart: ReturnType<typeof import("echarts")["init"]> | null = null;
|
let chart: ReturnType<typeof import("echarts")["init"]> | null = null;
|
||||||
|
|
||||||
import("echarts").then((ec) => {
|
import("echarts").then((ec) => {
|
||||||
echarts = ec;
|
|
||||||
if (!chartRef.current) return;
|
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 dates = data.map((d) => d.trade_date);
|
||||||
const ohlc = data.map((d) => [d.open, d.close, d.low, d.high]);
|
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",
|
type: "category",
|
||||||
data: dates,
|
data: dates,
|
||||||
axisLine: { lineStyle: { color: "#475569" } },
|
axisLine: { lineStyle: { color: axisLineColor } },
|
||||||
axisLabel: { fontSize: 10, color: "#94a3b8" },
|
axisLabel: { fontSize: 10, color: axisLabelColor },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
data: dates,
|
data: dates,
|
||||||
axisLabel: { show: false },
|
axisLabel: { show: false },
|
||||||
axisLine: { lineStyle: { color: "#475569" } },
|
axisLine: { lineStyle: { color: axisLineColor } },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
splitLine: { lineStyle: { color: "#1e293b" } },
|
splitLine: { lineStyle: { color: splitLineColor } },
|
||||||
axisLabel: { fontSize: 10, color: "#94a3b8" },
|
axisLabel: { fontSize: 10, color: axisLabelColor },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scale: true,
|
scale: true,
|
||||||
gridIndex: 1,
|
gridIndex: 1,
|
||||||
splitNumber: 2,
|
splitNumber: 2,
|
||||||
splitLine: { lineStyle: { color: "#1e293b" } },
|
splitLine: { lineStyle: { color: splitLineColor } },
|
||||||
axisLabel: { fontSize: 10, color: "#94a3b8" },
|
axisLabel: { fontSize: 10, color: axisLabelColor },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
@ -143,7 +149,7 @@ export default function KlineChart({ data }: { data: KlineData[] }) {
|
|||||||
return () => {
|
return () => {
|
||||||
chart?.dispose();
|
chart?.dispose();
|
||||||
};
|
};
|
||||||
}, [data]);
|
}, [data, theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bg-card rounded-xl p-4">
|
<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 */}
|
{/* Middle row: Broken rate + MA20 */}
|
||||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
<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>
|
<div className="text-xs text-text-muted mb-0.5">炸板率</div>
|
||||||
{hasBrokenRate ? (
|
{hasBrokenRate ? (
|
||||||
<div className="flex items-baseline gap-1">
|
<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>
|
<span className="text-sm text-text-muted/40">-</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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="text-xs text-text-muted mb-0.5">上证均线</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<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"}`} />
|
<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 && (
|
{indices && indices.length > 0 && (
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
{indices.map((idx) => (
|
{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">
|
<div className="text-xs text-text-muted mb-0.5">
|
||||||
{idx.name}
|
{idx.name}
|
||||||
{idx.realtime && <span className="text-emerald-400/60 ml-1">· 实时</span>}
|
{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 }) {
|
function StatCard({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
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-xs text-text-muted mb-0.5 font-medium">{label}</div>
|
||||||
<div className="text-sm">{children}</div>
|
<div className="text-sm">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
|
|
||||||
function DashboardIcon() {
|
function DashboardIcon() {
|
||||||
return (
|
return (
|
||||||
@ -61,8 +62,8 @@ function SideNavItem({ href, icon, label }: { href: string; icon: React.ReactNod
|
|||||||
href={href}
|
href={href}
|
||||||
className={`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 ${
|
className={`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm transition-all duration-200 ${
|
||||||
isActive
|
isActive
|
||||||
? "text-text-primary bg-white/[0.06]"
|
? "text-text-primary bg-surface-4"
|
||||||
: "text-text-secondary hover:text-text-primary hover:bg-white/[0.04]"
|
: "text-text-secondary hover:text-text-primary hover:bg-surface-3"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-base opacity-70">{icon}</span>
|
<span className="text-base opacity-70">{icon}</span>
|
||||||
@ -106,7 +107,7 @@ function MobileNavItem({ href, label, children }: { href: string; label: string;
|
|||||||
|
|
||||||
export function MobileBottomNav() {
|
export function MobileBottomNav() {
|
||||||
return (
|
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))]">
|
<div className="flex justify-around py-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]">
|
||||||
<MobileNavItem href="/" label="总览">
|
<MobileNavItem href="/" label="总览">
|
||||||
<DashboardIcon />
|
<DashboardIcon />
|
||||||
@ -120,6 +121,9 @@ export function MobileBottomNav() {
|
|||||||
<MobileNavItem href="/chat" label="对话">
|
<MobileNavItem href="/chat" label="对话">
|
||||||
<ChatIcon />
|
<ChatIcon />
|
||||||
</MobileNavItem>
|
</MobileNavItem>
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
interface SignalsData {
|
interface SignalsData {
|
||||||
score: number;
|
score: number;
|
||||||
@ -15,6 +16,7 @@ interface SignalsData {
|
|||||||
|
|
||||||
export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
||||||
const chartRef = useRef<HTMLDivElement>(null);
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartRef.current) return;
|
if (!chartRef.current) return;
|
||||||
@ -23,7 +25,13 @@ export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
|||||||
|
|
||||||
import("echarts").then((ec) => {
|
import("echarts").then((ec) => {
|
||||||
if (!chartRef.current) return;
|
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 = [
|
const indicators = [
|
||||||
{ name: "均线多头", max: 15 },
|
{ name: "均线多头", max: 15 },
|
||||||
@ -51,10 +59,10 @@ export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
|||||||
indicator: indicators,
|
indicator: indicators,
|
||||||
shape: "polygon",
|
shape: "polygon",
|
||||||
splitNumber: 3,
|
splitNumber: 3,
|
||||||
axisName: { color: "#94a3b8", fontSize: 10 },
|
axisName: { color: axisLabelColor, fontSize: 10 },
|
||||||
splitLine: { lineStyle: { color: "#334155" } },
|
splitLine: { lineStyle: { color: splitLineColor } },
|
||||||
splitArea: { areaStyle: { color: ["transparent"] } },
|
splitArea: { areaStyle: { color: ["transparent"] } },
|
||||||
axisLine: { lineStyle: { color: "#475569" } },
|
axisLine: { lineStyle: { color: axisLineColor } },
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@ -80,7 +88,7 @@ export default function ScoreRadar({ signals }: { signals: SignalsData }) {
|
|||||||
return () => {
|
return () => {
|
||||||
chart?.dispose();
|
chart?.dispose();
|
||||||
};
|
};
|
||||||
}, [signals]);
|
}, [signals, theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bg-card rounded-xl p-4">
|
<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"
|
? "bg-gradient-to-br from-slate-400/20 to-slate-500/15 text-slate-300 border border-slate-400/15"
|
||||||
: index === 2
|
: index === 2
|
||||||
? "bg-gradient-to-br from-amber-700/20 to-amber-800/15 text-amber-400 border border-amber-600/15"
|
? "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}
|
{index + 1}
|
||||||
</span>
|
</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 ${
|
<span className={`font-mono tabular-nums text-xs font-semibold px-2 py-1 rounded-lg min-w-[36px] text-center ${
|
||||||
isTop3
|
isTop3
|
||||||
? "bg-amber-500/20 text-amber-400 border border-amber-500/15"
|
? "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)}
|
{s.heat_score.toFixed(0)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export default function StockCard({ rec, showLLMLoading = false }: { rec: Recomm
|
|||||||
|
|
||||||
{/* Price reference */}
|
{/* Price reference */}
|
||||||
{rec.entry_price && (
|
{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>
|
<div>
|
||||||
<span className="text-text-muted">买入 </span>
|
<span className="text-text-muted">买入 </span>
|
||||||
<span className="text-red-400 font-mono tabular-nums">{rec.entry_price}</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)) {
|
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) => {
|
return parts.map((part, i) => {
|
||||||
if (part.startsWith("`") && part.endsWith("`")) {
|
if (part.startsWith("`") && part.endsWith("`")) {
|
||||||
return (
|
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)}
|
{part.slice(1, -1)}
|
||||||
</code>
|
</code>
|
||||||
);
|
);
|
||||||
@ -240,7 +240,7 @@ function ScoreBar({ label, value }: { label: string; value: number }) {
|
|||||||
<span className="font-medium">{label}</span>
|
<span className="font-medium">{label}</span>
|
||||||
<span className="font-mono tabular-nums">{value.toFixed(0)}</span>
|
<span className="font-mono tabular-nums">{value.toFixed(0)}</span>
|
||||||
</div>
|
</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
|
<div
|
||||||
className={`h-full rounded-full transition-all duration-700 ease-out ${gradientClass}`}
|
className={`h-full rounded-full transition-all duration-700 ease-out ${gradientClass}`}
|
||||||
style={{ width: `${width}%` }}
|
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 { useState } from "react";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { ChangePasswordDialog } from "@/components/change-password-dialog";
|
import { ChangePasswordDialog } from "@/components/change-password-dialog";
|
||||||
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
|
|
||||||
export function UserMenu() {
|
export function UserMenu() {
|
||||||
const { user, logout } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
@ -14,10 +15,13 @@ export function UserMenu() {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-text-muted">{user.username}</span>
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-400/80">
|
<span className="text-xs text-text-muted">{user.username}</span>
|
||||||
{user.role}
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-400/80">
|
||||||
</span>
|
{user.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
darkMode: "class",
|
||||||
content: [
|
content: [
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
@ -7,23 +8,33 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
up: "#ff6b6b",
|
up: "var(--color-up)",
|
||||||
down: "#34d399",
|
down: "var(--color-down)",
|
||||||
hot: "#f59e0b",
|
hot: "var(--color-hot)",
|
||||||
bg: {
|
bg: {
|
||||||
primary: "#0a0a0c",
|
primary: "var(--bg-primary)",
|
||||||
secondary: "#101013",
|
secondary: "var(--bg-secondary)",
|
||||||
card: "#161619",
|
card: "var(--bg-card)",
|
||||||
elevated: "#1e1e22",
|
elevated: "var(--bg-elevated)",
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
primary: "#f5f5f7",
|
primary: "var(--text-primary)",
|
||||||
secondary: "#9898a4",
|
secondary: "var(--text-secondary)",
|
||||||
muted: "#58585f",
|
muted: "var(--text-muted)",
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
amber: "#f59e0b",
|
amber: "var(--accent-amber)",
|
||||||
cyan: "#22d3ee",
|
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: {
|
fontFamily: {
|
||||||
@ -35,9 +46,9 @@ module.exports = {
|
|||||||
"3xl": "20px",
|
"3xl": "20px",
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
card: "0 4px 24px rgba(0, 0, 0, 0.3)",
|
card: "var(--shadow-card)",
|
||||||
glow: "0 0 20px rgba(245, 158, 11, 0.12)",
|
glow: "var(--shadow-glow)",
|
||||||
"glow-sm": "0 0 10px rgba(245, 158, 11, 0.08)",
|
"glow-sm": "var(--shadow-glow-sm)",
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"fade-in-up": "fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1) both",
|
"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"
|
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
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:
|
next@^14.2.0:
|
||||||
version "14.2.35"
|
version "14.2.35"
|
||||||
resolved "https://registry.npmjs.org/next/-/next-14.2.35.tgz"
|
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"
|
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
|
||||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
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"
|
version "18.3.1"
|
||||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
|
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
|
||||||
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
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"
|
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
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"
|
version "18.3.1"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
|
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
|
||||||
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user