From 02e12d646d47fdfb3304cadbc412b302ba2c6b80 Mon Sep 17 00:00:00 2001
From: jingyang <72259332+zjy365@users.noreply.github.com>
Date: Mon, 17 Jun 2024 13:16:16 +0800
Subject: [PATCH] feat: yearly/monthly billing for cloudserver (#4781)
* feat:yearly/monthly billing for cloudserver
* fix list servertype
* feat cost tip
* add tip
* add error tip
---
.../cloudserver/public/locales/en/common.json | 16 +-
.../cloudserver/public/locales/zh/common.json | 18 +-
.../src/components/Icon/icons/infoWarn.svg | 3 +
.../cloudserver/src/components/Icon/index.tsx | 1 +
.../cloudserver/src/hooks/useConfirm.tsx | 6 +-
.../src/pages/api/cloudserver/create.ts | 9 +-
.../src/pages/api/cloudserver/listType.ts | 1 -
.../src/pages/api/cloudserver/price.ts | 5 +-
.../create/components/ErrorModal.tsx | 4 +-
.../cloudserver/create/components/Form.tsx | 65 ++++-
.../cloudserver/create/components/Header.tsx | 251 +++++++++++-------
.../src/pages/cloudserver/create/index.tsx | 49 +++-
.../cloudserver/src/types/cloudserver.ts | 4 +-
.../providers/cloudserver/src/utils/tools.ts | 2 -
14 files changed, 294 insertions(+), 140 deletions(-)
create mode 100644 frontend/providers/cloudserver/src/components/Icon/icons/infoWarn.svg
diff --git a/frontend/providers/cloudserver/public/locales/en/common.json b/frontend/providers/cloudserver/public/locales/en/common.json
index d2ea5cbf2..7f98443cc 100644
--- a/frontend/providers/cloudserver/public/locales/en/common.json
+++ b/frontend/providers/cloudserver/public/locales/en/common.json
@@ -52,9 +52,9 @@
"Quantity tips": "Quantity is {{amount1}} to {{amount2}}",
"ReStart": "Restart",
"Reference fee": "Reference fee",
- "Reference fee disk tips": "{{price}}/GiB/hour",
- "Reference fee tip": "{{price}}/hour",
- "Reference fee bandwidth tips": "{{price}}/Mbps/hour",
+ "Reference fee disk tips": "{{price}}/GiB",
+ "Reference fee tip": "{{price}}",
+ "Reference fee bandwidth tips": "{{price}}/Mbps",
"Restarting": "Restarting",
"Set Password": "Set Password",
"Start": "Start",
@@ -111,5 +111,13 @@
"highPerformance": "High Performance",
"Public IP is not enabled": "Public IP is not enabled",
"interval": "Interval",
- "sold out": "Sold Out"
+ "sold out": "Sold Out",
+ "month": "month",
+ "indivual": "indivual",
+ "year": "year",
+ "duration": "duration",
+ "total": "total",
+ "After the annual and monthly cloud hosting is sold": "After the annual and monthly cloud hosting is sold",
+ "No refund allowed": "No refund allowed,",
+ "Pay Confirm tips": "Clicking \"Confirm\" indicates that you have understood and agreed to this clause. If you do not agree to the clause \"No refunds are allowed after the annual or monthly cloud hosting is sold\", please click the \"Cancel\" button. If you do not agree, this purchase will be canceled and you will not be charged any fees."
}
\ No newline at end of file
diff --git a/frontend/providers/cloudserver/public/locales/zh/common.json b/frontend/providers/cloudserver/public/locales/zh/common.json
index e71aee3c7..2f88bfefe 100644
--- a/frontend/providers/cloudserver/public/locales/zh/common.json
+++ b/frontend/providers/cloudserver/public/locales/zh/common.json
@@ -75,7 +75,7 @@
"Changing": "变更中",
"Restarting": "重启中",
"Reference fee": "参考费用",
- "Reference fee tip": "{{price}}/小时",
+ "Reference fee tip": "{{price}}",
"Configuration fee details": "配置费用明细",
"Instance": "实例",
"storage fees": "存储费用",
@@ -85,8 +85,8 @@
"publicIpAssigned tips": "请注意:未分配独立公网IP地址的情况下无法使用外网IP地址对外进行互相通信。",
"Fee inquiry in progress": "费用查询中...",
"hour": "小时",
- "Reference fee disk tips": "{{price}}/GiB/小时",
- "Reference fee bandwidth tips": "{{price}}/Mbps/小时",
+ "Reference fee disk tips": "{{price}}/GiB",
+ "Reference fee bandwidth tips": "{{price}}/Mbps",
"Billing rules": "计费规则",
"The maximum number of instances is 20": "最大数量是20",
"Billing model": "计费模式",
@@ -111,5 +111,13 @@
"highPerformance": "计算型",
"Public IP is not enabled": "未开启公网IP",
"interval": "区间",
- "sold out": "售空"
-}
+ "sold out": "售空",
+ "month": "月",
+ "indivual": "个",
+ "year": "年",
+ "duration": "时长",
+ "total": "总计",
+ "After the annual and monthly cloud hosting is sold": "包年包月云主机售卖后",
+ "No refund allowed": "不允许退费,",
+ "Pay Confirm tips": "点击“确认”表示您已知晓并同意此条款。如果您不同意“包年包月云主机售卖后不允许退费”的条款,请点击“取消”按钮。不同意后,本次购买将被撤销,您不会被收取任何费用。"
+}
\ No newline at end of file
diff --git a/frontend/providers/cloudserver/src/components/Icon/icons/infoWarn.svg b/frontend/providers/cloudserver/src/components/Icon/icons/infoWarn.svg
new file mode 100644
index 000000000..4eec564d3
--- /dev/null
+++ b/frontend/providers/cloudserver/src/components/Icon/icons/infoWarn.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/providers/cloudserver/src/components/Icon/index.tsx b/frontend/providers/cloudserver/src/components/Icon/index.tsx
index 100923795..fde128fba 100644
--- a/frontend/providers/cloudserver/src/components/Icon/index.tsx
+++ b/frontend/providers/cloudserver/src/components/Icon/index.tsx
@@ -29,6 +29,7 @@ const map = {
connection: require('./icons/connection.svg').default,
info: require('./icons/info.svg').default,
restore: require('./icons/restore.svg').default,
+ infoWarn: require('./icons/infoWarn.svg').default,
download: require('./icons/download.svg').default,
check: require('./icons/check.svg').default,
close: require('./icons/close.svg').default,
diff --git a/frontend/providers/cloudserver/src/hooks/useConfirm.tsx b/frontend/providers/cloudserver/src/hooks/useConfirm.tsx
index 92103a860..6c1a10b37 100644
--- a/frontend/providers/cloudserver/src/hooks/useConfirm.tsx
+++ b/frontend/providers/cloudserver/src/hooks/useConfirm.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useRef } from 'react';
+import { ReactNode, useCallback, useRef } from 'react';
import {
AlertDialog,
AlertDialogBody,
@@ -17,7 +17,7 @@ export const useConfirm = ({
confirmText = 'Confirm'
}: {
title?: string;
- content: string;
+ content: ReactNode;
confirmText?: string;
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
@@ -41,7 +41,7 @@ export const useConfirm = ({
() => (
-
+
{t(title)}
diff --git a/frontend/providers/cloudserver/src/pages/api/cloudserver/create.ts b/frontend/providers/cloudserver/src/pages/api/cloudserver/create.ts
index 1900c084e..3df18275e 100644
--- a/frontend/providers/cloudserver/src/pages/api/cloudserver/create.ts
+++ b/frontend/providers/cloudserver/src/pages/api/cloudserver/create.ts
@@ -31,15 +31,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
zone: form.zone,
virtualMachineType: form.virtualMachineType,
virtualMachineArch: form.virtualMachineArch,
- chareType: form.chargeType
+ chargeType: form.chargeType,
+ period: parseInt(form.period)
};
- const { data } = await POST('/action/create', payload, {
+ const { data, error } = await POST('/action/create', payload, {
headers: {
Authorization: req.headers.authorization
}
});
+ if (error) {
+ return jsonRes(res, { code: 500, error: error });
+ }
+
return jsonRes(res, {
data: data
});
diff --git a/frontend/providers/cloudserver/src/pages/api/cloudserver/listType.ts b/frontend/providers/cloudserver/src/pages/api/cloudserver/listType.ts
index 1a2b39e32..b29edefea 100644
--- a/frontend/providers/cloudserver/src/pages/api/cloudserver/listType.ts
+++ b/frontend/providers/cloudserver/src/pages/api/cloudserver/listType.ts
@@ -17,7 +17,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await verifyAccessToken(req);
const payload = req.body as ServerTypePayload;
- console.log(payload);
const { data, error } = await POST('/action/get-virtual-machine-package', payload, {
headers: {
diff --git a/frontend/providers/cloudserver/src/pages/api/cloudserver/price.ts b/frontend/providers/cloudserver/src/pages/api/cloudserver/price.ts
index 773bacd90..7943eaf36 100644
--- a/frontend/providers/cloudserver/src/pages/api/cloudserver/price.ts
+++ b/frontend/providers/cloudserver/src/pages/api/cloudserver/price.ts
@@ -31,7 +31,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
zone: form.zone,
virtualMachineType: form.virtualMachineType,
virtualMachineArch: form.virtualMachineArch,
- chareType: form.chargeType
+ chargeType: form.chargeType,
+ period: parseInt(form.period)
};
const result = await POST('/action/get-price', payload, {
@@ -44,6 +45,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
data: result?.data
});
} catch (error) {
- jsonRes(res, { code: 500, error: error });
+ jsonRes(res, { code: 500, error: 'error' });
}
}
diff --git a/frontend/providers/cloudserver/src/pages/cloudserver/create/components/ErrorModal.tsx b/frontend/providers/cloudserver/src/pages/cloudserver/create/components/ErrorModal.tsx
index 707bf7ced..1f886603a 100644
--- a/frontend/providers/cloudserver/src/pages/cloudserver/create/components/ErrorModal.tsx
+++ b/frontend/providers/cloudserver/src/pages/cloudserver/create/components/ErrorModal.tsx
@@ -22,14 +22,14 @@ const ErrorModal = ({
return (
-
+
{title}
-
+
{content}
diff --git a/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Form.tsx b/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Form.tsx
index ffad9be2f..0c50a8d3a 100644
--- a/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Form.tsx
+++ b/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Form.tsx
@@ -3,7 +3,13 @@ import MyIcon from '@/components/Icon';
import { MyTable, TableColumnsType } from '@/components/MyTable';
import MyTooltip from '@/components/MyTooltip';
import { CloudServerStatus, CloudServerType, EditForm, StorageType } from '@/types/cloudserver';
-import { CVMArchType, CVMRegionType, CVMZoneType, VirtualMachineType } from '@/types/region';
+import {
+ CVMArchType,
+ CVMChargeType,
+ CVMRegionType,
+ CVMZoneType,
+ VirtualMachineType
+} from '@/types/region';
import {
Box,
Button,
@@ -261,6 +267,15 @@ function OuterTabs({ systemRegion }: { systemRegion: CVMRegionType[] }) {
onChange={(e) => {
const item = systemRegion[e];
formHook?.setValue('chargeType', item.chargeType);
+ formHook?.setValue('virtualMachineArch', item.zone[0].arch[0].arch);
+ formHook.setValue(
+ 'virtualMachineType',
+ item.zone[0].arch[0].virtualMachineType[0].virtualMachineType
+ );
+ formHook.setValue(
+ 'virtualMachinePackageFamily',
+ item.zone[0].arch[0].virtualMachineType[0].virtualMachinePackageFamily[0]
+ );
}}
>
@@ -430,7 +445,10 @@ export default function Form({
item.status === CloudServerStatus.Unavailable ? 'grayModern.500' : 'brightBlue.700'
}
>
- {t('Reference fee tip', { price: item.instancePrice })}{' '}
+ {t('Reference fee tip', { price: item.instancePrice })}/
+ {formHook.getValues('chargeType') === CVMChargeType.postPaidByHour
+ ? t('hour')
+ : t('month')}
);
}
@@ -555,6 +573,29 @@ export default function Form({
[getValues, register, removeStorages, setValue, t]
);
+ const monthsArray = useMemo(() => {
+ const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24, 36];
+ return months.map((month) => {
+ let label;
+ if (month < 12) {
+ label = `${month} ${t('indivual')} ${t('month')}`;
+ } else if (month === 12) {
+ label = `1${t('year')}`;
+ } else {
+ const years = Math.floor(month / 12);
+ const remainingMonths = month % 12;
+ label =
+ remainingMonths === 0
+ ? `${years} ${t('year')}`
+ : `${years} ${t('year')}${remainingMonths} ${t('indivual')} ${t('month')}`;
+ }
+ return {
+ value: month.toString(),
+ label: label
+ };
+ });
+ }, [t]);
+
return (
)}
+
+ {formHook.getValues('chargeType') === CVMChargeType.prePaid && (
+
+
+
+ {
+ setValue('period', id);
+ formHook.clearErrors();
+ }}
+ />
+
+
+ )}
);
diff --git a/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Header.tsx b/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Header.tsx
index 22819bb94..d1d99ff86 100644
--- a/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Header.tsx
+++ b/frontend/providers/cloudserver/src/pages/cloudserver/create/components/Header.tsx
@@ -8,24 +8,166 @@ import {
Divider,
Flex,
Popover,
- PopoverArrow,
- PopoverBody,
PopoverContent,
- PopoverHeader,
PopoverTrigger,
Text
} from '@chakra-ui/react';
+import { Decimal } from 'decimal.js';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
-import { Decimal } from 'decimal.js';
+
+export const priceItemStyle = {
+ alignItems: 'center',
+ py: '4px',
+ px: '6px',
+ borderRadius: 'base',
+ _hover: {
+ bg: 'grayModern.100'
+ }
+};
+
+export const CostTipContent = ({
+ prices,
+ instanceType,
+ isMonth,
+ isModalTip
+}: {
+ isMonth: boolean;
+ prices?: CloudServerPrice;
+ instanceType?: CloudServerType;
+ isModalTip?: boolean;
+}) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {prices && isModalTip && (
+ <>
+
+
+
+ {t('After the annual and monthly cloud hosting is sold')}
+
+ {t('No refund allowed')}
+
+ {t('Pay Confirm tips')}
+
+
+
+
+ {t('total')}
+
+
+
+ ¥
+ {new Decimal(prices?.diskPrice)
+ .plus(new Decimal(prices?.instancePrice))
+ .plus(new Decimal(prices?.networkPrice))
+ .toNumber()}
+
+
+ >
+ )}
+
+ {t('Configuration fee details')}
+
+
+
+
+ {t('Instance')}
+
+ ¥{prices?.instancePrice}
+ {!isMonth && `/${t('hour')}`}
+
+
+
+
+
+ {t('storage fees')}
+
+
+ ¥{prices?.diskPrice}
+ {!isMonth && `/${t('hour')}`}
+
+
+
+
+
+ {t('Public network bandwidth')}
+
+
+ ¥{prices?.networkPrice}
+ {!isMonth && `/${t('hour')}`}
+
+
+
+
+ {/* Billing rules */}
+
+ {t('Billing rules')}
+
+
+
+
+
+ {t('Storage')}
+
+
+ {t('Reference fee disk tips', { price: instanceType?.diskPerG })}/
+ {isMonth ? t('month') : t('hour')}
+
+
+
+
+ {t('BandWidth')}
+
+
+
+ {t('interval')}
+ {t('price')}
+
+ {instanceType?.bandwidthPricingTiers.map((item, index) => {
+ return (
+
+ {`(${item.minBandwidth} , ${
+ item.maxBandwidth === null ? '∞' : item.maxBandwidth
+ }]`}
+
+ {t('Reference fee bandwidth tips', { price: item.pricePerMbps })}/
+ {isMonth ? t('month') : t('hour')}
+
+
+ );
+ })}
+
+
+
+
+ );
+};
const Header = ({
title,
applyCb,
applyBtnText,
prices,
- instanceType
+ instanceType,
+ isMonth
}: {
+ isMonth: boolean;
title: string;
applyCb: () => void;
applyBtnText: string;
@@ -36,16 +178,6 @@ const Header = ({
const router = useRouter();
const { lastRoute } = useGlobalStore();
- const priceItemStyle = {
- alignItems: 'center',
- py: '4px',
- px: '6px',
- borderRadius: 'base',
- _hover: {
- bg: 'grayModern.100'
- }
- };
-
return (
router.replace(lastRoute)}>
@@ -75,94 +207,7 @@ const Header = ({
-
- {t('Configuration fee details')}
-
-
-
-
-
- {t('Instance')}
-
- ¥{prices?.instancePrice}/{t('hour')}
-
-
-
-
-
- {t('storage fees')}
-
-
- ¥{prices?.diskPrice}/{t('hour')}
-
-
-
-
-
- {t('Public network bandwidth')}
-
-
- ¥{prices?.networkPrice}/{t('hour')}
-
-
-
-
-
- {/* Billing rules */}
-
- {t('Billing rules')}
-
-
-
-
-
-
- {t('Storage')}
-
-
- {t('Reference fee disk tips', { price: instanceType?.diskPerG })}
-
-
-
-
- {t('BandWidth')}
-
-
-
- {t('interval')}
- {t('price')}
-
- {instanceType?.bandwidthPricingTiers.map((item, index) => {
- return (
-
- {`[${item.minBandwidth} , ${
- item.maxBandwidth === null ? '∞' : item.maxBandwidth
- })`}
-
- {t('Reference fee bandwidth tips', { price: item.pricePerMbps })}
-
-
- );
- })}
-
-
-
-
+
) : (
diff --git a/frontend/providers/cloudserver/src/pages/cloudserver/create/index.tsx b/frontend/providers/cloudserver/src/pages/cloudserver/create/index.tsx
index 9ec438d77..bfcbba972 100644
--- a/frontend/providers/cloudserver/src/pages/cloudserver/create/index.tsx
+++ b/frontend/providers/cloudserver/src/pages/cloudserver/create/index.tsx
@@ -4,6 +4,7 @@ import { useLoading } from '@/hooks/useLoading';
import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global';
import { CloudServerType, EditForm } from '@/types/cloudserver';
+import { CVMChargeType } from '@/types/region';
import { serviceSideProps } from '@/utils/i18n';
import { Box, Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
@@ -13,7 +14,7 @@ import { useCallback, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import ErrorModal from './components/ErrorModal';
import Form from './components/Form';
-import Header from './components/Header';
+import Header, { CostTipContent } from './components/Header';
export default function EditOrder() {
const [errorMessage, setErrorMessage] = useState('');
@@ -25,10 +26,6 @@ export default function EditOrder() {
const { lastRoute } = useGlobalStore();
const [instanceType, setInstanceType] = useState();
- const { openConfirm, ConfirmChild } = useConfirm({
- content: t('Are you sure to create a cloud host?')
- });
-
// form
const formHook = useForm({
defaultValues: {
@@ -42,7 +39,8 @@ export default function EditOrder() {
amount: 1
}
],
- systemImageId: ''
+ systemImageId: '',
+ period: '1'
// chargeType: CVMChargeType.postPaidByHour,
// zone: 'Guangzhou-6',
// virtualMachineArch: 'x86_64',
@@ -56,10 +54,16 @@ export default function EditOrder() {
setForceUpdate(!forceUpdate);
});
- const { data: prices } = useQuery(['getCloudServerPrice', forceUpdate], () => {
- const temp = formHook.getValues();
- return getCloudServerPrice(temp);
- });
+ const { data: prices } = useQuery(
+ ['getCloudServerPrice', forceUpdate],
+ () => {
+ const temp = formHook.getValues();
+ return getCloudServerPrice(temp);
+ },
+ {
+ enabled: !!formHook.getValues('virtualMachinePackageName')
+ }
+ );
const submitSuccess = async (data: EditForm) => {
console.log(data);
@@ -96,6 +100,23 @@ export default function EditOrder() {
});
}, [formHook.formState.errors, t, toast]);
+ const { openConfirm, ConfirmChild } = useConfirm({
+ content: (
+
+ {formHook.getValues('chargeType') === CVMChargeType.prePaid ? (
+
+ ) : (
+ {t('Are you sure to create a cloud host?')}
+ )}
+
+ )
+ });
+
return (
- formHook.handleSubmit((data) => openConfirm(() => submitSuccess(data))(), submitError)()
- }
+ applyCb={() => {
+ formHook.handleSubmit((data) => openConfirm(() => submitSuccess(data))(), submitError)();
+ // openConfirm()();
+ }}
applyBtnText="Submit"
/>
diff --git a/frontend/providers/cloudserver/src/types/cloudserver.ts b/frontend/providers/cloudserver/src/types/cloudserver.ts
index dbb28b75b..1dfddc9f4 100644
--- a/frontend/providers/cloudserver/src/types/cloudserver.ts
+++ b/frontend/providers/cloudserver/src/types/cloudserver.ts
@@ -14,6 +14,7 @@ export type EditForm = {
virtualMachineArch: string;
chargeType: CVMChargeType;
zone: string;
+ period: string;
};
export type StorageType = {
@@ -152,5 +153,6 @@ export type CreateCloudServerPayload = {
zone: string;
virtualMachineType: string;
virtualMachineArch: string;
- chareType: CVMChargeType;
+ chargeType: CVMChargeType;
+ period: number;
};
diff --git a/frontend/providers/cloudserver/src/utils/tools.ts b/frontend/providers/cloudserver/src/utils/tools.ts
index e15ed75a8..b899b68e3 100644
--- a/frontend/providers/cloudserver/src/utils/tools.ts
+++ b/frontend/providers/cloudserver/src/utils/tools.ts
@@ -1,7 +1,5 @@
-import { useToast } from '@/hooks/useToast';
import { addHours, format, set, startOfDay } from 'date-fns';
import dayjs from 'dayjs';
-import { useTranslation } from 'next-i18next';
export const formatTime = (time: string | number | Date, format = 'YYYY-MM-DD HH:mm:ss') => {
return dayjs(time).format(format);