mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 09:44:47 +00:00
monorepo packages (#344)
This commit is contained in:
10
projects/app/src/pages/account/components/ApiKeyTable.tsx
Normal file
10
projects/app/src/pages/account/components/ApiKeyTable.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ApiKeyTable from '@/components/support/apikey/Table';
|
||||
|
||||
const ApiKey = () => {
|
||||
const { t } = useTranslation();
|
||||
return <ApiKeyTable tips={t('openapi.key tips')}></ApiKeyTable>;
|
||||
};
|
||||
|
||||
export default ApiKey;
|
83
projects/app/src/pages/account/components/BillDetail.tsx
Normal file
83
projects/app/src/pages/account/components/BillDetail.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
ModalBody,
|
||||
Flex,
|
||||
Box,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer
|
||||
} from '@chakra-ui/react';
|
||||
import { UserBillType } from '@/types/user';
|
||||
import dayjs from 'dayjs';
|
||||
import { BillSourceMap } from '@/constants/user';
|
||||
import { formatPrice } from '@fastgpt/common/bill/index';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const BillDetail = ({ bill, onClose }: { bill: UserBillType; onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const filterBillList = useMemo(
|
||||
() => bill.list.filter((item) => item && item.moduleName),
|
||||
[bill.list]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal isOpen={true} onClose={onClose} title={t('user.Bill Detail')}>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<Box flex={'0 0 80px'}>订单号:</Box>
|
||||
<Box>{bill.id}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<Box flex={'0 0 80px'}>生成时间:</Box>
|
||||
<Box>{dayjs(bill.time).format('YYYY/MM/DD HH:mm:ss')}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<Box flex={'0 0 80px'}>应用名:</Box>
|
||||
<Box>{t(bill.appName) || '-'}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<Box flex={'0 0 80px'}>来源:</Box>
|
||||
<Box>{BillSourceMap[bill.source]}</Box>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} pb={4}>
|
||||
<Box flex={'0 0 80px'}>总金额:</Box>
|
||||
<Box fontWeight={'bold'}>{bill.total}元</Box>
|
||||
</Flex>
|
||||
<Box pb={4}>
|
||||
<Box flex={'0 0 80px'} mb={1}>
|
||||
扣费模块
|
||||
</Box>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>模块名</Th>
|
||||
<Th>AI模型</Th>
|
||||
<Th>Token长度</Th>
|
||||
<Th>费用</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{filterBillList.map((item, i) => (
|
||||
<Tr key={i}>
|
||||
<Td>{item.moduleName}</Td>
|
||||
<Td>{item.model}</Td>
|
||||
<Td>{item.tokenLen}</Td>
|
||||
<Td>{formatPrice(item.amount)}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default BillDetail;
|
109
projects/app/src/pages/account/components/BillTable.tsx
Normal file
109
projects/app/src/pages/account/components/BillTable.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Flex,
|
||||
Box,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import { BillSourceMap } from '@/constants/user';
|
||||
import { getUserBills } from '@/api/user';
|
||||
import type { UserBillType } from '@/types/user';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import dayjs from 'dayjs';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
|
||||
import { addDays } from 'date-fns';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
const BillDetail = dynamic(() => import('./BillDetail'));
|
||||
|
||||
const BillTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const { Loading } = useLoading();
|
||||
const [dateRange, setDateRange] = useState<DateRangeType>({
|
||||
from: addDays(new Date(), -7),
|
||||
to: new Date()
|
||||
});
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const {
|
||||
data: bills,
|
||||
isLoading,
|
||||
Pagination,
|
||||
getData
|
||||
} = usePagination<UserBillType>({
|
||||
api: getUserBills,
|
||||
pageSize: isPc ? 20 : 10,
|
||||
params: {
|
||||
dateStart: dateRange.from || new Date(),
|
||||
dateEnd: addDays(dateRange.to || new Date(), 1)
|
||||
}
|
||||
});
|
||||
|
||||
const [billDetail, setBillDetail] = useState<UserBillType>();
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
|
||||
<TableContainer px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{t('user.Time')}</Th>
|
||||
<Th>{t('user.Source')}</Th>
|
||||
<Th>{t('user.Application Name')}</Th>
|
||||
<Th>{t('user.Total Amount')}</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{bills.map((item) => (
|
||||
<Tr key={item.id}>
|
||||
<Td>{dayjs(item.time).format('YYYY/MM/DD HH:mm:ss')}</Td>
|
||||
<Td>{BillSourceMap[item.source]}</Td>
|
||||
<Td>{t(item.appName) || '-'}</Td>
|
||||
<Td>{item.total}元</Td>
|
||||
<Td>
|
||||
<Button size={'sm'} variant={'base'} onClick={() => setBillDetail(item)}>
|
||||
详情
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{!isLoading && bills.length === 0 && (
|
||||
<Flex flex={'1 0 0'} flexDirection={'column'} alignItems={'center'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
无使用记录~
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
<Flex w={'100%'} mt={4} px={[3, 8]} alignItems={'center'} justifyContent={'flex-end'}>
|
||||
<DateRangePicker
|
||||
defaultDate={dateRange}
|
||||
position="top"
|
||||
onChange={setDateRange}
|
||||
onSuccess={() => getData(1)}
|
||||
/>
|
||||
<Box ml={3}>
|
||||
<Pagination />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
{!!billDetail && <BillDetail bill={billDetail} onClose={() => setBillDetail(undefined)} />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default BillTable;
|
307
projects/app/src/pages/account/components/Info.tsx
Normal file
307
projects/app/src/pages/account/components/Info.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Button,
|
||||
useDisclosure,
|
||||
useTheme,
|
||||
Divider,
|
||||
Select,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { UserUpdateParams } from '@/types/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { UserType } from '@/types/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useSelectFile } from '@/hooks/useSelectFile';
|
||||
import { compressImg } from '@/utils/web/file';
|
||||
import { feConfigs, systemVersion } from '@/store/static';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { timezoneList } from '@/utils/user';
|
||||
import Loading from '@/components/Loading';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { getLangStore, LangEnum, langMap, setLangStore } from '@/utils/web/i18n';
|
||||
import { useRouter } from 'next/router';
|
||||
import MyMenu from '@/components/MyMenu';
|
||||
import MySelect from '@/components/Select';
|
||||
|
||||
const PayModal = dynamic(() => import('./PayModal'), {
|
||||
loading: () => <Loading fixed={false} />,
|
||||
ssr: false
|
||||
});
|
||||
const UpdatePswModal = dynamic(() => import('./UpdatePswModal'), {
|
||||
loading: () => <Loading fixed={false} />,
|
||||
ssr: false
|
||||
});
|
||||
const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'), {
|
||||
loading: () => <Loading fixed={false} />,
|
||||
ssr: false
|
||||
});
|
||||
|
||||
const UserInfo = () => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
|
||||
const timezones = useRef(timezoneList());
|
||||
const { reset } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
isOpen: isOpenPayModal,
|
||||
onClose: onClosePayModal,
|
||||
onOpen: onOpenPayModal
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenUpdatePsw,
|
||||
onClose: onCloseUpdatePsw,
|
||||
onOpen: onOpenUpdatePsw
|
||||
} = useDisclosure();
|
||||
const { isOpen: isOpenOpenai, onClose: onCloseOpenai, onOpen: onOpenOpenai } = useDisclosure();
|
||||
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: '.jpg,.png',
|
||||
multiple: false
|
||||
});
|
||||
|
||||
const [language, setLanguage] = useState<`${LangEnum}`>(getLangStore());
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserType) => {
|
||||
await updateUserInfo({
|
||||
avatar: data.avatar,
|
||||
timezone: data.timezone,
|
||||
openaiAccount: data.openaiAccount
|
||||
});
|
||||
reset(data);
|
||||
toast({
|
||||
title: '更新数据成功',
|
||||
status: 'success'
|
||||
});
|
||||
},
|
||||
[reset, toast, updateUserInfo]
|
||||
);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
if (!file || !userInfo) return;
|
||||
try {
|
||||
const src = await compressImg({
|
||||
file,
|
||||
maxW: 100,
|
||||
maxH: 100
|
||||
});
|
||||
|
||||
onclickSave({
|
||||
...userInfo,
|
||||
avatar: src
|
||||
});
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : '头像选择异常',
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
},
|
||||
[onclickSave, toast, userInfo]
|
||||
);
|
||||
|
||||
useQuery(['init'], initUserInfo, {
|
||||
onSuccess(res) {
|
||||
reset(res);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
display={['block', 'flex']}
|
||||
py={[2, 10]}
|
||||
justifyContent={'center'}
|
||||
alignItems={'flex-start'}
|
||||
fontSize={['lg', 'xl']}
|
||||
>
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenSelectFile}
|
||||
>
|
||||
<MyTooltip label={'更换头像'}>
|
||||
<Box
|
||||
w={['44px', '54px']}
|
||||
h={['44px', '54px']}
|
||||
borderRadius={'50%'}
|
||||
border={theme.borders.base}
|
||||
overflow={'hidden'}
|
||||
p={'2px'}
|
||||
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
|
||||
mb={2}
|
||||
>
|
||||
<Avatar src={userInfo?.avatar} borderRadius={'50%'} w={'100%'} h={'100%'} />
|
||||
</Box>
|
||||
</MyTooltip>
|
||||
|
||||
<Flex alignItems={'center'} fontSize={'sm'} color={'myGray.600'}>
|
||||
<MyIcon mr={1} name={'edit'} w={'14px'} />
|
||||
{t('user.Replace')}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box
|
||||
display={['flex', 'block']}
|
||||
flexDirection={'column'}
|
||||
alignItems={'center'}
|
||||
ml={[0, 10]}
|
||||
mt={[6, 0]}
|
||||
>
|
||||
<Flex alignItems={'center'} w={['85%', '300px']}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Account')}: </Box>
|
||||
<Box flex={1}>{userInfo?.username}</Box>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Language')}: </Box>
|
||||
<Box flex={'1 0 0'}>
|
||||
<MySelect
|
||||
value={language}
|
||||
list={Object.entries(langMap).map(([key, lang]) => ({
|
||||
label: lang.label,
|
||||
value: key
|
||||
}))}
|
||||
onchange={(val: any) => {
|
||||
const lang = val;
|
||||
setLangStore(lang);
|
||||
setLanguage(lang);
|
||||
i18n?.changeLanguage?.(lang);
|
||||
router.reload();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Timezone')}: </Box>
|
||||
<Select
|
||||
value={userInfo?.timezone}
|
||||
onChange={(e) => {
|
||||
if (!userInfo) return;
|
||||
onclickSave({ ...userInfo, timezone: e.target.value });
|
||||
}}
|
||||
>
|
||||
{timezones.current.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
<Flex mt={6} alignItems={'center'} w={['85%', '300px']}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Password')}: </Box>
|
||||
<Box flex={1}>*****</Box>
|
||||
<Button size={['sm', 'md']} variant={'base'} ml={5} onClick={onOpenUpdatePsw}>
|
||||
{t('user.Change')}
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box mt={6} whiteSpace={'nowrap'} w={['85%', '300px']}>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 80px'}>{t('user.Balance')}: </Box>
|
||||
<Box flex={1}>
|
||||
<strong>{userInfo?.balance.toFixed(3)}</strong> 元
|
||||
</Box>
|
||||
{feConfigs?.show_pay && (
|
||||
<Button size={['sm', 'md']} ml={5} onClick={onOpenPayModal}>
|
||||
{t('user.Pay')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
{feConfigs?.show_doc && (
|
||||
<>
|
||||
<Flex
|
||||
mt={4}
|
||||
w={['85%', '300px']}
|
||||
py={3}
|
||||
px={6}
|
||||
border={theme.borders.sm}
|
||||
borderWidth={'1.5px'}
|
||||
borderRadius={'md'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
onClick={() => {
|
||||
window.open(`https://doc.fastgpt.run/docs/intro`);
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'courseLight'} w={'18px'} />
|
||||
<Box ml={2} flex={1}>
|
||||
{t('system.Help Document')}
|
||||
</Box>
|
||||
<Box w={'8px'} h={'8px'} borderRadius={'50%'} bg={'#67c13b'} />
|
||||
<Box fontSize={'md'} ml={2}>
|
||||
V{systemVersion}
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
{feConfigs?.show_openai_account && (
|
||||
<>
|
||||
<Divider my={3} />
|
||||
|
||||
<MyTooltip label={'点击配置账号'}>
|
||||
<Flex
|
||||
w={['85%', '300px']}
|
||||
py={3}
|
||||
px={6}
|
||||
border={theme.borders.sm}
|
||||
borderWidth={'1.5px'}
|
||||
borderRadius={'md'}
|
||||
bg={'myWhite.300'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
userSelect={'none'}
|
||||
onClick={onOpenOpenai}
|
||||
>
|
||||
<Avatar src={'/imgs/openai.png'} w={'18px'} />
|
||||
<Box ml={2} flex={1}>
|
||||
OpenAI 账号
|
||||
</Box>
|
||||
<Box
|
||||
w={'9px'}
|
||||
h={'9px'}
|
||||
borderRadius={'50%'}
|
||||
bg={userInfo?.openaiAccount?.key ? '#67c13b' : 'myGray.500'}
|
||||
/>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isOpenPayModal && <PayModal onClose={onClosePayModal} />}
|
||||
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
|
||||
{isOpenOpenai && userInfo && (
|
||||
<OpenAIAccountModal
|
||||
defaultData={userInfo?.openaiAccount}
|
||||
onSuccess={(data) =>
|
||||
onclickSave({
|
||||
...userInfo,
|
||||
openaiAccount: data
|
||||
})
|
||||
}
|
||||
onClose={onCloseOpenai}
|
||||
/>
|
||||
)}
|
||||
<File onSelect={onSelectFile} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserInfo;
|
89
projects/app/src/pages/account/components/InformTable.tsx
Normal file
89
projects/app/src/pages/account/components/InformTable.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { getInforms, readInform } from '@/api/user';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import type { informSchema } from '@/types/mongoSchema';
|
||||
import { formatTimeToChatTime } from '@/utils/tools';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
const BillTable = () => {
|
||||
const theme = useTheme();
|
||||
const { Loading } = useLoading();
|
||||
const { isPc } = useGlobalStore();
|
||||
const {
|
||||
data: informs,
|
||||
isLoading,
|
||||
total,
|
||||
pageSize,
|
||||
Pagination,
|
||||
getData,
|
||||
pageNum
|
||||
} = usePagination<informSchema>({
|
||||
api: getInforms,
|
||||
pageSize: isPc ? 20 : 10
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} py={[0, 5]} h={'100%'} position={'relative'}>
|
||||
<Box px={[3, 8]} position={'relative'} flex={'1 0 0'} h={0} overflowY={'auto'}>
|
||||
{informs.map((item) => (
|
||||
<Box
|
||||
key={item._id}
|
||||
border={theme.borders.md}
|
||||
py={2}
|
||||
px={4}
|
||||
borderRadius={'md'}
|
||||
cursor={item.read ? 'default' : 'pointer'}
|
||||
position={'relative'}
|
||||
_notLast={{ mb: 3 }}
|
||||
onClick={async () => {
|
||||
if (!item.read) {
|
||||
await readInform(item._id);
|
||||
getData(pageNum);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
<Box>{item.title}</Box>
|
||||
<Box ml={2} color={'myGray.500'}>
|
||||
{formatTimeToChatTime(item.time)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'sm'} color={'myGray.600'}>
|
||||
{item.content}
|
||||
</Box>
|
||||
{!item.read && (
|
||||
<Box
|
||||
w={'5px'}
|
||||
h={'5px'}
|
||||
borderRadius={'10px'}
|
||||
bg={'myRead.600'}
|
||||
position={'absolute'}
|
||||
bottom={'8px'}
|
||||
right={'8px'}
|
||||
></Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{!isLoading && informs.length === 0 && (
|
||||
<Flex flex={'1 0 0'} flexDirection={'column'} alignItems={'center'} pt={'-48px'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
暂无通知~
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{total > pageSize && (
|
||||
<Flex w={'100%'} mt={4} px={[3, 8]} justifyContent={'flex-end'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<Loading loading={isLoading && informs.length === 0} fixed={false} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default BillTable;
|
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { UserType } from '@/types/user';
|
||||
|
||||
const OpenAIAccountModal = ({
|
||||
defaultData,
|
||||
onSuccess,
|
||||
onClose
|
||||
}: {
|
||||
defaultData: UserType['openaiAccount'];
|
||||
onSuccess: (e: UserType['openaiAccount']) => Promise<any>;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: defaultData
|
||||
});
|
||||
|
||||
const { mutate: onSubmit, isLoading } = useRequest({
|
||||
mutationFn: async (data: UserType['openaiAccount']) => onSuccess(data),
|
||||
onSuccess(res) {
|
||||
onClose();
|
||||
},
|
||||
errorToast: t('user.Set OpenAI Account Failed')
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} title={t('user.OpenAI Account Setting')}>
|
||||
<ModalBody>
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
如果你填写了该内容,在线上平台使用 OpenAI Chat 模型不会计费(不包含知识库训练、索引生成)
|
||||
</Box>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 65px'}>API Key:</Box>
|
||||
<Input flex={1} {...register('key')}></Input>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 65px'}>BaseUrl:</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
{...register('baseUrl')}
|
||||
placeholder={'请求地址,默认为 openai 官方。可填中转地址,未自动补全 "v1"'}
|
||||
></Input>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'base'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenAIAccountModal;
|
140
projects/app/src/pages/account/components/PayModal.tsx
Normal file
140
projects/app/src/pages/account/components/PayModal.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { ModalFooter, ModalBody, Button, Input, Box, Grid } from '@chakra-ui/react';
|
||||
import { getPayCode, checkPayResult } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatPrice } from '@fastgpt/common/bill/index';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { vectorModelList, chatModelList, qaModel } from '@/store/static';
|
||||
|
||||
const PayModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const [inputVal, setInputVal] = useState<number | ''>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [payId, setPayId] = useState('');
|
||||
|
||||
const handleClickPay = useCallback(async () => {
|
||||
if (!inputVal || inputVal <= 0 || isNaN(+inputVal)) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取支付二维码
|
||||
const res = await getPayCode(inputVal);
|
||||
new window.QRCode(document.getElementById('payQRCode'), {
|
||||
text: res.codeUrl,
|
||||
width: 128,
|
||||
height: 128,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: window.QRCode.CorrectLevel.H
|
||||
});
|
||||
setPayId(res.payId);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: getErrText(err),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [inputVal, toast]);
|
||||
|
||||
useQuery(
|
||||
[payId],
|
||||
() => {
|
||||
if (!payId) return null;
|
||||
return checkPayResult(payId);
|
||||
},
|
||||
{
|
||||
enabled: !!payId,
|
||||
refetchInterval: 3000,
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
toast({
|
||||
title: '充值成功',
|
||||
status: 'success'
|
||||
});
|
||||
router.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={payId ? undefined : onClose}
|
||||
title={t('user.Pay')}
|
||||
isCentered={!payId}
|
||||
>
|
||||
<ModalBody py={0}>
|
||||
{!payId && (
|
||||
<>
|
||||
<Grid gridTemplateColumns={'repeat(4,1fr)'} gridGap={5} mb={4}>
|
||||
{[10, 20, 50, 100].map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
variant={item === inputVal ? 'solid' : 'outline'}
|
||||
onClick={() => setInputVal(item)}
|
||||
>
|
||||
{item}元
|
||||
</Button>
|
||||
))}
|
||||
</Grid>
|
||||
<Box mb={4}>
|
||||
<Input
|
||||
value={inputVal}
|
||||
type={'number'}
|
||||
step={1}
|
||||
placeholder={'其他金额,请取整数'}
|
||||
onChange={(e) => {
|
||||
setInputVal(Math.floor(+e.target.value));
|
||||
}}
|
||||
></Input>
|
||||
</Box>
|
||||
<Markdown
|
||||
source={`
|
||||
| 计费项 | 价格: 元/ 1K tokens(包含上下文)|
|
||||
| --- | --- |
|
||||
${vectorModelList
|
||||
.map((item) => `| 索引-${item.name} | ${formatPrice(item.price, 1000)} |`)
|
||||
.join('\n')}
|
||||
${chatModelList
|
||||
.map((item) => `| 对话-${item.name} | ${formatPrice(item.price, 1000)} |`)
|
||||
.join('\n')}
|
||||
| 文件QA拆分 | ${formatPrice(qaModel.price, 1000)} |`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* 付费二维码 */}
|
||||
<Box textAlign={'center'}>
|
||||
{payId && <Box mb={3}>请微信扫码支付: {inputVal}元,请勿关闭页面</Box>}
|
||||
<Box id={'payQRCode'} display={'inline-block'}></Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{!payId && (
|
||||
<>
|
||||
<Button variant={'base'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
ml={3}
|
||||
isLoading={loading}
|
||||
isDisabled={!inputVal || inputVal === 0}
|
||||
onClick={handleClickPay}
|
||||
>
|
||||
获取充值二维码
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayModal;
|
108
projects/app/src/pages/account/components/PayRecordTable.tsx
Normal file
108
projects/app/src/pages/account/components/PayRecordTable.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
Flex,
|
||||
Box
|
||||
} from '@chakra-ui/react';
|
||||
import { getPayOrders, checkPayResult } from '@/api/user';
|
||||
import { PaySchema } from '@/types/mongoSchema';
|
||||
import dayjs from 'dayjs';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { formatPrice } from '@fastgpt/common/bill/index';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import MyIcon from '@/components/Icon';
|
||||
|
||||
const PayRecordTable = () => {
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const [payOrders, setPayOrders] = useState<PaySchema[]>([]);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { isInitialLoading, refetch } = useQuery(['initPayOrder'], getPayOrders, {
|
||||
onSuccess(res) {
|
||||
setPayOrders(res);
|
||||
}
|
||||
});
|
||||
|
||||
const handleRefreshPayOrder = useCallback(
|
||||
async (payId: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const data = await checkPayResult(payId);
|
||||
toast({
|
||||
title: data,
|
||||
status: 'success'
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: error?.message,
|
||||
status: 'warning'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
refetch();
|
||||
} catch (error) {}
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
[refetch, setIsLoading, toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box position={'relative'} h={'100%'}>
|
||||
{!isInitialLoading && payOrders.length === 0 ? (
|
||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} justifyContent={'center'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
无支付记录~
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
<TableContainer py={[0, 5]} px={[3, 8]} h={'100%'} overflow={'overlay'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>订单号</Th>
|
||||
<Th>时间</Th>
|
||||
<Th>金额</Th>
|
||||
<Th>状态</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{payOrders.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>{item.orderId}</Td>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{formatPrice(item.price)}元</Td>
|
||||
<Td>{item.status}</Td>
|
||||
<Td>
|
||||
{item.status === 'NOTPAY' && (
|
||||
<Button onClick={() => handleRefreshPayOrder(item._id)} size={'sm'}>
|
||||
更新
|
||||
</Button>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
<Loading loading={isInitialLoading} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayRecordTable;
|
148
projects/app/src/pages/account/components/Promotion.tsx
Normal file
148
projects/app/src/pages/account/components/Promotion.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Box,
|
||||
Flex,
|
||||
BoxProps,
|
||||
useTheme,
|
||||
Button,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPromotionInitData, getPromotionRecords } from '@/api/user';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useCopyData } from '@/hooks/useCopyData';
|
||||
import { usePagination } from '@/hooks/usePagination';
|
||||
import { PromotionRecordType } from '@/api/response/user';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const Promotion = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const { userInfo } = useUserStore();
|
||||
const { Loading } = useLoading();
|
||||
|
||||
const {
|
||||
data: promotionRecords,
|
||||
isLoading,
|
||||
total,
|
||||
pageSize,
|
||||
Pagination
|
||||
} = usePagination<PromotionRecordType>({
|
||||
api: getPromotionRecords
|
||||
});
|
||||
|
||||
const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery(
|
||||
['getPromotionInitData'],
|
||||
getPromotionInitData
|
||||
);
|
||||
|
||||
const statisticsStyles: BoxProps = {
|
||||
p: [4, 5],
|
||||
border: theme.borders.base,
|
||||
textAlign: 'center',
|
||||
fontSize: ['md', 'xl'],
|
||||
borderRadius: 'md'
|
||||
};
|
||||
const titleStyles: BoxProps = {
|
||||
mt: 2,
|
||||
fontSize: ['lg', '28px'],
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} py={[0, 5]} px={5} h={'100%'} position={'relative'}>
|
||||
<Grid gridTemplateColumns={['1fr 1fr', 'repeat(2,1fr)', 'repeat(4,1fr)']} gridGap={5}>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('user.Amount of inviter')}</Box>
|
||||
<Box {...titleStyles}>{invitedAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Box>{t('user.Amount of earnings')}</Box>
|
||||
<Box {...titleStyles}>{earningsAmount}</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('user.Promotion Rate')}</Box>
|
||||
<MyTooltip label={t('user.Promotion rate tip')}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box {...titleStyles}>{userInfo?.promotionRate || 15}%</Box>
|
||||
</Box>
|
||||
<Box {...statisticsStyles}>
|
||||
<Flex alignItems={'center'} justifyContent={'center'}>
|
||||
<Box>{t('user.Invite Url')}</Box>
|
||||
<MyTooltip label={t('user.Invite url tip')}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Button
|
||||
mt={4}
|
||||
variant={'base'}
|
||||
fontSize={'sm'}
|
||||
onClick={() => {
|
||||
copyData(`${location.origin}/?hiId=${userInfo?._id}`);
|
||||
}}
|
||||
>
|
||||
{t('user.Copy invite url')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Box mt={5}>
|
||||
<TableContainer position={'relative'} overflow={'hidden'} minH={'100px'}>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>时间</Th>
|
||||
<Th>类型</Th>
|
||||
<Th>金额(¥)</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody fontSize={'sm'}>
|
||||
{promotionRecords.map((item) => (
|
||||
<Tr key={item._id}>
|
||||
<Td>
|
||||
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
|
||||
</Td>
|
||||
<Td>{t(`user.promotion.${item.type}`)}</Td>
|
||||
<Td>{item.amount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{!isLoading && promotionRecords.length === 0 && (
|
||||
<Flex mt={'10vh'} flexDirection={'column'} alignItems={'center'}>
|
||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
||||
<Box mt={2} color={'myGray.500'}>
|
||||
无邀请记录~
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
{total > pageSize && (
|
||||
<Flex mt={4} justifyContent={'flex-end'}>
|
||||
<Pagination />
|
||||
</Flex>
|
||||
)}
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Promotion;
|
87
projects/app/src/pages/account/components/UpdatePswModal.tsx
Normal file
87
projects/app/src/pages/account/components/UpdatePswModal.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { ModalBody, Box, Flex, Input, ModalFooter, Button } from '@chakra-ui/react';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { updatePasswordByOld } from '@/api/user';
|
||||
|
||||
type FormType = {
|
||||
oldPsw: string;
|
||||
newPsw: string;
|
||||
confirmPsw: string;
|
||||
};
|
||||
|
||||
const UpdatePswModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const { register, handleSubmit } = useForm<FormType>({
|
||||
defaultValues: {
|
||||
oldPsw: '',
|
||||
newPsw: '',
|
||||
confirmPsw: ''
|
||||
}
|
||||
});
|
||||
|
||||
const { mutate: onSubmit, isLoading } = useRequest({
|
||||
mutationFn: (data: FormType) => {
|
||||
if (data.newPsw !== data.confirmPsw) {
|
||||
return Promise.reject(t('common.Password inconsistency'));
|
||||
}
|
||||
return updatePasswordByOld(data);
|
||||
},
|
||||
onSuccess() {
|
||||
onClose();
|
||||
},
|
||||
successToast: t('user.Update password succseful'),
|
||||
errorToast: t('user.Update password failed')
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal isOpen onClose={onClose} title={t('user.Update Password')}>
|
||||
<ModalBody>
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 70px'}>旧密码:</Box>
|
||||
<Input flex={1} type={'password'} {...register('oldPsw', { required: true })}></Input>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'}>新密码:</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'password'}
|
||||
{...register('newPsw', {
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: '密码最少 4 位最多 20 位'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} mt={5}>
|
||||
<Box flex={'0 0 70px'}>确认密码:</Box>
|
||||
<Input
|
||||
flex={1}
|
||||
type={'password'}
|
||||
{...register('confirmPsw', {
|
||||
required: true,
|
||||
maxLength: {
|
||||
value: 20,
|
||||
message: '密码最少 4 位最多 20 位'
|
||||
}
|
||||
})}
|
||||
></Input>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={3} variant={'base'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button isLoading={isLoading} onClick={handleSubmit((data) => onSubmit(data))}>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdatePswModal;
|
173
projects/app/src/pages/account/index.tsx
Normal file
173
projects/app/src/pages/account/index.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { useRouter } from 'next/router';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { clearToken } from '@/utils/user';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useConfirm } from '@/hooks/useConfirm';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
import SideTabs from '@/components/SideTabs';
|
||||
import Tabs from '@/components/Tabs';
|
||||
import UserInfo from './components/Info';
|
||||
import { serviceSideProps } from '@/utils/web/i18n';
|
||||
import { feConfigs } from '@/store/static';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Script from 'next/script';
|
||||
|
||||
const Promotion = dynamic(() => import('./components/Promotion'));
|
||||
const BillTable = dynamic(() => import('./components/BillTable'));
|
||||
const PayRecordTable = dynamic(() => import('./components/PayRecordTable'));
|
||||
const InformTable = dynamic(() => import('./components/InformTable'));
|
||||
const ApiKeyTable = dynamic(() => import('./components/ApiKeyTable'));
|
||||
|
||||
enum TabEnum {
|
||||
'info' = 'info',
|
||||
'promotion' = 'promotion',
|
||||
'bill' = 'bill',
|
||||
'pay' = 'pay',
|
||||
'inform' = 'inform',
|
||||
'apikey' = 'apikey',
|
||||
'loginout' = 'loginout'
|
||||
}
|
||||
|
||||
const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
const { t } = useTranslation();
|
||||
const tabList = [
|
||||
{
|
||||
icon: 'meLight',
|
||||
label: t('user.Personal Information'),
|
||||
id: TabEnum.info
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'billRecordLight',
|
||||
label: t('user.Usage Record'),
|
||||
id: TabEnum.bill
|
||||
},
|
||||
...(feConfigs?.show_promotion
|
||||
? [
|
||||
{
|
||||
icon: 'promotionLight',
|
||||
label: t('user.Promotion Record'),
|
||||
id: TabEnum.promotion
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(feConfigs?.show_pay
|
||||
? [
|
||||
{
|
||||
icon: 'payRecordLight',
|
||||
label: t('user.Recharge Record'),
|
||||
id: TabEnum.pay
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
icon: 'apikey',
|
||||
label: t('user.apikey.key'),
|
||||
id: TabEnum.apikey
|
||||
},
|
||||
{
|
||||
icon: 'informLight',
|
||||
label: t('user.Notice'),
|
||||
id: TabEnum.inform
|
||||
},
|
||||
{
|
||||
icon: 'loginoutLight',
|
||||
label: t('user.Sign Out'),
|
||||
id: TabEnum.loginout
|
||||
}
|
||||
];
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
content: '确认退出登录?'
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
const { isPc } = useGlobalStore();
|
||||
const { setUserInfo } = useUserStore();
|
||||
|
||||
const setCurrentTab = useCallback(
|
||||
(tab: string) => {
|
||||
if (tab === TabEnum.loginout) {
|
||||
openConfirm(() => {
|
||||
clearToken();
|
||||
setUserInfo(null);
|
||||
router.replace('/login');
|
||||
})();
|
||||
} else {
|
||||
router.replace({
|
||||
query: {
|
||||
currentTab: tab
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[openConfirm, router, setUserInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script src="/js/qrcode.min.js" strategy="lazyOnload"></Script>
|
||||
<PageContainer>
|
||||
<Flex flexDirection={['column', 'row']} h={'100%'} pt={[4, 0]}>
|
||||
{isPc ? (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
p={4}
|
||||
h={'100%'}
|
||||
flex={'0 0 200px'}
|
||||
borderRight={theme.borders.base}
|
||||
>
|
||||
<SideTabs
|
||||
flex={1}
|
||||
mx={'auto'}
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
list={tabList}
|
||||
activeId={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box mb={3}>
|
||||
<Tabs
|
||||
m={'auto'}
|
||||
size={isPc ? 'md' : 'sm'}
|
||||
list={tabList.map((item) => ({
|
||||
id: item.id,
|
||||
label: item.label
|
||||
}))}
|
||||
activeId={currentTab}
|
||||
onChange={setCurrentTab}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box flex={'1 0 0'} h={'100%'} pb={[4, 0]}>
|
||||
{currentTab === TabEnum.info && <UserInfo />}
|
||||
{currentTab === TabEnum.promotion && <Promotion />}
|
||||
{currentTab === TabEnum.bill && <BillTable />}
|
||||
{currentTab === TabEnum.pay && <PayRecordTable />}
|
||||
{currentTab === TabEnum.inform && <InformTable />}
|
||||
{currentTab === TabEnum.apikey && <ApiKeyTable />}
|
||||
</Box>
|
||||
</Flex>
|
||||
<ConfirmModal />
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
currentTab: content?.query?.currentTab || TabEnum.info,
|
||||
...(await serviceSideProps(content))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default Account;
|
Reference in New Issue
Block a user