diff --git a/web/public/locales/en/translation.json b/web/public/locales/en/translation.json index 2b1453e3fc..7adfadd1cc 100644 --- a/web/public/locales/en/translation.json +++ b/web/public/locales/en/translation.json @@ -44,7 +44,6 @@ "Copy": "copy", "Create": "new application", "CreateNow": "create now", - "CreateTime": "Created time", "Custom": "customize", "Days": "days", "Delete": "Delete ", @@ -284,5 +283,6 @@ "Price": { "Free": "Free" }, - "star-us-on-github": "Star us on GitHub" -} \ No newline at end of file + "star-us-on-github": "Star us on GitHub", + "Time": "Time" +} diff --git a/web/public/locales/zh-CN/translation.json b/web/public/locales/zh-CN/translation.json index 2b7a0fa1e8..9d3adcc3e2 100644 --- a/web/public/locales/zh-CN/translation.json +++ b/web/public/locales/zh-CN/translation.json @@ -44,7 +44,6 @@ "Copy": "复制", "Create": "新建", "CreateNow": "立即创建", - "CreateTime": "创建时间", "Custom": "自定义", "Days": "天", "Delete": "删除", @@ -146,7 +145,7 @@ "UpdateConfirm": "更新环境变量将重新启动应用,是否继续?", "AppInfo": "应用信息", "BaseInfo": "基础信息", - "Detail": "规则详情", + "Detail": "规格详情", "Memory": "内存", "Disk": "硬盘", "DB": "数据库", @@ -284,5 +283,6 @@ "Price": { "Free": "免费" }, - "star-us-on-github": "在 GitHub 上支持我们" + "star-us-on-github": "在 GitHub 上支持我们", + "Time": "时间" } \ No newline at end of file diff --git a/web/public/locales/zh/translation.json b/web/public/locales/zh/translation.json index f085f5a409..78c8a0cf9c 100644 --- a/web/public/locales/zh/translation.json +++ b/web/public/locales/zh/translation.json @@ -44,7 +44,6 @@ "Copy": "复制", "Create": "新建", "CreateNow": "立即创建", - "CreateTime": "创建时间", "Custom": "自定义", "Days": "天", "Delete": "删除", @@ -146,7 +145,7 @@ "UpdateConfirm": "更新环境变量将重新启动应用,是否继续?", "AppInfo": "应用信息", "BaseInfo": "基础信息", - "Detail": "规则详情", + "Detail": "规格详情", "Memory": "内存", "Disk": "硬盘", "DB": "数据库", @@ -284,5 +283,6 @@ "Price": { "Free": "免费" }, - "star-us-on-github": "在 GitHub 上支持我们" + "star-us-on-github": "在 GitHub 上支持我们", + "Time": "时间" } \ No newline at end of file diff --git a/web/src/App.css b/web/src/App.css index 771d58cde4..21615c5eb0 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -36,12 +36,12 @@ body { font-size: 12px; } -[data-theme=light] body { +[data-theme="light"] body { background-color: #f1f3f5 !important; background-image: url("/bg.png") !important; } -[data-theme=dark] ::-webkit-scrollbar-thumb { +[data-theme="dark"] ::-webkit-scrollbar-thumb { /* 滚动条滑块 */ background: rgba(21, 22, 26, 0.4); } @@ -64,4 +64,4 @@ a { color: white; background: black; } -} \ No newline at end of file +} diff --git a/web/src/apis/typing.d.ts b/web/src/apis/typing.d.ts index 457356f928..0fa08a8efd 100644 --- a/web/src/apis/typing.d.ts +++ b/web/src/apis/typing.d.ts @@ -1,4 +1,4 @@ -export type TApplication = { +export type TApplicationDetail = { id: string; name: string; appid: string; @@ -26,14 +26,13 @@ export type TApplication = { export type TBundle = { id: string; - appid: string; name: string; displayName: string; + priority: number; + state: string; resource: TResource; - price: number; - specialPrice: number; - createdAt: string; - updatedAt: string; + limitCountPerUser: number; + subscriptionOptions: TSubscriptionOption[]; }; export type TResource = { @@ -44,15 +43,24 @@ export type TResource = { databaseCapacity: number; storageCapacity: number; networkTrafficOutbound: number; - limitCountPerUser: number; limitCountOfCloudFunction: number; limitCountOfBucket: number; limitCountOfDatabasePolicy: number; limitCountOfTrigger: number; + limitCountOfWebsiteHosting: number; + reservedTimeAfterExpired: number; limitDatabaseTPS: number; limitStorageTPS: number; }; +export type TSubscriptionOption = { + name: string; + displayName: string; + duration: number; + price: number; + specialPrice: number; +}; + export type TRuntime = { id: string; name: string; @@ -257,3 +265,76 @@ export type TProfile = { createdAt: string; updatedAt: string; }; + +export type TApplicationItem = { + id: string; + name: string; + appid: string; + regionId: string; + runtimeId: string; + tags: Array; + state: string; + phase: string; + createdAt: string; + updatedAt: string; + lockedAt: string; + createdBy: string; + bundle: { + id: string; + appid: string; + bundleId: string; + name: string; + displayName: string; + resource: { + limitCPU: number; + limitMemory: number; + requestCPU: number; + requestMemory: number; + databaseCapacity: number; + storageCapacity: number; + networkTrafficOutbound: number; + limitCountOfCloudFunction: number; + limitCountOfBucket: number; + limitCountOfDatabasePolicy: number; + limitCountOfTrigger: number; + limitCountOfWebsiteHosting: number; + reservedTimeAfterExpired: number; + limitDatabaseTPS: number; + limitStorageTPS: number; + }; + createdAt: string; + updatedAt: string; + }; + runtime: { + id: string; + name: string; + type: string; + image: { + main: string; + init: string; + sidecar: any; + }; + state: string; + version: string; + latest: boolean; + }; + subscription: { + id: string; + input: { + name: string; + state: string; + runtimeId: string; + regionId: string; + }; + bundleId: string; + appid: string; + state: string; + phase: string; + renewalPlan: string; + expiredAt: string; + lockedAt: string; + createdAt: string; + updatedAt: string; + createdBy: string; + }; +}; diff --git a/web/src/apis/v1/accounts.ts b/web/src/apis/v1/accounts.ts new file mode 100644 index 0000000000..0cf6e255d9 --- /dev/null +++ b/web/src/apis/v1/accounts.ts @@ -0,0 +1,78 @@ +// @ts-ignore +/* eslint-disable */ +/////////////////////////////////////////////////////////////////////// +// // +// this file is autogenerated by service-generate // +// do not edit this file manually // +// // +/////////////////////////////////////////////////////////////////////// +/// +import request from "@/utils/request"; + +/** + * Get account info + */ +export async function AccountControllerFindOne( + params: Paths.AccountControllerFindOne.BodyParameters | any, +): Promise { + // /v1/accounts + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/accounts`, { + method: "GET", + params: params, + }); +} + +/** + * Get charge order + */ +export async function AccountControllerGetChargeOrder( + params: Paths.AccountControllerGetChargeOrder.BodyParameters | any, +): Promise { + // /v1/accounts/charge-order/{id} + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/accounts/charge-order/${_params.id}`, { + method: "GET", + params: params, + }); +} + +/** + * Create charge order + */ +export async function AccountControllerCharge( + params: Definitions.CreateChargeOrderDto | any, +): Promise { + // /v1/accounts/charge-order + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/accounts/charge-order`, { + method: "POST", + data: params, + }); +} + +/** + * + */ +export async function AccountControllerWechatNotify( + params: Paths.AccountControllerWechatNotify.BodyParameters | any, +): Promise { + // /v1/accounts/payment/wechat-notify + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/accounts/payment/wechat-notify`, { + method: "POST", + data: params, + }); +} diff --git a/web/src/apis/v1/api-auto.d.ts b/web/src/apis/v1/api-auto.d.ts index f0150ea086..f89da64554 100644 --- a/web/src/apis/v1/api-auto.d.ts +++ b/web/src/apis/v1/api-auto.d.ts @@ -18,14 +18,6 @@ declare namespace Definitions { code?: string /* The source code of the function */; }; - export type CreateApplicationDto = { - name?: string; - state?: string; - regionId?: string; - bundleId?: string; - runtimeId?: string; - }; - export type UpdateApplicationDto = { name?: string; state?: string; @@ -98,6 +90,27 @@ declare namespace Definitions { cron?: string; target?: string; }; + + export type CreateSubscriptionDto = { + name?: string; + state?: string; + regionId?: string; + bundleId?: string; + runtimeId?: string; + duration?: number; + }; + + export type RenewSubscriptionDto = { + duration?: number; + }; + + export type UpgradeSubscriptionDto = {}; + + export type CreateChargeOrderDto = { + amount?: number; + channel?: string; + currency?: string; + }; } declare namespace Paths { @@ -157,14 +170,6 @@ declare namespace Paths { export type Responses = any; } - namespace ApplicationControllerCreate { - export type QueryParameters = any; - - export type BodyParameters = Definitions.CreateApplicationDto; - - export type Responses = any; - } - namespace ApplicationControllerFindAll { export type QueryParameters = any; @@ -189,14 +194,6 @@ declare namespace Paths { export type Responses = any; } - namespace ApplicationControllerRemove { - export type QueryParameters = any; - - export type BodyParameters = any; - - export type Responses = any; - } - namespace EnvironmentVariableControllerAdd { export type QueryParameters = any; @@ -556,4 +553,84 @@ declare namespace Paths { export type Responses = any; } + + namespace SubscriptionControllerCreate { + export type QueryParameters = any; + + export type BodyParameters = Definitions.CreateSubscriptionDto; + + export type Responses = any; + } + + namespace SubscriptionControllerFindAll { + export type QueryParameters = any; + + export type BodyParameters = any; + + export type Responses = any; + } + + namespace SubscriptionControllerFindOne { + export type QueryParameters = any; + + export type BodyParameters = any; + + export type Responses = any; + } + + namespace SubscriptionControllerRenew { + export type QueryParameters = any; + + export type BodyParameters = Definitions.RenewSubscriptionDto; + + export type Responses = any; + } + + namespace SubscriptionControllerUpgrade { + export type QueryParameters = any; + + export type BodyParameters = Definitions.UpgradeSubscriptionDto; + + export type Responses = any; + } + + namespace SubscriptionControllerRemove { + export type QueryParameters = any; + + export type BodyParameters = any; + + export type Responses = any; + } + + namespace AccountControllerFindOne { + export type QueryParameters = any; + + export type BodyParameters = any; + + export type Responses = any; + } + + namespace AccountControllerGetChargeOrder { + export type QueryParameters = any; + + export type BodyParameters = any; + + export type Responses = any; + } + + namespace AccountControllerCharge { + export type QueryParameters = any; + + export type BodyParameters = Definitions.CreateChargeOrderDto; + + export type Responses = any; + } + + namespace AccountControllerWechatNotify { + export type QueryParameters = any; + + export type BodyParameters = any; + + export type Responses = any; + } } diff --git a/web/src/apis/v1/applications.ts b/web/src/apis/v1/applications.ts index 5f09a104ed..2e4d8c62b2 100644 --- a/web/src/apis/v1/applications.ts +++ b/web/src/apis/v1/applications.ts @@ -9,23 +9,6 @@ /// import request from "@/utils/request"; -/** - * Create a new application - */ -export async function ApplicationControllerCreate( - params: Definitions.CreateApplicationDto | any, -): Promise { - // /v1/applications - let _params: { [key: string]: any } = { - appid: localStorage.getItem("app"), - ...params, - }; - return request(`/v1/applications`, { - method: "POST", - data: params, - }); -} - /** * Get user application list */ @@ -76,20 +59,3 @@ export async function ApplicationControllerUpdate( data: params, }); } - -/** - * Delete an application - */ -export async function ApplicationControllerRemove( - params: Paths.ApplicationControllerRemove.BodyParameters | any, -): Promise { - // /v1/applications/{appid} - let _params: { [key: string]: any } = { - appid: localStorage.getItem("app"), - ...params, - }; - return request(`/v1/applications/${_params.appid}`, { - method: "DELETE", - data: params, - }); -} diff --git a/web/src/apis/v1/subscriptions.ts b/web/src/apis/v1/subscriptions.ts new file mode 100644 index 0000000000..05d9516083 --- /dev/null +++ b/web/src/apis/v1/subscriptions.ts @@ -0,0 +1,112 @@ +// @ts-ignore +/* eslint-disable */ +/////////////////////////////////////////////////////////////////////// +// // +// this file is autogenerated by service-generate // +// do not edit this file manually // +// // +/////////////////////////////////////////////////////////////////////// +/// +import request from "@/utils/request"; + +/** + * Create a new subscription + */ +export async function SubscriptionControllerCreate( + params: Definitions.CreateSubscriptionDto | any, +): Promise { + // /v1/subscriptions + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/subscriptions`, { + method: "POST", + data: params, + }); +} + +/** + * Get user's subscriptions + */ +export async function SubscriptionControllerFindAll( + params: Paths.SubscriptionControllerFindAll.BodyParameters | any, +): Promise { + // /v1/subscriptions + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/subscriptions`, { + method: "GET", + params: params, + }); +} + +/** + * Get subscription by appid + */ +export async function SubscriptionControllerFindOne( + params: Paths.SubscriptionControllerFindOne.BodyParameters | any, +): Promise { + // /v1/subscriptions/{appid} + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/subscriptions/${_params.appid}`, { + method: "GET", + params: params, + }); +} + +/** + * Renew a subscription + */ +export async function SubscriptionControllerRenew( + params: Definitions.RenewSubscriptionDto | any, +): Promise { + // /v1/subscriptions/{id}/renewal + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/subscriptions/${_params.id}/renewal`, { + method: "POST", + data: params, + }); +} + +/** + * Upgrade a subscription (TODO) + */ +export async function SubscriptionControllerUpgrade( + params: Definitions.UpgradeSubscriptionDto | any, +): Promise { + // /v1/subscriptions/{id}/upgrade + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/subscriptions/${_params.id}/upgrade`, { + method: "PATCH", + data: params, + }); +} + +/** + * Delete a subscription + */ +export async function SubscriptionControllerRemove( + params: Paths.SubscriptionControllerRemove.BodyParameters | any, +): Promise { + // /v1/subscriptions/{id} + let _params: { [key: string]: any } = { + appid: localStorage.getItem("app"), + ...params, + }; + return request(`/v1/subscriptions/${_params.id}`, { + method: "DELETE", + data: params, + }); +} diff --git a/web/src/components/ChargeButton/index.tsx b/web/src/components/ChargeButton/index.tsx index 33aea8af0d..71cc4527ec 100644 --- a/web/src/components/ChargeButton/index.tsx +++ b/web/src/components/ChargeButton/index.tsx @@ -2,6 +2,8 @@ import React from "react"; import { Button, Input, + InputGroup, + InputLeftAddon, Modal, ModalBody, ModalCloseButton, @@ -10,34 +12,107 @@ import { ModalOverlay, useDisclosure, } from "@chakra-ui/react"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { t } from "i18next"; import { QRCodeSVG } from "qrcode.react"; -export default function ChargeButton(props: { children: React.ReactElement }) { +import { CHARGE_CHANNEL, CURRENCY } from "@/constants"; +import { convertMoney, formatPrice } from "@/utils/format"; + +import { AccountControllerCharge, AccountControllerGetChargeOrder } from "@/apis/v1/accounts"; +import { useAccountQuery } from "@/pages/home/service"; + +export default function ChargeButton(props: { amount?: number; children: React.ReactElement }) { const { children } = props; const { isOpen, onOpen, onClose } = useDisclosure(); + + const [amount, setAmount] = React.useState(props.amount || 0); + + const [phaseStatus, setPhaseStatus] = React.useState<"Pending" | "Paid" | undefined>(); + + const createChargeOrder = useMutation( + ["AccountControllerCharge"], + (params: any) => AccountControllerCharge(params), + {}, + ); + + const accountQuery = useAccountQuery(); + + useQuery( + ["AccountControllerGetChargeOrder"], + () => + AccountControllerGetChargeOrder({ + id: createChargeOrder.data?.data?.order?.id, + }), + { + enabled: !!createChargeOrder.data?.data?.order?.id && isOpen, + refetchInterval: phaseStatus === "Pending" && isOpen ? 1000 : false, + onSuccess: (data) => { + setPhaseStatus(data.phase); + if (data.phase === "Paid") { + accountQuery.refetch(); + onClose(); + } + }, + }, + ); + return ( <> {React.cloneElement(children, { onClick: onOpen })} - + {t("Charge")} - -
-

当前余额

-

¥ 0.00

-

充值金额

- - + +
+

当前余额

+

+ {formatPrice(accountQuery.data?.balance)} +

+

充值金额

+ + + { + setAmount(Number(event.target.value)); + }} + /> + +
-
-

订单号:a2b6fc440f9978d53d7b4ad0a52e752e

- -

微信扫码支付

-
+ {createChargeOrder.data?.data?.result?.code_url && ( +
+

微信扫码支付

+ +

+ 订单号:{createChargeOrder.data?.data?.order?.id} +

+

支付状态: {phaseStatus}

+
+ )}
diff --git a/web/src/components/FileUpload/index.module.scss b/web/src/components/FileUpload/index.module.scss index 9c8f23411c..3af76944b6 100644 --- a/web/src/components/FileUpload/index.module.scss +++ b/web/src/components/FileUpload/index.module.scss @@ -40,4 +40,4 @@ right: 0px; bottom: 0px; left: 0px; -} \ No newline at end of file +} diff --git a/web/src/constants/index.ts b/web/src/constants/index.ts index 9ea9ab1791..4ebe005079 100644 --- a/web/src/constants/index.ts +++ b/web/src/constants/index.ts @@ -54,3 +54,13 @@ export enum BUCKET_POLICY_TYPE { export enum BUCKET_STATUS { Active = "Active", } + +export enum CHARGE_CHANNEL { + WeChat = "WeChat", + Alipay = "Alipay", +} + +export enum CURRENCY { + CNY = "CNY", + USD = "USD", +} diff --git a/web/src/pages/app/functions/mods/DependencePanel/AddDependenceModal/index.tsx b/web/src/pages/app/functions/mods/DependencePanel/AddDependenceModal/index.tsx index 54143912ec..0d268bcd99 100644 --- a/web/src/pages/app/functions/mods/DependencePanel/AddDependenceModal/index.tsx +++ b/web/src/pages/app/functions/mods/DependencePanel/AddDependenceModal/index.tsx @@ -66,12 +66,12 @@ const AddDependenceModal = () => { const addPackageMutation = useAddPackageMutation(() => { onClose(); - globalStore.updateCurrentApp(); + globalStore.updateCurrentApp(globalStore.currentApp!); }); const editPackageMutation = useEditPackageMutation(() => { onClose(); - globalStore.updateCurrentApp(); + globalStore.updateCurrentApp(globalStore.currentApp!); }); const packageSearchQuery = usePackageSearchQuery(name, (data) => { diff --git a/web/src/pages/app/functions/mods/DependencePanel/index.tsx b/web/src/pages/app/functions/mods/DependencePanel/index.tsx index 18eeae7b40..1e60a60b4a 100644 --- a/web/src/pages/app/functions/mods/DependencePanel/index.tsx +++ b/web/src/pages/app/functions/mods/DependencePanel/index.tsx @@ -20,7 +20,7 @@ export default function DependenceList() { const packageQuery = usePackageQuery(); const globalStore = useGlobalStore((state) => state); const delPackageMutation = useDelPackageMutation(() => { - globalStore.updateCurrentApp(); + globalStore.updateCurrentApp(globalStore.currentApp!); }); const { t } = useTranslation(); diff --git a/web/src/pages/app/setting/AppEnvList/index.tsx b/web/src/pages/app/setting/AppEnvList/index.tsx index 20d41feb5c..d929cb1ca7 100644 --- a/web/src/pages/app/setting/AppEnvList/index.tsx +++ b/web/src/pages/app/setting/AppEnvList/index.tsx @@ -77,7 +77,7 @@ const AppEnvList = (props: { onClose?: () => {} }) => { /> { - globalStore.updateCurrentApp(); + globalStore.updateCurrentApp(globalStore.currentApp!); props.onClose && props.onClose(); }} headerText={String(t("Update"))} diff --git a/web/src/pages/app/setting/AppInfoList/index.tsx b/web/src/pages/app/setting/AppInfoList/index.tsx index dc237d24d5..680060b820 100644 --- a/web/src/pages/app/setting/AppInfoList/index.tsx +++ b/web/src/pages/app/setting/AppInfoList/index.tsx @@ -47,7 +47,7 @@ const AppEnvList = () => { isDisabled={currentApp?.state === APP_PHASE_STATUS.Restarting} variant={"text"} onClick={() => { - updateCurrentApp(); + updateCurrentApp(currentApp!); }} > @@ -58,10 +58,9 @@ const AppEnvList = () => { fontWeight={"semibold"} size={"sm"} variant={"text"} - isDisabled={currentApp?.state === APP_PHASE_STATUS.Stopped} onClick={(event: any) => { event?.preventDefault(); - updateCurrentApp(APP_PHASE_STATUS.Stopped); + updateCurrentApp(currentApp!, APP_PHASE_STATUS.Stopped); }} > diff --git a/web/src/pages/app/setting/SysSetting/index.tsx b/web/src/pages/app/setting/SysSetting/index.tsx index d3ce21f28b..586e7c2c98 100644 --- a/web/src/pages/app/setting/SysSetting/index.tsx +++ b/web/src/pages/app/setting/SysSetting/index.tsx @@ -4,9 +4,12 @@ import { t } from "i18next"; import AppEnvList from "../AppEnvList"; import AppInfoList from "../AppInfoList"; -import { TApplication } from "@/apis/typing"; +import { TApplicationDetail } from "@/apis/typing"; import SettingModal from "@/pages/app/setting"; -export default function SysSetting(props: { children: React.ReactElement; setApp?: TApplication }) { +export default function SysSetting(props: { + children: React.ReactElement; + setApp?: TApplicationDetail; +}) { return ( { diff --git a/web/src/pages/app/storages/mods/CreateWebsiteModal/index.tsx b/web/src/pages/app/storages/mods/CreateWebsiteModal/index.tsx index 3929358e37..a47aab3b2f 100644 --- a/web/src/pages/app/storages/mods/CreateWebsiteModal/index.tsx +++ b/web/src/pages/app/storages/mods/CreateWebsiteModal/index.tsx @@ -46,7 +46,7 @@ function CreateWebsiteModal() { const deleteWebsiteMutation = useWebsiteDeleteMutation(); const updateWebsiteMutation = useWebSiteUpdateMutation(); const toast = useToast(); - const cnameDomain = currentStorage?.websiteHosting?.domain; + const cnameDomain = currentStorage?.domain?.domain; return ( <> diff --git a/web/src/pages/globalStore.ts b/web/src/pages/globalStore.ts index 10b04b606a..ffef553b67 100644 --- a/web/src/pages/globalStore.ts +++ b/web/src/pages/globalStore.ts @@ -6,12 +6,13 @@ import { immer } from "zustand/middleware/immer"; import { APP_PHASE_STATUS } from "@/constants"; import { formatPort } from "@/utils/format"; -import { TApplication, TRegion, TRuntime, TUserInfo } from "@/apis/typing"; -import { ApplicationControllerRemove, ApplicationControllerUpdate } from "@/apis/v1/applications"; +import { TApplicationDetail, TRegion, TRuntime, TUserInfo } from "@/apis/typing"; +import { ApplicationControllerUpdate } from "@/apis/v1/applications"; import { AuthControllerGetSigninUrl } from "@/apis/v1/login"; import { AuthControllerGetProfile } from "@/apis/v1/profile"; import { RegionControllerGetRegions } from "@/apis/v1/regions"; import { AppControllerGetRuntimes } from "@/apis/v1/runtimes"; +import { SubscriptionControllerRemove } from "@/apis/v1/subscriptions"; const { toast } = createStandaloneToast(); @@ -20,10 +21,10 @@ type State = { loading: boolean; runtimes?: TRuntime[]; regions?: TRegion[]; - currentApp: TApplication | undefined; - setCurrentApp(app: TApplication | undefined): void; + currentApp: TApplicationDetail | undefined; + setCurrentApp(app: TApplicationDetail | undefined): void; init(appid?: string): void; - updateCurrentApp(state?: APP_PHASE_STATUS): void; + updateCurrentApp(app: TApplicationDetail, state?: APP_PHASE_STATUS): void; deleteCurrentApp(): void; currentPageId: string | undefined; setCurrentPage: (pageId: string) => void; @@ -76,8 +77,10 @@ const useGlobalStore = create()( }); }, - updateCurrentApp: async (newState: APP_PHASE_STATUS = APP_PHASE_STATUS.Restarting) => { - const app = get().currentApp; + updateCurrentApp: async ( + app: TApplicationDetail, + newState: APP_PHASE_STATUS = APP_PHASE_STATUS.Restarting, + ) => { if (!app) { return; } @@ -100,7 +103,7 @@ const useGlobalStore = create()( if (!app) { return; } - const deleteRes = await ApplicationControllerRemove({ + const deleteRes = await SubscriptionControllerRemove({ appid: app.appid, }); if (!deleteRes.error) { diff --git a/web/src/pages/home/mods/CreateAppModal/BundleItem/index.tsx b/web/src/pages/home/mods/CreateAppModal/BundleItem/index.tsx index 8bc3d3a242..664e089674 100644 --- a/web/src/pages/home/mods/CreateAppModal/BundleItem/index.tsx +++ b/web/src/pages/home/mods/CreateAppModal/BundleItem/index.tsx @@ -2,6 +2,14 @@ import React from "react"; import clsx from "clsx"; import { t } from "i18next"; +import { + formatLimitCapacity, + formatLimitCPU, + formatLimitMemory, + formatLimitTraffic, + formatPrice, +} from "@/utils/format"; + import { TBundle } from "@/apis/typing"; const ListItem = (props: { item: { key: string; value: string | number } }) => { @@ -34,33 +42,41 @@ export default function BundleItem(props: { })} >

{bundle.displayName}

-

- {bundle.price === 0 ? t("Price.Free") : bundle.price} +

+ {bundle.subscriptionOptions[0].price === 0 + ? t("Price.Free") + : formatPrice(bundle.subscriptionOptions[0].price)}

diff --git a/web/src/pages/home/mods/CreateAppModal/index.tsx b/web/src/pages/home/mods/CreateAppModal/index.tsx index 0af629a067..05d8c8f3c3 100644 --- a/web/src/pages/home/mods/CreateAppModal/index.tsx +++ b/web/src/pages/home/mods/CreateAppModal/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { Controller, useForm, useWatch } from "react-hook-form"; import { CheckIcon } from "@chakra-ui/icons"; import { @@ -16,7 +16,6 @@ import { ModalHeader, ModalOverlay, Radio, - RadioGroup, Stack, useDisclosure, VStack, @@ -24,14 +23,19 @@ import { import { useMutation, useQueryClient } from "@tanstack/react-query"; import { t } from "i18next"; +import ChargeButton from "@/components/ChargeButton"; // import ChargeButton from "@/components/ChargeButton"; import { APP_STATUS } from "@/constants/index"; +import { formatPrice } from "@/utils/format"; + +import { useAccountQuery } from "../../service"; import BundleItem from "./BundleItem"; import RuntimeItem from "./RuntimeItem"; -import { TBundle } from "@/apis/typing"; -import { ApplicationControllerCreate, ApplicationControllerUpdate } from "@/apis/v1/applications"; +import { TBundle, TSubscriptionOption } from "@/apis/typing"; +import { ApplicationControllerUpdate } from "@/apis/v1/applications"; +import { SubscriptionControllerCreate } from "@/apis/v1/subscriptions"; import useGlobalStore from "@/pages/globalStore"; const CreateAppModal = (props: { application?: any; children: React.ReactElement }) => { @@ -43,13 +47,15 @@ const CreateAppModal = (props: { application?: any; children: React.ReactElement const { runtimes = [], regions = [] } = useGlobalStore(); + const accountQuery = useAccountQuery(); + type FormData = { name: string; state: APP_STATUS; regionId: string; bundleId: string; runtimeId: string; - duration: string; + subscriptionOption: TSubscriptionOption; }; const bundles = regions[0].bundles; @@ -59,7 +65,7 @@ const CreateAppModal = (props: { application?: any; children: React.ReactElement state: application.state || APP_STATUS.Running, regionId: application.regionId || regions[0].id, bundleId: application.bundleId || bundles[0].id, - duration: "1", + subscriptionOption: (bundles[0].subscriptionOptions && bundles[0].subscriptionOptions[0]) || {}, runtimeId: runtimes[0].id, }; @@ -69,24 +75,33 @@ const CreateAppModal = (props: { application?: any; children: React.ReactElement control, setFocus, reset, + setValue, formState: { errors }, } = useForm({ defaultValues, }); - const duration = useWatch({ + const subscriptionOption = useWatch({ control, - name: "duration", // without supply name will watch the entire form, or ['firstName', 'lastName'] to watch both + name: "subscriptionOption", }); const bundleId = useWatch({ control, - name: "bundleId", // without supply name will watch the entire form, or ['firstName', 'lastName'] to watch both + name: "bundleId", }); const { showSuccess, showError } = useGlobalStore(); - const appCreateMutation = useMutation((params: any) => ApplicationControllerCreate(params)); + const currentBundle = bundles.find((item: TBundle) => item.id === bundleId) || bundles[0]; + const totalPrice = subscriptionOption.specialPrice; + + const currentSubscription = currentBundle.subscriptionOptions[0]; + + const subscriptionControllerCreate = useMutation((params: any) => + SubscriptionControllerCreate(params), + ); + // const subscriptionOptionRenew = useMutation((params: any) => SubscriptionControllerRenew(params)); const updateAppMutation = useMutation((params: any) => ApplicationControllerUpdate(params)); @@ -95,7 +110,20 @@ const CreateAppModal = (props: { application?: any; children: React.ReactElement if (isEdit) { res = await updateAppMutation.mutateAsync({ ...data, appid: application.appid }); } else { - res = await appCreateMutation.mutateAsync(data); + res = await subscriptionControllerCreate.mutateAsync({ + ...data, + duration: subscriptionOption.duration, + }); + // if (res.error) { + // showError(res.error); + // return; + // } else { + // const subscriptionId = res?.data?.id; + // res = await subscriptionOptionRenew.mutateAsync({ + // id: subscriptionId, + // duration: subscriptionOption.duration, + // }); + // } } if (!res.error) { @@ -109,8 +137,9 @@ const CreateAppModal = (props: { application?: any; children: React.ReactElement } }; - const currentBundle = bundles.find((item: TBundle) => item.id === bundleId) || bundles[0]; - const totalPrice = parseInt(duration, 10) * currentBundle.price; + useEffect(() => { + setValue("subscriptionOption", currentSubscription); + }, [currentSubscription, setValue]); return ( <> @@ -142,8 +171,7 @@ const CreateAppModal = (props: { application?: any; children: React.ReactElement /> {errors?.name && errors?.name?.message} - -
diff --git a/web/src/pages/home/mods/List/BundleInfo.tsx b/web/src/pages/home/mods/List/BundleInfo.tsx new file mode 100644 index 0000000000..9483661d7d --- /dev/null +++ b/web/src/pages/home/mods/List/BundleInfo.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; + +import { formatLimitCPU, formatLimitMemory } from "@/utils/format"; + +function BundleInfo(props: { bundle: any }) { + const { t } = useTranslation(); + const { bundle } = props; + if (!bundle) return null; + return ( +
+ {formatLimitCPU(bundle?.resource?.limitCPU)} {t("Unit.CPU")} / + {formatLimitMemory(bundle?.resource?.limitMemory)} {t("Unit.MB")} +
+ ); +} + +export default BundleInfo; diff --git a/web/src/pages/home/mods/List/index.tsx b/web/src/pages/home/mods/List/index.tsx index 40db4d9d1a..b5497be722 100644 --- a/web/src/pages/home/mods/List/index.tsx +++ b/web/src/pages/home/mods/List/index.tsx @@ -30,20 +30,22 @@ import getRegionById from "@/utils/getRegionById"; import CreateAppModal from "../CreateAppModal"; import StatusBadge from "../StatusBadge"; -import { TApplication } from "@/apis/typing"; -import { ApplicationControllerRemove } from "@/apis/v1/applications"; +import BundleInfo from "./BundleInfo"; + +import { SubscriptionControllerRemove } from "@/apis/v1/subscriptions"; import useGlobalStore from "@/pages/globalStore"; function List(props: { appListQuery: any; setShouldRefetch: any }) { const navigate = useNavigate(); - const { setCurrentApp, regions } = useGlobalStore(); + const { setCurrentApp, updateCurrentApp, regions } = useGlobalStore(); const [searchKey, setSearchKey] = useState(""); const { appListQuery, setShouldRefetch } = props; const bg = useColorModeValue("lafWhite.200", "lafDark.200"); - const deleteAppMutation = useMutation((params: any) => ApplicationControllerRemove(params), { + + const deleteAppMutation = useMutation((params: any) => SubscriptionControllerRemove(params), { onSuccess: () => { setShouldRefetch(true); }, @@ -87,13 +89,13 @@ function List(props: { appListQuery: any; setShouldRefetch: any }) {
App ID
{t("HomePanel.State")}
{t("HomePanel.Region")}
-
{t("CreateTime")}
+
{t("Time")}
{t("Operation")}
{(appListQuery.data?.data || []) .filter((item: any) => item?.name.indexOf(searchKey) >= 0) - .map((item: TApplication) => { + .map((item: any) => { return (
{item?.name} - {/* */} + + {item?.bundle?.displayName} +
- {/*
CPU: 0.1 核 | RAM: 24 G
*/} +
{item?.appid} @@ -119,8 +121,8 @@ function List(props: { appListQuery: any; setShouldRefetch: any }) { {getRegionById(regions, item.regionId)?.displayName}
- {formatDate(item.createdAt)}
- {/* end: {formatDate(item.createdAt)} */} + 创建时间: {formatDate(item.createdAt)}
+ 到期时间: {formatDate(item.subscription.expiredAt)} 续期