Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View File

@@ -0,0 +1,434 @@
import React from "react";
import {
Flex,
Text,
Stack,
Grid,
Image,
Button,
Icon,
Input,
Box,
RangeSlider,
RangeSliderTrack,
RangeSliderFilledTrack,
RangeSliderThumb,
useColorModeValue,
} from "@chakra-ui/react";
import Card from "components/Card/Card";
import IconBox from "components/Icons/IconBox";
import { HSeparator, VSeparator } from "components/Separator/Separator";
import Map from "components/Map/Map";
import bgAutomotiveCard from "assets/img/automotive-background-card.png";
import tesla from "assets/img/tesla.png";
import drake from "assets/img/drake.png";
import { FaMap, FaPlay, FaPowerOff } from "react-icons/fa";
import {
AiFillCar,
AiOutlineSearch,
AiFillBackward,
AiFillForward,
AiOutlineUnorderedList,
} from "react-icons/ai";
import { BsBatteryCharging } from "react-icons/bs";
import { IoIosSpeedometer, IoIosMusicalNotes } from "react-icons/io";
import { IoHeadsetSharp, IoChatbubbleEllipsesSharp } from "react-icons/io5";
import { CgAppleWatch } from "react-icons/cg";
import { SpotifyLogo } from "components/Icons/Icons";
const Automotive = () => {
const bgCard = useColorModeValue("linear-gradient(81.62deg, #313860 2.25%, #151928 79.87%)", "navy.800")
return (
<Flex direction="column" pt={{ base: "150px" }}>
<Card
bgImage={bgAutomotiveCard}
bgSize="cover"
p={{ sm: "22px", lg: "60px 40px", xl: "100px 80px" }}
>
<Flex
justify="space-between"
align={{ sm: "center", lg: "normal" }}
w="100%"
direction={{ sm: "column", lg: "row" }}
>
<Flex direction="column" my={{ sm: "10px", lg: "0px" }}>
<Text color="white" fontSize="2xl" fontWeight="bold">
Since Last Charge
</Text>
<HSeparator mt="11px" mb="25px" />
<Stack
direction="row"
spacing="30px"
justify={{ sm: "center", lg: "normal" }}
>
<Flex direction="column">
<Text color="white" fontSize="xs">
Distance
</Text>
<Text color="white" fontSize="2xl" fontWeight="bold">
145{" "}
<Text
as="span"
fontSize="10px"
display="inline-block"
transform="translateY(-50%)"
>
KM
</Text>
</Text>
</Flex>
<Flex direction="column">
<Text color="white" fontSize="xs">
Average Energy
</Text>
<Text color="white" fontSize="2xl" fontWeight="bold">
300{" "}
<Text
as="span"
fontSize="10px"
display="inline-block"
transform="translateY(-50%)"
>
KW
</Text>
</Text>
</Flex>
</Stack>
</Flex>
<Flex
direction="column"
textAlign="center"
my={{ sm: "10px", lg: "0px" }}
>
<Image
src={tesla}
minW={{ md: "300px", lg: "450px" }}
mt={{ lg: "-170px" }}
mb="30px"
display={{ sm: "none", md: "block" }}
/>
<Text color="white" fontSize="lg" fontWeight="bold">
Available Range{" "}
<Text as="span" fontSize="2xl">
70{" "}
<Text
as="span"
fontSize="10px"
display="inline-block"
transform="translateY(-50%)"
>
%
</Text>
</Text>
</Text>
</Flex>
<Flex direction="column" my={{ sm: "10px", lg: "0px" }}>
<Text color="white" fontSize="2xl" fontWeight="bold">
Nearest Charger
</Text>
<HSeparator mt="11px" mb="25px" />
<Flex direction="row">
<Text color="white" fontSize="xs" fontWeight="bold" me="52px">
Miclan, DW <br />
891 Limarenda road
</Text>
<Button
p="9px"
borderRadius="50%"
variant="no-effects"
bg="rgba(255, 255, 255, 0.15)"
border="1px solid #fff"
>
<Icon as={FaMap} color="white" w="14px" h="14px" />
</Button>
</Flex>
</Flex>
</Flex>
</Card>
<Grid
templateColumns={{
sm: "1fr",
md: "repeat(2, 1fr)",
lg: "repeat(4, 1fr)",
}}
my="20px"
gap="20px"
>
<Flex
align="center"
p="18px"
justify="space-between"
bg="linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)"
borderRadius="20px"
>
<Flex direction="column" me="auto">
<Text fontSize="xs" color="white" mb="3px">
Today's Trip
</Text>
<Text color="#fff" fontSize="lg" fontWeight="bold">
143 KM
</Text>
</Flex>
<IconBox bg="white" w="45px" h="45px">
<Icon as={AiFillCar} color="blue.500" w="22px" h="22px" />
</IconBox>
</Flex>
<Flex
align="center"
p="18px"
justify="space-between"
bg="linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)"
borderRadius="20px"
>
<Flex direction="column" me="auto">
<Text fontSize="xs" color="white" mb="3px">
Battery Health
</Text>
<Text color="#fff" fontSize="lg" fontWeight="bold">
99%
</Text>
</Flex>
<IconBox bg="white" w="45px" h="45px">
<Icon as={BsBatteryCharging} color="blue.500" w="22px" h="22px" />
</IconBox>
</Flex>
<Flex
align="center"
p="18px"
justify="space-between"
bg="linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)"
borderRadius="20px"
>
<Flex direction="column" me="auto">
<Text fontSize="xs" color="white" mb="3px">
Average Speed
</Text>
<Text color="#fff" fontSize="lg" fontWeight="bold">
56 KM/h
</Text>
</Flex>
<IconBox bg="white" w="45px" h="45px">
<Icon as={IoIosSpeedometer} color="blue.500" w="22px" h="22px" />
</IconBox>
</Flex>
<Flex
align="center"
p="18px"
justify="space-between"
bg="linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)"
borderRadius="20px"
>
<Flex direction="column" me="auto">
<Text fontSize="xs" color="white" mb="3px">
Music Volume
</Text>
<Text color="#fff" fontSize="lg" fontWeight="bold">
15 / 100
</Text>
</Flex>
<IconBox bg="white" w="45px" h="45px">
<Icon as={IoIosMusicalNotes} color="blue.500" w="22px" h="22px" />
</IconBox>
</Flex>
</Grid>
<Card
bg={bgCard}
px="0px"
>
<Flex
direction={{ sm: "column", md: "row" }}
justify="space-between"
align="center"
px="22px"
>
<Flex align="center">
<Icon
as={AiOutlineSearch}
color="white"
w="20px"
h="20px"
me="8px"
/>
<Input
placeholder="Search anything..."
border={{}}
_hover={{}}
_focus={{}}
color="white"
/>
</Flex>
<Stack direction="row" spacing="10px" align="center">
<Icon as={IoHeadsetSharp} color="white" w="18px" h="18px" />
<Icon as={FaPlay} color="white" w="18px" h="18px" />
<Icon as={FaPowerOff} color="white" w="18px" h="18px" />
<Icon as={CgAppleWatch} color="white" w="18px" h="18px" />
<Text color="white" fontWeight="bold" fontSize="2xl">
10:45
</Text>
</Stack>
</Flex>
<HSeparator my="22px" />
<Flex
direction={{ sm: "column", md: "row" }}
justify="space-between"
align="center"
mb="20px"
px="22px"
>
<Flex align="center">
<Text color="white" fontSize="2xl" fontWeight="bold" me="10px">
11:43
</Text>
<Text color="gray.400" fontSize="xs" fontWeight="bold">
Estimated arrival time
</Text>
</Flex>
<VSeparator
h="30px"
bg="white"
display={{ sm: "none", md: "block" }}
/>
<Flex align="center">
<Text color="white" fontSize="2xl" fontWeight="bold" me="10px">
2.4{" "}
<Text
as="span"
color="white"
fontSize="10px"
display="inline-block"
transform="translateY(-50%)"
>
KM
</Text>
</Text>
<Text color="gray.400" fontSize="xs" fontWeight="bold">
Turn Right in 2.4 miles
</Text>
</Flex>
<VSeparator
h="30px"
bg="white"
display={{ sm: "none", md: "block" }}
/>
<Flex align="center">
<Text color="white" fontSize="2xl" fontWeight="bold" me="10px">
6.7{" "}
<Text
as="span"
color="white"
fontSize="10px"
display="inline-block"
transform="translateY(-50%)"
>
KM
</Text>
</Text>
<Text color="gray.400" fontSize="xs" fontWeight="bold">
Distance to Starbucks
</Text>
</Flex>
</Flex>
<Map />
<Flex
direction={{ sm: "column", lg: "row" }}
justify="space-between"
align="center"
my="20px"
px="22px"
>
<Stack direction="row" spacing="15px" align="center">
<Box position="relative">
<Image src={drake} w="70px" h="70px" borderRadius="50%" />
<SpotifyLogo
w="25px"
h="25px"
position="absolute"
right="-10px"
transform="translateY(-70%)"
/>
</Box>
<Flex direction="column">
<Text color="white" fontSize="sm" fontWeight="bold">
Life Is Good (feat. Drake)
</Text>
<Text color="gray.400" fontSize="sm">
Future, Drake - Hip-Hop
</Text>
</Flex>
</Stack>
<Stack direction="row" spacing="18px" my={{ sm: "15px", lg: "0px" }}>
<Button
variant="outline"
colorScheme="whiteAlpha"
borderRadius="50px"
w="45px"
h="45px"
>
<Icon as={AiFillBackward} color="#fff" w="26px" h="26px" />
</Button>
<Button
variant="outline"
colorScheme="whiteAlpha"
borderRadius="50px"
w="45px"
h="45px"
>
<Icon as={FaPlay} color="#fff" w="18px" h="18px" />
</Button>
<Button
variant="outline"
colorScheme="whiteAlpha"
borderRadius="50px"
w="45px"
h="45px"
>
<Icon as={AiFillForward} color="#fff" w="26px" h="26px" />
</Button>
</Stack>
<Flex align="center">
<Flex direction="column" me="80px">
<Text color="gray.400" fontSize="xs" fontWeight="bold">
Volume
</Text>
<RangeSlider
aria-label={["max"]}
colorScheme="blue"
defaultValue={[30]}
borderRadius="20px"
w={{ sm: "130px", md: "250px", lg: "200px" }}
>
<RangeSliderTrack>
<RangeSliderFilledTrack />
</RangeSliderTrack>
<RangeSliderThumb index={0} />
</RangeSlider>
</Flex>
<Stack direction="row" spacing="12px" align="center">
<Icon
as={AiOutlineUnorderedList}
color="white"
w="18px"
h="18px"
/>
<Icon
as={IoChatbubbleEllipsesSharp}
color="white"
w="18px"
h="18px"
/>
</Stack>
</Flex>
</Flex>
</Card>
</Flex>
);
};
export default Automotive;

