feat(fe): balance conversion button and modal (#2491)

* feat: add balance conversion api declaration

* feat(fe): add conversion modal

* fix: show button when standplan and the user has manage permission

* feat: hide balance when <= 0
This commit is contained in:
Finley Ge
2024-08-23 17:14:07 +08:00
committed by GitHub
parent 6288dc9492
commit eaaf6f5978
7 changed files with 165 additions and 42 deletions

View File

@@ -30,9 +30,9 @@ export const iconPaths = {
'common/gitInlight': () => import('./icons/common/gitInlight.svg'),
'common/gitLight': () => import('./icons/common/gitLight.svg'),
'common/googleFill': () => import('./icons/common/googleFill.svg'),
'common/help': () => import('./icons/common/help.svg'),
'common/importLight': () => import('./icons/common/importLight.svg'),
'common/info': () => import('./icons/common/info.svg'),
'common/help': () => import('./icons/common/help.svg'),
'common/inviteLight': () => import('./icons/common/inviteLight.svg'),
'common/language/en': () => import('./icons/common/language/en.svg'),
'common/language/zh': () => import('./icons/common/language/zh.svg'),
@@ -244,10 +244,7 @@ export const iconPaths = {
delete: () => import('./icons/delete.svg'),
edit: () => import('./icons/edit.svg'),
empty: () => import('./icons/empty.svg'),
paragraph: () => import('./icons/paragraph.svg'),
export: () => import('./icons/export.svg'),
point: () => import('./icons/point.svg'),
infoRounded: () => import('./icons/infoRounded.svg'),
'file/csv': () => import('./icons/file/csv.svg'),
'file/fill/csv': () => import('./icons/file/fill/csv.svg'),
'file/fill/doc': () => import('./icons/file/fill/doc.svg'),
@@ -268,6 +265,7 @@ export const iconPaths = {
'file/qaImport': () => import('./icons/file/qaImport.svg'),
'file/uploadFile': () => import('./icons/file/uploadFile.svg'),
history: () => import('./icons/history.svg'),
infoRounded: () => import('./icons/infoRounded.svg'),
kbTest: () => import('./icons/kbTest.svg'),
menu: () => import('./icons/menu.svg'),
minus: () => import('./icons/minus.svg'),
@@ -283,11 +281,13 @@ export const iconPaths = {
more: () => import('./icons/more.svg'),
moreLine: () => import('./icons/moreLine.svg'),
out: () => import('./icons/out.svg'),
paragraph: () => import('./icons/paragraph.svg'),
'phoneTabbar/me': () => import('./icons/phoneTabbar/me.svg'),
'phoneTabbar/tool': () => import('./icons/phoneTabbar/tool.svg'),
'phoneTabbar/toolFill': () => import('./icons/phoneTabbar/toolFill.svg'),
'plugins/doc2x': () => import('./icons/plugins/doc2x.svg'),
'plugins/textEditor': () => import('./icons/plugins/textEditor.svg'),
point: () => import('./icons/point.svg'),
'price/bg': () => import('./icons/price/bg.svg'),
'price/right': () => import('./icons/price/right.svg'),
save: () => import('./icons/save.svg'),
@@ -301,6 +301,7 @@ export const iconPaths = {
'support/bill/payRecordLight': () => import('./icons/support/bill/payRecordLight.svg'),
'support/bill/priceLight': () => import('./icons/support/bill/priceLight.svg'),
'support/bill/shoppingCart': () => import('./icons/support/bill/shoppingCart.svg'),
'support/bill/wallet': () => import('./icons/support/bill/wallet.svg'),
'support/outlink/apikeyFill': () => import('./icons/support/outlink/apikeyFill.svg'),
'support/outlink/apikeyLight': () => import('./icons/support/outlink/apikeyLight.svg'),
'support/outlink/iframeLight': () => import('./icons/support/outlink/iframeLight.svg'),

View File

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.35199 3.84415C1.90161 4.09989 0.933159 5.48298 1.1889 6.93337L1.66737 9.6469V6.69182C1.66737 5.58725 2.5628 4.69182 3.66737 4.69182H6.22296L14.2786 3.27139C15.0038 3.14352 15.6953 3.62775 15.8232 4.35294L15.883 4.69182H16.3326C16.6749 4.69182 16.9971 4.7778 17.2787 4.92933L17.1363 4.12141C16.8806 2.67103 15.4975 1.70257 14.0471 1.95832L3.35199 3.84415Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.834045 6.69182C0.834045 5.12702 2.10257 3.85849 3.66738 3.85849H16.3326C17.8974 3.85849 19.1659 5.12702 19.1659 6.69182V15.2493C19.1659 16.8141 17.8974 18.0827 16.3326 18.0827H3.66738C2.10257 18.0827 0.834045 16.8141 0.834045 15.2493V6.69182ZM3.66738 5.52516C3.02305 5.52516 2.50071 6.04749 2.50071 6.69182V15.2493C2.50071 15.8937 3.02305 16.416 3.66738 16.416H16.3326C16.9769 16.416 17.4993 15.8937 17.4993 15.2493V6.69182C17.4993 6.04749 16.9769 5.52516 16.3326 5.52516H3.66738Z"/>
<path d="M15.7836 11.0202C15.7836 11.7539 15.1889 12.3487 14.4552 12.3487C13.7215 12.3487 13.1268 11.7539 13.1268 11.0202C13.1268 10.2866 13.7215 9.69182 14.4552 9.69182C15.1889 9.69182 15.7836 10.2866 15.7836 11.0202Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -14,6 +14,7 @@ import Avatar from '../Avatar';
export interface MyModalProps extends ModalContentProps {
iconSrc?: string;
iconColor?: string;
title?: any;
isCentered?: boolean;
isLoading?: boolean;
@@ -33,6 +34,7 @@ const MyModal = ({
w = 'auto',
maxW = ['90vw', '600px'],
closeOnOverlayClick = true,
iconColor,
...props
}: MyModalProps) => {
const { isPc } = useSystem();
@@ -71,6 +73,7 @@ const MyModal = ({
{iconSrc && (
<>
<Avatar
color={iconColor}
objectFit={'contain'}
alt=""
src={iconSrc}

View File

@@ -1,6 +1,17 @@
{
"bill": {
"not_need_invoice": "余额支付,无法开票"
"not_need_invoice": "余额支付,无法开票",
"conversion": "兑换",
"use_balance": "使用余额",
"price": "价格",
"tokens": "积分",
"token_expire_1year": "积分有效期一年",
"balance": "余额",
"you_can_convert": "您可兑换",
"use_balance_hint": "由于系统升级,原“自动续费从余额扣款”模式取消,余额充值入口关闭。您的余额可用于购买积分",
"contact_customer_service": "联系客服",
"convert_success": "兑换成功",
"convert_error": "兑换失败"
},
"bind_inform_account_error": "绑定通知账号异常",
"bind_inform_account_success": "绑定通知账号成功",

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { ModalBody, Box, Button, VStack, HStack, Link } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import Icon from '@fastgpt/web/components/common/Icon';
import Tag from '@fastgpt/web/components/common/Tag';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { balanceConversion } from '@/web/support/wallet/bill/api';
import Loading from '@fastgpt/web/components/common/MyLoading';
const ConversionModal = ({
onClose,
balance,
tokens,
onOpenContact
}: {
onClose: () => void;
balance: string;
tokens: string;
onOpenContact: () => void;
}) => {
const { t } = useTranslation();
const { runAsync: onConvert, loading } = useRequest2(balanceConversion, {
successToast: t('user:bill.convert_success'),
errorToast: t('user:bill.convert_error')
});
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="support/bill/wallet"
iconColor="primary.600"
title={t('user:bill.use_balance')}
>
<ModalBody maxW={'450px'}>
{loading && <Loading />}
<VStack px="2.25" gap={2} pb="6">
<HStack px="4" py="2" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" mr="1" />
<Box fontSize={'sm'}>{t('user:bill.use_balance_hint')}</Box>
</HStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('user:bill.price')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
15/1000 {t('user:bill.tokens')}
</Box>
</VStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('user:bill.balance')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
{balance}
</Box>
</VStack>
<VStack mt={6}>
<Box fontSize={'sm'} color="myGray.600" fontWeight="500">
{t('user:bill.you_can_convert')}
</Box>
<Box fontSize={'xl'} fontWeight={'700'} color="myGray.900">
{tokens} {t('user:bill.tokens')}
</Box>
<Tag>{t('user:bill.token_expire_1year')}</Tag>
</VStack>
<VStack mt="6">
<Button
variant={'primary'}
alignItems={'center'}
fontSize={'sm'}
minW={'10rem'}
onClick={onConvert}
>
{t('user:bill.conversion')}
</Button>
<Link color="primary" mt="2" onClick={onOpenContact}>
{t('user:bill.contact_customer_service')}
</Link>
</VStack>
</VStack>
</ModalBody>
</MyModal>
);
};
export default ConversionModal;

View File

@@ -48,7 +48,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
const StandDetailModal = dynamic(() => import('./standardDetailModal'));
const TeamMenu = dynamic(() => import('@/components/support/user/team/TeamMenu'));
const PayModal = dynamic(() => import('./PayModal'));
const ConversionModal = dynamic(() => import('./ConversionModal'));
const UpdatePswModal = dynamic(() => import('./UpdatePswModal'));
const UpdateNotification = dynamic(() => import('./UpdateNotificationModal'));
const OpenAIAccountModal = dynamic(() => import('./OpenAIAccountModal'));
@@ -59,41 +59,45 @@ const Account = () => {
const { isPc } = useSystem();
const { teamPlanStatus } = useUserStore();
const standardPlan = teamPlanStatus?.standardConstants;
const { isOpen: isOpenContact, onClose: onCloseContact, onOpen: onOpenContact } = useDisclosure();
const { initUserInfo } = useUserStore();
useQuery(['init'], initUserInfo);
return (
<Box py={[3, '28px']} maxW={['95vw', '1080px']} px={[5, 10]} mx={'auto'}>
{isPc ? (
<Flex justifyContent={'center'}>
<Box flex={'0 0 330px'}>
<MyInfo />
<Box mt={9}>
<Other />
<>
<Box py={[3, '28px']} maxW={['95vw', '1080px']} px={[5, 10]} mx={'auto'}>
{isPc ? (
<Flex justifyContent={'center'}>
<Box flex={'0 0 330px'}>
<MyInfo onOpenContact={onOpenContact} />
<Box mt={9}>
<Other onOpenContact={onOpenContact} />
</Box>
</Box>
</Box>
{!!standardPlan && (
<Box ml={'45px'} flex={'1 0 0'} maxW={'600px'}>
<PlanUsage />
</Box>
)}
</Flex>
) : (
<>
<MyInfo />
{standardPlan && <PlanUsage />}
<Other />
</>
)}
</Box>
{!!standardPlan && (
<Box ml={'45px'} flex={'1 0 0'} maxW={'600px'}>
<PlanUsage />
</Box>
)}
</Flex>
) : (
<>
<MyInfo onOpenContact={onOpenContact} />
{standardPlan && <PlanUsage />}
<Other onOpenContact={onOpenContact} />
</>
)}
</Box>
{isOpenContact && <CommunityModal onClose={onCloseContact} />}
</>
);
};
export default React.memo(Account);
const MyInfo = () => {
const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
const theme = useTheme();
const { feConfigs } = useSystemStore();
const { t } = useTranslation();
@@ -101,13 +105,14 @@ const MyInfo = () => {
const { reset } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const { teamPlanStatus } = useUserStore();
const standardPlan = teamPlanStatus?.standardConstants;
const { isPc } = useSystem();
const { toast } = useToast();
const {
isOpen: isOpenPayModal,
onClose: onClosePayModal,
onOpen: onOpenPayModal
isOpen: isOpenConversionModal,
onClose: onCloseConversionModal,
onOpen: onOpenConversionModal
} = useDisclosure();
const {
isOpen: isOpenUpdatePsw,
@@ -293,23 +298,30 @@ const MyInfo = () => {
<TeamMenu />
</Box>
</Flex>
{feConfigs?.isPlus && (
{feConfigs?.isPlus && (userInfo?.team?.balance ?? 0) > 0 && (
<Box mt={6} whiteSpace={'nowrap'}>
<Flex alignItems={'center'}>
<Box {...labelStyles}>{t('common:user.team.Balance')}:&nbsp;</Box>
<Box flex={1}>
<strong>{formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}</strong>
</Box>
{feConfigs?.show_pay && userInfo?.team?.permission.hasWritePer && (
<Button variant={'whitePrimary'} size={'sm'} ml={5} onClick={onOpenPayModal}>
{t('common:user.Pay')}
{userInfo?.permission.hasManagePer && !!standardPlan && (
<Button variant={'primary'} size={'sm'} ml={5} onClick={onOpenConversionModal}>
{t('user:bill.conversion')}
</Button>
)}
</Flex>
</Box>
)}
</Box>
{isOpenPayModal && <PayModal onClose={onClosePayModal} />}
{isOpenConversionModal && (
<ConversionModal
onClose={onCloseConversionModal}
balance={formatStorePrice2Read(userInfo?.team?.balance).toFixed(3)}
tokens={String((userInfo?.team?.balance ?? 0) / 15 / 10)}
onOpenContact={onOpenContact}
/>
)}
{isOpenUpdatePsw && <UpdatePswModal onClose={onCloseUpdatePsw} />}
{isOpenUpdateNotification && <UpdateNotification onClose={onCloseUpdateNotification} />}
<File onSelect={onSelectFile} />
@@ -552,7 +564,8 @@ const PlanUsage = () => {
</Box>
) : null;
};
const Other = () => {
const Other = ({ onOpenContact }: { onOpenContact: () => void }) => {
const theme = useTheme();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
@@ -563,7 +576,6 @@ const Other = () => {
});
const { isOpen: isOpenLaf, onClose: onCloseLaf, onOpen: onOpenLaf } = useDisclosure();
const { isOpen: isOpenOpenai, onClose: onCloseOpenai, onOpen: onOpenOpenai } = useDisclosure();
const { isOpen: isOpenConcat, onClose: onCloseConcat, onOpen: onOpenConcat } = useDisclosure();
const onclickSave = useCallback(
async (data: UserType) => {
@@ -686,7 +698,7 @@ const Other = () => {
variant={'whiteBase'}
justifyContent={'flex-start'}
leftIcon={<MyIcon name={'modal/concat'} w={'18px'} color={'myGray.600'} />}
onClick={onOpenConcat}
onClick={onOpenContact}
h={'48px'}
fontSize={'sm'}
>
@@ -710,7 +722,6 @@ const Other = () => {
onClose={onCloseOpenai}
/>
)}
{isOpenConcat && <CommunityModal onClose={onCloseConcat} />}
</Box>
);
};

View File

@@ -20,3 +20,5 @@ export const checkBalancePayResult = (payId: string) =>
} catch (error) {}
return data;
});
export const balanceConversion = () => GET<string>(`/proApi/support/wallet/bill/balanceConversion`);