Initial commit
This commit is contained in:
415
src/views/Pages/Account/Billing.js
Normal file
415
src/views/Pages/Account/Billing.js
Normal file
@@ -0,0 +1,415 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Grid,
|
||||
Icon,
|
||||
Spacer,
|
||||
Text,
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
// Assets
|
||||
import BackgroundCard1 from 'assets/img/BackgroundCard1.png';
|
||||
// Custom components
|
||||
import Card from 'components/Card/Card.js';
|
||||
import CardHeader from 'components/Card/CardHeader.js';
|
||||
import IconBox from 'components/Icons/IconBox';
|
||||
import { MastercardIcon, VisaIcon } from 'components/Icons/Icons';
|
||||
import { HSeparator } from 'components/Separator/Separator';
|
||||
import BillingRow from 'components/Tables/BillingRow';
|
||||
import InvoicesRow from 'components/Tables/InvoicesRow';
|
||||
import TransactionRow from 'components/Tables/TransactionRow';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
FaPaypal,
|
||||
FaPencilAlt,
|
||||
FaRegCalendarAlt,
|
||||
FaWallet,
|
||||
FaGem,
|
||||
} from 'react-icons/fa';
|
||||
import { RiMastercardFill } from 'react-icons/ri';
|
||||
import {
|
||||
billingData,
|
||||
invoicesData,
|
||||
newestTransactions,
|
||||
olderTransactions,
|
||||
} from 'variables/general';
|
||||
|
||||
function Billing() {
|
||||
// Chakra color mode
|
||||
const iconBlue = useColorModeValue('blue.500', 'blue.500');
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const borderColor = useColorModeValue('#dee2e6', 'transparent');
|
||||
const { colorMode } = useColorMode();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Flex direction='column' pt={{ base: '120px', md: '75px' }}>
|
||||
<Grid templateColumns={{ sm: '1fr', lg: '2fr 1.2fr' }} templateRows='1fr'>
|
||||
<Box>
|
||||
<Grid
|
||||
templateColumns={{
|
||||
sm: '1fr',
|
||||
md: '1fr 1fr',
|
||||
xl: '1fr 1fr 1fr 1fr',
|
||||
}}
|
||||
templateRows={{ sm: 'auto auto auto', md: '1fr auto', xl: '1fr' }}
|
||||
gap='26px'
|
||||
>
|
||||
<Card
|
||||
backgroundImage={
|
||||
colorMode === 'dark'
|
||||
? 'linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)'
|
||||
: BackgroundCard1
|
||||
}
|
||||
backgroundRepeat='no-repeat'
|
||||
background='cover'
|
||||
bgPosition='10%'
|
||||
p='16px'
|
||||
h={{ sm: '220px', xl: '100%' }}
|
||||
gridArea={{ md: '1 / 1 / 2 / 3', xl: '1 / 1 / 2 / 3' }}
|
||||
>
|
||||
<Flex
|
||||
direction='column'
|
||||
color='white'
|
||||
h='100%'
|
||||
p='0px 10px 20px 10px'
|
||||
w='100%'
|
||||
>
|
||||
<Flex justify='space-between' align='center'>
|
||||
<Text fontSize='md' fontWeight='bold'>
|
||||
Argon x Chakra
|
||||
</Text>
|
||||
<Icon
|
||||
as={RiMastercardFill}
|
||||
w='48px'
|
||||
h='auto'
|
||||
color='gray.400'
|
||||
/>
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<Flex direction='column'>
|
||||
<Box>
|
||||
<Text fontSize='2xl' letterSpacing='2px' fontWeight='bold'>
|
||||
7812 2139 0823 XXXX
|
||||
</Text>
|
||||
</Box>
|
||||
<Flex mt='14px'>
|
||||
<Flex direction='column' me='34px'>
|
||||
<Text fontSize='xs'>VALID THRU</Text>
|
||||
<Text fontSize='xs' fontWeight='bold'>
|
||||
05/24
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text fontSize='xs'>CVV</Text>
|
||||
<Text fontSize='xs' fontWeight='bold'>
|
||||
09X
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card p='16px' display='flex' align='center' justify='center'>
|
||||
<Flex direction='column' align='center' w='100%' py='14px'>
|
||||
<IconBox h={'60px'} w={'60px'} bg={iconBlue}>
|
||||
<Icon h={'24px'} w={'24px'} color='white' as={FaWallet} />
|
||||
</IconBox>
|
||||
<Flex
|
||||
direction='column'
|
||||
m='14px'
|
||||
justify='center'
|
||||
textAlign='center'
|
||||
align='center'
|
||||
w='100%'
|
||||
>
|
||||
<Text fontSize='md' color={textColor} fontWeight='bold'>
|
||||
Salary
|
||||
</Text>
|
||||
<Text
|
||||
mb='24px'
|
||||
fontSize='xs'
|
||||
color='gray.400'
|
||||
fontWeight='semibold'
|
||||
>
|
||||
Belong Interactive
|
||||
</Text>
|
||||
<HSeparator />
|
||||
</Flex>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
+$2000
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card p='16px' display='flex' align='center' justify='center'>
|
||||
<Flex
|
||||
direction='column'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
py='14px'
|
||||
>
|
||||
<IconBox h={'60px'} w={'60px'} bg='purple.500'>
|
||||
<Icon h={'24px'} w={'24px'} color='white' as={FaGem} />
|
||||
</IconBox>
|
||||
<Flex
|
||||
direction='column'
|
||||
m='14px'
|
||||
justify='center'
|
||||
textAlign='center'
|
||||
align='center'
|
||||
w='100%'
|
||||
>
|
||||
<Text fontSize='md' color={textColor} fontWeight='bold'>
|
||||
订阅服务
|
||||
</Text>
|
||||
<Text
|
||||
mb='24px'
|
||||
fontSize='xs'
|
||||
color='gray.400'
|
||||
fontWeight='semibold'
|
||||
>
|
||||
Pro & Max 版本
|
||||
</Text>
|
||||
<HSeparator />
|
||||
</Flex>
|
||||
<Button
|
||||
size='sm'
|
||||
colorScheme='purple'
|
||||
onClick={() => navigate('/home/pages/account/subscription')}
|
||||
>
|
||||
管理订阅
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Card p='16px' mt='24px'>
|
||||
<CardHeader>
|
||||
<Flex
|
||||
justify='space-between'
|
||||
align='center'
|
||||
minHeight='60px'
|
||||
w='100%'
|
||||
>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
Payment Method
|
||||
</Text>
|
||||
<Button variant={colorMode === 'dark' ? 'primary' : 'dark'}>
|
||||
ADD A NEW CARD
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Flex
|
||||
direction={{ sm: 'column', md: 'row' }}
|
||||
align='center'
|
||||
w='100%'
|
||||
justify='center'
|
||||
py='1rem'
|
||||
>
|
||||
<Flex
|
||||
p='1rem'
|
||||
bg={colorMode === 'dark' ? 'navy.900' : 'transparent'}
|
||||
borderRadius='15px'
|
||||
width='100%'
|
||||
border='1px solid'
|
||||
borderColor={borderColor}
|
||||
align='center'
|
||||
mb={{ sm: '24px', md: '0px' }}
|
||||
me={{ sm: '0px', md: '24px' }}
|
||||
>
|
||||
<IconBox me='10px' w='25px' h='22px'>
|
||||
<MastercardIcon w='100%' h='100%' />
|
||||
</IconBox>
|
||||
<Text color='gray.400' fontSize='md' fontWeight='semibold'>
|
||||
7812 2139 0823 XXXX
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Button p='0px' w='16px' h='16px' variant='no-effects'>
|
||||
<Icon
|
||||
as={FaPencilAlt}
|
||||
color={colorMode === 'dark' && 'white'}
|
||||
/>
|
||||
</Button>
|
||||
</Flex>
|
||||
<Flex
|
||||
p='16px'
|
||||
bg={colorMode === 'dark' ? 'navy.900' : 'transparent'}
|
||||
borderRadius='15px'
|
||||
width='100%'
|
||||
border='1px solid'
|
||||
borderColor={borderColor}
|
||||
align='center'
|
||||
>
|
||||
<IconBox me='10px' w='25px' h='25px'>
|
||||
<VisaIcon w='100%' h='100%' />
|
||||
</IconBox>
|
||||
<Text color='gray.400' fontSize='md' fontWeight='semibold'>
|
||||
7812 2139 0823 XXXX
|
||||
</Text>
|
||||
<Spacer />
|
||||
<Button
|
||||
p='0px'
|
||||
bg='transparent'
|
||||
w='16px'
|
||||
h='16px'
|
||||
variant='no-effects'
|
||||
>
|
||||
<Icon
|
||||
as={FaPencilAlt}
|
||||
color={colorMode === 'dark' && 'white'}
|
||||
/>
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Box>
|
||||
<Card
|
||||
p='22px'
|
||||
my={{ sm: '24px', lg: '0px' }}
|
||||
ms={{ sm: '0px', lg: '24px' }}
|
||||
>
|
||||
<CardHeader>
|
||||
<Flex justify='space-between' align='center' mb='1rem' w='100%'>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
Invoices
|
||||
</Text>
|
||||
<Button
|
||||
variant='outlined'
|
||||
color={colorMode === 'dark' && 'white'}
|
||||
borderColor={colorMode === 'dark' && 'white'}
|
||||
_hover={colorMode === 'dark' && 'none'}
|
||||
minW='110px'
|
||||
maxH='35px'
|
||||
>
|
||||
VIEW ALL
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Flex direction='column' w='100%'>
|
||||
{invoicesData.map((row, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<InvoicesRow
|
||||
date={row.date}
|
||||
code={row.code}
|
||||
price={row.price}
|
||||
logo={row.logo}
|
||||
format={row.format}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid templateColumns={{ sm: '1fr', lg: '1.6fr 1.2fr' }}>
|
||||
<Card my={{ lg: '24px' }} me={{ lg: '24px' }}>
|
||||
<Flex direction='column'>
|
||||
<CardHeader py='12px'>
|
||||
<Text color={textColor} fontSize='lg' fontWeight='bold'>
|
||||
Billing Information
|
||||
</Text>
|
||||
</CardHeader>
|
||||
|
||||
<Flex direction='column' w='100%'>
|
||||
{billingData.map((row, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<BillingRow
|
||||
name={row.name}
|
||||
company={row.company}
|
||||
email={row.email}
|
||||
number={row.number}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card my='24px' ms={{ lg: '24px' }}>
|
||||
<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' w='100%'>
|
||||
<Text
|
||||
color='gray.400'
|
||||
fontSize={{ sm: 'sm', md: 'md' }}
|
||||
fontWeight='semibold'
|
||||
my='12px'
|
||||
>
|
||||
NEWEST
|
||||
</Text>
|
||||
{newestTransactions.map((row, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<TransactionRow
|
||||
name={row.name}
|
||||
logo={row.logo}
|
||||
date={row.date}
|
||||
price={row.price}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<Text
|
||||
color='gray.400'
|
||||
fontSize={{ sm: 'sm', md: 'md' }}
|
||||
fontWeight='semibold'
|
||||
my='12px'
|
||||
>
|
||||
OLDER
|
||||
</Text>
|
||||
{olderTransactions.map((row, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<TransactionRow
|
||||
name={row.name}
|
||||
logo={row.logo}
|
||||
date={row.date}
|
||||
price={row.price}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default Billing;
|
||||
407
src/views/Pages/Account/Invoice.js
Normal file
407
src/views/Pages/Account/Invoice.js
Normal file
@@ -0,0 +1,407 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* 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,
|
||||
Button,
|
||||
Flex,
|
||||
Image,
|
||||
Stack,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useColorModeValue,
|
||||
useColorMode,
|
||||
} from "@chakra-ui/react";
|
||||
import { ArgonLogoLight, ArgonLogoDark } from "components/Icons/Icons";
|
||||
// Custom components
|
||||
import Card from "components/Card/Card";
|
||||
import CardBody from "components/Card/CardBody";
|
||||
import CardHeader from "components/Card/CardHeader";
|
||||
import React, { useRef } from "react";
|
||||
import { useReactToPrint } from "react-to-print";
|
||||
|
||||
class ComponentToPrint extends React.Component {
|
||||
// for react-to-print to work, it must be called from a class based component
|
||||
render() {
|
||||
const { textColor, secondaryColor, borderColor, colorMode } = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
w={{ sm: "330px", md: "500px", lg: "900px" }}
|
||||
justifySelf="center"
|
||||
alignSelf="center"
|
||||
mt="50px"
|
||||
p={{ sm: "24px", md: "44px" }}
|
||||
>
|
||||
<CardHeader mb={{ sm: "60px", md: "95px" }}>
|
||||
<Flex direction="column" w="100%">
|
||||
{colorMode === "light" ? (
|
||||
<ArgonLogoDark w="112px" h="41px" />
|
||||
) : (
|
||||
<ArgonLogoLight w="112px" h="41px" />
|
||||
)}
|
||||
<Flex
|
||||
direction={{ sm: "column", md: "row" }}
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
mt="22px"
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
maxW={{ sm: "100%", md: "150px", lg: "300px" }}
|
||||
mb={{ sm: "48px", md: "0px" }}
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="bold"
|
||||
fontSize="lg"
|
||||
mb="12px"
|
||||
>
|
||||
St. Independence Embankment, 050105 Bucharest, Romania
|
||||
</Text>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
tel: +4 (074) 1090873
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
textAlign={{ sm: "start", md: "end" }}
|
||||
maxW={{ sm: "100%", md: "170px" }}
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="bold"
|
||||
fontSize="lg"
|
||||
mb="12px"
|
||||
>
|
||||
Billed to: John Doe
|
||||
</Text>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
4006 Locust View Drive San Francisco CA California
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Flex direction="column" w="100%">
|
||||
<Flex
|
||||
direction={{ sm: "column", md: "row" }}
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
mb="60px"
|
||||
>
|
||||
<Flex direction="column" mb={{ sm: "16px", md: "0px" }}>
|
||||
<Text
|
||||
color="gray.400"
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
mb="8px"
|
||||
>
|
||||
Invoice no
|
||||
</Text>
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="lg">
|
||||
#0453119
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction="column">
|
||||
<Stack direction="row" mb="8px" justify={{ md: "end" }}>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
Invoice date:{" "}
|
||||
</Text>
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="lg">
|
||||
06/03/2022
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" justify={{ md: "end" }}>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
Due date:{" "}
|
||||
</Text>
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="lg">
|
||||
29/07/2022
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box overflowX={{ sm: "scroll", lg: "hidden" }}>
|
||||
<Table mb="85px" overflowX={{ sm: "scroll", lg: "hidden" }}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th
|
||||
borderColor={borderColor}
|
||||
color="gray.400"
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
ps="0px"
|
||||
>
|
||||
Item
|
||||
</Th>
|
||||
<Th
|
||||
borderColor={borderColor}
|
||||
color="gray.400"
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
>
|
||||
Quantity
|
||||
</Th>
|
||||
<Th
|
||||
borderColor={borderColor}
|
||||
color="gray.400"
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
>
|
||||
Rate
|
||||
</Th>
|
||||
<Th
|
||||
borderColor={borderColor}
|
||||
color="gray.400"
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
>
|
||||
Amount
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td
|
||||
borderColor={borderColor}
|
||||
ps="0px"
|
||||
minW={{ sm: "300px" }}
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
Premium Support
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</Td>
|
||||
<Td
|
||||
borderColor={borderColor}
|
||||
minW="125px"
|
||||
boxSizing="border-box"
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 9.00
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 9.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td
|
||||
borderColor={borderColor}
|
||||
ps="0px"
|
||||
minW={{ sm: "300px" }}
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
Chakra UI - Dashboard PRO
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
3
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 99.00
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 297.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td
|
||||
borderColor={borderColor}
|
||||
ps="0px"
|
||||
minW={{ sm: "300px" }}
|
||||
border="none"
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
Parts for Service
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor} border="none">
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor} border="none">
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 89.00
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor} border="none">
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 89.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td
|
||||
borderColor={borderColor}
|
||||
ps="0px"
|
||||
minW={{ sm: "300px" }}
|
||||
></Td>
|
||||
<Td borderColor={borderColor}></Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text color={textColor} fontWeight="bold" fontSize="xl">
|
||||
Total
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text color={textColor} fontWeight="bold" fontSize="xl">
|
||||
$ 9.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
<Flex
|
||||
direction={{ sm: "column", md: "row" }}
|
||||
justify="space-between"
|
||||
>
|
||||
<Flex direction="column" maxW="270px">
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="xl">
|
||||
Thank You!
|
||||
</Text>
|
||||
<Text
|
||||
color="gray.400"
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
mt="6px"
|
||||
mb="30px"
|
||||
>
|
||||
If you encounter any issues related to the invoice you can
|
||||
contact us at:
|
||||
</Text>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
email:{" "}
|
||||
<Text as="span" color={secondaryColor} fontWeight="bold">
|
||||
support@creative-tim.com
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
<Button
|
||||
onClick={() => this.props.handlePrint()}
|
||||
variant="primary"
|
||||
w="100px"
|
||||
h="35px"
|
||||
alignSelf={{ sm: "flex-start", md: "flex-end" }}
|
||||
mt={{ sm: "16px", md: "0px" }}
|
||||
>
|
||||
PRINT
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Invoice() {
|
||||
const textColor = useColorModeValue("gray.700", "white");
|
||||
const secondaryColor = useColorModeValue("gray.500", "white");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.600");
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const componentRef = useRef();
|
||||
|
||||
const handlePrint = useReactToPrint({
|
||||
content: () => componentRef.current,
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt={{ sm: "100px", lg: "50px" }}>
|
||||
<ComponentToPrint
|
||||
ref={componentRef}
|
||||
handlePrint={handlePrint}
|
||||
textColor={textColor}
|
||||
secondaryColor={secondaryColor}
|
||||
borderColor={borderColor}
|
||||
colorMode={colorMode}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default Invoice;
|
||||
1708
src/views/Pages/Account/Settings.js
Normal file
1708
src/views/Pages/Account/Settings.js
Normal file
File diff suppressed because it is too large
Load Diff
886
src/views/Pages/Account/Subscription.js
Normal file
886
src/views/Pages/Account/Subscription.js
Normal file
@@ -0,0 +1,886 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Grid,
|
||||
Icon,
|
||||
Text,
|
||||
Badge,
|
||||
Spacer,
|
||||
VStack,
|
||||
HStack,
|
||||
useColorMode,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useDisclosure,
|
||||
Image,
|
||||
Progress,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
// Custom components
|
||||
import Card from 'components/Card/Card.js';
|
||||
import CardHeader from 'components/Card/CardHeader.js';
|
||||
import IconBox from 'components/Icons/IconBox';
|
||||
import { HSeparator } from 'components/Separator/Separator';
|
||||
|
||||
// Icons
|
||||
import {
|
||||
FaWeixin,
|
||||
FaGem,
|
||||
FaStar,
|
||||
FaCheck,
|
||||
FaQrcode,
|
||||
FaClock,
|
||||
FaRedo
|
||||
} from 'react-icons/fa';
|
||||
import { BiScan } from 'react-icons/bi';
|
||||
|
||||
function Subscription() {
|
||||
// Chakra color mode
|
||||
const { colorMode } = useColorMode();
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const borderColor = useColorModeValue('#dee2e6', 'transparent');
|
||||
const iconBlue = useColorModeValue('blue.500', 'blue.500');
|
||||
const iconGreen = useColorModeValue('green.500', 'green.500');
|
||||
const bgCard = useColorModeValue('white', 'gray.800');
|
||||
const bgAccent = useColorModeValue('blue.50', 'blue.900');
|
||||
|
||||
const toast = useToast();
|
||||
const { isOpen: isPaymentModalOpen, onOpen: onPaymentModalOpen, onClose: onPaymentModalClose } = useDisclosure();
|
||||
|
||||
// State
|
||||
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||||
const [selectedCycle, setSelectedCycle] = useState('monthly');
|
||||
const [paymentOrder, setPaymentOrder] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [paymentCountdown, setPaymentCountdown] = useState(0);
|
||||
const [checkingPayment, setCheckingPayment] = useState(false);
|
||||
const [autoCheckInterval, setAutoCheckInterval] = useState(null);
|
||||
const [forceUpdating, setForceUpdating] = useState(false);
|
||||
|
||||
// 加载订阅套餐数据
|
||||
useEffect(() => {
|
||||
fetchSubscriptionPlans();
|
||||
fetchCurrentUser();
|
||||
}, []);
|
||||
|
||||
// 倒计时更新
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (paymentCountdown > 0) {
|
||||
timer = setInterval(() => {
|
||||
setPaymentCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
handlePaymentExpired();
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [paymentCountdown]);
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopAutoPaymentCheck();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchSubscriptionPlans = async () => {
|
||||
try {
|
||||
console.log('🔄 正在获取订阅套餐...');
|
||||
const response = await fetch('/api/subscription/plans');
|
||||
console.log('📡 套餐API响应状态:', response.status);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('📦 套餐API响应数据:', data);
|
||||
|
||||
if (data.success && Array.isArray(data.data)) {
|
||||
// 确保每个套餐都有必要的字段
|
||||
const validPlans = data.data.filter(plan =>
|
||||
plan &&
|
||||
plan.name &&
|
||||
typeof plan.monthly_price === 'number' &&
|
||||
typeof plan.yearly_price === 'number'
|
||||
);
|
||||
console.log('✅ 有效套餐数量:', validPlans.length);
|
||||
setSubscriptionPlans(validPlans);
|
||||
} else {
|
||||
console.warn('⚠️ 套餐数据格式异常:', data);
|
||||
setSubscriptionPlans([]);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ 获取套餐失败,状态码:', response.status);
|
||||
setSubscriptionPlans([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 获取订阅套餐失败:', error);
|
||||
setSubscriptionPlans([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/session', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('👤 用户数据:', data);
|
||||
if (data.success) {
|
||||
setCurrentUser(data.user);
|
||||
console.log('✅ 用户信息已更新:', data.user);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubscribe = (plan) => {
|
||||
if (!currentUser) {
|
||||
toast({
|
||||
title: '请先登录',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plan || !plan.name) {
|
||||
toast({
|
||||
title: '套餐信息错误',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedPlan(plan);
|
||||
onPaymentModalOpen();
|
||||
};
|
||||
|
||||
const handleCreateOrder = async () => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/payment/create-order', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
plan_name: selectedPlan.name,
|
||||
billing_cycle: selectedCycle
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setPaymentOrder(data.data);
|
||||
// 设置30分钟倒计时
|
||||
setPaymentCountdown(30 * 60);
|
||||
|
||||
// 开始自动检查支付状态(每10秒检查一次)
|
||||
startAutoPaymentCheck(data.data.id);
|
||||
|
||||
toast({
|
||||
title: '订单创建成功',
|
||||
description: '请使用微信扫描二维码完成支付',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.message || '创建订单失败');
|
||||
}
|
||||
} else {
|
||||
throw new Error('网络错误');
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '创建订单失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaymentExpired = () => {
|
||||
setPaymentOrder(null);
|
||||
setPaymentCountdown(0);
|
||||
stopAutoPaymentCheck();
|
||||
toast({
|
||||
title: '支付二维码已过期',
|
||||
description: '请重新创建订单',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
};
|
||||
|
||||
// 自动检查支付状态
|
||||
const startAutoPaymentCheck = (orderId) => {
|
||||
console.log('🔄 开始自动检查支付状态...');
|
||||
|
||||
const checkInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/payment/order/${orderId}/status`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('📡 支付状态检查结果:', data);
|
||||
|
||||
if (data.success && data.payment_success) {
|
||||
// 支付成功
|
||||
clearInterval(checkInterval);
|
||||
setAutoCheckInterval(null);
|
||||
|
||||
toast({
|
||||
title: '支付成功!',
|
||||
description: '订阅已激活,正在跳转...',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// 延迟2秒后跳转到个人中心
|
||||
setTimeout(() => {
|
||||
onPaymentModalClose();
|
||||
window.location.reload(); // 刷新页面以更新订阅状态
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动检查支付状态失败:', error);
|
||||
}
|
||||
}, 10000); // 每10秒检查一次
|
||||
|
||||
setAutoCheckInterval(checkInterval);
|
||||
};
|
||||
|
||||
const stopAutoPaymentCheck = () => {
|
||||
if (autoCheckInterval) {
|
||||
clearInterval(autoCheckInterval);
|
||||
setAutoCheckInterval(null);
|
||||
console.log('⏹️ 停止自动检查支付状态');
|
||||
}
|
||||
};
|
||||
|
||||
// 强制刷新用户状态
|
||||
const handleRefreshUserStatus = async () => {
|
||||
try {
|
||||
await fetchCurrentUser();
|
||||
toast({
|
||||
title: '用户状态已刷新',
|
||||
description: '订阅信息已更新',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '刷新失败',
|
||||
description: '请稍后重试',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 强制更新支付状态
|
||||
const handleForceUpdatePayment = async () => {
|
||||
if (!paymentOrder) return;
|
||||
|
||||
setForceUpdating(true);
|
||||
try {
|
||||
const response = await fetch(`/api/payment/order/${paymentOrder.id}/force-update`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('🔧 强制更新支付状态结果:', data);
|
||||
|
||||
if (data.success && data.payment_success) {
|
||||
// 支付成功
|
||||
stopAutoPaymentCheck();
|
||||
|
||||
toast({
|
||||
title: '状态更新成功!',
|
||||
description: '订阅已激活,正在刷新页面...',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onPaymentModalClose();
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
toast({
|
||||
title: '无法更新状态',
|
||||
description: data.error || '支付状态未更新',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error('网络错误');
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '强制更新失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
setForceUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 手动检查支付状态
|
||||
const handleCheckPaymentStatus = async () => {
|
||||
if (!paymentOrder) return;
|
||||
|
||||
setCheckingPayment(true);
|
||||
try {
|
||||
const response = await fetch(`/api/payment/order/${paymentOrder.id}/status`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('🔍 手动检查支付状态结果:', data);
|
||||
console.log('🔍 支付成功标志:', data.payment_success);
|
||||
console.log('🔍 订单数据:', data.data);
|
||||
|
||||
if (data.success) {
|
||||
if (data.payment_success) {
|
||||
// 支付成功
|
||||
stopAutoPaymentCheck();
|
||||
|
||||
toast({
|
||||
title: '支付成功!',
|
||||
description: '订阅已激活,正在跳转...',
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
onPaymentModalClose();
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
|
||||
} else {
|
||||
// 还未支付
|
||||
toast({
|
||||
title: '支付状态检查',
|
||||
description: data.message || '还未检测到支付,请继续等待',
|
||||
status: 'info',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.error || '查询失败');
|
||||
}
|
||||
} else {
|
||||
throw new Error('网络错误');
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '查询失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
setCheckingPayment(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const getCurrentPrice = (plan) => {
|
||||
if (!plan) return 0;
|
||||
return selectedCycle === 'monthly' ? plan.monthly_price : plan.yearly_price;
|
||||
};
|
||||
|
||||
const getSavingsText = (plan) => {
|
||||
if (!plan || selectedCycle !== 'yearly') return null;
|
||||
const yearlyTotal = plan.monthly_price * 12;
|
||||
const savings = yearlyTotal - plan.yearly_price;
|
||||
const percentage = Math.round((savings / yearlyTotal) * 100);
|
||||
return `年付节省 ${percentage}%`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction='column' pt={{ base: '120px', md: '75px' }}>
|
||||
{/* 当前订阅状态 */}
|
||||
{currentUser && (
|
||||
<Card p='20px' mb='20px'>
|
||||
<CardHeader pb='12px'>
|
||||
<Flex justify='space-between' align='center'>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
当前订阅状态
|
||||
</Text>
|
||||
<Button
|
||||
size='sm'
|
||||
leftIcon={<Icon as={FaRedo} />}
|
||||
onClick={handleRefreshUserStatus}
|
||||
colorScheme='blue'
|
||||
variant='outline'
|
||||
>
|
||||
刷新状态
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<Flex align='center' justify='space-between'>
|
||||
<Box>
|
||||
<HStack spacing={2} mb={2}>
|
||||
<Badge
|
||||
colorScheme={currentUser.subscription_status === 'active' ? 'green' : 'gray'}
|
||||
variant='solid'
|
||||
>
|
||||
{currentUser.subscription_type === 'free' ? '免费版' :
|
||||
currentUser.subscription_type === 'pro' ? 'Pro版' : 'Max版'}
|
||||
</Badge>
|
||||
<Badge
|
||||
colorScheme={currentUser.subscription_status === 'active' ? 'green' : 'red'}
|
||||
>
|
||||
{currentUser.subscription_status === 'active' ? '已激活' : '未激活'}
|
||||
</Badge>
|
||||
</HStack>
|
||||
{currentUser.subscription_end_date && (
|
||||
<Text fontSize='sm' color='gray.500'>
|
||||
到期时间: {new Date(currentUser.subscription_end_date).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
{/* 调试信息 */}
|
||||
<Text fontSize='xs' color='gray.400' mt={2}>
|
||||
用户ID: {currentUser.id} | 类型: {currentUser.subscription_type} | 状态: {currentUser.subscription_status}
|
||||
</Text>
|
||||
</Box>
|
||||
{currentUser.subscription_status === 'active' && (
|
||||
<Icon as={FaGem} color='yellow.400' boxSize={6} />
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 计费周期选择 */}
|
||||
<Card p='20px' mb='20px'>
|
||||
<Flex justify='center' mb={6}>
|
||||
<HStack spacing={0} bg={bgAccent} borderRadius='lg' p={1}>
|
||||
<Button
|
||||
variant={selectedCycle === 'monthly' ? 'solid' : 'ghost'}
|
||||
colorScheme={selectedCycle === 'monthly' ? 'blue' : 'gray'}
|
||||
size='sm'
|
||||
onClick={() => setSelectedCycle('monthly')}
|
||||
>
|
||||
按月付费
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedCycle === 'yearly' ? 'solid' : 'ghost'}
|
||||
colorScheme={selectedCycle === 'yearly' ? 'blue' : 'gray'}
|
||||
size='sm'
|
||||
onClick={() => setSelectedCycle('yearly')}
|
||||
rightIcon={<Badge colorScheme='red' fontSize='xs'>省20%</Badge>}
|
||||
>
|
||||
按年付费
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
{/* 订阅套餐 */}
|
||||
<Grid
|
||||
templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }}
|
||||
gap='24px'
|
||||
mb='24px'
|
||||
>
|
||||
{subscriptionPlans.length === 0 ? (
|
||||
// 加载状态或空状态
|
||||
<Box gridColumn={{ base: '1', md: '1 / -1' }} textAlign='center' py={8}>
|
||||
<Text color='gray.500'>正在加载订阅套餐...</Text>
|
||||
</Box>
|
||||
) : (
|
||||
subscriptionPlans.filter(plan => plan && plan.name).map((plan) => (
|
||||
<Card
|
||||
key={plan.id}
|
||||
p='24px'
|
||||
position='relative'
|
||||
border={plan.name === 'max' ? '2px solid' : '1px solid'}
|
||||
borderColor={plan.name === 'max' ? 'blue.500' : borderColor}
|
||||
bg={bgCard}
|
||||
>
|
||||
{plan.name === 'max' && (
|
||||
<Badge
|
||||
position='absolute'
|
||||
top='-12px'
|
||||
left='50%'
|
||||
transform='translateX(-50%)'
|
||||
colorScheme='blue'
|
||||
variant='solid'
|
||||
px={3}
|
||||
py={1}
|
||||
fontSize='xs'
|
||||
>
|
||||
推荐
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<VStack spacing={4} align='stretch'>
|
||||
{/* 套餐头部 */}
|
||||
<Flex align='center' justify='space-between'>
|
||||
<Box>
|
||||
<Text fontSize='xl' fontWeight='bold' color={textColor}>
|
||||
{plan.display_name}
|
||||
</Text>
|
||||
<Text fontSize='sm' color='gray.500' mt={1}>
|
||||
{plan.description}
|
||||
</Text>
|
||||
</Box>
|
||||
<IconBox bg={plan.name === 'pro' ? iconBlue : iconGreen} color='white'>
|
||||
<Icon as={plan.name === 'pro' ? FaStar : FaGem} />
|
||||
</IconBox>
|
||||
</Flex>
|
||||
|
||||
{/* 价格 */}
|
||||
<Box textAlign='center' py={4}>
|
||||
<HStack justify='center' align='baseline'>
|
||||
<Text fontSize='3xl' fontWeight='bold' color={textColor}>
|
||||
¥{getCurrentPrice(plan).toFixed(2)}
|
||||
</Text>
|
||||
<Text color='gray.500'>
|
||||
/ {selectedCycle === 'monthly' ? '月' : '年'}
|
||||
</Text>
|
||||
</HStack>
|
||||
{getSavingsText(plan) && (
|
||||
<Badge colorScheme='green' mt={2}>
|
||||
{getSavingsText(plan)}
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 功能列表 */}
|
||||
<VStack spacing={3} align='stretch'>
|
||||
<Text fontSize='sm' fontWeight='semibold' color={textColor}>
|
||||
包含功能:
|
||||
</Text>
|
||||
{plan.features.map((feature, index) => (
|
||||
<HStack key={index} spacing={3}>
|
||||
<Icon as={FaCheck} color='green.500' boxSize={3} />
|
||||
<Text fontSize='sm' color={textColor}>
|
||||
{feature}
|
||||
</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
|
||||
{/* 订阅按钮 */}
|
||||
<Button
|
||||
colorScheme={plan.name === 'max' ? 'blue' : 'gray'}
|
||||
variant={plan.name === 'max' ? 'solid' : 'outline'}
|
||||
size='lg'
|
||||
onClick={() => handleSubscribe(plan)}
|
||||
isDisabled={
|
||||
currentUser?.subscription_type === plan.name &&
|
||||
currentUser?.subscription_status === 'active'
|
||||
}
|
||||
>
|
||||
{currentUser?.subscription_type === plan.name &&
|
||||
currentUser?.subscription_status === 'active'
|
||||
? '已订阅'
|
||||
: `选择 ${plan.display_name}`
|
||||
}
|
||||
</Button>
|
||||
</VStack>
|
||||
</Card>
|
||||
)))}
|
||||
</Grid>
|
||||
|
||||
{/* 支付模态框 */}
|
||||
<Modal
|
||||
isOpen={isPaymentModalOpen}
|
||||
onClose={() => {
|
||||
stopAutoPaymentCheck();
|
||||
setPaymentOrder(null);
|
||||
setPaymentCountdown(0);
|
||||
onPaymentModalClose();
|
||||
}}
|
||||
size='lg'
|
||||
closeOnOverlayClick={false}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
<Icon as={FaWeixin} color='green.500' />
|
||||
<Text>微信支付</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>
|
||||
{!paymentOrder ? (
|
||||
/* 订单确认 */
|
||||
<VStack spacing={4} align='stretch'>
|
||||
{selectedPlan ? (
|
||||
<Card p={4} bg={bgAccent}>
|
||||
<Text fontSize='lg' fontWeight='bold' mb={2}>
|
||||
订单确认
|
||||
</Text>
|
||||
<HStack justify='space-between' mb={2}>
|
||||
<Text>套餐:</Text>
|
||||
<Text fontWeight='bold'>{selectedPlan.display_name}</Text>
|
||||
</HStack>
|
||||
<HStack justify='space-between' mb={2}>
|
||||
<Text>计费周期:</Text>
|
||||
<Text>{selectedCycle === 'monthly' ? '按月付费' : '按年付费'}</Text>
|
||||
</HStack>
|
||||
<HStack justify='space-between' mb={2}>
|
||||
<Text>价格:</Text>
|
||||
<Text fontSize='lg' fontWeight='bold' color='blue.500'>
|
||||
¥{getCurrentPrice(selectedPlan).toFixed(2)}
|
||||
</Text>
|
||||
</HStack>
|
||||
{getSavingsText(selectedPlan) && (
|
||||
<Badge colorScheme='green' alignSelf='flex-end'>
|
||||
{getSavingsText(selectedPlan)}
|
||||
</Badge>
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<Card p={4} bg='red.50'>
|
||||
<Text color='red.600'>请先选择一个订阅套餐</Text>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Button
|
||||
colorScheme='green'
|
||||
size='lg'
|
||||
leftIcon={<Icon as={FaWeixin} />}
|
||||
onClick={handleCreateOrder}
|
||||
isLoading={loading}
|
||||
loadingText='创建订单中...'
|
||||
isDisabled={!selectedPlan}
|
||||
>
|
||||
创建微信支付订单
|
||||
</Button>
|
||||
</VStack>
|
||||
) : (
|
||||
/* 支付二维码 */
|
||||
<VStack spacing={4} align='stretch'>
|
||||
<Text textAlign='center' fontSize='lg' fontWeight='bold'>
|
||||
请使用微信扫码支付
|
||||
</Text>
|
||||
|
||||
{/* 倒计时 */}
|
||||
<Card p={4} bg='orange.50'>
|
||||
<HStack justify='center' spacing={2}>
|
||||
<Icon as={FaClock} color='orange.500' />
|
||||
<Text color='orange.700'>
|
||||
二维码有效时间: {formatTime(paymentCountdown)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Progress
|
||||
value={(paymentCountdown / (30 * 60)) * 100}
|
||||
colorScheme='orange'
|
||||
size='sm'
|
||||
mt={2}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 二维码 */}
|
||||
<Box textAlign='center'>
|
||||
{paymentOrder.qr_code_url ? (
|
||||
<Image
|
||||
src={paymentOrder.qr_code_url}
|
||||
alt='微信支付二维码'
|
||||
mx='auto'
|
||||
maxW='200px'
|
||||
border='1px solid'
|
||||
borderColor={borderColor}
|
||||
borderRadius='md'
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
w='200px'
|
||||
h='200px'
|
||||
mx='auto'
|
||||
bg='gray.100'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
border='1px solid'
|
||||
borderColor={borderColor}
|
||||
borderRadius='md'
|
||||
>
|
||||
<Icon as={FaQrcode} color='gray.400' boxSize={12} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* 订单信息 */}
|
||||
<Card p={4} bg={bgAccent}>
|
||||
<Text fontSize='sm' color='gray.600' mb={2}>订单号: {paymentOrder.order_no}</Text>
|
||||
<HStack justify='space-between'>
|
||||
<Text>支付金额:</Text>
|
||||
<Text fontSize='lg' fontWeight='bold' color='green.500'>
|
||||
¥{paymentOrder.amount}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<VStack spacing={3}>
|
||||
<HStack spacing={3} w="100%">
|
||||
<Button
|
||||
variant='outline'
|
||||
leftIcon={<Icon as={FaRedo} />}
|
||||
onClick={handleCheckPaymentStatus}
|
||||
isLoading={checkingPayment}
|
||||
loadingText='检查中...'
|
||||
flex={1}
|
||||
>
|
||||
检查支付状态
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ghost'
|
||||
onClick={() => {
|
||||
console.log('🔍 当前支付订单:', paymentOrder);
|
||||
console.log('🔍 用户信息:', currentUser);
|
||||
}}
|
||||
>
|
||||
调试信息
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{/* 强制更新按钮 */}
|
||||
<Button
|
||||
colorScheme='orange'
|
||||
variant='solid'
|
||||
size='sm'
|
||||
onClick={handleForceUpdatePayment}
|
||||
isLoading={forceUpdating}
|
||||
loadingText='强制更新中...'
|
||||
w="100%"
|
||||
>
|
||||
🚀 强制更新支付状态
|
||||
</Button>
|
||||
|
||||
<Text fontSize='xs' color='gray.500' textAlign='center'>
|
||||
如果支付完成但页面未更新,请点击上方"强制更新"按钮
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
{/* 支付状态提示 */}
|
||||
{autoCheckInterval && (
|
||||
<Card p={3} bg='blue.50' borderColor='blue.200'>
|
||||
<HStack justify='center' spacing={2}>
|
||||
<Text fontSize='sm' color='blue.700'>
|
||||
🔄 正在自动检查支付状态...
|
||||
</Text>
|
||||
</HStack>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 支付说明 */}
|
||||
<Box fontSize='sm' color='gray.500'>
|
||||
<Text mb={1}>• 请使用微信"扫一扫"功能扫描上方二维码</Text>
|
||||
<Text mb={1}>• 支付完成后系统将自动检测并激活订阅</Text>
|
||||
<Text mb={1}>• 系统每10秒自动检查一次支付状态</Text>
|
||||
<Text>• 如遇问题请联系客服支持</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* 调试面板 */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Card p='20px' mt='20px' bg='gray.50' borderColor='gray.200'>
|
||||
<Text fontSize='md' fontWeight='bold' mb={3} color='gray.700'>
|
||||
🔧 调试信息
|
||||
</Text>
|
||||
<VStack spacing={2} align='stretch' fontSize='sm'>
|
||||
<HStack justify='space-between'>
|
||||
<Text color='gray.600'>支付订单:</Text>
|
||||
<Text color={paymentOrder ? 'green.600' : 'gray.400'}>
|
||||
{paymentOrder ? `ID: ${paymentOrder.id}` : '无'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify='space-between'>
|
||||
<Text color='gray.600'>自动检查:</Text>
|
||||
<Text color={autoCheckInterval ? 'blue.600' : 'gray.400'}>
|
||||
{autoCheckInterval ? '运行中' : '已停止'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify='space-between'>
|
||||
<Text color='gray.600'>订阅状态:</Text>
|
||||
<Text color={currentUser?.subscription_status === 'active' ? 'green.600' : 'red.600'}>
|
||||
{currentUser?.subscription_status || '未知'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify='space-between'>
|
||||
<Text color='gray.600'>订阅类型:</Text>
|
||||
<Text>{currentUser?.subscription_type || '未知'}</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<Divider my={3} />
|
||||
<HStack spacing={2}>
|
||||
<Button size='sm' onClick={() => console.log('当前状态:', { currentUser, paymentOrder, autoCheckInterval })}>
|
||||
打印状态
|
||||
</Button>
|
||||
<Button size='sm' onClick={handleRefreshUserStatus}>
|
||||
强制刷新
|
||||
</Button>
|
||||
</HStack>
|
||||
</Card>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default Subscription;
|
||||
Reference in New Issue
Block a user