update pay function

This commit is contained in:
2025-11-30 18:31:13 +08:00
parent 6763151c57
commit 8843c81d8b
4 changed files with 178 additions and 35 deletions

View File

@@ -1,8 +1,8 @@
// src/components/ChatBot/EChartsRenderer.js // src/components/ChatBot/EChartsRenderer.js
// ECharts 图表渲染组件 // ECharts 图表渲染组件
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef, useCallback, useState } from 'react';
import { Box, useColorModeValue } from '@chakra-ui/react'; import { Box, useColorModeValue, Skeleton } from '@chakra-ui/react';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
/** /**
@@ -67,6 +67,9 @@ const isValidOption = (option) => {
export const EChartsRenderer = ({ option, height = 400, variant = 'auto' }) => { export const EChartsRenderer = ({ option, height = 400, variant = 'auto' }) => {
const chartRef = useRef(null); const chartRef = useRef(null);
const chartInstance = useRef(null); const chartInstance = useRef(null);
const resizeObserverRef = useRef(null);
const [isReady, setIsReady] = useState(false);
const initTimeoutRef = useRef(null);
// 系统颜色模式 // 系统颜色模式
const systemBgColor = useColorModeValue('white', 'transparent'); const systemBgColor = useColorModeValue('white', 'transparent');
@@ -76,7 +79,8 @@ export const EChartsRenderer = ({ option, height = 400, variant = 'auto' }) => {
const isDarkMode = variant === 'dark' ? true : variant === 'light' ? false : systemIsDark; const isDarkMode = variant === 'dark' ? true : variant === 'light' ? false : systemIsDark;
const bgColor = variant === 'dark' ? 'transparent' : variant === 'light' ? 'white' : systemBgColor; const bgColor = variant === 'dark' ? 'transparent' : variant === 'light' ? 'white' : systemBgColor;
useEffect(() => { // 初始化或更新图表的函数
const initChart = useCallback(() => {
if (!chartRef.current || !option) return; if (!chartRef.current || !option) return;
// 验证配置是否有效 // 验证配置是否有效
@@ -85,9 +89,21 @@ export const EChartsRenderer = ({ option, height = 400, variant = 'auto' }) => {
return; return;
} }
// 确保容器有有效尺寸
const containerWidth = chartRef.current.offsetWidth;
const containerHeight = chartRef.current.offsetHeight;
if (containerWidth < 50 || containerHeight < 50) {
// 容器尺寸太小,延迟重试
initTimeoutRef.current = setTimeout(initChart, 100);
return;
}
// 初始化图表(支持深色模式) // 初始化图表(支持深色模式)
if (!chartInstance.current) { if (!chartInstance.current) {
chartInstance.current = echarts.init(chartRef.current, isDarkMode ? 'dark' : null); chartInstance.current = echarts.init(chartRef.current, isDarkMode ? 'dark' : null, {
renderer: 'canvas',
});
} }
// 深色模式下的默认文字颜色 // 深色模式下的默认文字颜色
@@ -117,6 +133,7 @@ export const EChartsRenderer = ({ option, height = 400, variant = 'auto' }) => {
// 设置图表配置(使用 try-catch 防止 ECharts 内部错误) // 设置图表配置(使用 try-catch 防止 ECharts 内部错误)
try { try {
chartInstance.current.setOption(defaultOption, true); chartInstance.current.setOption(defaultOption, true);
setIsReady(true);
} catch (error) { } catch (error) {
console.error('EChartsRenderer: Failed to render chart', error); console.error('EChartsRenderer: Failed to render chart', error);
// 销毁出错的图表实例 // 销毁出错的图表实例
@@ -124,36 +141,93 @@ export const EChartsRenderer = ({ option, height = 400, variant = 'auto' }) => {
chartInstance.current = null; chartInstance.current = null;
return; return;
} }
}, [option, isDarkMode]);
// 响应式调整大小 // 处理容器尺寸变化
const handleResize = () => { const handleResize = useCallback(() => {
if (chartInstance.current) {
// 使用 requestAnimationFrame 确保在下一帧渲染时调整大小
requestAnimationFrame(() => {
chartInstance.current?.resize(); chartInstance.current?.resize();
}; });
}
}, []);
useEffect(() => {
// 使用 setTimeout 确保 DOM 已经完全渲染
initTimeoutRef.current = setTimeout(() => {
initChart();
}, 50);
// 使用 ResizeObserver 监听容器尺寸变化
if (chartRef.current && typeof ResizeObserver !== 'undefined') {
resizeObserverRef.current = new ResizeObserver((entries) => {
// 防抖处理
if (initTimeoutRef.current) {
clearTimeout(initTimeoutRef.current);
}
initTimeoutRef.current = setTimeout(() => {
handleResize();
}, 100);
});
resizeObserverRef.current.observe(chartRef.current);
}
// 窗口 resize 事件作为备用
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return () => { return () => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
// chartInstance.current?.dispose(); // 不要销毁,避免重新渲染时闪烁 if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect();
}
if (initTimeoutRef.current) {
clearTimeout(initTimeoutRef.current);
}
}; };
}, [option, isDarkMode, variant]); }, [initChart, handleResize]);
// option 变化时重新渲染
useEffect(() => {
if (chartInstance.current && option && isValidOption(option)) {
initChart();
}
}, [option, initChart]);
// 组件卸载时销毁图表 // 组件卸载时销毁图表
useEffect(() => { useEffect(() => {
return () => { return () => {
chartInstance.current?.dispose(); if (chartInstance.current) {
chartInstance.current.dispose();
chartInstance.current = null; chartInstance.current = null;
}
}; };
}, []); }, []);
return ( return (
<Box position="relative" width="100%" height={`${height}px`}>
{!isReady && (
<Skeleton
position="absolute"
top={0}
left={0}
width="100%"
height="100%"
borderRadius="md"
startColor={isDarkMode ? 'gray.700' : 'gray.100'}
endColor={isDarkMode ? 'gray.600' : 'gray.200'}
/>
)}
<Box <Box
ref={chartRef} ref={chartRef}
width="100%" width="100%"
height={`${height}px`} height="100%"
bg={bgColor} bg={bgColor}
borderRadius="md" borderRadius="md"
boxShadow="sm" boxShadow="sm"
opacity={isReady ? 1 : 0}
transition="opacity 0.3s ease"
/> />
</Box>
); );
}; };

View File

@@ -121,6 +121,17 @@ const parseMarkdownWithCharts = (markdown) => {
export const MarkdownWithCharts = ({ content, variant = 'auto' }) => { export const MarkdownWithCharts = ({ content, variant = 'auto' }) => {
const parts = parseMarkdownWithCharts(content); const parts = parseMarkdownWithCharts(content);
// 开发环境调试日志
if (process.env.NODE_ENV === 'development' && content?.includes('echarts')) {
logger.info('[MarkdownWithCharts] 解析结果', {
contentLength: content?.length,
contentPreview: content?.substring(0, 100),
partsCount: parts.length,
partTypes: parts.map(p => p.type),
hasChartPart: parts.some(p => p.type === 'chart'),
});
}
// 系统颜色模式 // 系统颜色模式
const systemTextColor = useColorModeValue('gray.700', 'gray.100'); const systemTextColor = useColorModeValue('gray.700', 'gray.100');
const systemHeadingColor = useColorModeValue('gray.800', 'gray.50'); const systemHeadingColor = useColorModeValue('gray.800', 'gray.50');
@@ -182,15 +193,41 @@ export const MarkdownWithCharts = ({ content, variant = 'auto' }) => {
{children} {children}
</Box> </Box>
), ),
code: ({ inline, children }) => // 处理代码块和行内代码
inline ? ( code: ({ node, inline, className, children, ...props }) => {
<Code fontSize="sm" px={1} bg={codeBg}> // 检查是否是代码块(通过父元素是否为 pre 或通过 className 判断)
const isCodeBlock = !inline && (className || (node?.position?.start?.line !== node?.position?.end?.line));
if (isCodeBlock) {
// 代码块样式
return (
<Code
display="block"
p={3}
borderRadius="md"
fontSize="sm"
whiteSpace="pre-wrap"
bg={codeBg}
overflowX="auto"
maxW="100%"
{...props}
>
{children} {children}
</Code> </Code>
) : ( );
<Code display="block" p={3} borderRadius="md" fontSize="sm" whiteSpace="pre-wrap" bg={codeBg}> }
// 行内代码样式
return (
<Code fontSize="sm" px={1} bg={codeBg} {...props}>
{children} {children}
</Code> </Code>
);
},
// 处理 pre 元素,防止嵌套问题
pre: ({ children }) => (
<Box as="pre" my={2} overflow="hidden" borderRadius="md">
{children}
</Box>
), ),
blockquote: ({ children }) => ( blockquote: ({ children }) => (
<Box <Box
@@ -262,9 +299,12 @@ export const MarkdownWithCharts = ({ content, variant = 'auto' }) => {
} else if (part.type === 'chart') { } else if (part.type === 'chart') {
// 渲染 ECharts 图表 // 渲染 ECharts 图表
try { try {
// 清理可能的 Markdown 残留符号 // 清理可能的 Markdown 残留符号和代码块标记
let cleanContent = part.content.trim(); let cleanContent = part.content.trim();
// 移除可能残留的代码块结束标记
cleanContent = cleanContent.replace(/```\s*$/g, '').trim();
// 移除可能的前后空白和不可见字符 // 移除可能的前后空白和不可见字符
cleanContent = cleanContent.replace(/^\s+|\s+$/g, ''); cleanContent = cleanContent.replace(/^\s+|\s+$/g, '');
@@ -427,7 +467,14 @@ export const MarkdownWithCharts = ({ content, variant = 'auto' }) => {
} }
return ( return (
<Box key={index}> <Box
key={index}
w="100%"
minW="300px"
my={3}
borderRadius="md"
overflow="hidden"
>
<EChartsRenderer option={chartOption} height={350} variant={variant} /> <EChartsRenderer option={chartOption} height={350} variant={variant} />
</Box> </Box>
); );

View File

@@ -120,19 +120,21 @@ const MessageRenderer = ({ message, userAvatar }) => {
case MessageTypes.AGENT_RESPONSE: case MessageTypes.AGENT_RESPONSE:
return ( return (
<Flex justify="flex-start"> <Flex justify="flex-start" w="100%">
<HStack align="start" spacing={3} maxW="75%"> <HStack align="start" spacing={3} maxW={{ base: '95%', md: '85%', lg: '80%' }} w="100%">
<Avatar <Avatar
src="/images/agent/基金经理.png" src="/images/agent/基金经理.png"
icon={<Cpu className="w-4 h-4" />} icon={<Cpu className="w-4 h-4" />}
size="sm" size="sm"
bgGradient="linear(to-br, purple.500, pink.500)" bgGradient="linear(to-br, purple.500, pink.500)"
boxShadow="0 0 12px rgba(236, 72, 153, 0.4)" boxShadow="0 0 12px rgba(236, 72, 153, 0.4)"
flexShrink={0}
/> />
<motion.div <motion.div
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }} transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{ flex: 1, minWidth: 0 }}
> >
<Card <Card
bg="rgba(255, 255, 255, 0.05)" bg="rgba(255, 255, 255, 0.05)"
@@ -140,12 +142,15 @@ const MessageRenderer = ({ message, userAvatar }) => {
border="1px solid" border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)" borderColor="rgba(255, 255, 255, 0.1)"
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)" boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
w="100%"
> >
<CardBody px={5} py={3}> <CardBody px={5} py={3}>
<Box <Box
fontSize="sm" fontSize="sm"
color="gray.100" color="gray.100"
lineHeight="relaxed" lineHeight="relaxed"
w="100%"
overflow="hidden"
sx={{ sx={{
'& p': { mb: 2 }, '& p': { mb: 2 },
'& h1, & h2, & h3': { color: 'gray.50' }, '& h1, & h2, & h3': { color: 'gray.50' },

View File

@@ -108,6 +108,23 @@ export const useAgentSessions = ({
} }
}, [user?.id]); }, [user?.id]);
/**
* 安全解析 JSON 字符串,如果已经是对象则直接返回
*/
const safeJsonParse = (value: unknown): unknown => {
if (!value) return null;
if (typeof value === 'object') return value;
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
console.warn('JSON 解析失败:', value?.toString().substring(0, 100));
return null;
}
}
return null;
};
/** /**
* 加载指定会话的历史消息 * 加载指定会话的历史消息
*/ */
@@ -125,9 +142,9 @@ export const useAgentSessions = ({
const formattedMessages: Message[] = history.map((msg: any, idx: number) => ({ const formattedMessages: Message[] = history.map((msg: any, idx: number) => ({
id: `${sessionId}-${idx}`, id: `${sessionId}-${idx}`,
type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE, type: msg.message_type === 'user' ? MessageTypes.USER : MessageTypes.AGENT_RESPONSE,
content: msg.message, content: msg.message || '',
plan: msg.plan ? JSON.parse(msg.plan) : null, plan: safeJsonParse(msg.plan),
stepResults: msg.steps ? JSON.parse(msg.steps) : null, stepResults: safeJsonParse(msg.steps),
timestamp: msg.timestamp, timestamp: msg.timestamp,
})); }));