pref: 删除备份文件
This commit is contained in:
@@ -1,493 +0,0 @@
|
|||||||
// src/views/Dashboard/components/InvestmentCalendarChakra.js
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Heading,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalFooter,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
useDisclosure,
|
|
||||||
Badge,
|
|
||||||
IconButton,
|
|
||||||
Flex,
|
|
||||||
Grid,
|
|
||||||
useColorModeValue,
|
|
||||||
Divider,
|
|
||||||
Tooltip,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Textarea,
|
|
||||||
Select,
|
|
||||||
useToast,
|
|
||||||
Spinner,
|
|
||||||
Center,
|
|
||||||
Tag,
|
|
||||||
TagLabel,
|
|
||||||
TagLeftIcon,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
FiCalendar,
|
|
||||||
FiClock,
|
|
||||||
FiStar,
|
|
||||||
FiTrendingUp,
|
|
||||||
FiPlus,
|
|
||||||
FiEdit2,
|
|
||||||
FiTrash2,
|
|
||||||
FiSave,
|
|
||||||
FiX,
|
|
||||||
} from 'react-icons/fi';
|
|
||||||
import FullCalendar from '@fullcalendar/react';
|
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
|
||||||
import moment from 'moment';
|
|
||||||
import 'moment/locale/zh-cn';
|
|
||||||
import { logger } from '../../../utils/logger';
|
|
||||||
import './InvestmentCalendar.css';
|
|
||||||
|
|
||||||
moment.locale('zh-cn');
|
|
||||||
|
|
||||||
export default function InvestmentCalendarChakra() {
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
// 颜色主题
|
|
||||||
const bgColor = useColorModeValue('white', 'gray.800');
|
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
|
||||||
const textColor = useColorModeValue('gray.700', 'white');
|
|
||||||
const secondaryText = useColorModeValue('gray.600', 'gray.400');
|
|
||||||
|
|
||||||
const [events, setEvents] = useState([]);
|
|
||||||
const [selectedDate, setSelectedDate] = useState(null);
|
|
||||||
const [selectedDateEvents, setSelectedDateEvents] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [newEvent, setNewEvent] = useState({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
type: 'plan',
|
|
||||||
importance: 3,
|
|
||||||
stocks: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载事件数据
|
|
||||||
const loadEvents = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
|
||||||
|
|
||||||
// 直接加载用户相关的事件(投资计划 + 关注的未来事件)
|
|
||||||
const userResponse = await fetch(base + '/api/account/calendar/events', {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (userResponse.ok) {
|
|
||||||
const userData = await userResponse.json();
|
|
||||||
if (userData.success) {
|
|
||||||
const allEvents = (userData.data || []).map(event => ({
|
|
||||||
...event,
|
|
||||||
id: `${event.source || 'user'}-${event.id}`,
|
|
||||||
title: event.title,
|
|
||||||
start: event.event_date,
|
|
||||||
date: event.event_date,
|
|
||||||
backgroundColor: event.source === 'future' ? '#3182CE' : '#8B5CF6',
|
|
||||||
borderColor: event.source === 'future' ? '#3182CE' : '#8B5CF6',
|
|
||||||
extendedProps: {
|
|
||||||
...event,
|
|
||||||
isSystem: event.source === 'future',
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
setEvents(allEvents);
|
|
||||||
logger.debug('InvestmentCalendar', '日历事件加载成功', {
|
|
||||||
count: allEvents.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('InvestmentCalendar', 'loadEvents', error);
|
|
||||||
// ❌ 移除数据加载失败 toast(非关键操作)
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []); // ✅ 移除 toast 依赖
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadEvents();
|
|
||||||
}, [loadEvents]);
|
|
||||||
|
|
||||||
// 根据重要性获取颜色
|
|
||||||
const getEventColor = (importance) => {
|
|
||||||
if (importance >= 5) return '#E53E3E'; // 红色
|
|
||||||
if (importance >= 4) return '#ED8936'; // 橙色
|
|
||||||
if (importance >= 3) return '#ECC94B'; // 黄色
|
|
||||||
if (importance >= 2) return '#48BB78'; // 绿色
|
|
||||||
return '#3182CE'; // 蓝色
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理日期点击
|
|
||||||
const handleDateClick = (info) => {
|
|
||||||
const clickedDate = moment(info.date);
|
|
||||||
setSelectedDate(clickedDate);
|
|
||||||
|
|
||||||
// 筛选当天的事件
|
|
||||||
const dayEvents = events.filter(event =>
|
|
||||||
moment(event.start).isSame(clickedDate, 'day')
|
|
||||||
);
|
|
||||||
setSelectedDateEvents(dayEvents);
|
|
||||||
onOpen();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理事件点击
|
|
||||||
const handleEventClick = (info) => {
|
|
||||||
const event = info.event;
|
|
||||||
const clickedDate = moment(event.start);
|
|
||||||
setSelectedDate(clickedDate);
|
|
||||||
setSelectedDateEvents([{
|
|
||||||
title: event.title,
|
|
||||||
start: event.start,
|
|
||||||
extendedProps: {
|
|
||||||
...event.extendedProps,
|
|
||||||
},
|
|
||||||
}]);
|
|
||||||
onOpen();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加新事件
|
|
||||||
const handleAddEvent = async () => {
|
|
||||||
try {
|
|
||||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
...newEvent,
|
|
||||||
event_date: (selectedDate ? selectedDate.format('YYYY-MM-DD') : moment().format('YYYY-MM-DD')),
|
|
||||||
stocks: newEvent.stocks.split(',').map(s => s.trim()).filter(s => s),
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(base + '/api/account/calendar/events', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(eventData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
logger.info('InvestmentCalendar', '添加事件成功', {
|
|
||||||
eventTitle: eventData.title,
|
|
||||||
eventDate: eventData.event_date
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
title: '添加成功',
|
|
||||||
description: '投资计划已添加',
|
|
||||||
status: 'success',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
onAddClose();
|
|
||||||
loadEvents();
|
|
||||||
setNewEvent({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
type: 'plan',
|
|
||||||
importance: 3,
|
|
||||||
stocks: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('InvestmentCalendar', 'handleAddEvent', error, {
|
|
||||||
eventTitle: newEvent?.title
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
title: '添加失败',
|
|
||||||
description: '无法添加投资计划',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除用户事件
|
|
||||||
const handleDeleteEvent = async (eventId) => {
|
|
||||||
if (!eventId) {
|
|
||||||
logger.warn('InvestmentCalendar', '删除事件失败', '缺少事件 ID', { eventId });
|
|
||||||
toast({
|
|
||||||
title: '无法删除',
|
|
||||||
description: '缺少事件 ID',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
|
||||||
|
|
||||||
const response = await fetch(base + `/api/account/calendar/events/${eventId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
logger.info('InvestmentCalendar', '删除事件成功', { eventId });
|
|
||||||
toast({
|
|
||||||
title: '删除成功',
|
|
||||||
status: 'success',
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
loadEvents();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('InvestmentCalendar', 'handleDeleteEvent', error, { eventId });
|
|
||||||
toast({
|
|
||||||
title: '删除失败',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card bg={bgColor} shadow="md">
|
|
||||||
<CardHeader pb={4}>
|
|
||||||
<Flex justify="space-between" align="center">
|
|
||||||
<HStack>
|
|
||||||
<Icon as={FiCalendar} color="orange.500" boxSize={5} />
|
|
||||||
<Heading size="md">投资日历</Heading>
|
|
||||||
</HStack>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="blue"
|
|
||||||
leftIcon={<FiPlus />}
|
|
||||||
onClick={() => { if (!selectedDate) setSelectedDate(moment()); onAddOpen(); }}
|
|
||||||
>
|
|
||||||
添加计划
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody pt={0}>
|
|
||||||
{loading ? (
|
|
||||||
<Center h="560px">
|
|
||||||
<Spinner size="xl" color="blue.500" />
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<Box height={{ base: '500px', md: '600px' }}>
|
|
||||||
<FullCalendar
|
|
||||||
plugins={[dayGridPlugin, interactionPlugin]}
|
|
||||||
initialView="dayGridMonth"
|
|
||||||
locale="zh-cn"
|
|
||||||
headerToolbar={{
|
|
||||||
left: 'prev,next today',
|
|
||||||
center: 'title',
|
|
||||||
right: ''
|
|
||||||
}}
|
|
||||||
events={events}
|
|
||||||
dateClick={handleDateClick}
|
|
||||||
eventClick={handleEventClick}
|
|
||||||
height="100%"
|
|
||||||
dayMaxEvents={3}
|
|
||||||
moreLinkText="更多"
|
|
||||||
buttonText={{
|
|
||||||
today: '今天',
|
|
||||||
month: '月',
|
|
||||||
week: '周'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
|
||||||
|
|
||||||
{/* 查看事件详情 Modal - 条件渲染 */}
|
|
||||||
{isOpen && (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>
|
|
||||||
{selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
{selectedDateEvents.length === 0 ? (
|
|
||||||
<Center py={8}>
|
|
||||||
<VStack>
|
|
||||||
<Text color={secondaryText}>当天没有事件</Text>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="blue"
|
|
||||||
leftIcon={<FiPlus />}
|
|
||||||
onClick={() => {
|
|
||||||
onClose();
|
|
||||||
onAddOpen();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
添加投资计划
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<VStack align="stretch" spacing={4}>
|
|
||||||
{selectedDateEvents.map((event, idx) => (
|
|
||||||
<Box
|
|
||||||
key={idx}
|
|
||||||
p={4}
|
|
||||||
borderRadius="md"
|
|
||||||
border="1px"
|
|
||||||
borderColor={borderColor}
|
|
||||||
>
|
|
||||||
<Flex justify="space-between" align="start" mb={2}>
|
|
||||||
<VStack align="start" spacing={1} flex={1}>
|
|
||||||
<HStack>
|
|
||||||
<Text fontWeight="bold" fontSize="lg">
|
|
||||||
{event.title}
|
|
||||||
</Text>
|
|
||||||
{event.extendedProps?.isSystem ? (
|
|
||||||
<Badge colorScheme="blue" variant="subtle">系统事件</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge colorScheme="purple" variant="subtle">我的计划</Badge>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Icon as={FiStar} color="yellow.500" />
|
|
||||||
<Text fontSize="sm" color={secondaryText}>
|
|
||||||
重要度: {event.extendedProps?.importance || 3}/5
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
{!event.extendedProps?.isSystem && (
|
|
||||||
<IconButton
|
|
||||||
icon={<FiTrash2 />}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
colorScheme="red"
|
|
||||||
onClick={() => handleDeleteEvent(event.extendedProps?.id)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{event.extendedProps?.description && (
|
|
||||||
<Text fontSize="sm" color={secondaryText} mb={2}>
|
|
||||||
{event.extendedProps.description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{event.extendedProps?.stocks && event.extendedProps.stocks.length > 0 && (
|
|
||||||
<HStack spacing={2} flexWrap="wrap">
|
|
||||||
<Text fontSize="sm" color={secondaryText}>相关股票:</Text>
|
|
||||||
{event.extendedProps.stocks.map((stock, i) => (
|
|
||||||
<Tag key={i} size="sm" colorScheme="blue">
|
|
||||||
<TagLeftIcon as={FiTrendingUp} />
|
|
||||||
<TagLabel>{stock}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onClick={onClose}>关闭</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 添加投资计划 Modal - 条件渲染 */}
|
|
||||||
{isAddOpen && (
|
|
||||||
<Modal isOpen={isAddOpen} onClose={onAddClose} size="lg" closeOnOverlayClick={false} closeOnEsc={true}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>
|
|
||||||
添加投资计划
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
<VStack spacing={4}>
|
|
||||||
<FormControl isRequired>
|
|
||||||
<FormLabel>标题</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={newEvent.title}
|
|
||||||
onChange={(e) => setNewEvent({ ...newEvent, title: e.target.value })}
|
|
||||||
placeholder="例如:关注半导体板块"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>描述</FormLabel>
|
|
||||||
<Textarea
|
|
||||||
value={newEvent.description}
|
|
||||||
onChange={(e) => setNewEvent({ ...newEvent, description: e.target.value })}
|
|
||||||
placeholder="详细描述您的投资计划..."
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>类型</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={newEvent.type}
|
|
||||||
onChange={(e) => setNewEvent({ ...newEvent, type: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="plan">投资计划</option>
|
|
||||||
<option value="reminder">提醒事项</option>
|
|
||||||
<option value="analysis">分析任务</option>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>重要度</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={newEvent.importance}
|
|
||||||
onChange={(e) => setNewEvent({ ...newEvent, importance: parseInt(e.target.value) })}
|
|
||||||
>
|
|
||||||
<option value={5}>⭐⭐⭐⭐⭐ 非常重要</option>
|
|
||||||
<option value={4}>⭐⭐⭐⭐ 重要</option>
|
|
||||||
<option value={3}>⭐⭐⭐ 一般</option>
|
|
||||||
<option value={2}>⭐⭐ 次要</option>
|
|
||||||
<option value={1}>⭐ 不重要</option>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>相关股票(用逗号分隔)</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={newEvent.stocks}
|
|
||||||
onChange={(e) => setNewEvent({ ...newEvent, stocks: e.target.value })}
|
|
||||||
placeholder="例如:600519,000858,002415"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button variant="ghost" mr={3} onClick={onAddClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
onClick={handleAddEvent}
|
|
||||||
isDisabled={!newEvent.title}
|
|
||||||
>
|
|
||||||
添加
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,587 +0,0 @@
|
|||||||
// src/views/Dashboard/components/InvestmentPlansAndReviews.js
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
Heading,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalFooter,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
useDisclosure,
|
|
||||||
Badge,
|
|
||||||
IconButton,
|
|
||||||
Flex,
|
|
||||||
useColorModeValue,
|
|
||||||
Divider,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Textarea,
|
|
||||||
Select,
|
|
||||||
useToast,
|
|
||||||
Spinner,
|
|
||||||
Center,
|
|
||||||
Tag,
|
|
||||||
TagLabel,
|
|
||||||
TagLeftIcon,
|
|
||||||
TagCloseButton,
|
|
||||||
Grid,
|
|
||||||
Tabs,
|
|
||||||
TabList,
|
|
||||||
TabPanels,
|
|
||||||
Tab,
|
|
||||||
TabPanel,
|
|
||||||
InputGroup,
|
|
||||||
InputLeftElement,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import {
|
|
||||||
FiCalendar,
|
|
||||||
FiClock,
|
|
||||||
FiEdit2,
|
|
||||||
FiTrash2,
|
|
||||||
FiSave,
|
|
||||||
FiPlus,
|
|
||||||
FiFileText,
|
|
||||||
FiTarget,
|
|
||||||
FiTrendingUp,
|
|
||||||
FiHash,
|
|
||||||
FiCheckCircle,
|
|
||||||
FiXCircle,
|
|
||||||
FiAlertCircle,
|
|
||||||
} from 'react-icons/fi';
|
|
||||||
import moment from 'moment';
|
|
||||||
import 'moment/locale/zh-cn';
|
|
||||||
import { logger } from '../../../utils/logger';
|
|
||||||
|
|
||||||
moment.locale('zh-cn');
|
|
||||||
|
|
||||||
export default function InvestmentPlansAndReviews({ type = 'both' }) {
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
// 颜色主题
|
|
||||||
const bgColor = useColorModeValue('white', 'gray.800');
|
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
|
||||||
const textColor = useColorModeValue('gray.700', 'white');
|
|
||||||
const secondaryText = useColorModeValue('gray.600', 'gray.400');
|
|
||||||
const cardBg = useColorModeValue('gray.50', 'gray.700');
|
|
||||||
|
|
||||||
const [plans, setPlans] = useState([]);
|
|
||||||
const [reviews, setReviews] = useState([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [editingItem, setEditingItem] = useState(null);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
date: moment().format('YYYY-MM-DD'),
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
type: 'plan',
|
|
||||||
stocks: [],
|
|
||||||
tags: [],
|
|
||||||
status: 'active',
|
|
||||||
});
|
|
||||||
const [stockInput, setStockInput] = useState('');
|
|
||||||
const [tagInput, setTagInput] = useState('');
|
|
||||||
|
|
||||||
// 加载数据
|
|
||||||
const loadData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
|
||||||
|
|
||||||
const response = await fetch(base + '/api/account/investment-plans', {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
const allItems = data.data || [];
|
|
||||||
setPlans(allItems.filter(item => item.type === 'plan'));
|
|
||||||
setReviews(allItems.filter(item => item.type === 'review'));
|
|
||||||
logger.debug('InvestmentPlansAndReviews', '数据加载成功', {
|
|
||||||
plansCount: allItems.filter(item => item.type === 'plan').length,
|
|
||||||
reviewsCount: allItems.filter(item => item.type === 'review').length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('InvestmentPlansAndReviews', 'loadData', error);
|
|
||||||
// ❌ 移除数据加载失败 toast(非关键操作)
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []); // ✅ 移除 toast 依赖
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadData();
|
|
||||||
}, [loadData]);
|
|
||||||
|
|
||||||
// 打开编辑/新建模态框
|
|
||||||
const handleOpenModal = (item = null, itemType = 'plan') => {
|
|
||||||
if (item) {
|
|
||||||
setEditingItem(item);
|
|
||||||
setFormData({
|
|
||||||
...item,
|
|
||||||
date: moment(item.date).format('YYYY-MM-DD'),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setEditingItem(null);
|
|
||||||
setFormData({
|
|
||||||
date: moment().format('YYYY-MM-DD'),
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
type: itemType,
|
|
||||||
stocks: [],
|
|
||||||
tags: [],
|
|
||||||
status: 'active',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onOpen();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存数据
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
|
||||||
|
|
||||||
const url = editingItem
|
|
||||||
? base + `/api/account/investment-plans/${editingItem.id}`
|
|
||||||
: base + '/api/account/investment-plans';
|
|
||||||
|
|
||||||
const method = editingItem ? 'PUT' : 'POST';
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(formData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
logger.info('InvestmentPlansAndReviews', `${editingItem ? '更新' : '创建'}成功`, {
|
|
||||||
itemId: editingItem?.id,
|
|
||||||
title: formData.title,
|
|
||||||
type: formData.type
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
title: editingItem ? '更新成功' : '创建成功',
|
|
||||||
status: 'success',
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
onClose();
|
|
||||||
loadData();
|
|
||||||
} else {
|
|
||||||
throw new Error('保存失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('InvestmentPlansAndReviews', 'handleSave', error, {
|
|
||||||
itemId: editingItem?.id,
|
|
||||||
title: formData?.title
|
|
||||||
});
|
|
||||||
toast({
|
|
||||||
title: '保存失败',
|
|
||||||
description: '无法保存数据',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除数据
|
|
||||||
const handleDelete = async (id) => {
|
|
||||||
if (!window.confirm('确定要删除吗?')) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
|
||||||
|
|
||||||
const response = await fetch(base + `/api/account/investment-plans/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
logger.info('InvestmentPlansAndReviews', '删除成功', { itemId: id });
|
|
||||||
toast({
|
|
||||||
title: '删除成功',
|
|
||||||
status: 'success',
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('InvestmentPlansAndReviews', 'handleDelete', error, { itemId: id });
|
|
||||||
toast({
|
|
||||||
title: '删除失败',
|
|
||||||
status: 'error',
|
|
||||||
duration: 3000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加股票
|
|
||||||
const handleAddStock = () => {
|
|
||||||
if (stockInput.trim() && !formData.stocks.includes(stockInput.trim())) {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
stocks: [...formData.stocks, stockInput.trim()],
|
|
||||||
});
|
|
||||||
setStockInput('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加标签
|
|
||||||
const handleAddTag = () => {
|
|
||||||
if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
tags: [...formData.tags, tagInput.trim()],
|
|
||||||
});
|
|
||||||
setTagInput('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取状态图标和颜色
|
|
||||||
const getStatusInfo = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'completed':
|
|
||||||
return { icon: FiCheckCircle, color: 'green' };
|
|
||||||
case 'cancelled':
|
|
||||||
return { icon: FiXCircle, color: 'red' };
|
|
||||||
default:
|
|
||||||
return { icon: FiAlertCircle, color: 'blue' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染单个卡片
|
|
||||||
const renderCard = (item) => {
|
|
||||||
const statusInfo = getStatusInfo(item.status);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={item.id}
|
|
||||||
bg={cardBg}
|
|
||||||
shadow="sm"
|
|
||||||
_hover={{ shadow: 'md' }}
|
|
||||||
transition="all 0.2s"
|
|
||||||
>
|
|
||||||
<CardBody>
|
|
||||||
<VStack align="stretch" spacing={3}>
|
|
||||||
<Flex justify="space-between" align="start">
|
|
||||||
<VStack align="start" spacing={1} flex={1}>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={item.type === 'plan' ? FiTarget : FiFileText} color="blue.500" />
|
|
||||||
<Text fontWeight="bold" fontSize="lg">
|
|
||||||
{item.title}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Icon as={FiCalendar} boxSize={3} color={secondaryText} />
|
|
||||||
<Text fontSize="sm" color={secondaryText}>
|
|
||||||
{moment(item.date).format('YYYY年MM月DD日')}
|
|
||||||
</Text>
|
|
||||||
<Badge
|
|
||||||
colorScheme={statusInfo.color}
|
|
||||||
variant="subtle"
|
|
||||||
leftIcon={<Icon as={statusInfo.icon} />}
|
|
||||||
>
|
|
||||||
{item.status === 'active' ? '进行中' :
|
|
||||||
item.status === 'completed' ? '已完成' : '已取消'}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
<HStack>
|
|
||||||
<IconButton
|
|
||||||
icon={<FiEdit2 />}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleOpenModal(item)}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={<FiTrash2 />}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
colorScheme="red"
|
|
||||||
onClick={() => handleDelete(item.id)}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{item.content && (
|
|
||||||
<Text fontSize="sm" color={textColor} noOfLines={3}>
|
|
||||||
{item.content}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<HStack spacing={2} flexWrap="wrap">
|
|
||||||
{item.stocks && item.stocks.length > 0 && (
|
|
||||||
<>
|
|
||||||
{item.stocks.map((stock, idx) => (
|
|
||||||
<Tag key={idx} size="sm" colorScheme="blue" variant="subtle">
|
|
||||||
<TagLeftIcon as={FiTrendingUp} />
|
|
||||||
<TagLabel>{stock}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{item.tags && item.tags.length > 0 && (
|
|
||||||
<>
|
|
||||||
{item.tags.map((tag, idx) => (
|
|
||||||
<Tag key={idx} size="sm" colorScheme="purple" variant="subtle">
|
|
||||||
<TagLeftIcon as={FiHash} />
|
|
||||||
<TagLabel>{tag}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Tabs variant="enclosed" colorScheme="blue" defaultIndex={type === 'review' ? 1 : 0}>
|
|
||||||
<TabList>
|
|
||||||
<Tab>
|
|
||||||
<Icon as={FiTarget} mr={2} />
|
|
||||||
我的计划 ({plans.length})
|
|
||||||
</Tab>
|
|
||||||
<Tab>
|
|
||||||
<Icon as={FiFileText} mr={2} />
|
|
||||||
我的复盘 ({reviews.length})
|
|
||||||
</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanels>
|
|
||||||
{/* 计划面板 */}
|
|
||||||
<TabPanel px={0}>
|
|
||||||
<VStack align="stretch" spacing={4}>
|
|
||||||
<Flex justify="flex-end">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="blue"
|
|
||||||
leftIcon={<FiPlus />}
|
|
||||||
onClick={() => handleOpenModal(null, 'plan')}
|
|
||||||
>
|
|
||||||
新建计划
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<Center py={8}>
|
|
||||||
<Spinner size="xl" color="blue.500" />
|
|
||||||
</Center>
|
|
||||||
) : plans.length === 0 ? (
|
|
||||||
<Center py={8}>
|
|
||||||
<VStack spacing={3}>
|
|
||||||
<Icon as={FiTarget} boxSize={12} color="gray.300" />
|
|
||||||
<Text color={secondaryText}>暂无投资计划</Text>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="blue"
|
|
||||||
leftIcon={<FiPlus />}
|
|
||||||
onClick={() => handleOpenModal(null, 'plan')}
|
|
||||||
>
|
|
||||||
创建第一个计划
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4}>
|
|
||||||
{plans.map(renderCard)}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 复盘面板 */}
|
|
||||||
<TabPanel px={0}>
|
|
||||||
<VStack align="stretch" spacing={4}>
|
|
||||||
<Flex justify="flex-end">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="green"
|
|
||||||
leftIcon={<FiPlus />}
|
|
||||||
onClick={() => handleOpenModal(null, 'review')}
|
|
||||||
>
|
|
||||||
新建复盘
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<Center py={8}>
|
|
||||||
<Spinner size="xl" color="blue.500" />
|
|
||||||
</Center>
|
|
||||||
) : reviews.length === 0 ? (
|
|
||||||
<Center py={8}>
|
|
||||||
<VStack spacing={3}>
|
|
||||||
<Icon as={FiFileText} boxSize={12} color="gray.300" />
|
|
||||||
<Text color={secondaryText}>暂无复盘记录</Text>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorScheme="green"
|
|
||||||
leftIcon={<FiPlus />}
|
|
||||||
onClick={() => handleOpenModal(null, 'review')}
|
|
||||||
>
|
|
||||||
创建第一个复盘
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<Grid templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }} gap={4}>
|
|
||||||
{reviews.map(renderCard)}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
{/* 编辑/新建模态框 - 条件渲染 */}
|
|
||||||
{isOpen && (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="xl" closeOnOverlayClick={false} closeOnEsc={true}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>
|
|
||||||
{editingItem ? '编辑' : '新建'}
|
|
||||||
{formData.type === 'plan' ? '投资计划' : '复盘记录'}
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
<VStack spacing={4}>
|
|
||||||
<FormControl isRequired>
|
|
||||||
<FormLabel>日期</FormLabel>
|
|
||||||
<InputGroup>
|
|
||||||
<InputLeftElement pointerEvents="none">
|
|
||||||
<Icon as={FiCalendar} color={secondaryText} />
|
|
||||||
</InputLeftElement>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={formData.date}
|
|
||||||
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
|
|
||||||
/>
|
|
||||||
</InputGroup>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl isRequired>
|
|
||||||
<FormLabel>标题</FormLabel>
|
|
||||||
<Input
|
|
||||||
value={formData.title}
|
|
||||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
|
||||||
placeholder={formData.type === 'plan' ? '例如:布局新能源板块' : '例如:本周交易复盘'}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>内容</FormLabel>
|
|
||||||
<Textarea
|
|
||||||
value={formData.content}
|
|
||||||
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
|
|
||||||
placeholder={formData.type === 'plan' ?
|
|
||||||
'详细描述您的投资计划...' :
|
|
||||||
'记录您的交易心得和经验教训...'}
|
|
||||||
rows={6}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>相关股票</FormLabel>
|
|
||||||
<HStack>
|
|
||||||
<Input
|
|
||||||
value={stockInput}
|
|
||||||
onChange={(e) => setStockInput(e.target.value)}
|
|
||||||
placeholder="输入股票代码"
|
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleAddStock()}
|
|
||||||
/>
|
|
||||||
<Button onClick={handleAddStock}>添加</Button>
|
|
||||||
</HStack>
|
|
||||||
<HStack mt={2} spacing={2} flexWrap="wrap">
|
|
||||||
{formData.stocks.map((stock, idx) => (
|
|
||||||
<Tag key={idx} size="sm" colorScheme="blue">
|
|
||||||
<TagLeftIcon as={FiTrendingUp} />
|
|
||||||
<TagLabel>{stock}</TagLabel>
|
|
||||||
<TagCloseButton
|
|
||||||
onClick={() => setFormData({
|
|
||||||
...formData,
|
|
||||||
stocks: formData.stocks.filter((_, i) => i !== idx)
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>标签</FormLabel>
|
|
||||||
<HStack>
|
|
||||||
<Input
|
|
||||||
value={tagInput}
|
|
||||||
onChange={(e) => setTagInput(e.target.value)}
|
|
||||||
placeholder="输入标签"
|
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleAddTag()}
|
|
||||||
/>
|
|
||||||
<Button onClick={handleAddTag}>添加</Button>
|
|
||||||
</HStack>
|
|
||||||
<HStack mt={2} spacing={2} flexWrap="wrap">
|
|
||||||
{formData.tags.map((tag, idx) => (
|
|
||||||
<Tag key={idx} size="sm" colorScheme="purple">
|
|
||||||
<TagLeftIcon as={FiHash} />
|
|
||||||
<TagLabel>{tag}</TagLabel>
|
|
||||||
<TagCloseButton
|
|
||||||
onClick={() => setFormData({
|
|
||||||
...formData,
|
|
||||||
tags: formData.tags.filter((_, i) => i !== idx)
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</HStack>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<FormLabel>状态</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={formData.status}
|
|
||||||
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value="active">进行中</option>
|
|
||||||
<option value="completed">已完成</option>
|
|
||||||
<option value="cancelled">已取消</option>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button variant="ghost" mr={3} onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
onClick={handleSave}
|
|
||||||
isDisabled={!formData.title || !formData.date}
|
|
||||||
leftIcon={<FiSave />}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user