update pay function
This commit is contained in:
127
src/views/AgentChat/components/LeftSidebar/DateGroup.js
Normal file
127
src/views/AgentChat/components/LeftSidebar/DateGroup.js
Normal file
@@ -0,0 +1,127 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/DateGroup.js
|
||||
// 可折叠的日期分组组件
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Box, Text, HStack, VStack, Badge } from '@chakra-ui/react';
|
||||
import { ChevronDown, ChevronRight, Calendar } from 'lucide-react';
|
||||
import SessionCard from './SessionCard';
|
||||
|
||||
/**
|
||||
* DateGroup - 可折叠的日期分组组件
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} props.label - 日期标签(如"今天"、"昨天"、"11月28日")
|
||||
* @param {Array} props.sessions - 该日期下的会话列表
|
||||
* @param {string|null} props.currentSessionId - 当前选中的会话 ID
|
||||
* @param {Function} props.onSessionSwitch - 切换会话回调
|
||||
* @param {boolean} props.defaultExpanded - 默认是否展开
|
||||
* @param {number} props.index - 分组索引(用于动画延迟)
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const DateGroup = ({
|
||||
label,
|
||||
sessions,
|
||||
currentSessionId,
|
||||
onSessionSwitch,
|
||||
defaultExpanded = true,
|
||||
index = 0,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const hasActiveSession = sessions.some((s) => s.session_id === currentSessionId);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.05, duration: 0.2 }}
|
||||
>
|
||||
<Box mb={2}>
|
||||
{/* 分组标题 - 可点击折叠 */}
|
||||
<HStack
|
||||
as="button"
|
||||
w="100%"
|
||||
px={2}
|
||||
py={1.5}
|
||||
spacing={2}
|
||||
cursor="pointer"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
borderRadius="md"
|
||||
bg={hasActiveSession ? 'rgba(139, 92, 246, 0.1)' : 'transparent'}
|
||||
_hover={{
|
||||
bg: 'rgba(255, 255, 255, 0.05)',
|
||||
}}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
{/* 折叠图标 */}
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 0 : -90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<ChevronDown className="w-3.5 h-3.5" color="#9CA3AF" />
|
||||
</motion.div>
|
||||
|
||||
{/* 日历图标 */}
|
||||
<Calendar className="w-3.5 h-3.5" color={hasActiveSession ? '#A78BFA' : '#6B7280'} />
|
||||
|
||||
{/* 日期标签 */}
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontWeight="semibold"
|
||||
color={hasActiveSession ? 'purple.300' : 'gray.500'}
|
||||
flex={1}
|
||||
textAlign="left"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
{/* 会话数量徽章 */}
|
||||
<Badge
|
||||
size="sm"
|
||||
bg={hasActiveSession ? 'rgba(139, 92, 246, 0.2)' : 'rgba(255, 255, 255, 0.1)'}
|
||||
color={hasActiveSession ? 'purple.300' : 'gray.500'}
|
||||
borderRadius="full"
|
||||
px={2}
|
||||
py={0.5}
|
||||
fontSize="10px"
|
||||
fontWeight="semibold"
|
||||
>
|
||||
{sessions.length}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
{/* 会话列表 - 折叠动画 */}
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<VStack spacing={1.5} align="stretch" mt={1.5} pl={2}>
|
||||
{sessions.map((session, idx) => (
|
||||
<motion.div
|
||||
key={session.session_id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: idx * 0.03 }}
|
||||
>
|
||||
<SessionCard
|
||||
session={session}
|
||||
isActive={currentSessionId === session.session_id}
|
||||
onPress={() => onSessionSwitch(session.session_id)}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</VStack>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateGroup;
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/AgentChat/components/LeftSidebar/index.js
|
||||
// 左侧栏组件 - 对话历史列表
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Box,
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
HStack,
|
||||
VStack,
|
||||
Flex,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { MessageSquare, Plus, Search, ChevronLeft } from 'lucide-react';
|
||||
import { MessageSquare, Plus, Search, ChevronLeft, ChevronDown, MoreHorizontal } from 'lucide-react';
|
||||
import { animations } from '../../constants/animations';
|
||||
import { groupSessionsByDate } from '../../utils/sessionUtils';
|
||||
import SessionCard from './SessionCard';
|
||||
import DateGroup from './DateGroup';
|
||||
|
||||
/**
|
||||
* LeftSidebar - 左侧栏组件
|
||||
@@ -35,6 +36,9 @@ import SessionCard from './SessionCard';
|
||||
* @param {Object} props.user - 用户信息
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
// 最多显示的日期分组数量
|
||||
const MAX_VISIBLE_GROUPS = 10;
|
||||
|
||||
const LeftSidebar = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -46,18 +50,33 @@ const LeftSidebar = ({
|
||||
user,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// 按日期分组会话
|
||||
const sessionGroups = groupSessionsByDate(sessions);
|
||||
const [showAllGroups, setShowAllGroups] = useState(false);
|
||||
|
||||
// 搜索过滤
|
||||
const filteredSessions = searchQuery
|
||||
? sessions.filter(
|
||||
(s) =>
|
||||
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: sessions;
|
||||
const filteredSessions = useMemo(() => {
|
||||
if (!searchQuery) return sessions;
|
||||
return sessions.filter(
|
||||
(s) =>
|
||||
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [sessions, searchQuery]);
|
||||
|
||||
// 按日期分组会话(新版本返回数组)
|
||||
const sessionGroups = useMemo(() => {
|
||||
return groupSessionsByDate(filteredSessions);
|
||||
}, [filteredSessions]);
|
||||
|
||||
// 控制显示的分组数量
|
||||
const visibleGroups = useMemo(() => {
|
||||
if (showAllGroups || sessionGroups.length <= MAX_VISIBLE_GROUPS) {
|
||||
return sessionGroups;
|
||||
}
|
||||
return sessionGroups.slice(0, MAX_VISIBLE_GROUPS);
|
||||
}, [sessionGroups, showAllGroups]);
|
||||
|
||||
const hasMoreGroups = sessionGroups.length > MAX_VISIBLE_GROUPS;
|
||||
const hiddenGroupsCount = sessionGroups.length - MAX_VISIBLE_GROUPS;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@@ -170,86 +189,97 @@ const LeftSidebar = ({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 会话列表 */}
|
||||
<Box flex={1} p={3} overflowY="auto">
|
||||
{/* 会话列表 - 滚动容器 */}
|
||||
<Box
|
||||
flex={1}
|
||||
p={3}
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
css={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '6px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: '3px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'rgba(139, 92, 246, 0.3)',
|
||||
borderRadius: '3px',
|
||||
'&:hover': {
|
||||
background: 'rgba(139, 92, 246, 0.5)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* 按日期分组显示会话 */}
|
||||
{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>
|
||||
{visibleGroups.map((group, index) => (
|
||||
<DateGroup
|
||||
key={group.dateKey}
|
||||
label={group.label}
|
||||
sessions={group.sessions}
|
||||
currentSessionId={currentSessionId}
|
||||
onSessionSwitch={onSessionSwitch}
|
||||
defaultExpanded={index < 3} // 前3个分组默认展开
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* 查看更多按钮 */}
|
||||
{hasMoreGroups && !showAllGroups && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<Button
|
||||
w="100%"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
leftIcon={<MoreHorizontal className="w-4 h-4" />}
|
||||
onClick={() => setShowAllGroups(true)}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.400"
|
||||
border="1px dashed"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: 'rgba(139, 92, 246, 0.1)',
|
||||
borderColor: 'purple.400',
|
||||
color: 'purple.300',
|
||||
}}
|
||||
mt={2}
|
||||
>
|
||||
查看更多 ({hiddenGroupsCount} 个日期)
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
{/* 收起按钮 */}
|
||||
{showAllGroups && hasMoreGroups && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<Button
|
||||
w="100%"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
leftIcon={<ChevronDown className="w-4 h-4" style={{ transform: 'rotate(180deg)' }} />}
|
||||
onClick={() => setShowAllGroups(false)}
|
||||
bg="rgba(255, 255, 255, 0.05)"
|
||||
color="gray.400"
|
||||
border="1px dashed"
|
||||
borderColor="rgba(255, 255, 255, 0.1)"
|
||||
_hover={{
|
||||
bg: 'rgba(139, 92, 246, 0.1)',
|
||||
borderColor: 'purple.400',
|
||||
color: 'purple.300',
|
||||
}}
|
||||
mt={2}
|
||||
>
|
||||
收起
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* 加载状态 */}
|
||||
@@ -273,6 +303,15 @@ const LeftSidebar = ({
|
||||
<Text fontSize="xs">开始一个新对话吧!</Text>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* 搜索无结果 */}
|
||||
{searchQuery && filteredSessions.length === 0 && sessions.length > 0 && (
|
||||
<VStack textAlign="center" py={8} color="gray.500" fontSize="sm" spacing={2}>
|
||||
<Search className="w-8 h-8" style={{ opacity: 0.5, margin: '0 auto' }} />
|
||||
<Text>未找到匹配的对话</Text>
|
||||
<Text fontSize="xs">尝试其他关键词</Text>
|
||||
</VStack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 用户信息卡片 */}
|
||||
|
||||
@@ -2,17 +2,113 @@
|
||||
// 会话管理工具函数
|
||||
|
||||
/**
|
||||
* 按日期分组会话列表
|
||||
* 格式化日期为显示标签
|
||||
* @param {Date} date - 日期对象
|
||||
* @param {Date} today - 今天的日期
|
||||
* @returns {string} 格式化后的日期标签
|
||||
*/
|
||||
const formatDateLabel = (date, today) => {
|
||||
const daysDiff = Math.floor((today - date) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysDiff === 0) {
|
||||
return '今天';
|
||||
} else if (daysDiff === 1) {
|
||||
return '昨天';
|
||||
} else if (daysDiff < 7) {
|
||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
return weekDays[date.getDay()];
|
||||
} else {
|
||||
// 超过一周,显示具体日期
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取日期的纯日期字符串(用于分组 key)
|
||||
* @param {Date} date - 日期对象
|
||||
* @returns {string} YYYY-MM-DD 格式的日期字符串
|
||||
*/
|
||||
const getDateKey = (date) => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 按日期分组会话列表(新版本 - 按具体日期分组)
|
||||
*
|
||||
* @param {Array} sessions - 会话列表
|
||||
* @returns {Object} 分组后的会话对象 { today, yesterday, thisWeek, older }
|
||||
* @returns {Array} 分组后的会话数组 [{ dateKey, label, sessions, date }]
|
||||
*
|
||||
* @example
|
||||
* const groups = groupSessionsByDate(sessions);
|
||||
* console.log(groups.today); // 今天的会话
|
||||
* console.log(groups.yesterday); // 昨天的会话
|
||||
* // 返回: [
|
||||
* // { dateKey: '2025-11-30', label: '今天', sessions: [...], date: Date },
|
||||
* // { dateKey: '2025-11-29', label: '昨天', sessions: [...], date: Date },
|
||||
* // ...
|
||||
* // ]
|
||||
*/
|
||||
export const groupSessionsByDate = (sessions) => {
|
||||
if (!sessions || sessions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
// 按日期分组到 Map
|
||||
const groupMap = new Map();
|
||||
|
||||
sessions.forEach((session) => {
|
||||
const sessionDate = new Date(session.created_at || session.timestamp);
|
||||
if (isNaN(sessionDate.getTime())) {
|
||||
// 无效日期,归到今天
|
||||
const todayKey = getDateKey(today);
|
||||
if (!groupMap.has(todayKey)) {
|
||||
groupMap.set(todayKey, {
|
||||
dateKey: todayKey,
|
||||
label: '今天',
|
||||
sessions: [],
|
||||
date: today,
|
||||
});
|
||||
}
|
||||
groupMap.get(todayKey).sessions.push(session);
|
||||
return;
|
||||
}
|
||||
|
||||
const dateOnly = new Date(sessionDate);
|
||||
dateOnly.setHours(0, 0, 0, 0);
|
||||
const dateKey = getDateKey(dateOnly);
|
||||
|
||||
if (!groupMap.has(dateKey)) {
|
||||
groupMap.set(dateKey, {
|
||||
dateKey,
|
||||
label: formatDateLabel(dateOnly, today),
|
||||
sessions: [],
|
||||
date: dateOnly,
|
||||
});
|
||||
}
|
||||
groupMap.get(dateKey).sessions.push(session);
|
||||
});
|
||||
|
||||
// 转换为数组并按日期降序排序
|
||||
const groups = Array.from(groupMap.values()).sort((a, b) => b.date - a.date);
|
||||
|
||||
// 每个分组内部按时间降序排序
|
||||
groups.forEach((group) => {
|
||||
group.sessions.sort((a, b) => {
|
||||
const dateA = new Date(a.created_at || a.timestamp);
|
||||
const dateB = new Date(b.created_at || b.timestamp);
|
||||
return dateB - dateA;
|
||||
});
|
||||
});
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
/**
|
||||
* 旧版分组函数(保留兼容性)
|
||||
* @deprecated 请使用 groupSessionsByDate 替代
|
||||
*/
|
||||
export const groupSessionsByDateLegacy = (sessions) => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
Reference in New Issue
Block a user