Compare commits
3 Commits
c7a881c965
...
84f70f3329
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84f70f3329 | ||
|
|
601b06d79e | ||
|
|
0818a7bff7 |
@@ -2,7 +2,7 @@
|
||||
// Navbar 右侧功能区组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { HStack, Spinner, IconButton, Box } from '@chakra-ui/react';
|
||||
import { HStack, IconButton, Box } from '@chakra-ui/react';
|
||||
import { HamburgerIcon } from '@chakra-ui/icons';
|
||||
// import ThemeToggleButton from '../ThemeToggleButton'; // ❌ 已删除 - 不再支持深色模式切换
|
||||
import LoginButton from '../LoginButton';
|
||||
@@ -41,9 +41,15 @@ const NavbarActions = memo(({
|
||||
}) => {
|
||||
return (
|
||||
<HStack spacing={{ base: 2, md: 4 }}>
|
||||
{/* 显示加载状态 */}
|
||||
{/* 权限校验中 - 显示占位骨架,不显示登录按钮或用户菜单 */}
|
||||
{isLoading ? (
|
||||
<Spinner size="sm" color="blue.500" />
|
||||
<Box
|
||||
w={{ base: '80px', md: '120px' }}
|
||||
h="36px"
|
||||
borderRadius="md"
|
||||
bg="whiteAlpha.100"
|
||||
opacity={0.6}
|
||||
/>
|
||||
) : isAuthenticated && user ? (
|
||||
// 已登录状态 - 用户菜单 + 功能菜单排列
|
||||
<HStack spacing={{ base: 2, md: 3 }}>
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import * as echarts from 'echarts';
|
||||
import { stockService } from '@services/eventService';
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
/**
|
||||
* 股票信息
|
||||
@@ -72,11 +71,6 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('KLineChartModal', 'loadData', '开始加载K线数据', {
|
||||
stockCode: stock.stock_code,
|
||||
eventTime,
|
||||
});
|
||||
|
||||
const response = await stockService.getKlineData(
|
||||
stock.stock_code,
|
||||
'daily',
|
||||
@@ -91,12 +85,8 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
||||
|
||||
console.log('[KLineChartModal] 数据条数:', response.data.length);
|
||||
setData(response.data);
|
||||
logger.info('KLineChartModal', 'loadData', 'K线数据加载成功', {
|
||||
dataCount: response.data.length,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '数据加载失败';
|
||||
logger.error('KLineChartModal', 'loadData', err as Error);
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -33,9 +33,6 @@ import {
|
||||
// 工具函数
|
||||
import { createSubIndicators } from './utils';
|
||||
|
||||
// 日志
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
// ==================== 组件 Props ====================
|
||||
|
||||
export interface StockChartKLineModalProps {
|
||||
@@ -110,10 +107,6 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
||||
const handleChartTypeChange = useCallback((e: RadioChangeEvent) => {
|
||||
const newType = e.target.value as ChartType;
|
||||
setChartType(newType);
|
||||
|
||||
logger.debug('StockChartKLineModal', 'handleChartTypeChange', '切换图表类型', {
|
||||
newType,
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@@ -130,10 +123,6 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
||||
// 先移除所有副图指标(KLineChart 会自动移除)
|
||||
// 然后创建新的指标
|
||||
createSubIndicators(chart, values);
|
||||
|
||||
logger.debug('StockChartKLineModal', 'handleIndicatorChange', '切换副图指标', {
|
||||
indicators: values,
|
||||
});
|
||||
},
|
||||
[chart]
|
||||
);
|
||||
@@ -143,7 +132,6 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
||||
*/
|
||||
const handleRefresh = useCallback(() => {
|
||||
loadData();
|
||||
logger.debug('StockChartKLineModal', 'handleRefresh', '刷新数据');
|
||||
}, [loadData]);
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import * as echarts from 'echarts';
|
||||
import { stockService } from '@services/eventService';
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
/**
|
||||
* 股票信息
|
||||
@@ -76,11 +75,6 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('TimelineChartModal', 'loadData', '开始加载分时图数据', {
|
||||
stockCode: stock.stock_code,
|
||||
eventTime,
|
||||
});
|
||||
|
||||
const response = await stockService.getKlineData(
|
||||
stock.stock_code,
|
||||
'timeline',
|
||||
@@ -95,12 +89,8 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
||||
|
||||
console.log('[TimelineChartModal] 数据条数:', response.data.length);
|
||||
setData(response.data);
|
||||
logger.info('TimelineChartModal', 'loadData', '分时图数据加载成功', {
|
||||
dataCount: response.data.length,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '数据加载失败';
|
||||
logger.error('TimelineChartModal', 'loadData', err as Error);
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
createEventHighlightOverlay,
|
||||
removeAllEventMarkers,
|
||||
} from '../utils/eventMarkerUtils';
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
export interface UseEventMarkerOptions {
|
||||
/** KLineChart 实例 */
|
||||
@@ -77,10 +76,6 @@ export const useEventMarker = (
|
||||
const createMarker = useCallback(
|
||||
(time: string, label: string, color?: string) => {
|
||||
if (!chart || !data || data.length === 0) {
|
||||
logger.warn('useEventMarker', 'createMarker', '图表或数据未准备好', {
|
||||
hasChart: !!chart,
|
||||
dataLength: data?.length || 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,9 +88,6 @@ export const useEventMarker = (
|
||||
const overlay = createEventMarkerOverlay(eventMarker, data);
|
||||
|
||||
if (!overlay) {
|
||||
logger.warn('useEventMarker', 'createMarker', 'Overlay 创建失败', {
|
||||
eventMarker,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,9 +95,6 @@ export const useEventMarker = (
|
||||
const id = chart.createOverlay(overlay);
|
||||
|
||||
if (!id || (Array.isArray(id) && id.length === 0)) {
|
||||
logger.warn('useEventMarker', 'createMarker', '标记添加失败', {
|
||||
overlay,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,23 +107,9 @@ export const useEventMarker = (
|
||||
const highlightResult = chart.createOverlay(highlightOverlay);
|
||||
const actualHighlightId = Array.isArray(highlightResult) ? highlightResult[0] : highlightResult;
|
||||
setHighlightId(actualHighlightId as string);
|
||||
|
||||
logger.info('useEventMarker', 'createMarker', '事件高亮背景创建成功', {
|
||||
highlightId: actualHighlightId,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('useEventMarker', 'createMarker', '事件标记创建成功', {
|
||||
markerId: actualId,
|
||||
label,
|
||||
time,
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useEventMarker', 'createMarker', err as Error, {
|
||||
time,
|
||||
label,
|
||||
});
|
||||
// 忽略创建标记时的错误
|
||||
}
|
||||
},
|
||||
[chart, data]
|
||||
@@ -150,26 +125,17 @@ export const useEventMarker = (
|
||||
|
||||
try {
|
||||
if (markerId) {
|
||||
chart.removeOverlay(markerId);
|
||||
chart.removeOverlay({ id: markerId });
|
||||
}
|
||||
if (highlightId) {
|
||||
chart.removeOverlay(highlightId);
|
||||
chart.removeOverlay({ id: highlightId });
|
||||
}
|
||||
|
||||
setMarker(null);
|
||||
setMarkerId(null);
|
||||
setHighlightId(null);
|
||||
|
||||
logger.debug('useEventMarker', 'removeMarker', '移除事件标记和高亮', {
|
||||
markerId,
|
||||
highlightId,
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useEventMarker', 'removeMarker', err as Error, {
|
||||
markerId,
|
||||
highlightId,
|
||||
});
|
||||
// 忽略移除标记时的错误
|
||||
}
|
||||
}, [chart, markerId, highlightId]);
|
||||
|
||||
@@ -186,12 +152,8 @@ export const useEventMarker = (
|
||||
setMarker(null);
|
||||
setMarkerId(null);
|
||||
setHighlightId(null);
|
||||
|
||||
logger.debug('useEventMarker', 'removeAllMarkers', '移除所有事件标记和高亮', {
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useEventMarker', 'removeAllMarkers', err as Error);
|
||||
// 忽略移除所有标记时的错误
|
||||
}
|
||||
}, [chart]);
|
||||
|
||||
@@ -216,10 +178,10 @@ export const useEventMarker = (
|
||||
if (chart) {
|
||||
try {
|
||||
if (markerId) {
|
||||
chart.removeOverlay(markerId);
|
||||
chart.removeOverlay({ id: markerId });
|
||||
}
|
||||
if (highlightId) {
|
||||
chart.removeOverlay(highlightId);
|
||||
chart.removeOverlay({ id: highlightId });
|
||||
}
|
||||
} catch (err) {
|
||||
// 忽略清理时的错误
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { Chart } from 'klinecharts';
|
||||
// import { useColorMode } from '@chakra-ui/react'; // ❌ 已移除深色模式支持
|
||||
import { getTheme, getTimelineTheme } from '../config/klineTheme';
|
||||
import { CHART_INIT_OPTIONS } from '../config';
|
||||
import { logger } from '@utils/logger';
|
||||
import { avgPriceIndicator } from '../indicators/avgPriceIndicator';
|
||||
|
||||
export interface UseKLineChartOptions {
|
||||
@@ -65,11 +64,9 @@ export const useKLineChart = (
|
||||
// 全局注册自定义均价线指标(只执行一次)
|
||||
useEffect(() => {
|
||||
try {
|
||||
registerIndicator(avgPriceIndicator);
|
||||
logger.debug('useKLineChart', '✅ 自定义均价线指标(AVG)注册成功');
|
||||
registerIndicator(avgPriceIndicator as any);
|
||||
} catch (err) {
|
||||
// 如果已注册会报错,忽略即可
|
||||
logger.debug('useKLineChart', 'AVG指标已注册或注册失败', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -78,16 +75,10 @@ export const useKLineChart = (
|
||||
// 图表初始化函数
|
||||
const initChart = (): boolean => {
|
||||
if (!chartRef.current) {
|
||||
logger.warn('useKLineChart', 'init', '图表容器未挂载,将在 50ms 后重试', { containerId });
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug('useKLineChart', 'init', '开始初始化图表', {
|
||||
containerId,
|
||||
height,
|
||||
colorMode,
|
||||
});
|
||||
|
||||
// 初始化图表实例(KLineChart 10.0 API)
|
||||
// ✅ 根据 chartType 选择主题
|
||||
@@ -112,29 +103,16 @@ export const useKLineChart = (
|
||||
|
||||
// ✅ 新增:创建成交量指标窗格
|
||||
try {
|
||||
const volumePaneId = chartInstance.createIndicator('VOL', false, {
|
||||
chartInstance.createIndicator('VOL', false, {
|
||||
height: 100, // 固定高度 100px(约占整体的 20-25%)
|
||||
});
|
||||
|
||||
logger.debug('useKLineChart', 'init', '成交量窗格创建成功', {
|
||||
volumePaneId,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('useKLineChart', 'init', '成交量窗格创建失败', {
|
||||
error: err,
|
||||
});
|
||||
// 不阻塞主流程,继续执行
|
||||
}
|
||||
|
||||
logger.info('useKLineChart', 'init', '✅ 图表初始化成功', {
|
||||
containerId,
|
||||
chartId: chartInstance.id,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error('useKLineChart', 'init', error, { containerId });
|
||||
setError(error);
|
||||
setIsInitialized(false);
|
||||
return false;
|
||||
@@ -146,11 +124,6 @@ export const useKLineChart = (
|
||||
// 成功,直接返回清理函数
|
||||
return () => {
|
||||
if (chartInstanceRef.current) {
|
||||
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
|
||||
containerId,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
});
|
||||
|
||||
dispose(chartInstanceRef.current);
|
||||
chartInstanceRef.current = null;
|
||||
setChartInstance(null); // ✅ 新增:清空 state
|
||||
@@ -161,7 +134,6 @@ export const useKLineChart = (
|
||||
|
||||
// 失败则延迟重试(处理 Modal 动画延迟导致的 DOM 未挂载)
|
||||
const timer = setTimeout(() => {
|
||||
logger.debug('useKLineChart', 'init', '执行延迟重试', { containerId });
|
||||
initChart();
|
||||
}, 50);
|
||||
|
||||
@@ -169,11 +141,6 @@ export const useKLineChart = (
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
if (chartInstanceRef.current) {
|
||||
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
|
||||
containerId,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
});
|
||||
|
||||
dispose(chartInstanceRef.current);
|
||||
chartInstanceRef.current = null;
|
||||
setChartInstance(null); // ✅ 新增:清空 state
|
||||
@@ -195,14 +162,8 @@ export const useKLineChart = (
|
||||
? getTimelineTheme(colorMode)
|
||||
: getTheme(colorMode);
|
||||
chartInstanceRef.current.setStyles(newTheme);
|
||||
|
||||
logger.debug('useKLineChart', 'updateTheme', '更新图表主题', {
|
||||
colorMode,
|
||||
chartType,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('useKLineChart', 'updateTheme', err as Error, { colorMode, chartType });
|
||||
// 忽略主题更新错误
|
||||
}
|
||||
}, [colorMode, chartType, isInitialized]);
|
||||
|
||||
@@ -215,7 +176,6 @@ export const useKLineChart = (
|
||||
const handleResize = () => {
|
||||
if (chartInstanceRef.current) {
|
||||
chartInstanceRef.current.resize();
|
||||
logger.debug('useKLineChart', 'resize', '调整图表大小');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import type { Chart } from 'klinecharts';
|
||||
import type { ChartType, KLineDataPoint, RawDataPoint } from '../types';
|
||||
import { processChartData } from '../utils/dataAdapter';
|
||||
import { logger } from '@utils/logger';
|
||||
import { stockService } from '@services/eventService';
|
||||
import { klineDataCache, getCacheKey } from '@views/Community/components/StockDetailPanel/utils/klineDataCache';
|
||||
|
||||
@@ -78,7 +77,6 @@ export const useKLineData = (
|
||||
*/
|
||||
const loadData = useCallback(async () => {
|
||||
if (!stockCode) {
|
||||
logger.warn('useKLineData', 'loadData', '股票代码为空', { chartType });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,11 +84,6 @@ export const useKLineData = (
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('useKLineData', 'loadData', '开始加载数据', {
|
||||
stockCode,
|
||||
chartType,
|
||||
eventTime,
|
||||
});
|
||||
|
||||
// 1. 先检查缓存
|
||||
const cacheKey = getCacheKey(stockCode, eventTime, chartType);
|
||||
@@ -125,19 +118,8 @@ export const useKLineData = (
|
||||
const processedData = processChartData(rawDataList, chartType, eventTime);
|
||||
|
||||
setData(processedData);
|
||||
|
||||
logger.info('useKLineData', 'loadData', '数据加载成功', {
|
||||
stockCode,
|
||||
chartType,
|
||||
rawCount: rawDataList.length,
|
||||
processedCount: processedData.length,
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error('useKLineData', 'loadData', error, {
|
||||
stockCode,
|
||||
chartType,
|
||||
});
|
||||
setError(error);
|
||||
setData([]);
|
||||
setRawData([]);
|
||||
@@ -207,9 +189,7 @@ export const useKLineData = (
|
||||
(chart as any).setOffsetRightDistance(50);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('useKLineData', 'updateChartData', err as Error, {
|
||||
step: '调整可见范围失败',
|
||||
});
|
||||
// 忽略调整可见范围时的错误
|
||||
}
|
||||
}, 100); // 延迟 100ms 确保数据已加载和渲染
|
||||
|
||||
@@ -259,14 +239,8 @@ export const useKLineData = (
|
||||
}, 200); // 延迟 200ms,确保均价线创建完成后再添加
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
'useKLineData',
|
||||
`updateChartData - ${stockCode} (${chartType}) - ${klineData.length}条数据加载成功`
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error('useKLineData', 'updateChartData', err as Error, {
|
||||
dataCount: klineData.length,
|
||||
});
|
||||
// 忽略更新图表数据时的错误
|
||||
}
|
||||
},
|
||||
[chart, stockCode, chartType]
|
||||
@@ -279,11 +253,6 @@ export const useKLineData = (
|
||||
(newData: KLineDataPoint[]) => {
|
||||
setData(newData);
|
||||
updateChartData(newData);
|
||||
|
||||
logger.debug(
|
||||
'useKLineData',
|
||||
`updateData - ${stockCode} (${chartType}) - ${newData.length}条数据手动更新`
|
||||
);
|
||||
},
|
||||
[updateChartData]
|
||||
);
|
||||
@@ -298,7 +267,6 @@ export const useKLineData = (
|
||||
|
||||
if (chart) {
|
||||
chart.resetData();
|
||||
logger.debug('useKLineData', `clearData - chartId: ${(chart as any).id}`);
|
||||
}
|
||||
}, [chart]);
|
||||
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
* 计算公式:累计成交额 / 累计成交量
|
||||
*/
|
||||
|
||||
import type { Indicator, KLineData } from 'klinecharts';
|
||||
import type { KLineData } from 'klinecharts';
|
||||
|
||||
export const avgPriceIndicator: Indicator = {
|
||||
// 使用部分类型定义,因为 Indicator 类型很复杂
|
||||
export const avgPriceIndicator = {
|
||||
name: 'AVG',
|
||||
shortName: 'AVG',
|
||||
calcParams: [],
|
||||
calcParams: [] as number[],
|
||||
shouldOhlc: false, // 不显示 OHLC 信息
|
||||
shouldFormatBigNumber: false,
|
||||
precision: 2,
|
||||
minValue: null,
|
||||
maxValue: null,
|
||||
minValue: null as number | null,
|
||||
maxValue: null as number | null,
|
||||
|
||||
figures: [
|
||||
{
|
||||
@@ -61,33 +62,27 @@ export const avgPriceIndicator: Indicator = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Tooltip 格式化(显示均价 + 涨跌幅)
|
||||
* 自定义 Tooltip 数据源
|
||||
* 符合 IndicatorTooltipData 接口要求
|
||||
*/
|
||||
createTooltipDataSource: ({ kLineData, indicator, defaultStyles }: any) => {
|
||||
if (!indicator?.avg) {
|
||||
return {
|
||||
title: { text: '均价', color: defaultStyles.tooltip.text.color },
|
||||
value: { text: '--', color: '#FF9800' },
|
||||
};
|
||||
}
|
||||
|
||||
const avgPrice = indicator.avg;
|
||||
const prevClose = kLineData?.prev_close;
|
||||
|
||||
// 计算均价涨跌幅
|
||||
let changeText = `¥${avgPrice.toFixed(2)}`;
|
||||
if (prevClose && prevClose > 0) {
|
||||
const changePercent = ((avgPrice - prevClose) / prevClose * 100).toFixed(2);
|
||||
const changeValue = (avgPrice - prevClose).toFixed(2);
|
||||
changeText = `¥${avgPrice.toFixed(2)} (${changeValue}, ${changePercent}%)`;
|
||||
}
|
||||
const avgValue = kLineData?.avg;
|
||||
const lineColor = defaultStyles?.lines?.[0]?.color || '#FF9800';
|
||||
|
||||
return {
|
||||
title: { text: '均价', color: defaultStyles.tooltip.text.color },
|
||||
value: {
|
||||
text: changeText,
|
||||
color: '#FF9800',
|
||||
},
|
||||
name: 'AVG',
|
||||
calcParamsText: '',
|
||||
features: [] as any[],
|
||||
legends: [
|
||||
{
|
||||
title: { text: '均价: ', color: lineColor },
|
||||
value: {
|
||||
text: avgValue !== undefined ? avgValue.toFixed(2) : '--',
|
||||
color: lineColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* 包含图表初始化、技术指标管理等通用逻辑
|
||||
*/
|
||||
|
||||
import type { Chart } from 'klinecharts';
|
||||
import { logger } from '@utils/logger';
|
||||
import type { Chart, ActionType } from 'klinecharts';
|
||||
|
||||
/**
|
||||
* 安全地执行图表操作(捕获异常)
|
||||
@@ -21,7 +20,6 @@ export const safeChartOperation = <T>(
|
||||
try {
|
||||
return fn();
|
||||
} catch (error) {
|
||||
logger.error('chartUtils', operation, error as Error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -50,13 +48,6 @@ export const createIndicator = (
|
||||
isStack
|
||||
);
|
||||
|
||||
logger.debug('chartUtils', 'createIndicator', '创建技术指标', {
|
||||
indicatorName,
|
||||
params,
|
||||
isStack,
|
||||
indicatorId,
|
||||
});
|
||||
|
||||
return indicatorId;
|
||||
});
|
||||
};
|
||||
@@ -69,8 +60,11 @@ export const createIndicator = (
|
||||
*/
|
||||
export const removeIndicator = (chart: Chart, indicatorId?: string): void => {
|
||||
safeChartOperation('removeIndicator', () => {
|
||||
chart.removeIndicator(indicatorId);
|
||||
logger.debug('chartUtils', 'removeIndicator', '移除技术指标', { indicatorId });
|
||||
if (indicatorId) {
|
||||
chart.removeIndicator({ id: indicatorId });
|
||||
} else {
|
||||
chart.removeIndicator({});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -94,11 +88,6 @@ export const createSubIndicators = (
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'createSubIndicators', '批量创建副图指标', {
|
||||
indicators,
|
||||
createdIds: ids,
|
||||
});
|
||||
|
||||
return ids;
|
||||
};
|
||||
|
||||
@@ -130,10 +119,6 @@ export const setChartZoom = (chart: Chart, zoom: number): void => {
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'setChartZoom', '设置图表缩放', {
|
||||
zoom,
|
||||
newBarSpace,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -147,8 +132,6 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
|
||||
safeChartOperation('scrollToTimestamp', () => {
|
||||
// KLineChart 10.0: 使用 scrollToTimestamp 方法
|
||||
chart.scrollToTimestamp(timestamp);
|
||||
|
||||
logger.debug('chartUtils', 'scrollToTimestamp', '滚动到指定时间', { timestamp });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -160,7 +143,6 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
|
||||
export const resizeChart = (chart: Chart): void => {
|
||||
safeChartOperation('resizeChart', () => {
|
||||
chart.resize();
|
||||
logger.debug('chartUtils', 'resizeChart', '调整图表大小');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -194,7 +176,6 @@ export const getVisibleRange = (chart: Chart): { from: number; to: number } | nu
|
||||
export const clearChartData = (chart: Chart): void => {
|
||||
safeChartOperation('clearChartData', () => {
|
||||
chart.resetData();
|
||||
logger.debug('chartUtils', 'clearChartData', '清空图表数据');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -213,11 +194,6 @@ export const exportChartImage = (
|
||||
// KLineChart 10.0: 使用 getConvertPictureUrl 方法
|
||||
const imageData = chart.getConvertPictureUrl(includeOverlay, 'png', '#ffffff');
|
||||
|
||||
logger.debug('chartUtils', 'exportChartImage', '导出图表图片', {
|
||||
includeOverlay,
|
||||
hasData: !!imageData,
|
||||
});
|
||||
|
||||
return imageData;
|
||||
});
|
||||
};
|
||||
@@ -235,8 +211,6 @@ export const toggleCrosshair = (chart: Chart, show: boolean): void => {
|
||||
show,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'toggleCrosshair', '切换十字光标', { show });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -253,8 +227,6 @@ export const toggleGrid = (chart: Chart, show: boolean): void => {
|
||||
show,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'toggleGrid', '切换网格', { show });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -267,12 +239,11 @@ export const toggleGrid = (chart: Chart, show: boolean): void => {
|
||||
*/
|
||||
export const subscribeChartEvent = (
|
||||
chart: Chart,
|
||||
eventName: string,
|
||||
eventName: ActionType,
|
||||
handler: (...args: any[]) => void
|
||||
): void => {
|
||||
safeChartOperation(`subscribeChartEvent:${eventName}`, () => {
|
||||
chart.subscribeAction(eventName, handler);
|
||||
logger.debug('chartUtils', 'subscribeChartEvent', '订阅图表事件', { eventName });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -285,11 +256,10 @@ export const subscribeChartEvent = (
|
||||
*/
|
||||
export const unsubscribeChartEvent = (
|
||||
chart: Chart,
|
||||
eventName: string,
|
||||
eventName: ActionType,
|
||||
handler: (...args: any[]) => void
|
||||
): void => {
|
||||
safeChartOperation(`unsubscribeChartEvent:${eventName}`, () => {
|
||||
chart.unsubscribeAction(eventName, handler);
|
||||
logger.debug('chartUtils', 'unsubscribeChartEvent', '取消订阅图表事件', { eventName });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import type { KLineDataPoint, RawDataPoint, ChartType } from '../types';
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
/**
|
||||
* 将后端原始数据转换为 KLineChart 标准格式
|
||||
@@ -22,7 +21,6 @@ export const convertToKLineData = (
|
||||
eventTime?: string
|
||||
): KLineDataPoint[] => {
|
||||
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
|
||||
logger.warn('dataAdapter', 'convertToKLineData', '原始数据为空', { chartType });
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -37,15 +35,11 @@ export const convertToKLineData = (
|
||||
low: Number(item.low) || 0,
|
||||
close: Number(item.close) || 0,
|
||||
volume: Number(item.volume) || 0,
|
||||
turnover: item.turnover ? Number(item.turnover) : undefined,
|
||||
turnover: (item as any).turnover ? Number((item as any).turnover) : undefined,
|
||||
prev_close: item.prev_close ? Number(item.prev_close) : undefined, // ✅ 新增:昨收价(用于百分比计算和基准线)
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('dataAdapter', 'convertToKLineData', error as Error, {
|
||||
chartType,
|
||||
dataLength: rawData.length,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -90,7 +84,6 @@ const parseTimestamp = (
|
||||
}
|
||||
|
||||
// 默认返回当前时间(避免图表崩溃)
|
||||
logger.warn('dataAdapter', 'parseTimestamp', '无法解析时间戳,使用当前时间', { item });
|
||||
return Date.now();
|
||||
};
|
||||
|
||||
@@ -109,7 +102,6 @@ const parseTimelineTimestamp = (time: string, eventTime: string): number => {
|
||||
const eventDate = dayjs(eventTime).startOf('day');
|
||||
return eventDate.hour(hours).minute(minutes).second(0).valueOf();
|
||||
} catch (error) {
|
||||
logger.error('dataAdapter', 'parseTimelineTimestamp', error as Error, { time, eventTime });
|
||||
return dayjs(eventTime).valueOf();
|
||||
}
|
||||
};
|
||||
@@ -126,19 +118,16 @@ export const validateAndCleanData = (data: KLineDataPoint[]): KLineDataPoint[] =
|
||||
return data.filter((item) => {
|
||||
// 移除价格为 0 或负数的数据
|
||||
if (item.open <= 0 || item.high <= 0 || item.low <= 0 || item.close <= 0) {
|
||||
logger.warn('dataAdapter', 'validateAndCleanData', '价格异常,已移除', { item });
|
||||
return false;
|
||||
}
|
||||
|
||||
// 移除 high < low 的数据(数据错误)
|
||||
if (item.high < item.low) {
|
||||
logger.warn('dataAdapter', 'validateAndCleanData', '最高价 < 最低价,已移除', { item });
|
||||
return false;
|
||||
}
|
||||
|
||||
// 移除成交量为负数的数据
|
||||
if (item.volume < 0) {
|
||||
logger.warn('dataAdapter', 'validateAndCleanData', '成交量异常,已移除', { item });
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -213,17 +202,8 @@ export const trimDataByEventTime = (
|
||||
return item.timestamp >= startTime && item.timestamp <= endTime;
|
||||
});
|
||||
|
||||
logger.debug('dataAdapter', 'trimDataByEventTime', '数据时间范围裁剪完成', {
|
||||
originalLength: data.length,
|
||||
trimmedLength: trimmedData.length,
|
||||
eventTime,
|
||||
chartType,
|
||||
dateRange: `${dayjs(startTime).format('YYYY-MM-DD')} ~ ${dayjs(endTime).format('YYYY-MM-DD')}`,
|
||||
});
|
||||
|
||||
return trimmedData;
|
||||
} catch (error) {
|
||||
logger.error('dataAdapter', 'trimDataByEventTime', error as Error, { eventTime });
|
||||
return data; // 出错时返回原始数据
|
||||
}
|
||||
};
|
||||
@@ -260,13 +240,6 @@ export const processChartData = (
|
||||
data = trimDataByEventTime(data, eventTime, chartType);
|
||||
}
|
||||
|
||||
logger.debug('dataAdapter', 'processChartData', '数据处理完成', {
|
||||
rawLength: rawData.length,
|
||||
processedLength: data.length,
|
||||
chartType,
|
||||
hasEventTime: !!eventTime,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { OverlayCreate } from 'klinecharts';
|
||||
import type { EventMarker, KLineDataPoint } from '../types';
|
||||
import { EVENT_MARKER_CONFIG } from '../config';
|
||||
import { findClosestDataPoint } from './dataAdapter';
|
||||
import { logger } from '@utils/logger';
|
||||
|
||||
/**
|
||||
* 创建事件标记 Overlay(KLineChart 10.0 格式)
|
||||
@@ -27,10 +26,6 @@ export const createEventMarkerOverlay = (
|
||||
const closestPoint = findClosestDataPoint(data, marker.timestamp);
|
||||
|
||||
if (!closestPoint) {
|
||||
logger.warn('eventMarkerUtils', 'createEventMarkerOverlay', '未找到匹配的数据点', {
|
||||
markerId: marker.id,
|
||||
timestamp: marker.timestamp,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -64,10 +59,6 @@ export const createEventMarkerOverlay = (
|
||||
style: 'fill',
|
||||
color: marker.color,
|
||||
borderRadius: EVENT_MARKER_CONFIG.text.borderRadius,
|
||||
paddingLeft: EVENT_MARKER_CONFIG.text.padding,
|
||||
paddingRight: EVENT_MARKER_CONFIG.text.padding,
|
||||
paddingTop: EVENT_MARKER_CONFIG.text.padding,
|
||||
paddingBottom: EVENT_MARKER_CONFIG.text.padding,
|
||||
},
|
||||
},
|
||||
// 标记文本内容
|
||||
@@ -77,17 +68,8 @@ export const createEventMarkerOverlay = (
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlay', '创建事件标记', {
|
||||
markerId: marker.id,
|
||||
timestamp: closestPoint.timestamp,
|
||||
label: marker.label,
|
||||
});
|
||||
|
||||
return overlay;
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'createEventMarkerOverlay', error as Error, {
|
||||
markerId: marker.id,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -108,7 +90,6 @@ export const createEventHighlightOverlay = (
|
||||
const closestPoint = findClosestDataPoint(data, eventTimestamp);
|
||||
|
||||
if (!closestPoint) {
|
||||
logger.warn('eventMarkerUtils', 'createEventHighlightOverlay', '未找到匹配的数据点');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -135,14 +116,8 @@ export const createEventHighlightOverlay = (
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug('eventMarkerUtils', 'createEventHighlightOverlay', '创建事件高亮覆盖层', {
|
||||
timestamp: closestPoint.timestamp,
|
||||
eventTime,
|
||||
});
|
||||
|
||||
return overlay;
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'createEventHighlightOverlay', error as Error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -219,11 +194,6 @@ export const createEventMarkerOverlays = (
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlays', '批量创建事件标记', {
|
||||
totalMarkers: markers.length,
|
||||
createdOverlays: overlays.length,
|
||||
});
|
||||
|
||||
return overlays;
|
||||
};
|
||||
|
||||
@@ -235,10 +205,9 @@ export const createEventMarkerOverlays = (
|
||||
*/
|
||||
export const removeEventMarker = (chart: any, markerId: string): void => {
|
||||
try {
|
||||
chart.removeOverlay(markerId);
|
||||
logger.debug('eventMarkerUtils', 'removeEventMarker', '移除事件标记', { markerId });
|
||||
chart.removeOverlay({ id: markerId });
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'removeEventMarker', error as Error, { markerId });
|
||||
// 忽略移除标记时的错误
|
||||
}
|
||||
};
|
||||
|
||||
@@ -251,9 +220,8 @@ export const removeAllEventMarkers = (chart: any): void => {
|
||||
try {
|
||||
// KLineChart 10.0 API: removeOverlay() 不传参数时移除所有 overlays
|
||||
chart.removeOverlay();
|
||||
logger.debug('eventMarkerUtils', 'removeAllEventMarkers', '移除所有事件标记');
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'removeAllEventMarkers', error as Error);
|
||||
// 忽略移除所有标记时的错误
|
||||
}
|
||||
};
|
||||
|
||||
@@ -275,13 +243,8 @@ export const updateEventMarker = (
|
||||
|
||||
// 重新创建标记(KLineChart 10.0 不支持直接更新 overlay)
|
||||
// 注意:需要在调用方重新创建并添加 overlay
|
||||
|
||||
logger.debug('eventMarkerUtils', 'updateEventMarker', '更新事件标记', {
|
||||
markerId,
|
||||
updates,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'updateEventMarker', error as Error, { markerId });
|
||||
// 忽略更新标记时的错误
|
||||
}
|
||||
};
|
||||
|
||||
@@ -309,12 +272,8 @@ export const highlightEventMarker = (
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('eventMarkerUtils', 'highlightEventMarker', '高亮事件标记', {
|
||||
markerId,
|
||||
highlight,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'highlightEventMarker', error as Error, { markerId });
|
||||
// 忽略高亮标记时的错误
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,43 +9,44 @@ import React from 'react';
|
||||
*/
|
||||
export const lazyComponents = {
|
||||
// Home 模块
|
||||
HomePage: React.lazy(() => import('../views/Home/HomePage')),
|
||||
CenterDashboard: React.lazy(() => import('../views/Dashboard/Center')),
|
||||
ProfilePage: React.lazy(() => import('../views/Profile')),
|
||||
SettingsPage: React.lazy(() => import('../views/Settings/SettingsPage')),
|
||||
Subscription: React.lazy(() => import('../views/Pages/Account/Subscription')),
|
||||
PrivacyPolicy: React.lazy(() => import('../views/Pages/PrivacyPolicy')),
|
||||
UserAgreement: React.lazy(() => import('../views/Pages/UserAgreement')),
|
||||
WechatCallback: React.lazy(() => import('../views/Pages/WechatCallback')),
|
||||
// 首页使用专用入口组件,内置骨架屏 fallback
|
||||
HomePage: React.lazy(() => import('@views/Home')),
|
||||
CenterDashboard: React.lazy(() => import('@views/Dashboard/Center')),
|
||||
ProfilePage: React.lazy(() => import('@views/Profile')),
|
||||
SettingsPage: React.lazy(() => import('@views/Settings/SettingsPage')),
|
||||
Subscription: React.lazy(() => import('@views/Pages/Account/Subscription')),
|
||||
PrivacyPolicy: React.lazy(() => import('@views/Pages/PrivacyPolicy')),
|
||||
UserAgreement: React.lazy(() => import('@views/Pages/UserAgreement')),
|
||||
WechatCallback: React.lazy(() => import('@views/Pages/WechatCallback')),
|
||||
|
||||
// 社区/内容模块
|
||||
Community: React.lazy(() => import('../views/Community')),
|
||||
ConceptCenter: React.lazy(() => import('../views/Concept')),
|
||||
StockOverview: React.lazy(() => import('../views/StockOverview')),
|
||||
LimitAnalyse: React.lazy(() => import('../views/LimitAnalyse')),
|
||||
Community: React.lazy(() => import('@views/Community')),
|
||||
ConceptCenter: React.lazy(() => import('@views/Concept')),
|
||||
StockOverview: React.lazy(() => import('@views/StockOverview')),
|
||||
LimitAnalyse: React.lazy(() => import('@views/LimitAnalyse')),
|
||||
|
||||
// 交易模块
|
||||
TradingSimulation: React.lazy(() => import('../views/TradingSimulation')),
|
||||
TradingSimulation: React.lazy(() => import('@views/TradingSimulation')),
|
||||
|
||||
// 事件模块
|
||||
EventDetail: React.lazy(() => import('../views/EventDetail')),
|
||||
EventDetail: React.lazy(() => import('@views/EventDetail')),
|
||||
|
||||
// 公司相关模块
|
||||
CompanyIndex: React.lazy(() => import('../views/Company')),
|
||||
ForecastReport: React.lazy(() => import('../views/Company/ForecastReport')),
|
||||
FinancialPanorama: React.lazy(() => import('../views/Company/FinancialPanorama')),
|
||||
MarketDataView: React.lazy(() => import('../views/Company/MarketDataView')),
|
||||
CompanyIndex: React.lazy(() => import('@views/Company')),
|
||||
ForecastReport: React.lazy(() => import('@views/Company/ForecastReport')),
|
||||
FinancialPanorama: React.lazy(() => import('@views/Company/FinancialPanorama')),
|
||||
MarketDataView: React.lazy(() => import('@views/Company/MarketDataView')),
|
||||
|
||||
// Agent模块
|
||||
AgentChat: React.lazy(() => import('../views/AgentChat')),
|
||||
AgentChat: React.lazy(() => import('@views/AgentChat')),
|
||||
|
||||
// 价值论坛模块
|
||||
ValueForum: React.lazy(() => import('../views/ValueForum')),
|
||||
ForumPostDetail: React.lazy(() => import('../views/ValueForum/PostDetail')),
|
||||
PredictionTopicDetail: React.lazy(() => import('../views/ValueForum/PredictionTopicDetail')),
|
||||
ValueForum: React.lazy(() => import('@views/ValueForum')),
|
||||
ForumPostDetail: React.lazy(() => import('@views/ValueForum/PostDetail')),
|
||||
PredictionTopicDetail: React.lazy(() => import('@views/ValueForum/PredictionTopicDetail')),
|
||||
|
||||
// 数据浏览器模块
|
||||
DataBrowser: React.lazy(() => import('../views/DataBrowser')),
|
||||
DataBrowser: React.lazy(() => import('@views/DataBrowser')),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { Dispatch, SetStateAction, KeyboardEvent } from 'react';
|
||||
import axios from 'axios';
|
||||
import { logger } from '@utils/logger';
|
||||
import { MessageTypes, type Message } from '../constants/messageTypes';
|
||||
import type { UploadedFile } from './useFileUpload';
|
||||
import type { User } from './useAgentSessions';
|
||||
@@ -221,7 +220,7 @@ export const useAgentChat = ({
|
||||
loadSessions();
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Agent chat error', error);
|
||||
console.error('Agent chat error:', error);
|
||||
|
||||
// 移除 "思考中" 和 "执行中" 消息
|
||||
setMessages((prev) =>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import axios from 'axios';
|
||||
import { logger } from '@utils/logger';
|
||||
import { MessageTypes, type Message } from '../constants/messageTypes';
|
||||
|
||||
/**
|
||||
@@ -103,7 +102,7 @@ export const useAgentSessions = ({
|
||||
setSessions(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('加载会话列表失败', error);
|
||||
console.error('加载会话列表失败:', error);
|
||||
} finally {
|
||||
setIsLoadingSessions(false);
|
||||
}
|
||||
@@ -135,7 +134,7 @@ export const useAgentSessions = ({
|
||||
setMessages(formattedMessages);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('加载会话历史失败', error);
|
||||
console.error('加载会话历史失败:', error);
|
||||
}
|
||||
},
|
||||
[setMessages]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/**
|
||||
* 首页骨架屏组件
|
||||
* 模拟首页的 6 个功能卡片布局,减少白屏感知时间
|
||||
*
|
||||
* 使用 Chakra UI 的 Skeleton 组件
|
||||
* 匹配首页深色科技风格,减少白屏感知时间
|
||||
*
|
||||
* @module views/Home/components/HomePageSkeleton
|
||||
*/
|
||||
@@ -15,8 +13,6 @@ import {
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
VStack,
|
||||
HStack,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
// ============================================================
|
||||
@@ -24,163 +20,152 @@ import {
|
||||
// ============================================================
|
||||
|
||||
interface HomePageSkeletonProps {
|
||||
/** 是否显示动画效果 */
|
||||
isAnimated?: boolean;
|
||||
/** 骨架屏速度(秒) */
|
||||
speed?: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 单个卡片骨架
|
||||
// 单个卡片骨架 - 深色风格
|
||||
// ============================================================
|
||||
|
||||
const FeatureCardSkeleton: React.FC<{ isFeatured?: boolean }> = ({ isFeatured = false }) => {
|
||||
const bg = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.700');
|
||||
|
||||
const FeatureCardSkeleton: React.FC<{ isFeatured?: boolean; speed?: number }> = ({
|
||||
isFeatured = false,
|
||||
speed = 0.8,
|
||||
}) => {
|
||||
return (
|
||||
<Box
|
||||
bg={bg}
|
||||
borderRadius="xl"
|
||||
bg="rgba(255, 255, 255, 0.03)"
|
||||
borderRadius="2xl"
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
p={isFeatured ? 8 : 6}
|
||||
h={isFeatured ? '350px' : '280px'}
|
||||
boxShadow={isFeatured ? 'xl' : 'md'}
|
||||
position="relative"
|
||||
borderColor="whiteAlpha.100"
|
||||
p={{ base: 5, md: isFeatured ? 8 : 6 }}
|
||||
h={isFeatured ? { base: '280px', md: '320px' } : { base: '200px', md: '240px' }}
|
||||
backdropFilter="blur(10px)"
|
||||
>
|
||||
<VStack align="start" spacing={4} h="full">
|
||||
{/* 图标骨架 */}
|
||||
<Skeleton
|
||||
height={isFeatured ? '60px' : '48px'}
|
||||
width={isFeatured ? '60px' : '48px'}
|
||||
borderRadius="lg"
|
||||
startColor={isFeatured ? 'blue.100' : 'gray.100'}
|
||||
endColor={isFeatured ? 'blue.200' : 'gray.200'}
|
||||
height={isFeatured ? '56px' : '44px'}
|
||||
width={isFeatured ? '56px' : '44px'}
|
||||
borderRadius="xl"
|
||||
startColor="whiteAlpha.100"
|
||||
endColor="whiteAlpha.200"
|
||||
speed={speed}
|
||||
/>
|
||||
|
||||
{/* 标题骨架 */}
|
||||
<Skeleton height="28px" width="70%" borderRadius="md" />
|
||||
<Skeleton
|
||||
height="24px"
|
||||
width="60%"
|
||||
borderRadius="md"
|
||||
startColor="whiteAlpha.100"
|
||||
endColor="whiteAlpha.200"
|
||||
speed={speed}
|
||||
/>
|
||||
|
||||
{/* 描述骨架 */}
|
||||
<SkeletonText
|
||||
mt="2"
|
||||
noOfLines={isFeatured ? 4 : 3}
|
||||
noOfLines={isFeatured ? 3 : 2}
|
||||
spacing="3"
|
||||
skeletonHeight="2"
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
{/* 按钮骨架 */}
|
||||
<Skeleton
|
||||
height="40px"
|
||||
width={isFeatured ? '140px' : '100px'}
|
||||
borderRadius="md"
|
||||
mt="auto"
|
||||
skeletonHeight="3"
|
||||
width="90%"
|
||||
startColor="whiteAlpha.50"
|
||||
endColor="whiteAlpha.100"
|
||||
speed={speed}
|
||||
/>
|
||||
</VStack>
|
||||
|
||||
{/* Featured 徽章骨架 */}
|
||||
{isFeatured && (
|
||||
<Skeleton
|
||||
position="absolute"
|
||||
top="4"
|
||||
right="4"
|
||||
height="24px"
|
||||
width="80px"
|
||||
borderRadius="full"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Hero 区域骨架
|
||||
// ============================================================
|
||||
|
||||
const HeroSkeleton: React.FC<{ speed?: number }> = ({ speed = 0.8 }) => {
|
||||
return (
|
||||
<VStack spacing={{ base: 4, md: 6 }} align="center" py={{ base: 8, md: 12 }}>
|
||||
{/* 主标题骨架 */}
|
||||
<Skeleton
|
||||
height={{ base: '48px', md: '72px' }}
|
||||
width={{ base: '280px', md: '400px' }}
|
||||
borderRadius="lg"
|
||||
startColor="whiteAlpha.100"
|
||||
endColor="whiteAlpha.200"
|
||||
speed={speed}
|
||||
/>
|
||||
|
||||
{/* 副标题骨架 */}
|
||||
<Skeleton
|
||||
height={{ base: '20px', md: '24px' }}
|
||||
width={{ base: '200px', md: '300px' }}
|
||||
borderRadius="md"
|
||||
startColor="whiteAlpha.50"
|
||||
endColor="whiteAlpha.100"
|
||||
speed={speed}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 主骨架组件
|
||||
// ============================================================
|
||||
|
||||
export const HomePageSkeleton: React.FC<HomePageSkeletonProps> = ({
|
||||
isAnimated = true,
|
||||
speed = 0.8,
|
||||
}) => {
|
||||
const containerBg = useColorModeValue('gray.50', 'gray.900');
|
||||
|
||||
return (
|
||||
<Box
|
||||
w="full"
|
||||
minH="100vh"
|
||||
bg={containerBg}
|
||||
pt={{ base: '120px', md: '140px' }}
|
||||
pb={{ base: '60px', md: '80px' }}
|
||||
bg="linear-gradient(135deg, #0E0C15 0%, #15131D 50%, #252134 100%)"
|
||||
position="relative"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Container maxW="container.xl">
|
||||
<VStack spacing={{ base: 8, md: 12 }} align="stretch">
|
||||
{/* 顶部标题区域骨架 */}
|
||||
<VStack spacing={4} textAlign="center">
|
||||
{/* 主标题 */}
|
||||
<Skeleton
|
||||
height={{ base: '40px', md: '56px' }}
|
||||
width={{ base: '80%', md: '500px' }}
|
||||
borderRadius="md"
|
||||
speed={speed}
|
||||
/>
|
||||
{/* 背景装饰 - 模拟光晕效果 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="-20%"
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
w="800px"
|
||||
h="800px"
|
||||
bg="radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, transparent 70%)"
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
{/* 副标题 */}
|
||||
<Skeleton
|
||||
height={{ base: '20px', md: '24px' }}
|
||||
width={{ base: '90%', md: '600px' }}
|
||||
borderRadius="md"
|
||||
speed={speed}
|
||||
/>
|
||||
<Container maxW="7xl" position="relative" zIndex={10} px={{ base: 4, md: 6 }}>
|
||||
<VStack
|
||||
spacing={{ base: 8, md: 12, lg: 16 }}
|
||||
align="stretch"
|
||||
minH="100vh"
|
||||
justify="center"
|
||||
>
|
||||
{/* Hero 区域骨架 */}
|
||||
<HeroSkeleton speed={speed} />
|
||||
|
||||
{/* CTA 按钮 */}
|
||||
<HStack spacing={4} mt={4}>
|
||||
<Skeleton
|
||||
height="48px"
|
||||
width="140px"
|
||||
borderRadius="lg"
|
||||
speed={speed}
|
||||
/>
|
||||
<Skeleton
|
||||
height="48px"
|
||||
width="140px"
|
||||
borderRadius="lg"
|
||||
speed={speed}
|
||||
/>
|
||||
</HStack>
|
||||
</VStack>
|
||||
{/* 功能卡片区域骨架 */}
|
||||
<Box pb={{ base: 8, md: 12 }}>
|
||||
<VStack spacing={{ base: 6, md: 8 }}>
|
||||
{/* 特色功能卡片骨架 */}
|
||||
<Box w="100%">
|
||||
<FeatureCardSkeleton isFeatured speed={speed} />
|
||||
</Box>
|
||||
|
||||
{/* 功能卡片网格骨架 */}
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, md: 2, lg: 3 }}
|
||||
spacing={{ base: 6, md: 8 }}
|
||||
mt={8}
|
||||
>
|
||||
{/* 第一张卡片 - Featured (新闻中心) */}
|
||||
<Box gridColumn={{ base: 'span 1', lg: 'span 2' }}>
|
||||
<FeatureCardSkeleton isFeatured />
|
||||
</Box>
|
||||
|
||||
{/* 其余 5 张卡片 */}
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
<FeatureCardSkeleton key={index} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 底部装饰元素骨架 */}
|
||||
<HStack justify="center" spacing={8} mt={12}>
|
||||
{[1, 2, 3].map((index) => (
|
||||
<VStack key={index} spacing={2} align="center">
|
||||
<Skeleton
|
||||
height="40px"
|
||||
width="40px"
|
||||
borderRadius="full"
|
||||
speed={speed}
|
||||
/>
|
||||
<Skeleton height="16px" width="60px" borderRadius="md" speed={speed} />
|
||||
</VStack>
|
||||
))}
|
||||
</HStack>
|
||||
{/* 其他功能卡片骨架 */}
|
||||
<SimpleGrid
|
||||
columns={{ base: 1, md: 2, lg: 3 }}
|
||||
spacing={{ base: 4, md: 5, lg: 6 }}
|
||||
w="100%"
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
<FeatureCardSkeleton key={index} speed={speed} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
26
src/views/Home/index.tsx
Normal file
26
src/views/Home/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 首页入口组件
|
||||
* 使用专用骨架屏作为 Suspense fallback,优化加载体验
|
||||
*
|
||||
* @module views/Home
|
||||
*/
|
||||
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import { HomePageSkeleton } from './components/HomePageSkeleton';
|
||||
|
||||
// 懒加载实际首页组件
|
||||
const HomePage = lazy(() => import('./HomePage'));
|
||||
|
||||
/**
|
||||
* 首页入口 - 带骨架屏的懒加载包装
|
||||
* 使用深色风格骨架屏,与首页视觉风格一致
|
||||
*/
|
||||
const HomePageEntry: React.FC = () => {
|
||||
return (
|
||||
<Suspense fallback={<HomePageSkeleton />}>
|
||||
<HomePage />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePageEntry;
|
||||
Reference in New Issue
Block a user