feat: 拆分LeftSidebar 组件为ts组件
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user