Compare commits
3 Commits
feature_20
...
04248e5a99
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04248e5a99 | ||
|
|
af54d8e070 | ||
|
|
a3cb5e928e |
@@ -72,7 +72,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('KLineChartModal', 'loadData', '开始加载K线数据', {
|
||||
logger.debug('KLineChartModal', '开始加载K线数据 (loadData)', {
|
||||
stockCode: stock.stock_code,
|
||||
eventTime,
|
||||
});
|
||||
@@ -91,7 +91,7 @@ const KLineChartModal: React.FC<KLineChartModalProps> = ({
|
||||
|
||||
console.log('[KLineChartModal] 数据条数:', response.data.length);
|
||||
setData(response.data);
|
||||
logger.info('KLineChartModal', 'loadData', 'K线数据加载成功', {
|
||||
logger.info('KLineChartModal', 'K线数据加载成功 (loadData)', {
|
||||
dataCount: response.data.length,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -111,7 +111,7 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
||||
const newType = e.target.value as ChartType;
|
||||
setChartType(newType);
|
||||
|
||||
logger.debug('StockChartKLineModal', 'handleChartTypeChange', '切换图表类型', {
|
||||
logger.debug('StockChartKLineModal', '切换图表类型 (handleChartTypeChange)', {
|
||||
newType,
|
||||
});
|
||||
}, []);
|
||||
@@ -131,7 +131,7 @@ const StockChartKLineModal: React.FC<StockChartKLineModalProps> = ({
|
||||
// 然后创建新的指标
|
||||
createSubIndicators(chart, values);
|
||||
|
||||
logger.debug('StockChartKLineModal', 'handleIndicatorChange', '切换副图指标', {
|
||||
logger.debug('StockChartKLineModal', '切换副图指标 (handleIndicatorChange)', {
|
||||
indicators: values,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -76,7 +76,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('TimelineChartModal', 'loadData', '开始加载分时图数据', {
|
||||
logger.debug('TimelineChartModal', '开始加载分时图数据 (loadData)', {
|
||||
stockCode: stock.stock_code,
|
||||
eventTime,
|
||||
});
|
||||
@@ -95,7 +95,7 @@ const TimelineChartModal: React.FC<TimelineChartModalProps> = ({
|
||||
|
||||
console.log('[TimelineChartModal] 数据条数:', response.data.length);
|
||||
setData(response.data);
|
||||
logger.info('TimelineChartModal', 'loadData', '分时图数据加载成功', {
|
||||
logger.info('TimelineChartModal', '分时图数据加载成功 (loadData)', {
|
||||
dataCount: response.data.length,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -77,7 +77,7 @@ export const useEventMarker = (
|
||||
const createMarker = useCallback(
|
||||
(time: string, label: string, color?: string) => {
|
||||
if (!chart || !data || data.length === 0) {
|
||||
logger.warn('useEventMarker', 'createMarker', '图表或数据未准备好', {
|
||||
logger.warn('useEventMarker', '图表或数据未准备好 (createMarker)', {
|
||||
hasChart: !!chart,
|
||||
dataLength: data?.length || 0,
|
||||
});
|
||||
@@ -93,7 +93,7 @@ export const useEventMarker = (
|
||||
const overlay = createEventMarkerOverlay(eventMarker, data);
|
||||
|
||||
if (!overlay) {
|
||||
logger.warn('useEventMarker', 'createMarker', 'Overlay 创建失败', {
|
||||
logger.warn('useEventMarker', 'Overlay 创建失败 (createMarker)', {
|
||||
eventMarker,
|
||||
});
|
||||
return;
|
||||
@@ -103,7 +103,7 @@ export const useEventMarker = (
|
||||
const id = chart.createOverlay(overlay);
|
||||
|
||||
if (!id || (Array.isArray(id) && id.length === 0)) {
|
||||
logger.warn('useEventMarker', 'createMarker', '标记添加失败', {
|
||||
logger.warn('useEventMarker', '标记添加失败 (createMarker)', {
|
||||
overlay,
|
||||
});
|
||||
return;
|
||||
@@ -119,12 +119,12 @@ export const useEventMarker = (
|
||||
const actualHighlightId = Array.isArray(highlightResult) ? highlightResult[0] : highlightResult;
|
||||
setHighlightId(actualHighlightId as string);
|
||||
|
||||
logger.info('useEventMarker', 'createMarker', '事件高亮背景创建成功', {
|
||||
logger.info('useEventMarker', '事件高亮背景创建成功 (createMarker)', {
|
||||
highlightId: actualHighlightId,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('useEventMarker', 'createMarker', '事件标记创建成功', {
|
||||
logger.info('useEventMarker', '事件标记创建成功 (createMarker)', {
|
||||
markerId: actualId,
|
||||
label,
|
||||
time,
|
||||
@@ -160,7 +160,7 @@ export const useEventMarker = (
|
||||
setMarkerId(null);
|
||||
setHighlightId(null);
|
||||
|
||||
logger.debug('useEventMarker', 'removeMarker', '移除事件标记和高亮', {
|
||||
logger.debug('useEventMarker', '移除事件标记和高亮 (removeMarker)', {
|
||||
markerId,
|
||||
highlightId,
|
||||
chartId: chart.id,
|
||||
@@ -187,7 +187,7 @@ export const useEventMarker = (
|
||||
setMarkerId(null);
|
||||
setHighlightId(null);
|
||||
|
||||
logger.debug('useEventMarker', 'removeAllMarkers', '移除所有事件标记和高亮', {
|
||||
logger.debug('useEventMarker', '移除所有事件标记和高亮 (removeAllMarkers)', {
|
||||
chartId: chart.id,
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -78,12 +78,12 @@ export const useKLineChart = (
|
||||
// 图表初始化函数
|
||||
const initChart = (): boolean => {
|
||||
if (!chartRef.current) {
|
||||
logger.warn('useKLineChart', 'init', '图表容器未挂载,将在 50ms 后重试', { containerId });
|
||||
logger.warn('useKLineChart', '图表容器未挂载,将在 50ms 后重试 (init)', { containerId });
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug('useKLineChart', 'init', '开始初始化图表', {
|
||||
logger.debug('useKLineChart', '开始初始化图表 (init)', {
|
||||
containerId,
|
||||
height,
|
||||
colorMode,
|
||||
@@ -116,17 +116,17 @@ export const useKLineChart = (
|
||||
height: 100, // 固定高度 100px(约占整体的 20-25%)
|
||||
});
|
||||
|
||||
logger.debug('useKLineChart', 'init', '成交量窗格创建成功', {
|
||||
logger.debug('useKLineChart', '成交量窗格创建成功 (init)', {
|
||||
volumePaneId,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('useKLineChart', 'init', '成交量窗格创建失败', {
|
||||
logger.warn('useKLineChart', '成交量窗格创建失败 (init)', {
|
||||
error: err,
|
||||
});
|
||||
// 不阻塞主流程,继续执行
|
||||
}
|
||||
|
||||
logger.info('useKLineChart', 'init', '✅ 图表初始化成功', {
|
||||
logger.info('useKLineChart', '✅ 图表初始化成功 (init)', {
|
||||
containerId,
|
||||
chartId: chartInstance.id,
|
||||
});
|
||||
@@ -146,7 +146,7 @@ export const useKLineChart = (
|
||||
// 成功,直接返回清理函数
|
||||
return () => {
|
||||
if (chartInstanceRef.current) {
|
||||
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
|
||||
logger.debug('useKLineChart', '销毁图表实例 (dispose)', {
|
||||
containerId,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
});
|
||||
@@ -161,7 +161,7 @@ export const useKLineChart = (
|
||||
|
||||
// 失败则延迟重试(处理 Modal 动画延迟导致的 DOM 未挂载)
|
||||
const timer = setTimeout(() => {
|
||||
logger.debug('useKLineChart', 'init', '执行延迟重试', { containerId });
|
||||
logger.debug('useKLineChart', '执行延迟重试 (init)', { containerId });
|
||||
initChart();
|
||||
}, 50);
|
||||
|
||||
@@ -169,7 +169,7 @@ export const useKLineChart = (
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
if (chartInstanceRef.current) {
|
||||
logger.debug('useKLineChart', 'dispose', '销毁图表实例', {
|
||||
logger.debug('useKLineChart', '销毁图表实例 (dispose)', {
|
||||
containerId,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
});
|
||||
@@ -196,7 +196,7 @@ export const useKLineChart = (
|
||||
: getTheme(colorMode);
|
||||
chartInstanceRef.current.setStyles(newTheme);
|
||||
|
||||
logger.debug('useKLineChart', 'updateTheme', '更新图表主题', {
|
||||
logger.debug('useKLineChart', '更新图表主题 (updateTheme)', {
|
||||
colorMode,
|
||||
chartType,
|
||||
chartId: chartInstanceRef.current.id,
|
||||
|
||||
@@ -78,7 +78,7 @@ export const useKLineData = (
|
||||
*/
|
||||
const loadData = useCallback(async () => {
|
||||
if (!stockCode) {
|
||||
logger.warn('useKLineData', 'loadData', '股票代码为空', { chartType });
|
||||
logger.warn('useKLineData', '股票代码为空 (loadData)', { chartType });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export const useKLineData = (
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
logger.debug('useKLineData', 'loadData', '开始加载数据', {
|
||||
logger.debug('useKLineData', '开始加载数据 (loadData)', {
|
||||
stockCode,
|
||||
chartType,
|
||||
eventTime,
|
||||
@@ -126,7 +126,7 @@ export const useKLineData = (
|
||||
|
||||
setData(processedData);
|
||||
|
||||
logger.info('useKLineData', 'loadData', '数据加载成功', {
|
||||
logger.info('useKLineData', '数据加载成功 (loadData)', {
|
||||
stockCode,
|
||||
chartType,
|
||||
rawCount: rawDataList.length,
|
||||
|
||||
@@ -50,7 +50,7 @@ export const createIndicator = (
|
||||
isStack
|
||||
);
|
||||
|
||||
logger.debug('chartUtils', 'createIndicator', '创建技术指标', {
|
||||
logger.debug('chartUtils', '创建技术指标 (createIndicator)', {
|
||||
indicatorName,
|
||||
params,
|
||||
isStack,
|
||||
@@ -70,7 +70,7 @@ export const createIndicator = (
|
||||
export const removeIndicator = (chart: Chart, indicatorId?: string): void => {
|
||||
safeChartOperation('removeIndicator', () => {
|
||||
chart.removeIndicator(indicatorId);
|
||||
logger.debug('chartUtils', 'removeIndicator', '移除技术指标', { indicatorId });
|
||||
logger.debug('chartUtils', '移除技术指标 (removeIndicator)', { indicatorId });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@ export const createSubIndicators = (
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'createSubIndicators', '批量创建副图指标', {
|
||||
logger.debug('chartUtils', '批量创建副图指标 (createSubIndicators)', {
|
||||
indicators,
|
||||
createdIds: ids,
|
||||
});
|
||||
@@ -130,7 +130,7 @@ export const setChartZoom = (chart: Chart, zoom: number): void => {
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'setChartZoom', '设置图表缩放', {
|
||||
logger.debug('chartUtils', '设置图表缩放 (setChartZoom)', {
|
||||
zoom,
|
||||
newBarSpace,
|
||||
});
|
||||
@@ -148,7 +148,7 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
|
||||
// KLineChart 10.0: 使用 scrollToTimestamp 方法
|
||||
chart.scrollToTimestamp(timestamp);
|
||||
|
||||
logger.debug('chartUtils', 'scrollToTimestamp', '滚动到指定时间', { timestamp });
|
||||
logger.debug('chartUtils', '滚动到指定时间 (scrollToTimestamp)', { timestamp });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -160,7 +160,7 @@ export const scrollToTimestamp = (chart: Chart, timestamp: number): void => {
|
||||
export const resizeChart = (chart: Chart): void => {
|
||||
safeChartOperation('resizeChart', () => {
|
||||
chart.resize();
|
||||
logger.debug('chartUtils', 'resizeChart', '调整图表大小');
|
||||
logger.debug('chartUtils', '调整图表大小 (resizeChart)');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -194,7 +194,7 @@ export const getVisibleRange = (chart: Chart): { from: number; to: number } | nu
|
||||
export const clearChartData = (chart: Chart): void => {
|
||||
safeChartOperation('clearChartData', () => {
|
||||
chart.resetData();
|
||||
logger.debug('chartUtils', 'clearChartData', '清空图表数据');
|
||||
logger.debug('chartUtils', '清空图表数据 (clearChartData)');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -213,7 +213,7 @@ export const exportChartImage = (
|
||||
// KLineChart 10.0: 使用 getConvertPictureUrl 方法
|
||||
const imageData = chart.getConvertPictureUrl(includeOverlay, 'png', '#ffffff');
|
||||
|
||||
logger.debug('chartUtils', 'exportChartImage', '导出图表图片', {
|
||||
logger.debug('chartUtils', '导出图表图片 (exportChartImage)', {
|
||||
includeOverlay,
|
||||
hasData: !!imageData,
|
||||
});
|
||||
@@ -236,7 +236,7 @@ export const toggleCrosshair = (chart: Chart, show: boolean): void => {
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'toggleCrosshair', '切换十字光标', { show });
|
||||
logger.debug('chartUtils', '切换十字光标 (toggleCrosshair)', { show });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -254,7 +254,7 @@ export const toggleGrid = (chart: Chart, show: boolean): void => {
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('chartUtils', 'toggleGrid', '切换网格', { show });
|
||||
logger.debug('chartUtils', '切换网格 (toggleGrid)', { show });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -272,7 +272,7 @@ export const subscribeChartEvent = (
|
||||
): void => {
|
||||
safeChartOperation(`subscribeChartEvent:${eventName}`, () => {
|
||||
chart.subscribeAction(eventName, handler);
|
||||
logger.debug('chartUtils', 'subscribeChartEvent', '订阅图表事件', { eventName });
|
||||
logger.debug('chartUtils', '订阅图表事件 (subscribeChartEvent)', { eventName });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -290,6 +290,6 @@ export const unsubscribeChartEvent = (
|
||||
): void => {
|
||||
safeChartOperation(`unsubscribeChartEvent:${eventName}`, () => {
|
||||
chart.unsubscribeAction(eventName, handler);
|
||||
logger.debug('chartUtils', 'unsubscribeChartEvent', '取消订阅图表事件', { eventName });
|
||||
logger.debug('chartUtils', '取消订阅图表事件 (unsubscribeChartEvent)', { eventName });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const convertToKLineData = (
|
||||
eventTime?: string
|
||||
): KLineDataPoint[] => {
|
||||
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
|
||||
logger.warn('dataAdapter', 'convertToKLineData', '原始数据为空', { chartType });
|
||||
logger.warn('dataAdapter', '原始数据为空 (convertToKLineData)', { chartType });
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const parseTimestamp = (
|
||||
}
|
||||
|
||||
// 默认返回当前时间(避免图表崩溃)
|
||||
logger.warn('dataAdapter', 'parseTimestamp', '无法解析时间戳,使用当前时间', { item });
|
||||
logger.warn('dataAdapter', '无法解析时间戳,使用当前时间 (parseTimestamp)', { item });
|
||||
return Date.now();
|
||||
};
|
||||
|
||||
@@ -126,19 +126,19 @@ 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 });
|
||||
logger.warn('dataAdapter', '价格异常,已移除 (validateAndCleanData)', { item });
|
||||
return false;
|
||||
}
|
||||
|
||||
// 移除 high < low 的数据(数据错误)
|
||||
if (item.high < item.low) {
|
||||
logger.warn('dataAdapter', 'validateAndCleanData', '最高价 < 最低价,已移除', { item });
|
||||
logger.warn('dataAdapter', '最高价 < 最低价,已移除 (validateAndCleanData)', { item });
|
||||
return false;
|
||||
}
|
||||
|
||||
// 移除成交量为负数的数据
|
||||
if (item.volume < 0) {
|
||||
logger.warn('dataAdapter', 'validateAndCleanData', '成交量异常,已移除', { item });
|
||||
logger.warn('dataAdapter', '成交量异常,已移除 (validateAndCleanData)', { item });
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ export const trimDataByEventTime = (
|
||||
return item.timestamp >= startTime && item.timestamp <= endTime;
|
||||
});
|
||||
|
||||
logger.debug('dataAdapter', 'trimDataByEventTime', '数据时间范围裁剪完成', {
|
||||
logger.debug('dataAdapter', '数据时间范围裁剪完成 (trimDataByEventTime)', {
|
||||
originalLength: data.length,
|
||||
trimmedLength: trimmedData.length,
|
||||
eventTime,
|
||||
@@ -260,7 +260,7 @@ export const processChartData = (
|
||||
data = trimDataByEventTime(data, eventTime, chartType);
|
||||
}
|
||||
|
||||
logger.debug('dataAdapter', 'processChartData', '数据处理完成', {
|
||||
logger.debug('dataAdapter', '数据处理完成 (processChartData)', {
|
||||
rawLength: rawData.length,
|
||||
processedLength: data.length,
|
||||
chartType,
|
||||
|
||||
@@ -27,7 +27,7 @@ export const createEventMarkerOverlay = (
|
||||
const closestPoint = findClosestDataPoint(data, marker.timestamp);
|
||||
|
||||
if (!closestPoint) {
|
||||
logger.warn('eventMarkerUtils', 'createEventMarkerOverlay', '未找到匹配的数据点', {
|
||||
logger.warn('eventMarkerUtils', '未找到匹配的数据点', {
|
||||
markerId: marker.id,
|
||||
timestamp: marker.timestamp,
|
||||
});
|
||||
@@ -77,7 +77,7 @@ export const createEventMarkerOverlay = (
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlay', '创建事件标记', {
|
||||
logger.debug('eventMarkerUtils', '创建事件标记', {
|
||||
markerId: marker.id,
|
||||
timestamp: closestPoint.timestamp,
|
||||
label: marker.label,
|
||||
@@ -108,7 +108,7 @@ export const createEventHighlightOverlay = (
|
||||
const closestPoint = findClosestDataPoint(data, eventTimestamp);
|
||||
|
||||
if (!closestPoint) {
|
||||
logger.warn('eventMarkerUtils', 'createEventHighlightOverlay', '未找到匹配的数据点');
|
||||
logger.warn('eventMarkerUtils', '未找到匹配的数据点');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ export const createEventHighlightOverlay = (
|
||||
},
|
||||
};
|
||||
|
||||
logger.debug('eventMarkerUtils', 'createEventHighlightOverlay', '创建事件高亮覆盖层', {
|
||||
logger.debug('eventMarkerUtils', '创建事件高亮覆盖层', {
|
||||
timestamp: closestPoint.timestamp,
|
||||
eventTime,
|
||||
});
|
||||
@@ -219,7 +219,7 @@ export const createEventMarkerOverlays = (
|
||||
}
|
||||
});
|
||||
|
||||
logger.debug('eventMarkerUtils', 'createEventMarkerOverlays', '批量创建事件标记', {
|
||||
logger.debug('eventMarkerUtils', '批量创建事件标记', {
|
||||
totalMarkers: markers.length,
|
||||
createdOverlays: overlays.length,
|
||||
});
|
||||
@@ -236,7 +236,7 @@ export const createEventMarkerOverlays = (
|
||||
export const removeEventMarker = (chart: any, markerId: string): void => {
|
||||
try {
|
||||
chart.removeOverlay(markerId);
|
||||
logger.debug('eventMarkerUtils', 'removeEventMarker', '移除事件标记', { markerId });
|
||||
logger.debug('eventMarkerUtils', '移除事件标记', { markerId });
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'removeEventMarker', error as Error, { markerId });
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export const removeAllEventMarkers = (chart: any): void => {
|
||||
try {
|
||||
// KLineChart 10.0 API: removeOverlay() 不传参数时移除所有 overlays
|
||||
chart.removeOverlay();
|
||||
logger.debug('eventMarkerUtils', 'removeAllEventMarkers', '移除所有事件标记');
|
||||
logger.debug('eventMarkerUtils', '移除所有事件标记');
|
||||
} catch (error) {
|
||||
logger.error('eventMarkerUtils', 'removeAllEventMarkers', error as Error);
|
||||
}
|
||||
@@ -276,7 +276,7 @@ export const updateEventMarker = (
|
||||
// 重新创建标记(KLineChart 10.0 不支持直接更新 overlay)
|
||||
// 注意:需要在调用方重新创建并添加 overlay
|
||||
|
||||
logger.debug('eventMarkerUtils', 'updateEventMarker', '更新事件标记', {
|
||||
logger.debug('eventMarkerUtils', '更新事件标记', {
|
||||
markerId,
|
||||
updates,
|
||||
});
|
||||
@@ -309,7 +309,7 @@ export const highlightEventMarker = (
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('eventMarkerUtils', 'highlightEventMarker', '高亮事件标记', {
|
||||
logger.debug('eventMarkerUtils', '高亮事件标记', {
|
||||
markerId,
|
||||
highlight,
|
||||
});
|
||||
|
||||
261
src/views/AgentChat/components/LeftSidebar/README.md
Normal file
261
src/views/AgentChat/components/LeftSidebar/README.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# LeftSidebar 组件架构说明
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
LeftSidebar/
|
||||
├── index.tsx # 左侧栏主组件(200 行)- 组合层
|
||||
├── types.ts # TypeScript 类型定义
|
||||
├── SessionList.tsx # 会话列表组件(150 行)
|
||||
├── SessionCard.js # 会话卡片组件(保留原有)
|
||||
├── SessionSearchBar.tsx # 搜索框组件(60 行)
|
||||
└── UserInfoCard.tsx # 用户信息卡片(80 行)
|
||||
```
|
||||
|
||||
## 🎯 重构目标
|
||||
|
||||
将原来 315 行的单文件组件拆分为多个职责明确的子组件,提高代码可维护性和可测试性。
|
||||
|
||||
## 🏗️ 组件职责
|
||||
|
||||
### 1. `index.tsx` - 主组件(约 200 行)
|
||||
|
||||
**职责**:
|
||||
- 管理本地状态(搜索关键词)
|
||||
- 数据处理(搜索过滤、日期分组)
|
||||
- 布局组合(渲染标题栏、搜索框、会话列表、用户信息)
|
||||
- 处理侧边栏动画
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface LeftSidebarProps {
|
||||
isOpen: boolean; // 侧边栏是否展开
|
||||
onClose: () => void; // 关闭回调
|
||||
sessions: Session[]; // 会话列表
|
||||
currentSessionId: string | null; // 当前会话 ID
|
||||
onSessionSwitch: (id: string) => void; // 切换会话
|
||||
onNewSession: () => void; // 新建会话
|
||||
isLoadingSessions: boolean; // 加载状态
|
||||
user: UserInfo | null | undefined; // 用户信息
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `SessionList.tsx` - 会话列表(约 150 行)
|
||||
|
||||
**职责**:
|
||||
- 按日期分组渲染会话(今天、昨天、本周、更早)
|
||||
- 处理加载状态和空状态
|
||||
- 管理会话卡片的入场动画
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface SessionListProps {
|
||||
sessionGroups: SessionGroups; // 分组后的会话
|
||||
currentSessionId: string | null; // 当前会话 ID
|
||||
onSessionSwitch: (id: string) => void; // 切换会话
|
||||
isLoadingSessions: boolean; // 加载状态
|
||||
totalSessions: number; // 会话总数
|
||||
}
|
||||
```
|
||||
|
||||
**特性**:
|
||||
- "今天"分组的会话有渐进入场动画
|
||||
- 其他分组无动画(性能优化)
|
||||
- 空状态显示提示文案和图标
|
||||
|
||||
---
|
||||
|
||||
### 3. `SessionSearchBar.tsx` - 搜索框(约 60 行)
|
||||
|
||||
**职责**:
|
||||
- 提供搜索输入框
|
||||
- 显示搜索图标
|
||||
- 处理输入变化事件
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface SessionSearchBarProps {
|
||||
value: string; // 搜索关键词
|
||||
onChange: (value: string) => void; // 变化回调
|
||||
placeholder?: string; // 占位符
|
||||
}
|
||||
```
|
||||
|
||||
**设计**:
|
||||
- 毛玻璃效果背景
|
||||
- 聚焦时紫色发光边框
|
||||
- 左侧搜索图标
|
||||
|
||||
---
|
||||
|
||||
### 4. `UserInfoCard.tsx` - 用户信息卡片(约 80 行)
|
||||
|
||||
**职责**:
|
||||
- 展示用户头像和昵称
|
||||
- 展示用户订阅类型徽章
|
||||
- 处理未登录状态
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface UserInfoCardProps {
|
||||
user: UserInfo | null | undefined; // 用户信息
|
||||
}
|
||||
```
|
||||
|
||||
**设计**:
|
||||
- 头像使用渐变色背景和发光效果
|
||||
- 订阅类型使用渐变色徽章
|
||||
- 文本溢出时自动截断
|
||||
|
||||
---
|
||||
|
||||
### 5. `SessionCard.js` - 会话卡片(保留原有)
|
||||
|
||||
保留原有的 JavaScript 实现,作为原子组件被 SessionList 调用。
|
||||
|
||||
**未来可选优化**:迁移为 TypeScript。
|
||||
|
||||
---
|
||||
|
||||
### 6. `types.ts` - 类型定义
|
||||
|
||||
**导出类型**:
|
||||
```typescript
|
||||
// 会话数据结构
|
||||
interface Session {
|
||||
session_id: string;
|
||||
title?: string;
|
||||
created_at?: string;
|
||||
timestamp?: string;
|
||||
message_count?: number;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
// 按日期分组的会话
|
||||
interface SessionGroups {
|
||||
today: Session[];
|
||||
yesterday: Session[];
|
||||
thisWeek: Session[];
|
||||
older: Session[];
|
||||
}
|
||||
|
||||
// 用户信息
|
||||
interface UserInfo {
|
||||
avatar?: string;
|
||||
nickname?: string;
|
||||
subscription_type?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 数据流
|
||||
|
||||
```
|
||||
LeftSidebar (index.tsx)
|
||||
├─ 接收 sessions 数组
|
||||
├─ 管理 searchQuery 状态
|
||||
├─ 过滤和分组数据
|
||||
│
|
||||
├─→ SessionSearchBar
|
||||
│ └─ 更新 searchQuery
|
||||
│
|
||||
├─→ SessionList
|
||||
│ ├─ 接收 sessionGroups
|
||||
│ └─→ SessionCard(循环渲染)
|
||||
│ └─ 触发 onSessionSwitch
|
||||
│
|
||||
└─→ UserInfoCard
|
||||
└─ 展示用户信息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 依赖关系
|
||||
|
||||
- **外部依赖**:
|
||||
- `@chakra-ui/react` - UI 组件库
|
||||
- `framer-motion` - 动画库
|
||||
- `lucide-react` - 图标库
|
||||
|
||||
- **内部依赖**:
|
||||
- `../../constants/animations` - 动画配置
|
||||
- `../../utils/sessionUtils` - 会话分组工具函数
|
||||
|
||||
---
|
||||
|
||||
## 🎨 设计特性
|
||||
|
||||
1. **毛玻璃效果**:
|
||||
- `backdropFilter: blur(20px) saturate(180%)`
|
||||
- 半透明背景 `rgba(17, 24, 39, 0.8)`
|
||||
|
||||
2. **渐变色**:
|
||||
- 标题:蓝色到紫色渐变
|
||||
- 订阅徽章:蓝色到紫色渐变
|
||||
- 头像背景:蓝色到紫色渐变
|
||||
|
||||
3. **交互动画**:
|
||||
- 按钮悬停:缩放 1.1x
|
||||
- 按钮点击:缩放 0.9x
|
||||
- 会话卡片悬停:缩放 1.02x + 上移 4px
|
||||
|
||||
4. **发光效果**:
|
||||
- 头像发光:`0 0 12px rgba(139, 92, 246, 0.4)`
|
||||
- 聚焦发光:`0 0 12px rgba(139, 92, 246, 0.3)`
|
||||
|
||||
---
|
||||
|
||||
## ✅ 重构优势
|
||||
|
||||
1. **可维护性提升**:
|
||||
- 单文件从 315 行拆分为多个 60-200 行的小文件
|
||||
- 每个组件职责单一,易于理解和修改
|
||||
|
||||
2. **可测试性提升**:
|
||||
- 每个子组件可独立测试
|
||||
- 纯展示组件(SessionCard、UserInfoCard)易于编写单元测试
|
||||
|
||||
3. **可复用性提升**:
|
||||
- SessionSearchBar 可在其他地方复用
|
||||
- UserInfoCard 可在其他侧边栏复用
|
||||
|
||||
4. **类型安全**:
|
||||
- 使用 TypeScript 提供完整类型检查
|
||||
- 统一的类型定义文件(types.ts)
|
||||
|
||||
5. **性能优化**:
|
||||
- 拆分后的组件可独立优化(如 React.memo)
|
||||
- 减少不必要的重新渲染
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来优化方向
|
||||
|
||||
1. **SessionCard 迁移为 TypeScript**
|
||||
2. **添加单元测试**
|
||||
3. **使用 React.memo 优化渲染性能**
|
||||
4. **添加虚拟滚动(如会话超过 100 个)**
|
||||
5. **支持拖拽排序会话**
|
||||
6. **支持会话分组(自定义文件夹)**
|
||||
|
||||
---
|
||||
|
||||
## 📝 使用示例
|
||||
|
||||
```typescript
|
||||
import LeftSidebar from '@views/AgentChat/components/LeftSidebar';
|
||||
|
||||
<LeftSidebar
|
||||
isOpen={isLeftSidebarOpen}
|
||||
onClose={() => setIsLeftSidebarOpen(false)}
|
||||
sessions={sessions}
|
||||
currentSessionId={currentSessionId}
|
||||
onSessionSwitch={switchSession}
|
||||
onNewSession={createNewSession}
|
||||
isLoadingSessions={isLoadingSessions}
|
||||
user={user}
|
||||
/>
|
||||
```
|
||||
126
src/views/AgentChat/components/LeftSidebar/SessionList.tsx
Normal file
126
src/views/AgentChat/components/LeftSidebar/SessionList.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/SessionList.tsx
|
||||
// 会话列表组件 - 按日期分组显示会话
|
||||
|
||||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Box, Text, VStack, Flex, Spinner } from '@chakra-ui/react';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import SessionCard from './SessionCard';
|
||||
import type { Session, SessionGroups } from './types';
|
||||
|
||||
/**
|
||||
* SessionList 组件的 Props 类型
|
||||
*/
|
||||
interface SessionListProps {
|
||||
/** 按日期分组的会话对象 */
|
||||
sessionGroups: SessionGroups;
|
||||
/** 当前选中的会话 ID */
|
||||
currentSessionId: string | null;
|
||||
/** 切换会话回调 */
|
||||
onSessionSwitch: (sessionId: string) => void;
|
||||
/** 会话加载中状态 */
|
||||
isLoadingSessions: boolean;
|
||||
/** 会话总数(用于判断是否为空) */
|
||||
totalSessions: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SessionList - 会话列表组件
|
||||
*
|
||||
* 职责:
|
||||
* 1. 按日期分组显示会话(今天、昨天、本周、更早)
|
||||
* 2. 处理加载状态和空状态
|
||||
* 3. 渲染会话卡片列表
|
||||
*/
|
||||
const SessionList: React.FC<SessionListProps> = ({
|
||||
sessionGroups,
|
||||
currentSessionId,
|
||||
onSessionSwitch,
|
||||
isLoadingSessions,
|
||||
totalSessions,
|
||||
}) => {
|
||||
/**
|
||||
* 渲染会话分组
|
||||
* @param label - 分组标签(如"今天"、"昨天")
|
||||
* @param sessions - 会话数组
|
||||
* @param withAnimation - 是否应用入场动画(今天的会话有动画)
|
||||
*/
|
||||
const renderSessionGroup = (
|
||||
label: string,
|
||||
sessions: Session[],
|
||||
withAnimation: boolean = false
|
||||
): React.ReactNode => {
|
||||
if (sessions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box mb={4}>
|
||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
||||
{label}
|
||||
</Text>
|
||||
<VStack spacing={2} align="stretch">
|
||||
{sessions.map((session, idx) => {
|
||||
const sessionCard = (
|
||||
<SessionCard
|
||||
session={session}
|
||||
isActive={currentSessionId === session.session_id}
|
||||
onPress={() => onSessionSwitch(session.session_id)}
|
||||
/>
|
||||
);
|
||||
|
||||
// 今天的会话添加渐进入场动画
|
||||
if (withAnimation) {
|
||||
return (
|
||||
<motion.div
|
||||
key={session.session_id}
|
||||
custom={idx}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: idx * 0.05 }}
|
||||
>
|
||||
{sessionCard}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// 其他分组不添加动画
|
||||
return <div key={session.session_id}>{sessionCard}</div>;
|
||||
})}
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box flex={1} p={3} overflowY="auto">
|
||||
{/* 按日期分组显示会话 */}
|
||||
{renderSessionGroup('今天', sessionGroups.today, true)}
|
||||
{renderSessionGroup('昨天', sessionGroups.yesterday)}
|
||||
{renderSessionGroup('本周', sessionGroups.thisWeek)}
|
||||
{renderSessionGroup('更早', sessionGroups.older)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{isLoadingSessions && (
|
||||
<Flex justify="center" p={4}>
|
||||
<Spinner
|
||||
size="md"
|
||||
color="purple.500"
|
||||
emptyColor="gray.700"
|
||||
thickness="3px"
|
||||
speed="0.65s"
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{totalSessions === 0 && !isLoadingSessions && (
|
||||
<VStack textAlign="center" py={8} color="gray.500" fontSize="sm" spacing={2}>
|
||||
<MessageSquare className="w-8 h-8" style={{ opacity: 0.5, margin: '0 auto' }} />
|
||||
<Text>还没有对话历史</Text>
|
||||
<Text fontSize="xs">开始一个新对话吧!</Text>
|
||||
</VStack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionList;
|
||||
@@ -0,0 +1,72 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/SessionSearchBar.tsx
|
||||
// 会话搜索框组件
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Input } from '@chakra-ui/react';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* SessionSearchBar 组件的 Props 类型
|
||||
*/
|
||||
interface SessionSearchBarProps {
|
||||
/** 搜索关键词 */
|
||||
value: string;
|
||||
/** 搜索关键词变化回调 */
|
||||
onChange: (value: string) => void;
|
||||
/** 占位符文本 */
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SessionSearchBar - 会话搜索框组件
|
||||
*
|
||||
* 职责:
|
||||
* 1. 提供搜索输入框
|
||||
* 2. 显示搜索图标
|
||||
* 3. 处理输入变化事件
|
||||
*
|
||||
* 设计:
|
||||
* - 毛玻璃效果背景
|
||||
* - 聚焦时紫色发光边框
|
||||
* - 左侧搜索图标
|
||||
*/
|
||||
const SessionSearchBar: React.FC<SessionSearchBarProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '搜索对话...',
|
||||
}) => {
|
||||
return (
|
||||
<Box position="relative">
|
||||
{/* 搜索图标 */}
|
||||
<Box position="absolute" left={3} top="50%" transform="translateY(-50%)" zIndex={1}>
|
||||
<Search className="w-4 h-4" color="#9CA3AF" />
|
||||
</Box>
|
||||
|
||||
{/* 搜索输入框 */}
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
pl={10}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
color="white"
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
_hover={{
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
}}
|
||||
_focus={{
|
||||
borderColor: 'purple.400',
|
||||
boxShadow: '0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
|
||||
bg: 'rgba(255, 255, 255, 0.08)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SessionSearchBar;
|
||||
68
src/views/AgentChat/components/LeftSidebar/UserInfoCard.tsx
Normal file
68
src/views/AgentChat/components/LeftSidebar/UserInfoCard.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/UserInfoCard.tsx
|
||||
// 用户信息卡片组件
|
||||
|
||||
import React from 'react';
|
||||
import { Box, HStack, Avatar, Text, Badge } from '@chakra-ui/react';
|
||||
import type { UserInfo } from './types';
|
||||
|
||||
/**
|
||||
* UserInfoCard 组件的 Props 类型
|
||||
*/
|
||||
interface UserInfoCardProps {
|
||||
/** 用户信息 */
|
||||
user: UserInfo | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserInfoCard - 用户信息卡片组件
|
||||
*
|
||||
* 职责:
|
||||
* 1. 展示用户头像和昵称
|
||||
* 2. 展示用户订阅类型徽章
|
||||
* 3. 处理未登录状态
|
||||
*
|
||||
* 设计:
|
||||
* - 头像使用渐变色背景和发光效果
|
||||
* - 订阅类型使用渐变色徽章
|
||||
* - 文本溢出时自动截断
|
||||
*/
|
||||
const UserInfoCard: React.FC<UserInfoCardProps> = ({ user }) => {
|
||||
return (
|
||||
<Box p={4} borderTop="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||
<HStack spacing={3}>
|
||||
{/* 用户头像 */}
|
||||
<Avatar
|
||||
src={user?.avatar}
|
||||
name={user?.nickname}
|
||||
size="sm"
|
||||
bgGradient="linear(to-br, blue.500, purple.600)"
|
||||
boxShadow="0 0 12px rgba(139, 92, 246, 0.4)"
|
||||
/>
|
||||
|
||||
{/* 用户信息 */}
|
||||
<Box flex={1} minW={0}>
|
||||
{/* 用户昵称 */}
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.100" noOfLines={1}>
|
||||
{user?.nickname || '未登录'}
|
||||
</Text>
|
||||
|
||||
{/* 订阅类型徽章 */}
|
||||
<Badge
|
||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||
color="white"
|
||||
px={2}
|
||||
py={0.5}
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
fontWeight="semibold"
|
||||
textTransform="none"
|
||||
>
|
||||
{user?.subscription_type || 'free'}
|
||||
</Badge>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserInfoCard;
|
||||
@@ -1,314 +0,0 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/index.js
|
||||
// 左侧栏组件 - 对话历史列表
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Input,
|
||||
Avatar,
|
||||
Badge,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
HStack,
|
||||
VStack,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import { MessageSquare, Plus, Search, ChevronLeft } from 'lucide-react';
|
||||
import { animations } from '../../constants/animations';
|
||||
import { groupSessionsByDate } from '../../utils/sessionUtils';
|
||||
import SessionCard from './SessionCard';
|
||||
|
||||
/**
|
||||
* LeftSidebar - 左侧栏组件
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - 侧边栏是否展开
|
||||
* @param {Function} props.onClose - 关闭侧边栏回调
|
||||
* @param {Array} props.sessions - 会话列表
|
||||
* @param {string|null} props.currentSessionId - 当前选中的会话 ID
|
||||
* @param {Function} props.onSessionSwitch - 切换会话回调
|
||||
* @param {Function} props.onNewSession - 新建会话回调
|
||||
* @param {boolean} props.isLoadingSessions - 会话加载中状态
|
||||
* @param {Object} props.user - 用户信息
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
const LeftSidebar = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
sessions,
|
||||
currentSessionId,
|
||||
onSessionSwitch,
|
||||
onNewSession,
|
||||
isLoadingSessions,
|
||||
user,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 按日期分组会话
|
||||
const sessionGroups = groupSessionsByDate(sessions);
|
||||
|
||||
// 搜索过滤
|
||||
const filteredSessions = searchQuery
|
||||
? sessions.filter(
|
||||
(s) =>
|
||||
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: sessions;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={animations.slideInLeft}
|
||||
>
|
||||
<Box
|
||||
w="320px"
|
||||
h="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
bg="rgba(17, 24, 39, 0.8)"
|
||||
backdropFilter="blur(20px) saturate(180%)"
|
||||
borderRight="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
boxShadow="4px 0 24px rgba(0, 0, 0, 0.3)"
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
<Box p={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||
<HStack justify="space-between" mb={3}>
|
||||
<HStack spacing={2}>
|
||||
<MessageSquare className="w-5 h-5" color="#60A5FA" />
|
||||
<Text
|
||||
fontWeight="semibold"
|
||||
bgGradient="linear(to-r, blue.300, purple.300)"
|
||||
bgClip="text"
|
||||
fontSize="md"
|
||||
>
|
||||
对话历史
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={2}>
|
||||
<Tooltip label="新建对话">
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
onClick={onNewSession}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.300"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: 'rgba(59, 130, 246, 0.2)',
|
||||
borderColor: 'blue.400',
|
||||
color: 'blue.300',
|
||||
boxShadow: '0 0 12px rgba(59, 130, 246, 0.3)',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
<Tooltip label="收起侧边栏">
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<ChevronLeft className="w-4 h-4" />}
|
||||
onClick={onClose}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.300"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: 'rgba(255, 255, 255, 0.1)',
|
||||
borderColor: 'purple.400',
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<Box position="relative">
|
||||
<Box position="absolute" left={3} top="50%" transform="translateY(-50%)" zIndex={1}>
|
||||
<Search className="w-4 h-4" color="#9CA3AF" />
|
||||
</Box>
|
||||
<Input
|
||||
placeholder="搜索对话..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
pl={10}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
color="white"
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
_hover={{
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
}}
|
||||
_focus={{
|
||||
borderColor: 'purple.400',
|
||||
boxShadow:
|
||||
'0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
|
||||
bg: 'rgba(255, 255, 255, 0.08)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 会话列表 */}
|
||||
<Box flex={1} p={3} overflowY="auto">
|
||||
{/* 按日期分组显示会话 */}
|
||||
{sessionGroups.today.length > 0 && (
|
||||
<Box mb={4}>
|
||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
||||
今天
|
||||
</Text>
|
||||
<VStack spacing={2} align="stretch">
|
||||
{sessionGroups.today.map((session, idx) => (
|
||||
<motion.div
|
||||
key={session.session_id}
|
||||
custom={idx}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: idx * 0.05 }}
|
||||
>
|
||||
<SessionCard
|
||||
session={session}
|
||||
isActive={currentSessionId === session.session_id}
|
||||
onPress={() => onSessionSwitch(session.session_id)}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{sessionGroups.yesterday.length > 0 && (
|
||||
<Box mb={4}>
|
||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
||||
昨天
|
||||
</Text>
|
||||
<VStack spacing={2} align="stretch">
|
||||
{sessionGroups.yesterday.map((session) => (
|
||||
<SessionCard
|
||||
key={session.session_id}
|
||||
session={session}
|
||||
isActive={currentSessionId === session.session_id}
|
||||
onPress={() => onSessionSwitch(session.session_id)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{sessionGroups.thisWeek.length > 0 && (
|
||||
<Box mb={4}>
|
||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
||||
本周
|
||||
</Text>
|
||||
<VStack spacing={2} align="stretch">
|
||||
{sessionGroups.thisWeek.map((session) => (
|
||||
<SessionCard
|
||||
key={session.session_id}
|
||||
session={session}
|
||||
isActive={currentSessionId === session.session_id}
|
||||
onPress={() => onSessionSwitch(session.session_id)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{sessionGroups.older.length > 0 && (
|
||||
<Box mb={4}>
|
||||
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}>
|
||||
更早
|
||||
</Text>
|
||||
<VStack spacing={2} align="stretch">
|
||||
{sessionGroups.older.map((session) => (
|
||||
<SessionCard
|
||||
key={session.session_id}
|
||||
session={session}
|
||||
isActive={currentSessionId === session.session_id}
|
||||
onPress={() => onSessionSwitch(session.session_id)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
{isLoadingSessions && (
|
||||
<Flex justify="center" p={4}>
|
||||
<Spinner
|
||||
size="md"
|
||||
color="purple.500"
|
||||
emptyColor="gray.700"
|
||||
thickness="3px"
|
||||
speed="0.65s"
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{sessions.length === 0 && !isLoadingSessions && (
|
||||
<VStack textAlign="center" py={8} color="gray.500" fontSize="sm" spacing={2}>
|
||||
<MessageSquare className="w-8 h-8" style={{ opacity: 0.5, margin: '0 auto' }} />
|
||||
<Text>还没有对话历史</Text>
|
||||
<Text fontSize="xs">开始一个新对话吧!</Text>
|
||||
</VStack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 用户信息卡片 */}
|
||||
<Box p={4} borderTop="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||
<HStack spacing={3}>
|
||||
<Avatar
|
||||
src={user?.avatar}
|
||||
name={user?.nickname}
|
||||
size="sm"
|
||||
bgGradient="linear(to-br, blue.500, purple.600)"
|
||||
boxShadow="0 0 12px rgba(139, 92, 246, 0.4)"
|
||||
/>
|
||||
<Box flex={1} minW={0}>
|
||||
<Text fontSize="sm" fontWeight="medium" color="gray.100" noOfLines={1}>
|
||||
{user?.nickname || '未登录'}
|
||||
</Text>
|
||||
<Badge
|
||||
bgGradient="linear(to-r, blue.500, purple.500)"
|
||||
color="white"
|
||||
px={2}
|
||||
py={0.5}
|
||||
borderRadius="full"
|
||||
fontSize="xs"
|
||||
fontWeight="semibold"
|
||||
textTransform="none"
|
||||
>
|
||||
{user?.subscription_type || 'free'}
|
||||
</Badge>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSidebar;
|
||||
197
src/views/AgentChat/components/LeftSidebar/index.tsx
Normal file
197
src/views/AgentChat/components/LeftSidebar/index.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/index.tsx
|
||||
// 左侧栏组件 - 对话历史列表(重构版本)
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
IconButton,
|
||||
HStack,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { MessageSquare, Plus, ChevronLeft } from 'lucide-react';
|
||||
import { animations } from '../../constants/animations';
|
||||
import { groupSessionsByDate } from '../../utils/sessionUtils';
|
||||
import SessionSearchBar from './SessionSearchBar';
|
||||
import SessionList from './SessionList';
|
||||
import UserInfoCard from './UserInfoCard';
|
||||
import type { Session, UserInfo } from './types';
|
||||
|
||||
/**
|
||||
* LeftSidebar 组件的 Props 类型
|
||||
*/
|
||||
interface LeftSidebarProps {
|
||||
/** 侧边栏是否展开 */
|
||||
isOpen: boolean;
|
||||
/** 关闭侧边栏回调 */
|
||||
onClose: () => void;
|
||||
/** 会话列表 */
|
||||
sessions: Session[];
|
||||
/** 当前选中的会话 ID */
|
||||
currentSessionId: string | null;
|
||||
/** 切换会话回调 */
|
||||
onSessionSwitch: (sessionId: string) => void;
|
||||
/** 新建会话回调 */
|
||||
onNewSession: () => void;
|
||||
/** 会话加载中状态 */
|
||||
isLoadingSessions: boolean;
|
||||
/** 用户信息 */
|
||||
user: UserInfo | null | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* LeftSidebar - 左侧栏组件(重构版本)
|
||||
*
|
||||
* 架构改进:
|
||||
* - 将会话列表逻辑提取到 SessionList 组件(150 行)
|
||||
* - 将用户信息卡片提取到 UserInfoCard 组件(80 行)
|
||||
* - 将搜索框提取到 SessionSearchBar 组件(60 行)
|
||||
* - 主组件只负责状态管理和布局组合(200 行)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 管理搜索状态
|
||||
* 2. 过滤和分组会话数据
|
||||
* 3. 组合渲染子组件
|
||||
* 4. 处理侧边栏动画
|
||||
*/
|
||||
const LeftSidebar: React.FC<LeftSidebarProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
sessions,
|
||||
currentSessionId,
|
||||
onSessionSwitch,
|
||||
onNewSession,
|
||||
isLoadingSessions,
|
||||
user,
|
||||
}) => {
|
||||
// ==================== 本地状态 ====================
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
// ==================== 数据处理 ====================
|
||||
|
||||
// 搜索过滤
|
||||
const filteredSessions = searchQuery
|
||||
? sessions.filter(
|
||||
(s) =>
|
||||
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: sessions;
|
||||
|
||||
// 按日期分组会话
|
||||
const sessionGroups = groupSessionsByDate(filteredSessions);
|
||||
|
||||
// ==================== 渲染组件 ====================
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
style={{ width: '320px', display: 'flex', flexDirection: 'column' }}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
variants={animations.slideInLeft}
|
||||
>
|
||||
<Box
|
||||
w="320px"
|
||||
h="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
bg="rgba(17, 24, 39, 0.8)"
|
||||
backdropFilter="blur(20px) saturate(180%)"
|
||||
borderRight="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
boxShadow="4px 0 24px rgba(0, 0, 0, 0.3)"
|
||||
>
|
||||
{/* ==================== 标题栏 ==================== */}
|
||||
<Box p={4} borderBottom="1px solid" borderColor="rgba(255, 255, 255, 0.1)">
|
||||
{/* 标题和操作按钮 */}
|
||||
<HStack justify="space-between" mb={3}>
|
||||
{/* 左侧:标题 */}
|
||||
<HStack spacing={2}>
|
||||
<MessageSquare className="w-5 h-5" color="#60A5FA" />
|
||||
<Text
|
||||
fontWeight="semibold"
|
||||
bgGradient="linear(to-r, blue.300, purple.300)"
|
||||
bgClip="text"
|
||||
fontSize="md"
|
||||
>
|
||||
对话历史
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 右侧:新建对话 + 收起按钮 */}
|
||||
<HStack spacing={2}>
|
||||
{/* 新建对话按钮 */}
|
||||
<Tooltip label="新建对话">
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
onClick={onNewSession}
|
||||
aria-label="新建对话"
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.300"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: 'rgba(59, 130, 246, 0.2)',
|
||||
borderColor: 'blue.400',
|
||||
color: 'blue.300',
|
||||
boxShadow: '0 0 12px rgba(59, 130, 246, 0.3)',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
|
||||
{/* 收起侧边栏按钮 */}
|
||||
<Tooltip label="收起侧边栏">
|
||||
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
icon={<ChevronLeft className="w-4 h-4" />}
|
||||
onClick={onClose}
|
||||
aria-label="收起侧边栏"
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.300"
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: 'rgba(255, 255, 255, 0.1)',
|
||||
borderColor: 'purple.400',
|
||||
color: 'white',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 搜索框组件 */}
|
||||
<SessionSearchBar value={searchQuery} onChange={setSearchQuery} />
|
||||
</Box>
|
||||
|
||||
{/* ==================== 会话列表组件 ==================== */}
|
||||
<SessionList
|
||||
sessionGroups={sessionGroups}
|
||||
currentSessionId={currentSessionId}
|
||||
onSessionSwitch={onSessionSwitch}
|
||||
isLoadingSessions={isLoadingSessions}
|
||||
totalSessions={sessions.length}
|
||||
/>
|
||||
|
||||
{/* ==================== 用户信息卡片组件 ==================== */}
|
||||
<UserInfoCard user={user} />
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSidebar;
|
||||
33
src/views/AgentChat/components/LeftSidebar/types.ts
Normal file
33
src/views/AgentChat/components/LeftSidebar/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/types.ts
|
||||
// LeftSidebar 组件的 TypeScript 类型定义
|
||||
|
||||
/**
|
||||
* 会话数据结构
|
||||
*/
|
||||
export interface Session {
|
||||
session_id: string;
|
||||
title?: string;
|
||||
created_at?: string;
|
||||
timestamp?: string;
|
||||
message_count?: number;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按日期分组的会话数据
|
||||
*/
|
||||
export interface SessionGroups {
|
||||
today: Session[];
|
||||
yesterday: Session[];
|
||||
thisWeek: Session[];
|
||||
older: Session[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
export interface UserInfo {
|
||||
avatar?: string;
|
||||
nickname?: string;
|
||||
subscription_type?: string;
|
||||
}
|
||||
@@ -221,7 +221,7 @@ export const useAgentChat = ({
|
||||
loadSessions();
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Agent chat error', error);
|
||||
logger.error('useAgentChat', 'handleSendMessage', error as Error);
|
||||
|
||||
// 移除 "思考中" 和 "执行中" 消息
|
||||
setMessages((prev) =>
|
||||
|
||||
@@ -103,7 +103,7 @@ export const useAgentSessions = ({
|
||||
setSessions(response.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('加载会话列表失败', error);
|
||||
logger.error('useAgentSessions', 'loadSessions', error as Error);
|
||||
} finally {
|
||||
setIsLoadingSessions(false);
|
||||
}
|
||||
@@ -135,7 +135,7 @@ export const useAgentSessions = ({
|
||||
setMessages(formattedMessages);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('加载会话历史失败', error);
|
||||
logger.error('useAgentSessions', 'loadSessionHistory', error as Error);
|
||||
}
|
||||
},
|
||||
[setMessages]
|
||||
|
||||
Reference in New Issue
Block a user