539
src/views/Dashboard/CRM.js Normal file
View File

@@ -0,0 +1,539 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import {
Box,
Button,
Flex,
Grid,
Icon,
Image,
Portal,
Spacer,
Stack,
Stat,
StatHelpText,
StatLabel,
StatNumber,
Text,
useColorModeValue,
} from '@chakra-ui/react';
import CRMimage from 'assets/img/CRM-image.png';
import peopleImage from 'assets/img/people-image.png';
import EventCalendar from 'components/Calendars/EventCalendar';
import Card from 'components/Card/Card.js';
import CardHeader from 'components/Card/CardHeader.js';
import LineChart from 'components/Charts/LineChart';
import IconBox from 'components/Icons/IconBox';
import { DocumentIcon, RocketIcon, SettingsIcon } from 'components/Icons/Icons';
import TransactionRow from 'components/Tables/TransactionRow';
import React, { useRef } from 'react';
import { BsArrowRight } from 'react-icons/bs';
import { FaPlus, FaRegCalendarAlt } from 'react-icons/fa';
import { RiArrowDropRightLine } from 'react-icons/ri';
import { calendarDataCRM } from 'variables/calendar';
import {
lineChartDataCRM1,
lineChartDataCRM2,
lineChartOptionsCRM1,
lineChartOptionsCRM2,
} from 'variables/charts';
import { revenueCRM, transactionsCRM } from 'variables/general';
function CRM() {
const textColor = useColorModeValue('gray.700', 'white');
const secondaryColor = useColorModeValue('gray.500', 'white');
const iconBlue = useColorModeValue('gray.800', 'blue.500');
const iconBoxInside = useColorModeValue('white', 'white');
const overlayRef = useRef();
return (
<Flex direction='column' pt={{ sm: '120px', md: '75px' }}>
<Grid
templateColumns={{ sm: '1fr', lg: '1.6fr 1fr', xl: '2fr 1fr' }}
templateRows='1fr'
gap='24px'
mb={{ lg: '24px' }}
>
<Grid
templateColumns='auto'
templateRows={{ sm: '1fr auto', lg: '1fr 2.5fr' }}
gap='24px'
>
<Stack
direction={{ sm: 'column', md: 'row' }}
spacing='24px'
maxH={{ lg: '220px' }}
>
<Card px='0px' pb='0px'>
<CardHeader px='22px'>
<Stat>
<StatLabel fontSize='xs' color='gray.400'>
Visitors
</StatLabel>
<Flex>
<StatNumber fontSize='lg' me='4px' color={textColor}>
$5,927
</StatNumber>
<StatHelpText
color='green.400'
size='sm'
alignSelf='flex-end'
fontWeight='bold'
mb='0px'
>
+55%
</StatHelpText>
</Flex>
</Stat>
</CardHeader>
<Flex direction='column'>
<Box w='100%' h='100%'>
<LineChart
chartData={lineChartDataCRM1}
chartOptions={lineChartOptionsCRM1}
/>
</Box>
</Flex>
</Card>
<Card px='0px' pb='0px'>
<CardHeader px='22px'>
<Stat>
<StatLabel fontSize='xs' color='gray.400'>
Income
</StatLabel>
<Flex>
<StatNumber fontSize='lg' me='4px' color={textColor}>
$130,912
</StatNumber>
<StatHelpText
color='green.400'
size='sm'
alignSelf='flex-end'
fontWeight='bold'
mb='0px'
>
+90%
</StatHelpText>
</Flex>
</Stat>
</CardHeader>
<Flex direction='column'>
<Box w='100%'>
<LineChart
chartData={lineChartDataCRM2}
chartOptions={lineChartOptionsCRM2}
/>
</Box>
</Flex>
</Card>
<Card p='0px'>
<Button
p='0px'
w='100%'
h='100%'
bg='transparent'
color='gray.500'
borderRadius='15px'
>
<Flex
direction='column'
justifyContent='center'
align='center'
h='120px'
>
<Icon
as={FaPlus}
w='30px'
h='30px'
mb='12px'
color={secondaryColor}
/>
<Text fontSize='lg' fontWeight='bold' color={secondaryColor}>
New Tab
</Text>
</Flex>
</Button>
</Card>
</Stack>
<Card w={{ sm: '100%' }} h='575px'>
<CardHeader pt='6px' mb='8px'>
<Flex direction='column'>
<Text fontSize='lg' color={textColor} fontWeight='bold'>
Calendar
</Text>
<Text fontSize='sm' color='gray.400' fontWeight='normal'>
Wednesday, 2022
</Text>
</Flex>
</CardHeader>
<Flex
direction='column'
position='relative'
display='block'
height='100%'
>
<EventCalendar
initialDate='2022-10-01'
calendarData={calendarDataCRM}
/>
</Flex>
</Card>
</Grid>
<Stack direction='column' spacing='24px'>
<Card w={{ sm: '100%' }} minH={{ lg: '300px' }}>
<Flex
direction='column'
backgroundImage={peopleImage}
bgPosition='center'
bgRepeat='no-repeat'
w='100%'
h='100%'
minH={{ sm: '200px', lg: '100%' }}
bgSize='cover'
position='relative'
borderRadius='15px'
>
<Box
bg='linear-gradient(360deg, rgba(49, 56, 96, 0.16) 0%, rgba(21, 25, 40, 0.88) 100%)'
w='100%'
position='absolute'
h='100%'
borderRadius='inherit'
ref={overlayRef}
></Box>
<Portal containerRef={overlayRef}>
<Flex
flexDirection='column'
color='white'
p='24px 20px 4px 20px'
lineHeight='1.6'
h={{ md: '190px', lg: '240px' }}
>
<Text fontSize='lg' fontWeight='bold' pb='6px'>
Hello John!
</Text>
<Text fontSize='sm' fontWeight='normal' w={{ lg: '92%' }}>
Wealth creation is a revolutionary recent positive-sum game.
It is all about who takes the opportunity first.
</Text>
<Spacer />
<Flex
align='center'
mt={{ sm: '20px', lg: '40px', xl: '80px' }}
>
<Button
ps='0px'
pb={{ xl: '22px' }}
variant='no-effects'
bg='transparent'
>
<Text
fontSize='sm'
fontWeight='bold'
_hover={{ me: '4px' }}
color='#fff'
transition='all .5s ease'
>
Read more
</Text>
<Icon
as={BsArrowRight}
w='12px'
h='12px'
fontSize='xl'
transition='all .5s ease'
mx='4px'
color='#fff'
cursor='pointer'
_hover={{ transform: 'translateX(20%)' }}
/>
</Button>
</Flex>
</Flex>
</Portal>
</Flex>
</Card>
<Stack
direction={{ sm: 'column', md: 'row', lg: 'column' }}
maxW={{ md: '100%' }}
spacing='24px'
>
<Card>
<CardHeader>
<Text fontSize='lg' text={textColor} fontWeight='bold'>
Invoices
</Text>
</CardHeader>
<Flex direction='column' w='100%' pt='28px'>
<Stack direction='column' spacing='24px' w='100%'>
<Flex align='center' w='100%'>
<Flex align='center'>
<IconBox h={'40px'} w={'40px'} bg={iconBlue} me='18px'>
<RocketIcon
h={'20px'}
w={'20px'}
color={iconBoxInside}
/>
</IconBox>
<Flex direction='column'>
<Text fontSize='sm' fontWeight='bold' color={textColor}>
Devices
</Text>
<Text color='gray.400' fontSize='xs'>
250 in stock,{' '}
<Text as='span' fontWeight='bold'>
346+ sold
</Text>
</Text>
</Flex>
</Flex>
<Spacer />
<Button variant='no-effects' px='0px'>
<Icon
as={RiArrowDropRightLine}
color='gray.400'
w='30px'
h='30px'
cursor='pointer'
transition='all .25s ease'
_hover={{ transform: 'translateX(25%)' }}
/>
</Button>
</Flex>
<Flex align='center' w='100%'>
<Flex align='center'>
<IconBox h={'40px'} w={'40px'} bg={iconBlue} me='18px'>
<SettingsIcon
h={'20px'}
w={'20px'}
color={iconBoxInside}
/>
</IconBox>
<Flex direction='column'>
<Text fontSize='sm' fontWeight='bold' color={textColor}>
Tickets
</Text>
<Text color='gray.400' fontSize='xs'>
123 closed,{' '}
<Text as='span' fontWeight='bold'>
15 open
</Text>
</Text>
</Flex>
</Flex>
<Spacer />
<Button variant='no-effects' px='0px'>
<Icon
as={RiArrowDropRightLine}
color='gray.400'
w='30px'
h='30px'
cursor='pointer'
transition='all .25s ease'
_hover={{ transform: 'translateX(25%)' }}
/>
</Button>
</Flex>
<Flex align='center' w='100%'>
<Flex align='center'>
<IconBox h={'40px'} w={'40px'} bg={iconBlue} me='18px'>
<DocumentIcon
h={'20px'}
w={'20px'}
color={iconBoxInside}
/>
</IconBox>
<Flex direction='column'>
<Text fontSize='sm' fontWeight='bold' color={textColor}>
Error logs
</Text>
<Text color='gray.400' fontSize='xs'>
1 is active,{' '}
<Text as='span' fontWeight='bold'>
40 closed
</Text>
</Text>
</Flex>
</Flex>
<Spacer />
<Button variant='no-effects' px='0px'>
<Icon
as={RiArrowDropRightLine}
color='gray.400'
w='30px'
h='30px'
cursor='pointer'
transition='all .25s ease'
_hover={{ transform: 'translateX(25%)' }}
/>
</Button>
</Flex>
</Stack>
</Flex>
</Card>
<Card maxH={{ md: '130px', lg: '100%' }}>
<Flex
direction={{ sm: 'column', md: 'row', lg: 'row' }}
align='center'
>
<Box
minW={{
sm: '80px',
lg: '100px',
xl: '130px',
'2xl': '170px',
}}
h={{ sm: '80px', lg: '100px', xl: '130px', '2xl': '170px' }}
me={{ md: '36px' }}
mb={{ sm: '12px', md: '0px' }}
>
<Image src={CRMimage} w='100%' h='100%' borderRadius='15px' />
</Box>
<Flex
direction='column'
justify='center'
align={{ sm: 'center', md: 'flex-start' }}
>
<Text
fontWeight='bold'
textAlign={{ sm: 'center', md: 'start' }}
color={secondaryColor}
fontSize={{ sm: 'sm', md: 'xs', lg: 'sm' }}
mb={{ sm: '10px', lg: '22px' }}
>
Today's Martina's Birthday. Wish her the best of luck!
</Text>
<Button
h={{ sm: '32px' }}
variant='primary'
p={{ sm: '0px 32px', lg: '6px 22px' }}
>
SEND MESSAGE
</Button>
</Flex>
</Flex>
</Card>
</Stack>
</Stack>
</Grid>
<Stack
direction={{ sm: 'column', lg: 'row' }}
spacing='24px'
mt={{ sm: '24px', lg: '0px' }}
>
<Card>
<CardHeader mb='12px'>
<Flex direction='column' w='100%'>
<Flex
direction={{ sm: 'column', lg: 'row' }}
justify={{ sm: 'center', lg: 'space-between' }}
align={{ sm: 'center' }}
w='100%'
my={{ md: '12px' }}
>
<Text
color={textColor}
fontSize={{ sm: 'lg', md: 'xl', lg: 'lg' }}
fontWeight='bold'
>
Your Transactions
</Text>
<Flex align='center'>
<Icon
as={FaRegCalendarAlt}
color='gray.400'
fontSize='md'
me='6px'
></Icon>
<Text color='gray.400' fontSize='sm' fontWeight='semibold'>
23 - 30 March 2022
</Text>
</Flex>
</Flex>
</Flex>
</CardHeader>
<Flex direction='column'>
<Flex direction='column' w='100%' justify='center'>
{transactionsCRM.map((row, index) => {
return (
<TransactionRow
name={row.name}
logo={row.logo}
date={row.date}
price={row.price}
key={index}
/>
);
})}
</Flex>
</Flex>
</Card>
<Card>
<CardHeader mb='12px'>
<Flex direction='column' w='100%'>
<Flex
direction={{ sm: 'column', lg: 'row' }}
justify={{ sm: 'center', lg: 'space-between' }}
align={{ sm: 'center' }}
w='100%'
my={{ md: '12px' }}
>
<Text
color={textColor}
fontSize={{ sm: 'lg', md: 'xl', lg: 'lg' }}
fontWeight='bold'
>
Revenue
</Text>
<Flex align='center'>
<Icon
as={FaRegCalendarAlt}
color='gray.400'
fontSize='md'
me='6px'
></Icon>
<Text color='gray.400' fontSize='sm' fontWeight='semibold'>
23 - 30 March 2022
</Text>
</Flex>
</Flex>
</Flex>
</CardHeader>
<Flex direction='column'>
<Flex direction='column' w='100%' justify='center'>
{revenueCRM.map((row, index) => {
return (
<TransactionRow
name={row.name}
logo={row.logo}
date={row.date}
price={row.price}
key={index}
/>
);
})}
</Flex>
</Flex>
</Card>
</Stack>
</Flex>
);
}
export default CRM;

