diff --git a/frontend/providers/cloudserver/public/locales/en/common.json b/frontend/providers/cloudserver/public/locales/en/common.json index d2ea5cbf29f..7f98443cc07 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 e71aee3c755..2f88bfefeac 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 00000000000..4eec564d3cc --- /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 10092379595..fde128fba3b 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 92103a8601e..6c1a10b3728 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 1900c084e6f..3df18275e76 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 1a2b39e3253..b29edefea97 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 773bacd9017..7943eaf36b5 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 707bf7ced7e..1f886603afc 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 ffad9be2f57..0c50a8d3a79 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 22819bb9471..d1d99ff86d2 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 9ec438d7717..bfcbba9726a 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 dbb28b75b4d..1dfddc9f410 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 e15ed75a832..b899b68e361 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);