update pay function

This commit is contained in:
2025-11-30 23:39:48 +08:00
parent 2c4f5152e4
commit a72978c200
3 changed files with 356 additions and 94 deletions

View 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;

View File

@@ -1,7 +1,7 @@
// src/views/AgentChat/components/LeftSidebar/index.js // 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 { motion, AnimatePresence } from 'framer-motion';
import { import {
Box, Box,
@@ -15,11 +15,12 @@ import {
HStack, HStack,
VStack, VStack,
Flex, Flex,
Button,
} from '@chakra-ui/react'; } 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 { animations } from '../../constants/animations';
import { groupSessionsByDate } from '../../utils/sessionUtils'; import { groupSessionsByDate } from '../../utils/sessionUtils';
import SessionCard from './SessionCard'; import DateGroup from './DateGroup';
/** /**
* LeftSidebar - 左侧栏组件 * LeftSidebar - 左侧栏组件
@@ -35,6 +36,9 @@ import SessionCard from './SessionCard';
* @param {Object} props.user - 用户信息 * @param {Object} props.user - 用户信息
* @returns {JSX.Element|null} * @returns {JSX.Element|null}
*/ */
// 最多显示的日期分组数量
const MAX_VISIBLE_GROUPS = 10;
const LeftSidebar = ({ const LeftSidebar = ({
isOpen, isOpen,
onClose, onClose,
@@ -46,18 +50,33 @@ const LeftSidebar = ({
user, user,
}) => { }) => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [showAllGroups, setShowAllGroups] = useState(false);
// 按日期分组会话
const sessionGroups = groupSessionsByDate(sessions);
// 搜索过滤 // 搜索过滤
const filteredSessions = searchQuery const filteredSessions = useMemo(() => {
? sessions.filter( if (!searchQuery) return sessions;
return sessions.filter(
(s) => (s) =>
s.title?.toLowerCase().includes(searchQuery.toLowerCase()) || s.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.session_id?.toLowerCase().includes(searchQuery.toLowerCase()) s.session_id?.toLowerCase().includes(searchQuery.toLowerCase())
) );
: sessions; }, [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 ( return (
<AnimatePresence> <AnimatePresence>
@@ -170,86 +189,97 @@ const LeftSidebar = ({
</Box> </Box>
</Box> </Box>
{/* 会话列表 */} {/* 会话列表 - 滚动容器 */}
<Box flex={1} p={3} overflowY="auto"> <Box
{/* 按日期分组显示会话 */} flex={1}
{sessionGroups.today.length > 0 && ( p={3}
<Box mb={4}> overflowY="auto"
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}> overflowX="hidden"
今天 css={{
</Text> '&::-webkit-scrollbar': {
<VStack spacing={2} align="stretch"> width: '6px',
{sessionGroups.today.map((session, idx) => ( },
<motion.div '&::-webkit-scrollbar-track': {
key={session.session_id} background: 'rgba(255, 255, 255, 0.05)',
custom={idx} borderRadius: '3px',
initial={{ opacity: 0, x: -20 }} },
animate={{ opacity: 1, x: 0 }} '&::-webkit-scrollbar-thumb': {
transition={{ delay: idx * 0.05 }} background: 'rgba(139, 92, 246, 0.3)',
borderRadius: '3px',
'&:hover': {
background: 'rgba(139, 92, 246, 0.5)',
},
},
}}
> >
<SessionCard {/* 按日期分组显示会话 */}
session={session} {visibleGroups.map((group, index) => (
isActive={currentSessionId === session.session_id} <DateGroup
onPress={() => onSessionSwitch(session.session_id)} 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> </motion.div>
))}
</VStack>
</Box>
)} )}
{sessionGroups.yesterday.length > 0 && ( {/* 收起按钮 */}
<Box mb={4}> {showAllGroups && hasMoreGroups && (
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}> <motion.div
昨天 initial={{ opacity: 0 }}
</Text> animate={{ opacity: 1 }}
<VStack spacing={2} align="stretch"> >
{sessionGroups.yesterday.map((session) => ( <Button
<SessionCard w="100%"
key={session.session_id} size="sm"
session={session} variant="ghost"
isActive={currentSessionId === session.session_id} leftIcon={<ChevronDown className="w-4 h-4" style={{ transform: 'rotate(180deg)' }} />}
onPress={() => onSessionSwitch(session.session_id)} onClick={() => setShowAllGroups(false)}
/> bg="rgba(255, 255, 255, 0.05)"
))} color="gray.400"
</VStack> border="1px dashed"
</Box> borderColor="rgba(255, 255, 255, 0.1)"
)} _hover={{
bg: 'rgba(139, 92, 246, 0.1)',
{sessionGroups.thisWeek.length > 0 && ( borderColor: 'purple.400',
<Box mb={4}> color: 'purple.300',
<Text fontSize="xs" fontWeight="semibold" color="gray.500" mb={2} px={2}> }}
本周 mt={2}
</Text> >
<VStack spacing={2} align="stretch"> 收起
{sessionGroups.thisWeek.map((session) => ( </Button>
<SessionCard </motion.div>
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>
)} )}
{/* 加载状态 */} {/* 加载状态 */}
@@ -273,6 +303,15 @@ const LeftSidebar = ({
<Text fontSize="xs">开始一个新对话吧</Text> <Text fontSize="xs">开始一个新对话吧</Text>
</VStack> </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> </Box>
{/* 用户信息卡片 */} {/* 用户信息卡片 */}

View File

@@ -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 - 会话列表 * @param {Array} sessions - 会话列表
* @returns {Object} 分组后的会话对象 { today, yesterday, thisWeek, older } * @returns {Array} 分组后的会话数组 [{ dateKey, label, sessions, date }]
* *
* @example * @example
* const groups = groupSessionsByDate(sessions); * 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) => { 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 today = new Date();
const yesterday = new Date(today); const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);