View File

@@ -0,0 +1,734 @@
// src/views/Dashboard/Center.js
import React, { useEffect, useState, useCallback } from 'react';
import {
Box,
Flex,
Grid,
SimpleGrid,
Stack,
Text,
Badge,
Button,
VStack,
HStack,
Card,
CardHeader,
CardBody,
Heading,
useColorModeValue,
Icon,
IconButton,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
Divider,
Tag,
TagLabel,
TagLeftIcon,
Wrap,
WrapItem,
Avatar,
Tooltip,
Progress,
useToast,
LinkBox,
LinkOverlay,
Spinner,
Center,
Image,
} from '@chakra-ui/react';
import { useAuth } from '../../contexts/AuthContext';
import { useLocation, useNavigate, Link } from 'react-router-dom';
import {
FiTrendingUp,
FiEye,
FiMessageSquare,
FiThumbsUp,
FiClock,
FiCalendar,
FiRefreshCw,
FiTrash2,
FiExternalLink,
FiPlus,
FiBarChart2,
FiStar,
FiActivity,
FiAlertCircle,
} from 'react-icons/fi';
import MyFutureEvents from './components/MyFutureEvents';
import InvestmentCalendarChakra from './components/InvestmentCalendarChakra';
import InvestmentPlansAndReviews from './components/InvestmentPlansAndReviews';
export default function CenterDashboard() {
const { user } = useAuth();
const location = useLocation();
const navigate = useNavigate();
const toast = useToast();
// 颜色主题
const textColor = useColorModeValue('gray.700', 'white');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const bgColor = useColorModeValue('white', 'gray.800');
const hoverBg = useColorModeValue('gray.50', 'gray.700');
const secondaryText = useColorModeValue('gray.600', 'gray.400');
const cardBg = useColorModeValue('white', 'gray.800');
const sectionBg = useColorModeValue('gray.50', 'gray.900');
const [watchlist, setWatchlist] = useState([]);
const [realtimeQuotes, setRealtimeQuotes] = useState({});
const [followingEvents, setFollowingEvents] = useState([]);
const [eventComments, setEventComments] = useState([]);
const [subscriptionInfo, setSubscriptionInfo] = useState({ type: 'free', status: 'active', days_left: 999, is_active: true });
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [quotesLoading, setQuotesLoading] = useState(false);
const loadData = useCallback(async () => {
try {
setRefreshing(true);
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
const ts = Date.now();
const [w, e, c, s] = await Promise.all([
fetch(base + `/api/account/watchlist?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/account/events/following?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/account/events/comments?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/subscription/current?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
]);
const jw = await w.json();
const je = await e.json();
const jc = await c.json();
const js = await s.json();
if (jw.success) {
setWatchlist(Array.isArray(jw.data) ? jw.data : []);
// 加载实时行情
if (jw.data && jw.data.length > 0) {
loadRealtimeQuotes();
}
}
if (je.success) setFollowingEvents(Array.isArray(je.data) ? je.data : []);
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
if (js.success) setSubscriptionInfo(js.data);
} catch (err) {
console.warn('加载个人中心数据失败', err);
toast({
title: '数据加载失败',
description: '请检查网络连接后重试',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
setRefreshing(false);
}
}, [user, toast]);
// 加载实时行情
const loadRealtimeQuotes = useCallback(async () => {
try {
setQuotesLoading(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/watchlist/realtime', {
credentials: 'include',
cache: 'no-store',
headers: { 'Cache-Control': 'no-cache' }
});
if (response.ok) {
const data = await response.json();
if (data.success) {
const quotesMap = {};
data.data.forEach(item => {
quotesMap[item.stock_code] = item;
});
setRealtimeQuotes(quotesMap);
}
}
} catch (error) {
console.error('加载实时行情失败:', error);
} finally {
setQuotesLoading(false);
}
}, []);
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now - date);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 1) {
const diffHours = Math.ceil(diffTime / (1000 * 60 * 60));
if (diffHours < 1) {
const diffMinutes = Math.ceil(diffTime / (1000 * 60));
return `${diffMinutes}分钟前`;
}
return `${diffHours}小时前`;
} else if (diffDays < 7) {
return `${diffDays}天前`;
} else {
return date.toLocaleDateString('zh-CN');
}
};
// 格式化数字
const formatNumber = (num) => {
if (!num) return '0';
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toString();
};
// 获取事件热度颜色
const getHeatColor = (score) => {
if (score >= 80) return 'red';
if (score >= 60) return 'orange';
if (score >= 40) return 'yellow';
return 'green';
};
useEffect(() => {
if (user && location.pathname.includes('/home/center')) {
loadData();
}
const onVis = () => {
if (document.visibilityState === 'visible' && location.pathname.includes('/home/center')) {
loadData();
}
};
document.addEventListener('visibilitychange', onVis);
return () => document.removeEventListener('visibilitychange', onVis);
}, [user, location.pathname, loadData]);
// 定时刷新实时行情(每分钟一次)
useEffect(() => {
if (watchlist.length > 0) {
const interval = setInterval(() => {
loadRealtimeQuotes();
}, 60000); // 60秒刷新一次
return () => clearInterval(interval);
}
}, [watchlist.length, loadRealtimeQuotes]);
// 渲染加载状态
if (loading) {
return (
<Center h="60vh">
<VStack spacing={4}>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text color={secondaryText}>加载个人中心数据...</Text>
</VStack>
</Center>
);
}
return (
<Box bg={sectionBg} minH="100vh">
<Box px={{ base: 4, md: 8 }} py={6} maxW="1400px" mx="auto">
{/* 头部 */}
<Flex justify="space-between" align="center" mb={8}>
<VStack align="start" spacing={1}>
<Heading size="lg" color={textColor}>
个人中心
</Heading>
<Text color={secondaryText} fontSize="sm">
管理您的自选股事件关注和互动记录
</Text>
</VStack>
<Button
leftIcon={<FiRefreshCw />}
onClick={loadData}
isLoading={refreshing}
loadingText="刷新中"
variant="solid"
colorScheme="blue"
size="sm"
>
刷新数据
</Button>
</Flex>
{/* 统计卡片 */}
<SimpleGrid columns={{ base: 1, sm: 2, md: 4 }} spacing={4} mb={8}>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>自选股票</StatLabel>
<StatNumber fontSize="2xl">{watchlist.length}</StatNumber>
<StatHelpText>
<Icon as={FiTrendingUp} color="green.500" mr={1} />
关注市场动态
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>关注事件</StatLabel>
<StatNumber fontSize="2xl">{followingEvents.length}</StatNumber>
<StatHelpText>
<Icon as={FiActivity} color="blue.500" mr={1} />
追踪热点事件
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>我的评论</StatLabel>
<StatNumber fontSize="2xl">{eventComments.length}</StatNumber>
<StatHelpText>
<Icon as={FiMessageSquare} color="purple.500" mr={1} />
参与讨论
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm" cursor="pointer" onClick={() => navigate('/home/pages/account/subscription')} _hover={{ transform: 'translateY(-2px)', shadow: 'lg' }} transition="all 0.2s">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>订阅状态</StatLabel>
<StatNumber fontSize="xl" color={subscriptionInfo.type === 'free' ? 'gray.500' : subscriptionInfo.type === 'pro' ? 'blue.500' : 'purple.500'}>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
</StatNumber>
<StatHelpText>
<Icon as={FiStar} color={subscriptionInfo.type === 'free' ? 'gray.400' : 'orange.400'} mr={1} />
{subscriptionInfo.type === 'free' ? '点击升级' : `剩余${subscriptionInfo.days_left}`}
</StatHelpText>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
{/* 投资日历 */}
<Box mb={8}>
<InvestmentCalendarChakra />
</Box>
{/* 主要内容区域 */}
<Grid templateColumns={{ base: '1fr', lg: '1fr 2fr' }} gap={6}>
{/* 左侧:自选股 */}
<VStack spacing={6} align="stretch">
<Card bg={cardBg} shadow="md">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
<Icon as={FiBarChart2} color="blue.500" boxSize={5} />
<Heading size="md">自选股票</Heading>
<Badge colorScheme="blue" variant="subtle">
{watchlist.length}
</Badge>
{quotesLoading && <Spinner size="sm" color="blue.500" />}
</HStack>
<HStack>
<IconButton
icon={<FiRefreshCw />}
variant="ghost"
size="sm"
onClick={loadRealtimeQuotes}
isLoading={quotesLoading}
aria-label="刷新行情"
/>
<IconButton
icon={<FiPlus />}
variant="ghost"
size="sm"
onClick={() => navigate('/stock-analysis/overview')}
aria-label="添加自选股"
/>
</HStack>
</Flex>
</CardHeader>
<CardBody pt={0}>
{watchlist.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
<Icon as={FiBarChart2} boxSize={12} color="gray.300" />
<Text color={secondaryText} fontSize="sm">
暂无自选股
</Text>
<Button
size="sm"
variant="outline"
colorScheme="blue"
onClick={() => navigate('/stock-analysis/overview')}
>
添加自选股
</Button>
</VStack>
</Center>
) : (
<VStack align="stretch" spacing={2}>
{watchlist.slice(0, 10).map((stock) => (
<LinkBox
key={stock.id}
p={3}
borderRadius="md"
_hover={{ bg: hoverBg }}
transition="all 0.2s"
cursor="pointer"
>
<HStack justify="space-between">
<VStack align="start" spacing={0}>
<LinkOverlay
as={Link}
to={`/stock-analysis/company?scode=${stock.stock_code}`}
>
<Text fontWeight="medium" fontSize="sm">
{stock.stock_name || stock.stock_code}
</Text>
</LinkOverlay>
<HStack spacing={2}>
<Badge variant="subtle" fontSize="xs">
{stock.stock_code}
</Badge>
{realtimeQuotes[stock.stock_code] ? (
<Badge
colorScheme={realtimeQuotes[stock.stock_code].change_percent > 0 ? 'red' : 'green'}
fontSize="xs"
>
{realtimeQuotes[stock.stock_code].change_percent > 0 ? '+' : ''}
{realtimeQuotes[stock.stock_code].change_percent.toFixed(2)}%
</Badge>
) : stock.change_percent ? (
<Badge
colorScheme={stock.change_percent > 0 ? 'red' : 'green'}
fontSize="xs"
>
{stock.change_percent > 0 ? '+' : ''}
{stock.change_percent}%
</Badge>
) : null}
</HStack>
</VStack>
<VStack align="end" spacing={0}>
<Text fontWeight="bold" fontSize="sm">
{realtimeQuotes[stock.stock_code]?.current_price?.toFixed(2) || stock.current_price || '--'}
</Text>
<Text fontSize="xs" color={secondaryText}>
{realtimeQuotes[stock.stock_code]?.update_time || stock.industry || '未分类'}
</Text>
</VStack>
</HStack>
</LinkBox>
))}
{watchlist.length > 10 && (
<Button
size="sm"
variant="ghost"
onClick={() => navigate('/stock-analysis/overview')}
>
查看全部 ({watchlist.length})
</Button>
)}
</VStack>
)}
</CardBody>
</Card>
{/* 订阅管理 */}
<Card bg={cardBg} shadow="md">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
<Icon as={FiStar} color={subscriptionInfo.type === 'free' ? 'gray.500' : subscriptionInfo.type === 'pro' ? 'blue.500' : 'purple.500'} boxSize={5} />
<Heading size="md">我的订阅</Heading>
<Badge
colorScheme={subscriptionInfo.type === 'free' ? 'gray' : subscriptionInfo.type === 'pro' ? 'blue' : 'purple'}
variant="subtle"
>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
</Badge>
</HStack>
<Button
size="sm"
variant="ghost"
colorScheme={subscriptionInfo.type === 'free' ? 'blue' : 'purple'}
onClick={() => navigate('/home/pages/account/subscription')}
>
{subscriptionInfo.type === 'free' ? '升级' : '管理'}
</Button>
</Flex>
</CardHeader>
<CardBody pt={0}>
<VStack align="stretch" spacing={4}>
<Box p={4} borderRadius="md" bg={subscriptionInfo.type === 'free' ? 'gray.50' : subscriptionInfo.type === 'pro' ? 'blue.50' : 'purple.50'} border="1px" borderColor={subscriptionInfo.type === 'free' ? 'gray.200' : subscriptionInfo.type === 'pro' ? 'blue.200' : 'purple.200'}>
<HStack justify="space-between">
<VStack align="start" spacing={1}>
<Text fontSize="sm" fontWeight="medium" color={textColor}>
当前套餐
</Text>
<Text fontSize="lg" fontWeight="bold" color={subscriptionInfo.type === 'free' ? 'gray.600' : subscriptionInfo.type === 'pro' ? 'blue.600' : 'purple.600'}>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版本' : 'Max版本'}
</Text>
</VStack>
<VStack align="end" spacing={1}>
<Text fontSize="sm" color={secondaryText}>
{subscriptionInfo.type === 'free' ? '永久免费' : subscriptionInfo.is_active ? '已激活' : '已过期'}
</Text>
{subscriptionInfo.type !== 'free' && (
<Text fontSize="xs" color={subscriptionInfo.days_left > 7 ? 'green.500' : 'orange.500'}>
剩余 {subscriptionInfo.days_left}
</Text>
)}
</VStack>
</HStack>
</Box>
{subscriptionInfo.type === 'free' ? (
<VStack spacing={2}>
<Text fontSize="sm" color={secondaryText} textAlign="center">
升级到Pro或Max版本解锁更多功能
</Text>
<HStack spacing={2}>
<Button size="xs" colorScheme="blue" variant="outline" onClick={() => navigate('/home/pages/account/subscription')}>
Pro ¥0.01/
</Button>
<Button size="xs" colorScheme="purple" variant="outline" onClick={() => navigate('/home/pages/account/subscription')}>
Max ¥0.1/
</Button>
</HStack>
</VStack>
) : (
<Box textAlign="center">
<Text fontSize="sm" color={subscriptionInfo.is_active ? 'green.600' : 'orange.600'}>
{subscriptionInfo.is_active ? '订阅服务正常' : '订阅已过期,请续费'}
</Text>
</Box>
)}
</VStack>
</CardBody>
</Card>
</VStack>
{/* 右侧:事件相关 */}
<VStack spacing={6} align="stretch">
{/* 关注事件 */}
<Card bg={cardBg} shadow="md">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
<Icon as={FiStar} color="yellow.500" boxSize={5} />
<Heading size="md">关注事件</Heading>
<Badge colorScheme="yellow" variant="subtle">
{followingEvents.length}
</Badge>
</HStack>
<Button
size="sm"
variant="ghost"
onClick={() => navigate('/community')}
>
查看更多
</Button>
</Flex>
</CardHeader>
<CardBody pt={0}>
{followingEvents.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
<Icon as={FiActivity} boxSize={12} color="gray.300" />
<Text color={secondaryText} fontSize="sm">
暂无关注事件
</Text>
<Button
size="sm"
variant="outline"
colorScheme="blue"
onClick={() => navigate('/community')}
>
探索事件
</Button>
</VStack>
</Center>
) : (
<VStack align="stretch" spacing={3}>
{followingEvents.slice(0, 5).map((event) => (
<LinkBox
key={event.id}
p={4}
borderRadius="lg"
border="1px"
borderColor={borderColor}
_hover={{ shadow: 'md', transform: 'translateY(-2px)' }}
transition="all 0.2s"
>
<VStack align="stretch" spacing={3}>
<LinkOverlay
as={Link}
to={`/event-detail/${event.id}`}
>
<Text fontWeight="medium" fontSize="md" noOfLines={2}>
{event.title}
</Text>
</LinkOverlay>
{/* 事件标签 */}
{event.tags && event.tags.length > 0 && (
<Wrap>
{event.tags.slice(0, 3).map((tag, idx) => (
<WrapItem key={idx}>
<Tag size="sm" variant="subtle" colorScheme="blue">
<TagLabel>{tag}</TagLabel>
</Tag>
</WrapItem>
))}
</Wrap>
)}
{/* 事件统计 */}
<HStack spacing={4} fontSize="sm" color={secondaryText}>
<HStack spacing={1}>
<Icon as={FiEye} />
<Text>{formatNumber(event.view_count || 0)}</Text>
</HStack>
<HStack spacing={1}>
<Icon as={FiMessageSquare} />
<Text>{formatNumber(event.comment_count || 0)}</Text>
</HStack>
<HStack spacing={1}>
<Icon as={FiThumbsUp} />
<Text>{formatNumber(event.upvote_count || 0)}</Text>
</HStack>
{event.heat_score && (
<Badge colorScheme={getHeatColor(event.heat_score)} variant="subtle">
热度 {event.heat_score}
</Badge>
)}
</HStack>
{/* 事件信息 */}
<Flex justify="space-between" align="center">
<HStack spacing={2} fontSize="xs" color={secondaryText}>
<Avatar
size="xs"
name={event.creator?.username || '系统'}
src={event.creator?.avatar_url}
/>
<Text>{event.creator?.username || '系统'}</Text>
<Text>·</Text>
<Text>{formatDate(event.created_at)}</Text>
</HStack>
{event.exceed_expectation_score && (
<Badge
colorScheme={event.exceed_expectation_score > 70 ? 'red' : 'orange'}
variant="solid"
fontSize="xs"
>
超预期 {event.exceed_expectation_score}
</Badge>
)}
</Flex>
</VStack>
</LinkBox>
))}
{followingEvents.length > 5 && (
<Button
size="sm"
variant="ghost"
onClick={() => navigate('/community')}
>
查看全部 ({followingEvents.length})
</Button>
)}
</VStack>
)}
</CardBody>
</Card>
{/* 移除“未来事件”板块,根据需求不再展示 */}
{/* 我的评论 */}
<Card bg={cardBg} shadow="md">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
<Icon as={FiMessageSquare} color="purple.500" boxSize={5} />
<Heading size="md">我的评论</Heading>
<Badge colorScheme="purple" variant="subtle">
{eventComments.length}
</Badge>
</HStack>
</Flex>
</CardHeader>
<CardBody pt={0}>
{eventComments.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
<Icon as={FiMessageSquare} boxSize={12} color="gray.300" />
<Text color={secondaryText} fontSize="sm">
暂无评论记录
</Text>
<Text color={secondaryText} fontSize="xs" textAlign="center">
参与事件讨论分享您的观点
</Text>
</VStack>
</Center>
) : (
<VStack align="stretch" spacing={3}>
{eventComments.slice(0, 5).map((comment) => (
<Box
key={comment.id}
p={3}
borderRadius="md"
border="1px"
borderColor={borderColor}
_hover={{ bg: hoverBg }}
transition="all 0.2s"
>
<VStack align="stretch" spacing={2}>
<Text fontSize="sm" noOfLines={3}>
{comment.content}
</Text>
<HStack justify="space-between" fontSize="xs" color={secondaryText}>
<HStack>
<Icon as={FiClock} />
<Text>{formatDate(comment.created_at)}</Text>
</HStack>
{comment.event_title && (
<Tooltip label={comment.event_title}>
<Badge variant="subtle" fontSize="xs">
{comment.event_title.slice(0, 20)}...
</Badge>
</Tooltip>
)}
</HStack>
</VStack>
</Box>
))}
{eventComments.length > 5 && (
<Text fontSize="sm" color={secondaryText} textAlign="center">
{eventComments.length} 条评论
</Text>
)}
</VStack>
)}
</CardBody>
</Card>
</VStack>
</Grid>
{/* 我的复盘和计划 */}
<Box mt={8}>
<InvestmentPlansAndReviews />
</Box>
</Box>
</Box>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import { Box, Image, Link } from "@chakra-ui/react";
import Landing1 from "assets/img/Landing1.png";
import Landing2 from "assets/img/Landing2.png";
import Landing3 from "assets/img/Landing3.png";
import React from "react";
export default function Default() {
return (
<Box
flexDirection='column'
pt={{ base: "0px", lg: "0px", xl: "70px", "2xl": "0px" }}
m='auto'>
<Box
bg='linear-gradient(180deg, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0.25) 100%)'
backdropFilter='blur(20px)'
borderRadius={{ base: "12px", lg: "24px" }}
p={{ base: "15px", lg: "20px" }}
mx='auto'
mb={{ base: "20px", lg: "20px" }}
width={{
base: "95% !important",
xl: "80% !important",
"2xl": "92% !important",
}}
zIndex='3'
position={{ lg: "absolute" }}
transform={{
lg:
"scale(0.8) perspective(2000px) rotateY(-35deg) rotateX(2deg) rotate(0deg)",
xl:
"scale(0.9) perspective(2000px) rotateY(-35deg) rotateX(2deg) rotate(0deg)",
"2xl":
"scale(0.8) perspective(2000px) rotateY(-35deg) rotateX(2deg) rotate(0deg)",
}}
right={{ lg: "125px", xl: "290px", "2xl": "250px" }}
_hover={{
transform: "scale(0.85) rotateY(-25deg) ",
right: "225px",
}}
transition='0.3s linear'>
<Link href='#'>
<Image src={Landing1} borderRadius={{ base: "12px", lg: "24px" }} />
</Link>
</Box>
<Box
bg='linear-gradient(180deg, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0.25) 100%)'
backdropFilter='blur(20px)'
borderRadius={{ base: "12px", lg: "24px" }}
p={{ base: "15px", lg: "20px" }}
mx='auto'
mb={{ base: "20px", lg: "20px" }}
width={{
base: "95% !important",
xl: "80% !important",
"2xl": "92% !important",
}}
zIndex='2'
position={{ lg: "absolute" }}
transform={{
lg:
"scale(.75) perspective(2000px) rotateY(-32deg) rotateX(2deg) rotate(0deg)",
xl:
"scale(.85) perspective(2000px) rotateY(-32deg) rotateX(2deg) rotate(0deg)",
"2xl":
"scale(.75) perspective(2000px) rotateY(-32deg) rotateX(2deg) rotate(0deg)",
}}
right={{ lg: "55px", xl: "120px", "2xl": "80px" }}>
<Image src={Landing2} borderRadius={{ base: "12px", lg: "24px" }} />
</Box>
<Box
bg='linear-gradient(180deg, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0.25) 100%)'
backdropFilter='blur(20px)'
borderRadius={{ base: "12px", lg: "24px" }}
p={{ base: "15px", lg: "20px" }}
mx='auto'
mb={{ base: "20px", lg: "20px" }}
width={{
base: "95% !important",
xl: "80% !important",
"2xl": "92% !important",
}}
zIndex='1'
position={{ lg: "absolute" }}
transform={{
lg:
"scale(.7) perspective(2000px) rotateY(-30deg) rotateX(2deg) rotate(0deg)",
xl:
"scale(.8) perspective(2000px) rotateY(-30deg) rotateX(2deg) rotate(0deg)",
"2xl":
"scale(.7) perspective(2000px) rotateY(-30deg) rotateX(2deg) rotate(0deg)",
}}
right={{ lg: "-25px", xl: "-10px", "2xl": "-90px" }}>
<Image src={Landing3} borderRadius={{ base: "12px", lg: "24px" }} />
</Box>
</Box>
);
}

View File

@@ -0,0 +1,532 @@
import React, { useState } from "react";
import {
Box,
Button,
Flex,
Grid,
Icon,
Image,
Menu,
MenuButton,
MenuItem,
MenuList,
Stack,
Switch,
Text,
useColorModeValue,
useDisclosure,
useColorMode,
} from "@chakra-ui/react";
import bgWeather from "assets/img/BgMusicCard.png";
import bgWeatherDark from "assets/img/bgMusicCardDark.png";
import smartHome from "assets/img/smart-home.png";
import sunBehindCloud from "assets/img/sun-behind-cloud.png";
import Card from "components/Card/Card";
import BarChart from "components/Charts/BarChart";
import { HSeparator } from "components/Separator/Separator";
import VisxPieChart from "components/VisxPieChart/VisxPieChart";
import CircularSlider from "react-circular-slider-svg";
import { AiOutlineInfoCircle } from "react-icons/ai";
import { BiWater, BiWifi } from "react-icons/bi";
import { BsFillRecordCircleFill, BsThermometerHigh } from "react-icons/bs";
import { FaPlus, FaSnowflake } from "react-icons/fa";
import { IoBulbOutline, IoEllipsisVerticalSharp } from "react-icons/io5";
import {
barChartDataSmartHome,
barChartOptionsSmartHome,
} from "variables/charts";
import { rooms } from "variables/general";
const SmartHome = () => {
const { colorMode } = useColorMode();
const [temperature, setTemperature] = useState(21);
const [activeButton, setActiveButton] = useState({
kitchen: true,
living: false,
attic: false,
});
const { isOpen, onOpen, onClose } = useDisclosure();
const textColor = useColorModeValue("gray.700", "white");
const bgButtonGroup = useColorModeValue("gray.50", "navy.700");
const bgActiveButton = useColorModeValue("#fff", "navy.800");
const arcBackgroundColor = useColorModeValue("#EDF2F7", "#0B1437");
const borderColor = useColorModeValue("gray.200", "gray.600");
const iconColor = useColorModeValue("gray.400", "white");
const bgIcon = useColorModeValue(
"linear-gradient(81.62deg, #313860 2.25%, #151928 79.87%)",
"navy.800"
);
return (
<Flex direction='column' pt={{ sm: "125px", lg: "75px" }}>
<Grid templateColumns={{ sm: "1fr", lg: "1.5fr 1fr" }} gap='20px'>
<Card>
<Flex
direction={{ sm: "column", md: "row" }}
justify='space-between'
align={{ md: "center" }}
mb='16px'>
<Text
color={textColor}
fontSize='lg'
fontWeight='bold'
mb={{ sm: "12px", md: "0px" }}>
Cameras
</Text>
<Flex align='center'>
<Flex bg={bgButtonGroup} borderRadius='10px' p='6px' me='10px'>
<Button
variant='no-effects'
w={{ sm: "fit-content", xl: "135px" }}
h='40px'
fontSize='xs'
boxShadow={
activeButton.kitchen
? "0px 2px 5.5px rgba(0, 0, 0, 0.06)"
: "none"
}
bg={activeButton.kitchen ? bgActiveButton : "transparent"}
onClick={() =>
setActiveButton({
kitchen: true,
living: false,
attic: false,
})
}>
KITCHEN
</Button>
<Button
variant='no-effects'
w={{ sm: "fit-content", xl: "135px" }}
h='40px'
fontSize='xs'
boxShadow={
activeButton.living
? "0px 2px 5.5px rgba(0, 0, 0, 0.06)"
: "none"
}
bg={activeButton.living ? bgActiveButton : "transparent"}
onClick={() =>
setActiveButton({
kitchen: false,
living: true,
attic: false,
})
}>
LIVING
</Button>
<Button
variant='no-effects'
w={{ sm: "fit-content", xl: "135px" }}
h='40px'
fontSize='xs'
boxShadow={
activeButton.attic
? "0px 2px 5.5px rgba(0, 0, 0, 0.06)"
: "none"
}
bg={activeButton.attic ? bgActiveButton : "transparent"}
onClick={() =>
setActiveButton({
kitchen: false,
attic: true,
living: false,
})
}>
ATTIC
</Button>
</Flex>
<Menu isOpen={isOpen} onClose={onClose}>
<MenuButton onClick={onOpen}>
<Icon
as={IoEllipsisVerticalSharp}
color='gray.400'
w='20px'
h='20px'
/>
</MenuButton>
<MenuList>
<MenuItem>Action</MenuItem>
<MenuItem>Another action</MenuItem>
<MenuItem>Something else here</MenuItem>
</MenuList>
</Menu>
</Flex>
</Flex>
<Flex
p='20px'
justify='flex-end'
borderRadius='15px'
bgImage={smartHome}
minH='390px'>
<Flex
p='6px 12px'
align='center'
h='fit-content'
bg='rgba(255, 255, 255, 0.4)'
borderRadius='8px'>
<Icon
as={BsFillRecordCircleFill}
color='red.500'
w='10px'
h='10px'
me='4px'
/>
<Text color={textColor} fontSize='10px' fontWeight='bold'>
RECORDING
</Text>
</Flex>
</Flex>
</Card>
<Flex direction='column' justify='space-between' h='100%'>
<Card
bgImage={colorMode === "light" ? bgWeather : bgWeatherDark}
minH='140px'
bgSize='cover'
mb={{ sm: "20px", lg: "0px" }}>
<Flex align='center' justify='space-between' h='100%'>
<Flex direction='column'>
<Text color='white' fontSize='xs' mb='3px'>
Weather Today
</Text>
<Text color='white' fontSize='lg' fontWeight='bold'>
San Francisco - 34°C
</Text>
</Flex>
<Flex direction='column' align='center'>
<Image src={sunBehindCloud} w='65px' h='65px' />
<Text color='white' fontSize='xs' fontWeight='bold'>
Cloudly
</Text>
</Flex>
</Flex>
</Card>
<Grid
templateColumns={{ sm: "1fr", md: "repeat(2, 1fr)" }}
gap='20px'>
<Card display='flex' justify='center' align='center' minH='155px'>
<Flex direction='column'>
<Text color='blue.500' fontSize='3xl' fontWeight='bold'>
23{" "}
<Text as='span' fontSize='sm'>
°C
</Text>
</Text>
<Text color={textColor} fontSize='lg' fontWeight='bold'>
Living Room
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Temperature
</Text>
</Flex>
</Card>
<Card display='flex' justify='center' align='center' minH='155px'>
<Flex direction='column'>
<Text color='blue.500' fontSize='3xl' fontWeight='bold'>
44{" "}
<Text as='span' fontSize='sm'>
%
</Text>
</Text>
<Text color={textColor} fontSize='lg' fontWeight='bold'>
Outside
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Humidity
</Text>
</Flex>
</Card>
<Card display='flex' justify='center' align='center' minH='155px'>
<Flex direction='column'>
<Text color='blue.500' fontSize='3xl' fontWeight='bold'>
87{" "}
<Text as='span' fontSize='sm'>
</Text>
</Text>
<Text color={textColor} fontSize='lg' fontWeight='bold'>
Water
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Consumption
</Text>
</Flex>
</Card>
<Card display='flex' justify='center' align='center' minH='155px'>
<Flex direction='column'>
<Text color='blue.500' fontSize='3xl' fontWeight='bold'>
593{" "}
<Text as='span' fontSize='sm'>
GB
</Text>
</Text>
<Text color={textColor} fontSize='lg' fontWeight='bold'>
Internet
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
All Devices
</Text>
</Flex>
</Card>
</Grid>
</Flex>
</Grid>
<Grid
templateColumns={{
sm: "1fr",
md: "repeat(2, 1fr)",
lg: "1.5fr 1fr 1fr",
xl: "2fr 1fr 1fr",
}}
gap='20px'
my='20px'>
<Card gridColumn={{ md: "1 / 3", lg: "auto" }}>
<Flex justify='space-between' align='center' mb='30px'>
<Text fontSize='lg' color={textColor} fontWeight='bold'>
Consumption by Room
</Text>
<Icon
as={AiOutlineInfoCircle}
w='20px'
h='20px'
color={textColor}
/>
</Flex>
<Flex direction={{ sm: "column", md: "row" }} align='center'>
<Box mb={{ sm: "12px", lg: "0px" }} color='red'>
<VisxPieChart data={rooms} title={"473.1"} width={200} />
</Box>
<Stack
direction='column'
spacing='10px'
ms={{ md: "50px", lg: "10px", xl: "50px" }}
w='100%'>
{rooms.map((room, index, arr) => {
return (
<Flex
justify='space-between'
align='center'
key={index}
py='6px'
w='100%'
borderBottom={
index === arr.length - 1 ? "none" : "1px solid"
}
borderColor={borderColor}>
<Flex align='center'>
<Box
borderRadius='6px'
bg={room.color}
w='20px'
h='20px'
me='12px'
/>
<Text color={textColor} fontWeight='bold' fontSize='xs'>
{room.name}
</Text>
</Flex>
<Text
color='gray.400'
fontSize='xs'
fontWeight='bold'>{`${room.percentage} %`}</Text>
</Flex>
);
})}
</Stack>
</Flex>
</Card>
<Card>
<Text fontSize='lg' color={textColor} fontWeight='bold'>
Consumption per Day
</Text>
<BarChart
chartData={barChartDataSmartHome}
chartOptions={barChartOptionsSmartHome}
/>
</Card>
<Card>
<Text fontSize='lg' color={textColor} fontWeight='bold'>
Device Limit
</Text>
<Flex
direction='column'
align='center'
alignSelf='center'
textAlign='center'
position='relative'>
<CircularSlider
startAngle={45}
endAngle={315}
handleSize={6}
minValue={16}
maxValue={32}
size={220}
arcColor='#3182CE'
arcBackgroundColor={arcBackgroundColor}
handle1={{
value: temperature,
onChange: (v) => setTemperature(Math.round(v)),
}}
/>
<Text
color={textColor}
fontSize='32px'
fontWeight='bold'
position='absolute'
top='35%'>{`${temperature} °C`}</Text>
<Stack direction='row' spacing='16px'>
<Text fontSize='xs' color='gray.400' fontWeight='bold'>
16°C
</Text>
<Text fontSize='xs' color='gray.400' fontWeight='bold'>
Temperature
</Text>
<Text fontSize='xs' color='gray.400' fontWeight='bold'>
32°C
</Text>
</Stack>
</Flex>
</Card>
</Grid>
<HSeparator my='40px' />
<Grid
templateColumns={{
sm: "1fr",
md: "repeat(2, 1fr)",
lg: "repeat(6, 1fr)",
}}
gap='20px'>
<Card minH='210px'>
<Flex direction='column' justify='space-between' h='100%'>
<Flex justify='space-between' align='center' mb='auto'>
<Text fontSize='xs' color='gray.400'>
OFF
</Text>
<Switch colorScheme='blue' />
</Flex>
<Flex direction='column'>
<Icon as={BiWater} color='gray.400' w='62px' h='62px' mb='10px' />
<Text color={textColor} fontWeight='bold' mb='3px'>
Humidity
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Inactive since: 2 days
</Text>
</Flex>
</Flex>
</Card>
<Card minH='210px' bg={bgIcon}>
<Flex direction='column' justify='space-between' h='100%'>
<Flex justify='space-between' align='center' mb='auto'>
<Text fontSize='xs' color='gray.400'>
ON
</Text>
<Switch colorScheme='blue' defaultChecked />
</Flex>
<Flex direction='column'>
<Icon
as={BsThermometerHigh}
color={iconColor}
w='62px'
h='62px'
mb='10px'
/>
<Text color='white' fontWeight='bold' mb='3px'>
Temperature
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Active
</Text>
</Flex>
</Flex>
</Card>
<Card minH='210px'>
<Flex direction='column' justify='space-between' h='100%'>
<Flex justify='space-between' align='center' mb='auto'>
<Text fontSize='xs' color='gray.400'>
OFF
</Text>
<Switch colorScheme='blue' />
</Flex>
<Flex direction='column'>
<Icon
as={FaSnowflake}
color={iconColor}
w='62px'
h='62px'
mb='10px'
/>
<Text color={textColor} fontWeight='bold' mb='3px'>
Air Conditioner
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Inactive since: 1 hour
</Text>
</Flex>
</Flex>
</Card>
<Card minH='210px'>
<Flex direction='column' justify='space-between' h='100%'>
<Flex justify='space-between' align='center' mb='auto'>
<Text fontSize='xs' color='gray.400'>
OFF
</Text>
<Switch colorScheme='blue' />
</Flex>
<Flex direction='column'>
<Icon
as={IoBulbOutline}
color={iconColor}
w='62px'
h='62px'
mb='10px'
/>
<Text color={textColor} fontWeight='bold' mb='3px'>
Lights
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Inactive since: 27 min
</Text>
</Flex>
</Flex>
</Card>
<Card minH='210px' bg={bgIcon}>
<Flex direction='column' justify='space-between' h='100%'>
<Flex justify='space-between' align='center' mb='auto'>
<Text fontSize='xs' color='gray.400'>
ON
</Text>
<Switch colorScheme='blue' defaultChecked />
</Flex>
<Flex direction='column'>
<Icon as={BiWifi} color={iconColor} w='62px' h='62px' mb='10px' />
<Text color='white' fontWeight='bold' mb='3px'>
Wi-Fi
</Text>
<Text color='gray.400' fontSize='xs' fontWeight='bold'>
Active
</Text>
</Flex>
</Flex>
</Card>
<Card minH='210px' cursor='pointer'>
<Flex direction='column' align='center' justify='center' h='100%'>
<Icon as={FaPlus} color={iconColor} w='30px' h='30px' mb='11px' />
<Text
fontSize={{ sm: "lg", lg: "md", xl: "lg" }}
color={iconColor}
fontWeight='bold'>
New Device
</Text>
</Flex>
</Card>
</Grid>
</Flex>
);
};
export default SmartHome;

View File

@@ -0,0 +1,87 @@
/* src/views/Dashboard/components/InvestmentCalendar.css */
/* FullCalendar 自定义样式 */
.fc {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
.fc-theme-standard td,
.fc-theme-standard th {
border-color: #e2e8f0;
}
.fc-button-primary {
background-color: #3182ce !important;
border-color: #3182ce !important;
}
.fc-button-primary:hover {
background-color: #2c5282 !important;
border-color: #2c5282 !important;
}
.fc-button-primary:not(:disabled):active,
.fc-button-primary:not(:disabled).fc-button-active {
background-color: #2c5282 !important;
border-color: #2c5282 !important;
}
.fc-daygrid-day-number {
color: #2d3748;
font-weight: 500;
}
.fc-daygrid-day.fc-day-today {
background-color: #e6f3ff !important;
}
.fc-event {
border: none;
padding: 2px 4px;
font-size: 12px;
font-weight: 500;
}
.fc-daygrid-event-dot {
display: none;
}
.fc-daygrid-day-events {
margin-top: 2px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.fc-toolbar {
flex-direction: column;
}
.fc-toolbar-title {
margin: 0.5em 0;
}
.fc-button-group {
margin: 0.2em;
}
}
/* 暗色模式支持 */
@media (prefers-color-scheme: dark) {
.fc-theme-standard td,
.fc-theme-standard th {
border-color: #4a5568;
}
.fc-daygrid-day-number {
color: #e2e8f0;
}
.fc-daygrid-day.fc-day-today {
background-color: #2d3748 !important;
}
.fc-col-header-cell-cushion,
.fc-daygrid-day-number {
color: #e2e8f0;
}
}

View File

@@ -0,0 +1,482 @@
// 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 './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);
}
}
} catch (error) {
console.error('加载日历事件失败:', error);
toast({
title: '加载失败',
description: '无法加载日历事件',
status: 'error',
duration: 3000,
});
} 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) {
toast({
title: '添加成功',
description: '投资计划已添加',
status: 'success',
duration: 3000,
});
onAddClose();
loadEvents();
setNewEvent({
title: '',
description: '',
type: 'plan',
importance: 3,
stocks: '',
});
}
}
} catch (error) {
console.error('添加事件失败:', error);
toast({
title: '添加失败',
description: '无法添加投资计划',
status: 'error',
duration: 3000,
});
}
};
// 删除用户事件
const handleDeleteEvent = async (eventId) => {
if (!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) {
toast({
title: '删除成功',
status: 'success',
duration: 2000,
});
loadEvents();
}
} catch (error) {
console.error('删除事件失败:', error);
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 */}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<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 */}
<Modal isOpen={isAddOpen} onClose={onAddClose} size="lg">
<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>
);
}

View File

@@ -0,0 +1,576 @@
// 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';
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'));
}
}
} catch (error) {
console.error('加载数据失败:', error);
toast({
title: '加载失败',
description: '无法加载投资计划和复盘记录',
status: 'error',
duration: 3000,
});
} 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) {
toast({
title: editingItem ? '更新成功' : '创建成功',
status: 'success',
duration: 2000,
});
onClose();
loadData();
} else {
throw new Error('保存失败');
}
} catch (error) {
console.error('保存失败:', error);
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) {
toast({
title: '删除成功',
status: 'success',
duration: 2000,
});
loadData();
}
} catch (error) {
console.error('删除失败:', error);
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>
{/* 编辑/新建模态框 */}
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<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>
);
}

View File

@@ -0,0 +1,303 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Box,
VStack,
HStack,
Text,
Badge,
Button,
Icon,
Center,
Spinner,
LinkBox,
LinkOverlay,
Tooltip,
Tag,
TagLabel,
Wrap,
WrapItem,
useColorModeValue,
useToast,
} from '@chakra-ui/react';
import { Link } from 'react-router-dom';
import {
FiCalendar,
FiClock,
FiStar,
FiTrendingUp,
FiAlertCircle,
} from 'react-icons/fi';
import { eventService } from '../../../services/eventService';
import moment from 'moment';
export default function MyFutureEvents({ limit = 5 }) {
const [futureEvents, setFutureEvents] = useState([]);
const [loading, setLoading] = useState(true);
const toast = useToast();
// 颜色主题
const borderColor = useColorModeValue('gray.200', 'gray.600');
const hoverBg = useColorModeValue('gray.50', 'gray.700');
const secondaryText = useColorModeValue('gray.600', 'gray.400');
const importanceBg = useColorModeValue('yellow.50', 'yellow.900');
const importanceColor = useColorModeValue('yellow.600', 'yellow.300');
// 加载关注的未来事件
const loadFutureEvents = useCallback(async () => {
try {
setLoading(true);
const response = await eventService.calendar.getFollowingEvents();
if (response.success) {
// 按时间排序,最近的在前
const sortedEvents = (response.data || []).sort((a, b) =>
moment(a.calendar_time).valueOf() - moment(b.calendar_time).valueOf()
);
setFutureEvents(sortedEvents);
}
} catch (error) {
console.error('加载未来事件失败:', error);
toast({
title: '加载失败',
description: '无法加载关注的未来事件',
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
}
}, [toast]);
useEffect(() => {
loadFutureEvents();
}, [loadFutureEvents]);
// 取消关注
const handleUnfollow = async (eventId) => {
try {
const response = await eventService.calendar.toggleFollow(eventId);
if (response.success) {
setFutureEvents(prev => prev.filter(event => event.id !== eventId));
toast({
title: '取消关注成功',
status: 'success',
duration: 2000,
isClosable: true,
});
}
} catch (error) {
console.error('取消关注失败:', error);
toast({
title: '操作失败',
description: '取消关注失败,请重试',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
// 格式化时间
const formatEventTime = (time) => {
const eventTime = moment(time);
const now = moment();
const daysDiff = eventTime.diff(now, 'days');
if (daysDiff === 0) {
return {
date: '今天',
time: eventTime.format('HH:mm'),
color: 'red',
urgent: true
};
} else if (daysDiff === 1) {
return {
date: '明天',
time: eventTime.format('HH:mm'),
color: 'orange',
urgent: true
};
} else if (daysDiff <= 7) {
return {
date: `${daysDiff}天后`,
time: eventTime.format('MM/DD HH:mm'),
color: 'yellow',
urgent: false
};
} else {
return {
date: eventTime.format('MM月DD日'),
time: eventTime.format('HH:mm'),
color: 'gray',
urgent: false
};
}
};
// 获取重要性颜色
const getImportanceColor = (star) => {
if (star >= 5) return 'red';
if (star >= 4) return 'orange';
if (star >= 3) return 'yellow';
return 'blue';
};
if (loading) {
return (
<Center py={4}>
<Spinner size="sm" />
</Center>
);
}
if (futureEvents.length === 0) {
return (
<Center py={8}>
<VStack spacing={3}>
<Icon as={FiCalendar} boxSize={12} color="gray.300" />
<Text color={secondaryText} fontSize="sm">
暂无关注的未来事件
</Text>
<Button
size="sm"
variant="outline"
colorScheme="blue"
as={Link}
to="/community"
>
探索投资日历
</Button>
</VStack>
</Center>
);
}
return (
<VStack align="stretch" spacing={3}>
{futureEvents.slice(0, limit).map((event) => {
const timeInfo = formatEventTime(event.calendar_time);
return (
<LinkBox
key={event.id}
p={4}
borderRadius="lg"
border="1px"
borderColor={borderColor}
bg={timeInfo.urgent ? importanceBg : 'transparent'}
_hover={{ shadow: 'md', transform: 'translateY(-2px)' }}
transition="all 0.2s"
position="relative"
>
{/* 紧急标记 */}
{timeInfo.urgent && (
<Box
position="absolute"
top={2}
right={2}
>
<Badge colorScheme={timeInfo.color} variant="solid" fontSize="xs">
即将发生
</Badge>
</Box>
)}
<VStack align="stretch" spacing={3}>
{/* 标题 */}
<LinkOverlay as={Link} to="/community">
<Text fontWeight="medium" fontSize="md" noOfLines={2}>
{event.title}
</Text>
</LinkOverlay>
{/* 时间和重要性 */}
<HStack justify="space-between">
<HStack spacing={3}>
<Badge colorScheme={timeInfo.color} variant="subtle">
<HStack spacing={1}>
<Icon as={FiClock} boxSize={3} />
<Text>{timeInfo.date}</Text>
</HStack>
</Badge>
<Text fontSize="sm" color={secondaryText}>
{timeInfo.time}
</Text>
</HStack>
{/* 重要性星级 */}
<HStack spacing={0}>
{[...Array(5)].map((_, i) => (
<Icon
key={i}
as={FiStar}
boxSize={3}
color={i < event.star ? importanceColor : 'gray.300'}
fill={i < event.star ? 'currentColor' : 'none'}
/>
))}
</HStack>
</HStack>
{/* 标签和相关信息 */}
<HStack justify="space-between" flexWrap="wrap">
<HStack spacing={2}>
{event.type && (
<Tag size="sm" variant="subtle" colorScheme={event.type === 'event' ? 'blue' : 'green'}>
<TagLabel>{event.type === 'event' ? '事件' : '数据'}</TagLabel>
</Tag>
)}
{event.related_stocks && event.related_stocks.length > 0 && (
<Tag size="sm" variant="subtle" colorScheme="purple">
<Icon as={FiTrendingUp} boxSize={3} mr={1} />
<TagLabel>{event.related_stocks.length}只相关股票</TagLabel>
</Tag>
)}
</HStack>
{/* 操作按钮 */}
<Button
size="xs"
variant="ghost"
colorScheme="red"
onClick={(e) => {
e.preventDefault();
handleUnfollow(event.id);
}}
>
取消关注
</Button>
</HStack>
{/* 预测信息 */}
{event.forecast && (
<Box>
<HStack spacing={1} mb={1}>
<Icon as={FiAlertCircle} boxSize={3} color={secondaryText} />
<Text fontSize="xs" color={secondaryText}>
预测
</Text>
</HStack>
<Text fontSize="sm" color={secondaryText} noOfLines={2}>
{event.forecast}
</Text>
</Box>
)}
</VStack>
</LinkBox>
);
})}
{/* 查看更多 */}
{futureEvents.length > limit && (
<Button
size="sm"
variant="ghost"
as={Link}
to="/community"
>
查看全部 ({futureEvents.length})
</Button>
)}
</VStack>
);
}