515 lines
14 KiB
TypeScript
515 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { Loading } from "@/components/dashboard/loading";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { toast } from "@/components/ui/toaster";
|
|
// import { trpc } from "@/lib/trpc/client";
|
|
// import { PostHogEvent } from "@/providers/PostHogProvider";
|
|
// import { type Workspace } from "@aigxion/db";
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
import React, { useEffect, useState } from "react";
|
|
|
|
// import { getTenantId } from "@/lib/auth";
|
|
// import { db } from "@/lib/db";
|
|
import { ArrowLeft } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { ChangePlanButton } from "./button";
|
|
import { PayInfo, PaymentInfo, PaymentOrder, payStaff, queryPayStatus, queryProductionList } from "@/lib/http/staff";
|
|
import { FadeIn } from "@/components/landing/fade-in";
|
|
import { cn } from "@/lib/utils";
|
|
import { Modal, QRCode, message } from "antd";
|
|
import Image from "next/image";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
type Props = {
|
|
newPlan: "free" | "pro";
|
|
// workspace: Workspace;
|
|
// tier: string;
|
|
tiers: ProductInfo
|
|
};
|
|
|
|
interface ProductInfoKey {
|
|
[key: string]: ProductInfo;
|
|
}
|
|
|
|
interface ProductInfo {
|
|
id: number;
|
|
name: string;
|
|
href: string;
|
|
price: number | string;
|
|
description: string;
|
|
buttonText: string;
|
|
features: string[];
|
|
footnotes: never[];
|
|
|
|
serial_no?: string;
|
|
currency?: string;
|
|
state?: number;
|
|
language?: string;
|
|
trial_days?: number;
|
|
expiring_days?: number;
|
|
pay_url?: string;
|
|
is_inactive?: boolean; //如果是true就弹窗提示
|
|
is_deleted?: boolean;
|
|
create_user?: string;
|
|
edit_user?: string;
|
|
created_time?: string;
|
|
updated_time?: string;
|
|
};
|
|
|
|
|
|
|
|
enum OrderStatus {
|
|
WaitingForPayment = 0,
|
|
PaymentCompleted = 1,
|
|
PaymentTimeoutClosed = 2,
|
|
Refunded = 3,
|
|
EndedNonRefundable = 4,
|
|
PaymentPlatformException = 5
|
|
}
|
|
|
|
function getOrderStatusString(status: OrderStatus): string {
|
|
switch (status) {
|
|
case OrderStatus.WaitingForPayment:
|
|
return '等待付款';
|
|
case OrderStatus.PaymentCompleted:
|
|
return '已完成付款';
|
|
case OrderStatus.PaymentTimeoutClosed:
|
|
return '付款超时关闭';
|
|
case OrderStatus.Refunded:
|
|
return '已退款';
|
|
case OrderStatus.EndedNonRefundable:
|
|
return '已结束[不可退款]';
|
|
case OrderStatus.PaymentPlatformException:
|
|
return '支付平台异常';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
|
|
export const PlanCard: React.FC<Props> = ({ newPlan = "free", tiers }) => {
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
console.log("----PlanCard----", tiers)
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [payQrcode, setPayQrcode] = useState("");
|
|
|
|
const path = usePathname()
|
|
const { t, i18n } = useTranslation()
|
|
|
|
const searchParams = useSearchParams()
|
|
const staffId = searchParams.get('staffId')
|
|
|
|
console.log("---PlanCard----", staffId, path)
|
|
|
|
const isChatPage = path.includes('chat')
|
|
|
|
|
|
|
|
|
|
// const changePlan = trpc.workspace.changePlan.useMutation({
|
|
// onSuccess: (data, variables, _context) => {
|
|
// toast.success(data.title, {
|
|
// description: data.message,
|
|
// });
|
|
// PostHogEvent({
|
|
// name: "plan_changed",
|
|
// properties: { plan: variables.plan, workspace: variables.workspaceId },
|
|
// });
|
|
// setOpen(false);
|
|
// router.refresh();
|
|
// },
|
|
// onError: (error) => {
|
|
// toast.error(error.message);
|
|
// },
|
|
// });
|
|
|
|
|
|
|
|
async function pollOrder(orderId: string, interval: number, maxAttempts: number): Promise<PaymentOrder | null> {
|
|
let attempts = 0;
|
|
|
|
console.log("开始轮训订单", orderId)
|
|
|
|
const poll = async (): Promise<PaymentOrder | null> => {
|
|
|
|
try {
|
|
// 发送订单查询请求,假设返回的数据结构为 Order
|
|
const result = await queryPayStatus(orderId);
|
|
const order = result.data as PaymentOrder;
|
|
|
|
console.log("---pollOrder------", order, attempts)
|
|
|
|
//支付状态(0=等待付款 1=已完成付款 2=付款超时关闭 3=已退款 4=已结束[不可退款] 5=支付平台异常)
|
|
|
|
// 如果订单状态为已完成,或者达到最大尝试次数,则停止轮询并返回订单
|
|
if (order.state >= 2 || attempts >= maxAttempts) {
|
|
message.warning(getOrderStatusString(order.state))
|
|
router.push(`/manage/staffs`)
|
|
return order;
|
|
} else if (order.state == 1) {
|
|
message.warning(getOrderStatusString(order.state))
|
|
router.push(`/manage/staffs`)
|
|
return order;
|
|
}
|
|
|
|
// 增加尝试次数
|
|
attempts++;
|
|
|
|
// 等待一段时间后继续轮询
|
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
|
|
// 递归调用自身,继续轮询
|
|
return poll();
|
|
} catch (error) {
|
|
// 处理轮询过程中的错误
|
|
console.error("Error occurred while polling order:", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return poll();
|
|
}
|
|
|
|
const isSamePlan = true;
|
|
return (
|
|
<div
|
|
key={tiers.id}
|
|
// dark:bg-black
|
|
className={
|
|
"border border-border flex w-full flex-col justify-between rounded-lg bg-white p-8 lg:w-1/3 xl:p-10 "
|
|
}
|
|
>
|
|
<div className="flex items-center justify-between gap-x-4">
|
|
<h2 id={tiers.serial_no} className={"text-content text-2xl font-semibold leading-8"}>
|
|
{tiers.name}
|
|
</h2>
|
|
</div>
|
|
{/* <p className="mt-4 min-h-[3rem] text-sm leading-6 text-content-subtle">
|
|
{tiers.description}
|
|
</p> */}
|
|
<p className="flex items-center mx-auto mt-6 gap-x-1">
|
|
{typeof tiers.price === "number" ? (
|
|
<>
|
|
<span className="text-4xl font-bold tracking-tight text-center text-content">
|
|
{`${tiers.price}`}
|
|
</span>
|
|
<span className="mx-auto text-sm font-semibold leading-6 text-center text-content-subtle">
|
|
{"/month"}
|
|
</span>
|
|
</>
|
|
) : (
|
|
|
|
|
|
<>
|
|
<span className="mx-auto text-4xl font-bold tracking-tight text-center text-content">
|
|
{tiers.price}
|
|
</span>
|
|
<span className="mx-auto text-sm font-semibold leading-6 text-center text-content-subtle">
|
|
/{tiers.expiring_days}天
|
|
</span>
|
|
</>
|
|
)}
|
|
</p>
|
|
|
|
<div className="flex flex-col justify-between grow">
|
|
<ul className="mt-8 space-y-3 text-sm leading-6 text-content-subtle xl:mt-10">
|
|
{tiers.features.map((feature) => (
|
|
<li key={feature} className="flex gap-x-3">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
className="flex-none w-5 h-6 text-gray-700"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
fill="currentColor"
|
|
fillRule="evenodd"
|
|
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353l8.493-12.739a.75.75 0 0 1 1.04-.208Z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
|
|
{feature}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{tiers.footnotes && (
|
|
<ul className="mt-6 mb-8">
|
|
{tiers.footnotes.map((footnote, i) => (
|
|
<li
|
|
// biome-ignore lint/suspicious/noArrayIndexKey: I got nothing better right now
|
|
key={`note-${i}`}
|
|
className="flex text-xs text-content-subtle gap-x-3"
|
|
>
|
|
{footnote}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
{/* {tier === "custom" ? (
|
|
<Link href="mailto:support@unkey.dev">
|
|
<Button
|
|
className="w-full col-span-1"
|
|
// variant={workspace.plan === "enterprise" ? "disabled" : "primary"}
|
|
// disabled={workspace.plan === "enterprise"}
|
|
>
|
|
|
|
Go
|
|
</Button>
|
|
</Link>
|
|
) : (
|
|
<ChangePlanButton
|
|
// workspace={workspace}
|
|
newPlan={newPlan}
|
|
label={tiers[tier].buttonText}
|
|
/>
|
|
)} */}
|
|
|
|
|
|
|
|
{/* <Button
|
|
className="w-full col-span-1"
|
|
// variant={workspace.plan === "enterprise" ? "disabled" : "primary"}
|
|
// disabled={workspace.plan === "enterprise"}
|
|
>
|
|
|
|
Go
|
|
</Button> */}
|
|
|
|
{
|
|
(staffId && !tiers.is_inactive) ? <Button
|
|
disabled={isLoading}
|
|
className="mt-4 "
|
|
type="submit"
|
|
|
|
onClick={async (e) => {
|
|
e.preventDefault()
|
|
setIsLoading(true)
|
|
// setTimeout(() => {
|
|
// setOpen(true)
|
|
// setIsLoading(false)
|
|
// }, 2000)
|
|
if (!!Number(staffId)) {
|
|
const result = await payStaff({
|
|
id: Number(staffId),
|
|
production_id: tiers.id,
|
|
pay_type: "alipay",
|
|
} as PayInfo)
|
|
|
|
|
|
const typedData = result.data as PaymentInfo;
|
|
|
|
|
|
console.log("----payStaff---", typedData)
|
|
|
|
setPayQrcode(typedData.pay_url)
|
|
|
|
setOpen(true)
|
|
|
|
// 启动轮训
|
|
setTimeout(async () => {
|
|
await pollOrder(typedData.trade_no, 2000, 60)
|
|
|
|
// router.push(`/manage/staffs`);
|
|
setIsLoading(false)
|
|
})
|
|
|
|
|
|
} else {
|
|
message.warning("无效员工ID")
|
|
}
|
|
|
|
|
|
}}
|
|
>
|
|
{isLoading ? <Loading /> : "支付"}
|
|
</Button> : <Button
|
|
disabled={tiers.is_inactive}
|
|
className="mt-4 "
|
|
|
|
type="submit">敬请期待</Button>
|
|
}
|
|
|
|
</div>
|
|
|
|
|
|
<Modal
|
|
// title="Modal 1000px width"
|
|
centered
|
|
open={open}
|
|
onOk={() => {
|
|
if (setOpen) {
|
|
setOpen(false)
|
|
}
|
|
}}
|
|
onCancel={() => {
|
|
setOpen(false)
|
|
setIsLoading(false)
|
|
}}
|
|
footer=""
|
|
// width={"50%"}
|
|
|
|
className={cn('min-w-[95%] lg:min-w-[700px] p-[20px]', "")}
|
|
>
|
|
|
|
<div className="mx-auto sm:max-w-2xl min-h-[500px] mb-[3rem] lg:mb-[5rem] rounded bg-[#fff]">
|
|
|
|
<FadeIn >
|
|
<h3 className='text-center px-6 sm:px-4 text-2xl font-bold py-[1.2rem]'>请使用支付宝进行支付</h3>
|
|
|
|
{/* <div className='flex flex-row items-center justify-around bg-[#F5F5F5] text-[#666666] mb-[2rem]'>
|
|
<Link href="/xauth/login" >
|
|
<Button className="h-8 " >Login</Button>
|
|
</Link>
|
|
<Link href="/xauth/login">
|
|
<Button className="h-8" >Signup</Button>
|
|
</Link>
|
|
<Link href="/xauth/login">
|
|
<Button className="h-8" >Download</Button>
|
|
</Link>
|
|
</div> */}
|
|
|
|
{payQrcode && <div className="rounded bg-clip-border overflow-clip">
|
|
{/* <figure
|
|
className='w-full'
|
|
><Image fill={true} src={payQrcode} className='w-full' alt="Hero Image" /></figure> */}
|
|
<QRCode
|
|
value={payQrcode}
|
|
size={200} // 二维码的大小
|
|
style={{ margin: 'auto' }}
|
|
imageSettings={{ // 二维码中间的logo图片
|
|
src: 'logoUrl',
|
|
height: 100,
|
|
width: 100,
|
|
excavate: true, // 中间图片所在的位置是否镂空
|
|
}}
|
|
/>
|
|
|
|
|
|
{/* <figure
|
|
className="w-full min-h-[146px] lg:min-h-[246px] "
|
|
// style={{ width: '100%', minHeight: "246px" }}
|
|
>
|
|
<img src={payQrcode} alt="Hero Image" style={{ width: '100%', marginBottom: '8px' }} />
|
|
</figure> */}
|
|
</div>}
|
|
|
|
</FadeIn>
|
|
</div>
|
|
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
|
|
export const PlanCardView: React.FC = () => {
|
|
|
|
|
|
const [tiers, setTiers] = useState<ProductInfo[]>([
|
|
{
|
|
id: 1,
|
|
name: "月度计划",
|
|
href: "/manage",
|
|
price: 299,
|
|
description: "启动下一个 API 所需的一切!",
|
|
buttonText: "基础",
|
|
features: [
|
|
// "100 个每月活动密钥",
|
|
// "每月 2500 次成功验证",
|
|
"无限的API",
|
|
"7天免费保留",
|
|
],
|
|
footnotes: [],
|
|
},
|
|
{
|
|
id: 2,
|
|
name: "季度计划",
|
|
href: "/manage",
|
|
price: 699,
|
|
description: "对于那些有团队和更苛刻需求的人",
|
|
buttonText: "Pro",
|
|
features: [
|
|
// "250 每月 xxxxxxxx*",
|
|
// "150,000 成功 xxxxxxx 包括 **",
|
|
// "xxxxxxxx APIs",
|
|
"与团队成员的工作空间",
|
|
"90 天分析保留",
|
|
"90 天审核日志保留",
|
|
],
|
|
footnotes: [
|
|
// " * Additional active keys are billed at $0.10",
|
|
// " ** Additional verifications are billed at $10 per 100,000",
|
|
],
|
|
},
|
|
{
|
|
id: 3,
|
|
name: "年度计划",
|
|
href: "/manage",
|
|
price: 1299,
|
|
description: "我们为有批量需求的人提供定制定价",
|
|
buttonText: "Go",
|
|
features: [
|
|
// "自定义验证限制",
|
|
// "自定义活动键限制",
|
|
"自定义分析保留",
|
|
"专用支持合同",
|
|
"每个API的白名单IP",
|
|
],
|
|
footnotes: [],
|
|
},
|
|
]);
|
|
const [clientReady, setClientReady] = useState<boolean>(false);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
async function initFunc() {
|
|
const result = await queryProductionList()
|
|
console.log(result)
|
|
|
|
let newTiers = result.data.list as ProductInfo[]
|
|
|
|
// 使用map方法更新原来的数组
|
|
const updatedTiers = tiers.map(originalTier => {
|
|
const newTier = newTiers.find(newTier => newTier.id === originalTier.id);
|
|
|
|
if (newTier) {
|
|
newTier.features = newTier.description.split('\n')
|
|
|
|
}
|
|
return newTier ? { ...originalTier, ...newTier } : originalTier;
|
|
});
|
|
|
|
// 更新状态
|
|
setTiers(updatedTiers);
|
|
setClientReady(true)
|
|
|
|
}
|
|
initFunc()
|
|
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
{clientReady && <div className="flex flex-col gap-6 mt-6 md:flex-col lg:flex-row max-w-7xl m-auto">
|
|
{tiers.map((tier) => (<PlanCard key={tier.id} newPlan={"free"} tiers={tier} />))}
|
|
</div>}
|
|
</>
|
|
);
|
|
};
|
|
|
|
|