diff --git a/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js b/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js index f0ec5140..930027e0 100644 --- a/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js +++ b/src/views/StockOverview/components/HotspotOverview/components/ConceptAlertList.js @@ -2,7 +2,7 @@ * 概念异动列表组件 - V2 科技感设计 * 展示当日的概念异动记录,点击可展开显示相关股票 */ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { Box, VStack, @@ -87,11 +87,10 @@ const getAlertIcon = (alertType) => { * 指标提示组件 - 带详细说明 */ const MetricTooltip = ({ metricKey, children }) => { + const tooltipBg = useColorModeValue('gray.800', 'gray.700'); const config = METRIC_CONFIG[metricKey]; if (!config) return children; - const tooltipBg = useColorModeValue('gray.800', 'gray.700'); - return ( { /** * 概念异动列表 */ -const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight = '400px' }) => { +const ConceptAlertList = ({ + alerts = [], + onAlertClick, + selectedAlert, + maxHeight = '400px', + autoExpandAlertKey = null, + onAutoExpandComplete, +}) => { const [expandedId, setExpandedId] = useState(null); const [conceptStocks, setConceptStocks] = useState({}); const [loadingConcepts, setLoadingConcepts] = useState({}); @@ -678,13 +684,12 @@ const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight const subTextColor = useColorModeValue('gray.500', 'gray.400'); const emptyBg = useColorModeValue('gray.50', '#111111'); - // 获取概念相关股票 + // 获取概念相关股票 - 使用 ref 避免依赖循环 const fetchConceptStocks = useCallback(async (conceptId) => { - if (conceptStocks[conceptId] || loadingConcepts[conceptId]) { - return; - } - - setLoadingConcepts(prev => ({ ...prev, [conceptId]: true })); + setLoadingConcepts(prev => { + if (prev[conceptId]) return prev; + return { ...prev, [conceptId]: true }; + }); try { const response = await axios.get(`/api/concept/${encodeURIComponent(conceptId)}/stocks`); @@ -702,7 +707,21 @@ const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight } finally { setLoadingConcepts(prev => ({ ...prev, [conceptId]: false })); } - }, [conceptStocks, loadingConcepts]); + }, []); + + // 处理自动展开 + useEffect(() => { + if (autoExpandAlertKey && autoExpandAlertKey !== expandedId) { + setExpandedId(autoExpandAlertKey); + // 找到对应的 alert 并获取股票数据 + const [conceptId] = autoExpandAlertKey.split('-'); + if (conceptId) { + fetchConceptStocks(conceptId); + } + // 通知父组件自动展开已完成 + onAutoExpandComplete?.(); + } + }, [autoExpandAlertKey, expandedId, fetchConceptStocks, onAutoExpandComplete]); // 切换展开状态 const handleToggle = useCallback((alert) => { @@ -718,7 +737,7 @@ const ConceptAlertList = ({ alerts = [], onAlertClick, selectedAlert, maxHeight } onAlertClick?.(alert); - }, [expandedId, fetchConceptStocks, onAlertClick]); + }, [expandedId, fetchConceptStocks, onAlertClick, conceptStocks, loadingConcepts]); if (!alerts || alerts.length === 0) { return ( diff --git a/src/views/StockOverview/components/HotspotOverview/components/IndexMinuteChart.js b/src/views/StockOverview/components/HotspotOverview/components/IndexMinuteChart.js index a7c360a5..c78e0e86 100644 --- a/src/views/StockOverview/components/HotspotOverview/components/IndexMinuteChart.js +++ b/src/views/StockOverview/components/HotspotOverview/components/IndexMinuteChart.js @@ -90,21 +90,28 @@ const IndexMinuteChart = ({ indexData, alerts = [], onAlertClick, height = '350p // 检查是否有异动 const alertsAtTime = alerts.filter((a) => a.time === time); if (alertsAtTime.length > 0) { - html += '
'; - html += '
概念异动:
'; - alertsAtTime.forEach((alert) => { + html += '
'; + html += `
📍 概念异动 (${alertsAtTime.length})
`; + alertsAtTime.slice(0, 5).forEach((alert) => { const typeLabel = { - surge: '急涨', - surge_up: '暴涨', - surge_down: '暴跌', - limit_up: '涨停增加', + surge: '异动', + surge_up: '急涨', + surge_down: '急跌', + volume_surge_up: '放量急涨', + shrink_surge_up: '缩量急涨', + volume_oscillation: '放量震荡', + limit_up: '涨停潮', rank_jump: '排名跃升', volume_spike: '放量', }[alert.alert_type] || alert.alert_type; - const typeColor = alert.alert_type === 'surge_down' ? '#2ed573' : '#ff6b6b'; - const alpha = alert.alpha ? ` (α${alert.alpha > 0 ? '+' : ''}${alert.alpha.toFixed(2)}%)` : ''; - html += `
• ${alert.concept_name} (${typeLabel}${alpha})
`; + const typeColor = alert.alert_type === 'surge_down' ? '#52c41a' : '#ff4d4f'; + const alpha = alert.alpha ? ` α${alert.alpha > 0 ? '+' : ''}${alert.alpha.toFixed(1)}%` : ''; + const score = alert.final_score ? ` [${Math.round(alert.final_score)}分]` : ''; + html += `
• ${alert.concept_name} (${typeLabel}${alpha}${score})
`; }); + if (alertsAtTime.length > 5) { + html += `
还有 ${alertsAtTime.length - 5} 个异动...
`; + } html += '
'; } @@ -216,12 +223,19 @@ const IndexMinuteChart = ({ indexData, alerts = [], onAlertClick, height = '350p chartInstance.current.setOption(chartOption, true); - // 点击事件 + // 点击事件 - 支持多个异动 if (onAlertClick) { chartInstance.current.off('click'); chartInstance.current.on('click', 'series.line.markPoint', (params) => { if (params.data && params.data.alertData) { - onAlertClick(params.data.alertData); + const alertData = params.data.alertData; + // 如果是数组(多个异动),传递第一个(最高分) + // 调用方可以从 alertData 中获取所有异动 + if (Array.isArray(alertData)) { + onAlertClick(alertData[0]); + } else { + onAlertClick(alertData); + } } }); } diff --git a/src/views/StockOverview/components/HotspotOverview/index.js b/src/views/StockOverview/components/HotspotOverview/index.js index 327e87a2..cf20bc6f 100644 --- a/src/views/StockOverview/components/HotspotOverview/index.js +++ b/src/views/StockOverview/components/HotspotOverview/index.js @@ -1,5 +1,5 @@ /** - * 热点概览组件 - 科技感设计 + * 热点概览组件 - Modern Spatial & Glassmorphism 设计 * 展示大盘分时走势 + 概念异动标注 * * 布局设计: @@ -20,12 +20,11 @@ import { Flex, Spacer, Tooltip, - useColorModeValue, IconButton, Collapse, SimpleGrid, } from '@chakra-ui/react'; -import { keyframes } from '@emotion/react'; +import { keyframes, css } from '@emotion/react'; import { Flame, List, @@ -37,11 +36,19 @@ import { AlertCircle, TrendingUp, TrendingDown, + Sparkles, } from 'lucide-react'; import { useHotspotData } from './hooks'; import { IndexMinuteChart, ConceptAlertList, AlertSummary } from './components'; import { ALERT_TYPE_CONFIG, getAlertTypeLabel } from './utils/chartHelpers'; +import { + glassEffect, + colors, + glowEffects, + getMarketColor, + getMarketGlow, +} from '../../theme/glassTheme'; // 动画效果 const gradientShift = keyframes` @@ -51,68 +58,94 @@ const gradientShift = keyframes` `; const pulseGlow = keyframes` - 0%, 100% { opacity: 0.5; } - 50% { opacity: 1; } + 0%, 100% { opacity: 0.6; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.02); } +`; + +const floatAnimation = keyframes` + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-3px); } +`; + +const shimmer = keyframes` + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } `; /** - * 紧凑型异动卡片(用于横向滚动) + * 紧凑型异动卡片(用于横向滚动)- Glassmorphism 风格 */ const CompactAlertCard = ({ alert, onClick, isSelected }) => { - const cardBg = useColorModeValue('white', '#0d0d0d'); - const borderColor = useColorModeValue('gray.200', '#2d2d2d'); - const textColor = useColorModeValue('gray.800', 'white'); - const subTextColor = useColorModeValue('gray.500', 'gray.400'); - const config = ALERT_TYPE_CONFIG[alert.alert_type] || ALERT_TYPE_CONFIG.surge; const isUp = alert.alert_type !== 'surge_down'; return ( onClick?.(alert)} - transition="all 0.2s" - _hover={{ - borderColor: config.color, - transform: 'translateY(-2px)', - boxShadow: `0 4px 15px ${config.color}25`, - }} + transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" position="relative" overflow="hidden" + _hover={{ + bg: 'rgba(255, 255, 255, 0.05)', + border: `1px solid ${config.color}50`, + transform: 'translateY(-4px)', + boxShadow: `0 8px 25px ${config.color}20, inset 0 1px 0 rgba(255,255,255,0.1)`, + }} + css={isSelected ? css`animation: ${floatAnimation} 3s ease-in-out infinite;` : undefined} > - {/* 顶部渐变条 */} + {/* 顶部渐变发光条 */} + {/* 背景光晕 */} + {isSelected && ( + + )} + {/* 时间 + 类型 */} - - + + {alert.time} {getAlertTypeLabel(alert.alert_type)} @@ -124,9 +157,10 @@ const CompactAlertCard = ({ alert, onClick, isSelected }) => { {alert.concept_name} @@ -134,15 +168,20 @@ const CompactAlertCard = ({ alert, onClick, isSelected }) => { {/* 分数 + Alpha */} - 评分 - + 评分 + {Math.round(alert.final_score || 0)} {alert.alpha != null && ( = 0 ? '#ff4d4f' : '#52c41a'} + color={getMarketColor(alert.alpha)} + css={css`text-shadow: 0 0 10px ${getMarketColor(alert.alpha)}50;`} > α {alert.alpha >= 0 ? '+' : ''}{alert.alpha.toFixed(1)}% @@ -160,53 +199,76 @@ const CompactAlertCard = ({ alert, onClick, isSelected }) => { const HotspotOverview = ({ selectedDate }) => { const [selectedAlert, setSelectedAlert] = useState(null); const [showDetailList, setShowDetailList] = useState(false); + const [autoExpandAlertKey, setAutoExpandAlertKey] = useState(null); // 获取数据 const { loading, error, data } = useHotspotData(selectedDate); - // 颜色主题 - const cardBg = useColorModeValue('white', '#0a0a0a'); - const borderColor = useColorModeValue('gray.200', '#1f1f1f'); - const textColor = useColorModeValue('gray.800', 'white'); - const subTextColor = useColorModeValue('gray.600', 'gray.400'); - const sectionBg = useColorModeValue('gray.50', '#0d0d0d'); - const scrollbarColor = useColorModeValue('#ddd', '#333'); + // Glassmorphism 颜色主题 + const cardBg = glassEffect.card.bg; + const borderColor = colors.border.primary; + const textColor = colors.text.primary; + const subTextColor = colors.text.secondary; + const sectionBg = glassEffect.light.bg; + const scrollbarColor = 'rgba(139, 92, 246, 0.3)'; - // 点击异动标注 + // 点击异动标注 - 自动展开详细列表并选中 const handleAlertClick = useCallback((alert) => { setSelectedAlert(alert); + // 自动展开详细列表并设置需要展开的项 + setShowDetailList(true); + const alertKey = `${alert.concept_id}-${alert.time}`; + setAutoExpandAlertKey(alertKey); }, []); - // 渲染加载状态 + // 渲染加载状态 - Glassmorphism 风格 if (loading) { return ( + {/* 极光背景 */} + {/* 顶部发光条 */} +
- + - + - - 加载热点概览数据 + + + 加载热点概览数据 + 正在获取市场异动信息... @@ -215,24 +277,42 @@ const HotspotOverview = ({ selectedDate }) => { ); } - // 渲染错误状态 + // 渲染错误状态 - Glassmorphism 风格 if (error) { return ( - +
- - + + - 数据加载失败 + + 数据加载失败 + {error} @@ -245,124 +325,258 @@ const HotspotOverview = ({ selectedDate }) => { const { index, alerts, alert_summary } = data; + // 计算市场颜色 + const marketColor = getMarketColor(index?.change_pct || 0); + const marketGlow = getMarketGlow(index?.change_pct || 0); + return ( - {/* 顶部装饰条 */} + {/* 极光背景装饰 */} + - - {/* 头部 */} - - + {/* 顶部发光装饰条 */} + + + + {/* 头部 - Glassmorphism */} + + - + {/* 图标发光效果 */} + + - 热点概览 - 实时概念异动监控 + + 热点概览 + + + + 实时概念异动监控 + - + {alerts.length > 0 && ( - - - {alerts.length} + + + + {alerts.length} + )} - + - {/* 统计摘要 - 简化版 */} - - {/* 指数信息 */} - + {/* 统计摘要 - Glassmorphism Bento Grid */} + + {/* 指数信息卡片 */} + + {/* 背景光晕 */} + - - {index?.name || '上证指数'} + + + {index?.name || '上证指数'} + = 0 ? '#ff4d4f' : '#52c41a'} + color={marketColor} + css={css`text-shadow: 0 0 30px ${marketColor}60;`} > {index?.latest_price?.toFixed(2) || '-'} - + = 0 ? 'red.50' : 'green.50'} + bg={`${marketColor}15`} + border={`1px solid ${marketColor}25`} > = 0 ? TrendingUp : TrendingDown} - boxSize={3} - color={(index?.change_pct || 0) >= 0 ? '#ff4d4f' : '#52c41a'} + boxSize={4} + color={marketColor} + css={css`filter: drop-shadow(0 0 4px ${marketColor});`} /> = 0 ? '#ff4d4f' : '#52c41a'} + color={marketColor} + css={css`text-shadow: 0 0 10px ${marketColor}50;`} > {(index?.change_pct || 0) >= 0 ? '+' : ''}{(index?.change_pct || 0).toFixed(2)}% - - {index?.high?.toFixed(2)} - {index?.low?.toFixed(2)} + + + + {index?.high?.toFixed(2)} + + + + {index?.low?.toFixed(2)} + - {/* 异动统计 */} - - - 今日异动 - {alerts.length} 次 + {/* 异动统计卡片 */} + + + 今日异动 + + {alerts.length} 次 + {Object.entries(alert_summary || {}) .filter(([_, count]) => count > 0) - .slice(0, 4) + .slice(0, 5) .map(([type, count]) => { const config = ALERT_TYPE_CONFIG[type]; if (!config) return null; return ( {config.label} - {count} + + {count} + ); })} @@ -370,22 +584,54 @@ const HotspotOverview = ({ selectedDate }) => { - {/* 大尺寸分时图 */} + {/* 大尺寸分时图 - Glassmorphism */} - - - + {/* 图表区域背景光晕 */} + + + + - 大盘分时走势 + + 大盘分时走势 + - + { /> - {/* 异动列表 - 横向滚动 */} + {/* 异动列表 - Glassmorphism 横向滚动 */} {alerts.length > 0 && ( - - - - + + + + 异动记录 - (横向滚动查看更多) + (点击卡片查看个股详情) } size="sm" variant="ghost" - borderRadius="lg" + borderRadius="12px" + color={colors.text.secondary} + _hover={{ + bg: 'rgba(255,255,255,0.05)', + color: textColor, + }} onClick={() => setShowDetailList(!showDetailList)} aria-label="切换详细列表" /> @@ -422,13 +683,14 @@ const HotspotOverview = ({ selectedDate }) => { {/* 横向滚动卡片 */} @@ -446,33 +708,84 @@ const HotspotOverview = ({ selectedDate }) => { - {/* 详细列表(可展开) */} + {/* 详细列表(可展开) - Glassmorphism */} + {/* 背景光晕 */} + setAutoExpandAlertKey(null)} /> )} - {/* 无异动提示 */} + {/* 无异动提示 - Glassmorphism */} {alerts.length === 0 && ( -
- - - 当日暂无概念异动数据 +
+ {/* 背景光晕 */} + + + + + + 当日暂无概念异动数据
)} diff --git a/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js b/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js index cd28aeff..b9539054 100644 --- a/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js +++ b/src/views/StockOverview/components/HotspotOverview/utils/chartHelpers.js @@ -281,53 +281,96 @@ export const getAlertTypeColor = (alertType) => { }; /** - * 生成图表标注点数据 + * 生成图表标注点数据 - 支持同一时间多个异动折叠显示 * @param {Array} alerts - 异动数据数组 * @param {Array} times - 时间数组 * @param {Array} prices - 价格数组 * @param {number} priceMax - 最高价格(用于无法匹配时间时的默认位置) - * @param {number} maxCount - 最大显示数量 + * @param {number} maxTimePoints - 最大显示的时间点数量 * @returns {Array} ECharts markPoint data */ -export const getAlertMarkPoints = (alerts, times, prices, priceMax, maxCount = 15) => { +export const getAlertMarkPoints = (alerts, times, prices, priceMax, maxTimePoints = 20) => { if (!alerts || alerts.length === 0) return []; - // 按重要性排序,限制显示数量 - const sortedAlerts = [...alerts] - .sort((a, b) => (b.final_score || b.importance_score || 0) - (a.final_score || a.importance_score || 0)) - .slice(0, maxCount); + // 1. 按时间分组异动 + const alertsByTime = {}; + alerts.forEach(alert => { + const time = alert.time; + if (!alertsByTime[time]) { + alertsByTime[time] = []; + } + alertsByTime[time].push(alert); + }); - return sortedAlerts.map((alert) => { - // 找到对应时间的价格 - const timeIndex = times.indexOf(alert.time); - const price = timeIndex >= 0 ? prices[timeIndex] : (alert.index_price || priceMax); + // 2. 对每个时间点的异动按重要性排序 + Object.keys(alertsByTime).forEach(time => { + alertsByTime[time].sort((a, b) => + (b.final_score || b.importance_score || 0) - (a.final_score || a.importance_score || 0) + ); + }); + // 3. 按时间点的最高分排序,限制数量 + const sortedTimePoints = Object.entries(alertsByTime) + .map(([time, timeAlerts]) => ({ + time, + alerts: timeAlerts, + maxScore: Math.max(...timeAlerts.map(a => a.final_score || a.importance_score || 0)), + })) + .sort((a, b) => b.maxScore - a.maxScore) + .slice(0, maxTimePoints); + + // 4. 生成标记点数据 + return sortedTimePoints.map(({ time, alerts: timeAlerts }) => { + const timeIndex = times.indexOf(time); + const price = timeIndex >= 0 ? prices[timeIndex] : priceMax; + + const alertCount = timeAlerts.length; + const topAlert = timeAlerts[0]; // 最高分的异动 + const hasMultiple = alertCount > 1; + + // 使用最高分异动的样式 const { color, gradient, symbol, symbolSize } = getAlertStyle( - alert.alert_type, - alert.final_score / 100 || alert.importance_score || 0.5 + topAlert.alert_type, + topAlert.final_score / 100 || topAlert.importance_score || 0.5 ); - // 格式化标签 - let label = alert.concept_name || ''; - if (label.length > 6) { - label = label.substring(0, 5) + '...'; + // 生成标签 + let label; + if (hasMultiple) { + // 多个异动时显示数量和最高分概念 + const topName = topAlert.concept_name || ''; + const shortName = topName.length > 4 ? topName.substring(0, 3) + '..' : topName; + label = `${shortName} +${alertCount - 1}`; + } else { + // 单个异动显示概念名称 + label = topAlert.concept_name || ''; + if (label.length > 6) { + label = label.substring(0, 5) + '..'; + } } - // 添加涨停数量(如果有) - if (alert.limit_up_count > 0) { - label += `\n涨停: ${alert.limit_up_count}`; - } + const isDown = topAlert.alert_type === 'surge_down'; - const isDown = alert.alert_type === 'surge_down'; + // 多个异动时使用更醒目的样式 + const finalSymbolSize = hasMultiple ? symbolSize + 8 : symbolSize; + const borderWidth = hasMultiple ? 3 : 2; return { - name: alert.concept_name, - coord: [alert.time, price], + name: hasMultiple ? `${time} (${alertCount}个异动)` : topAlert.concept_name, + coord: [time, price], value: label, - symbol, - symbolSize, + symbol: hasMultiple ? 'pin' : symbol, // 多个异动用 pin 图标 + symbolSize: finalSymbolSize, itemStyle: { - color: { + color: hasMultiple ? { + type: 'radial', + x: 0.5, y: 0.5, r: 0.8, + colorStops: [ + { offset: 0, color: gradient[0] }, + { offset: 0.7, color: gradient[1] }, + { offset: 1, color: `${color}88` }, + ], + } : { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ @@ -335,27 +378,37 @@ export const getAlertMarkPoints = (alerts, times, prices, priceMax, maxCount = 1 { offset: 1, color: gradient[1] }, ], }, - borderColor: 'rgba(255,255,255,0.8)', - borderWidth: 2, - shadowBlur: 8, - shadowColor: `${color}66`, + borderColor: hasMultiple ? '#ffffff' : 'rgba(255,255,255,0.8)', + borderWidth, + shadowBlur: hasMultiple ? 15 : 8, + shadowColor: `${color}${hasMultiple ? '99' : '66'}`, }, label: { show: true, position: isDown ? 'bottom' : 'top', - formatter: '{b}', - fontSize: 10, - fontWeight: 500, + formatter: hasMultiple ? `{b|${label}}` : '{b}', + fontSize: hasMultiple ? 11 : 10, + fontWeight: hasMultiple ? 700 : 500, color: isDown ? '#52c41a' : '#ff4d4f', - backgroundColor: 'rgba(255,255,255,0.95)', - padding: [3, 6], - borderRadius: 4, + backgroundColor: hasMultiple ? 'rgba(255,255,255,0.98)' : 'rgba(255,255,255,0.95)', + padding: hasMultiple ? [4, 8] : [3, 6], + borderRadius: hasMultiple ? 6 : 4, borderColor: color, - borderWidth: 1, - shadowBlur: 4, - shadowColor: 'rgba(0,0,0,0.1)', + borderWidth: hasMultiple ? 2 : 1, + shadowBlur: hasMultiple ? 8 : 4, + shadowColor: hasMultiple ? `${color}40` : 'rgba(0,0,0,0.1)', + rich: hasMultiple ? { + b: { + fontSize: 11, + fontWeight: 700, + color: isDown ? '#52c41a' : '#ff4d4f', + }, + } : undefined, }, - alertData: alert, // 存储原始数据 + // 存储所有该时间点的异动数据 + alertData: hasMultiple ? timeAlerts : topAlert, + alertCount, + time, }; }); }; diff --git a/src/views/StockOverview/theme/glassTheme.js b/src/views/StockOverview/theme/glassTheme.js new file mode 100644 index 00000000..d761bcac --- /dev/null +++ b/src/views/StockOverview/theme/glassTheme.js @@ -0,0 +1,314 @@ +/** + * Glassmorphism 主题系统 - Modern Spatial Design + * + * 设计理念:打造漂浮在深空中的半透明玻璃态数据终端 + * 强调光影深度、弥散背景光和极致圆角 + */ + +// 极光背景渐变配置 +export const auroraGradients = { + // 主背景 - 深空极光 + primary: ` + radial-gradient(ellipse at 20% 0%, rgba(120, 119, 198, 0.15) 0%, transparent 50%), + radial-gradient(ellipse at 80% 0%, rgba(74, 144, 226, 0.12) 0%, transparent 50%), + radial-gradient(ellipse at 40% 100%, rgba(131, 58, 180, 0.1) 0%, transparent 50%), + radial-gradient(ellipse at 60% 50%, rgba(29, 78, 216, 0.08) 0%, transparent 50%), + linear-gradient(180deg, #0a0a0f 0%, #0d0d14 50%, #0a0a0f 100%) + `, + // 轻量版 - 用于嵌套组件 + light: ` + radial-gradient(ellipse at 50% 0%, rgba(120, 119, 198, 0.08) 0%, transparent 70%), + linear-gradient(180deg, rgba(15, 15, 25, 0.95) 0%, rgba(10, 10, 15, 0.98) 100%) + `, + // 高亮版 - 用于活跃状态 + highlight: ` + radial-gradient(ellipse at 50% 50%, rgba(139, 92, 246, 0.2) 0%, transparent 60%), + radial-gradient(ellipse at 30% 80%, rgba(59, 130, 246, 0.15) 0%, transparent 50%) + `, +}; + +// 毛玻璃效果配置 +export const glassEffect = { + // 标准玻璃卡片 + card: { + bg: 'rgba(255, 255, 255, 0.03)', + backdropFilter: 'blur(20px) saturate(180%)', + border: '1px solid rgba(255, 255, 255, 0.08)', + borderRadius: '24px', + boxShadow: ` + 0 8px 32px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.05) + `, + }, + // 轻量玻璃 - 用于内部元素 + light: { + bg: 'rgba(255, 255, 255, 0.02)', + backdropFilter: 'blur(12px)', + border: '1px solid rgba(255, 255, 255, 0.05)', + borderRadius: '16px', + }, + // 强调玻璃 - 用于重要元素 + accent: { + bg: 'rgba(139, 92, 246, 0.1)', + backdropFilter: 'blur(16px) saturate(200%)', + border: '1px solid rgba(139, 92, 246, 0.2)', + borderRadius: '20px', + boxShadow: '0 4px 24px rgba(139, 92, 246, 0.15)', + }, + // 悬浮玻璃 - 用于弹出层 + floating: { + bg: 'rgba(20, 20, 30, 0.9)', + backdropFilter: 'blur(24px) saturate(200%)', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '20px', + boxShadow: ` + 0 25px 50px -12px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(255, 255, 255, 0.05) + `, + }, +}; + +// 发光效果配置 +export const glowEffects = { + // 文字发光 + textGlow: (color = '#8b5cf6') => ({ + textShadow: `0 0 20px ${color}60, 0 0 40px ${color}30`, + }), + // 边框发光 + borderGlow: (color = '#8b5cf6') => ({ + boxShadow: `0 0 20px ${color}30, inset 0 0 20px ${color}10`, + }), + // 图标发光 + iconGlow: (color = '#8b5cf6') => ({ + filter: `drop-shadow(0 0 8px ${color}80)`, + }), + // 数据发光 - 用于重要数值 + dataGlow: { + up: { textShadow: '0 0 20px rgba(239, 68, 68, 0.5)' }, + down: { textShadow: '0 0 20px rgba(34, 197, 94, 0.5)' }, + neutral: { textShadow: '0 0 15px rgba(148, 163, 184, 0.3)' }, + }, +}; + +// 颜色系统 +export const colors = { + // 主要颜色 + primary: { + 50: 'rgba(139, 92, 246, 0.1)', + 100: 'rgba(139, 92, 246, 0.2)', + 200: 'rgba(139, 92, 246, 0.3)', + 300: '#a78bfa', + 400: '#8b5cf6', + 500: '#7c3aed', + }, + // 背景色 + background: { + primary: '#0a0a0f', + secondary: '#0d0d14', + tertiary: '#12121a', + card: 'rgba(255, 255, 255, 0.03)', + hover: 'rgba(255, 255, 255, 0.06)', + }, + // 文字颜色 + text: { + primary: 'rgba(255, 255, 255, 0.95)', + secondary: 'rgba(255, 255, 255, 0.7)', + tertiary: 'rgba(255, 255, 255, 0.5)', + muted: 'rgba(255, 255, 255, 0.35)', + }, + // 边框颜色 + border: { + primary: 'rgba(255, 255, 255, 0.08)', + secondary: 'rgba(255, 255, 255, 0.05)', + accent: 'rgba(139, 92, 246, 0.3)', + }, + // 涨跌颜色 + market: { + up: '#ef4444', + upGlow: 'rgba(239, 68, 68, 0.5)', + upBg: 'rgba(239, 68, 68, 0.1)', + down: '#22c55e', + downGlow: 'rgba(34, 197, 94, 0.5)', + downBg: 'rgba(34, 197, 94, 0.1)', + neutral: '#94a3b8', + }, + // 强调色 + accent: { + purple: '#8b5cf6', + blue: '#3b82f6', + cyan: '#06b6d4', + pink: '#ec4899', + orange: '#f97316', + }, +}; + +// 动画配置 +export const animations = { + // 极光动画 CSS + auroraKeyframes: ` + @keyframes aurora { + 0%, 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + } + `, + // 呼吸发光 + breatheKeyframes: ` + @keyframes breathe { + 0%, 100% { + opacity: 0.5; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.02); + } + } + `, + // 浮动 + floatKeyframes: ` + @keyframes float { + 0%, 100% { + transform: translateY(0px); + } + 50% { + transform: translateY(-5px); + } + } + `, +}; + +// 交互效果 +export const interactions = { + // 卡片悬浮效果 + cardHover: { + transform: 'translateY(-4px)', + boxShadow: ` + 0 20px 40px rgba(0, 0, 0, 0.4), + 0 0 30px rgba(139, 92, 246, 0.1), + inset 0 1px 0 rgba(255, 255, 255, 0.1) + `, + borderColor: 'rgba(139, 92, 246, 0.3)', + }, + // 按钮悬浮 + buttonHover: { + transform: 'translateY(-2px)', + boxShadow: '0 10px 20px rgba(139, 92, 246, 0.2)', + }, + // 列表项悬浮 + listItemHover: { + bg: 'rgba(255, 255, 255, 0.05)', + borderColor: 'rgba(255, 255, 255, 0.1)', + }, +}; + +// 预设样式组合 - 可直接用于 Chakra sx prop +export const presets = { + // 页面容器 + pageContainer: { + minH: '100vh', + bg: colors.background.primary, + backgroundImage: auroraGradients.primary, + backgroundAttachment: 'fixed', + color: colors.text.primary, + }, + // 主卡片 + glassCard: { + ...glassEffect.card, + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + _hover: interactions.cardHover, + }, + // 内容区块 + contentSection: { + ...glassEffect.light, + p: 5, + }, + // 标题样式 + sectionTitle: { + fontSize: 'lg', + fontWeight: 'bold', + color: colors.text.primary, + ...glowEffects.textGlow(colors.accent.purple), + letterSpacing: '0.5px', + }, + // 数据标签 + dataLabel: { + fontSize: 'xs', + color: colors.text.tertiary, + textTransform: 'uppercase', + letterSpacing: '1px', + }, + // 上涨数值 + valueUp: { + color: colors.market.up, + fontWeight: 'bold', + ...glowEffects.dataGlow.up, + }, + // 下跌数值 + valueDown: { + color: colors.market.down, + fontWeight: 'bold', + ...glowEffects.dataGlow.down, + }, +}; + +// 指标提示样式 +export const tooltipStyles = { + container: { + bg: 'rgba(15, 15, 25, 0.95)', + backdropFilter: 'blur(16px)', + border: '1px solid rgba(255, 255, 255, 0.1)', + borderRadius: '12px', + boxShadow: '0 20px 40px rgba(0, 0, 0, 0.5)', + p: 3, + }, + title: { + color: colors.accent.purple, + fontWeight: 'bold', + fontSize: 'sm', + mb: 1, + }, + description: { + color: colors.text.secondary, + fontSize: 'xs', + lineHeight: 1.5, + }, +}; + +// 工具函数:获取涨跌颜色 +export const getMarketColor = (value) => { + if (value > 0) return colors.market.up; + if (value < 0) return colors.market.down; + return colors.market.neutral; +}; + +// 工具函数:获取涨跌背景 +export const getMarketBg = (value) => { + if (value > 0) return colors.market.upBg; + if (value < 0) return colors.market.downBg; + return 'rgba(148, 163, 184, 0.1)'; +}; + +// 工具函数:获取涨跌发光 +export const getMarketGlow = (value) => { + if (value > 0) return glowEffects.dataGlow.up; + if (value < 0) return glowEffects.dataGlow.down; + return glowEffects.dataGlow.neutral; +}; + +export default { + auroraGradients, + glassEffect, + glowEffects, + colors, + animations, + interactions, + presets, + tooltipStyles, + getMarketColor, + getMarketBg, + getMarketGlow, +};