Initial commit
This commit is contained in:
434
src/views/Dashboard/Automotive.js
Normal file
434
src/views/Dashboard/Automotive.js
Normal 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
539
src/views/Dashboard/CRM.js
Normal 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;
|
||||
734
src/views/Dashboard/Center.js
Normal file
734
src/views/Dashboard/Center.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
1421
src/views/Dashboard/Default.js
Normal file
1421
src/views/Dashboard/Default.js
Normal file
File diff suppressed because it is too large
Load Diff
115
src/views/Dashboard/Landing.js
Normal file
115
src/views/Dashboard/Landing.js
Normal 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>
|
||||
);
|
||||
}
|
||||
532
src/views/Dashboard/SmartHome.js
Normal file
532
src/views/Dashboard/SmartHome.js
Normal 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'>
|
||||
m³
|
||||
</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;
|
||||
87
src/views/Dashboard/components/InvestmentCalendar.css
Normal file
87
src/views/Dashboard/components/InvestmentCalendar.css
Normal 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;
|
||||
}
|
||||
}
|
||||
482
src/views/Dashboard/components/InvestmentCalendarChakra.js
Normal file
482
src/views/Dashboard/components/InvestmentCalendarChakra.js
Normal 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>
|
||||
);
|
||||
}
|
||||
576
src/views/Dashboard/components/InvestmentPlansAndReviews.js
Normal file
576
src/views/Dashboard/components/InvestmentPlansAndReviews.js
Normal 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>
|
||||
);
|
||||
}
|
||||
303
src/views/Dashboard/components/MyFutureEvents.js
Normal file
303
src/views/Dashboard/components/MyFutureEvents.js
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user