feat: add redemption code feature to user dashboard

- Add redemption code input component in dashboard
- Integrate redemption API endpoint
- Add i18n support for Chinese and English
- Display redemption section alongside subscriptions
This commit is contained in:
EUForest 2026-02-06 23:59:31 +08:00
parent 902e5fe59a
commit 1f9b7ea1db
5 changed files with 123 additions and 2 deletions

View File

@ -31,5 +31,13 @@
"subscriptionUrl": "Subscription URL", "subscriptionUrl": "Subscription URL",
"totalTraffic": "Total Traffic", "totalTraffic": "Total Traffic",
"unknown": "Unknown", "unknown": "Unknown",
"used": "Used" "used": "Used",
"redeemCode": "Redeem Code",
"redeemCodeTitle": "Redeem Code",
"redeemCodeDescription": "Enter redemption code to get subscription plan",
"enterRedemptionCode": "Please enter redemption code",
"redeemButton": "Redeem Now",
"redeeming": "Redeeming...",
"redeemSuccess": "Redemption successful! Subscription has been activated.",
"redeemFailed": "Redemption failed"
} }

View File

@ -31,5 +31,13 @@
"subscriptionUrl": "订阅链接", "subscriptionUrl": "订阅链接",
"totalTraffic": "总流量", "totalTraffic": "总流量",
"unknown": "未知", "unknown": "未知",
"used": "已使用" "used": "已使用",
"redeemCode": "兑换 CDK",
"redeemCodeTitle": "CDK 兑换",
"redeemCodeDescription": "输入兑换码以获取订阅套餐",
"enterRedemptionCode": "请输入兑换码",
"redeemButton": "立即兑换",
"redeeming": "兑换中...",
"redeemSuccess": "兑换成功!",
"redeemFailed": "兑换失败"
} }

View File

@ -47,6 +47,7 @@ import Subscribe from "../../subscribe";
import Renewal from "../../subscribe/renewal"; import Renewal from "../../subscribe/renewal";
import ResetTraffic from "../../subscribe/reset-traffic"; import ResetTraffic from "../../subscribe/reset-traffic";
import Unsubscribe from "../../subscribe/unsubscribe"; import Unsubscribe from "../../subscribe/unsubscribe";
import RedeemCode from "./redeem-code";
const platforms: (keyof API.DownloadLink)[] = [ const platforms: (keyof API.DownloadLink)[] = [
"windows", "windows",
@ -164,6 +165,7 @@ export default function Content() {
</Button> </Button>
</div> </div>
</div> </div>
<RedeemCode onSuccess={refetch} />
<div className="flex flex-wrap justify-between gap-4"> <div className="flex flex-wrap justify-between gap-4">
{availablePlatforms.length > 0 && ( {availablePlatforms.length > 0 && (
<Tabs <Tabs

View File

@ -0,0 +1,85 @@
import { useMutation } from "@tanstack/react-query";
import { Button } from "@workspace/ui/components/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@workspace/ui/components/card";
import { Input } from "@workspace/ui/components/input";
import { Label } from "@workspace/ui/components/label";
import { redeemCode } from "@workspace/ui/services/user/user";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
interface RedeemCodeProps {
onSuccess?: () => void;
}
export default function RedeemCode({ onSuccess }: RedeemCodeProps) {
const { t } = useTranslation("dashboard");
const [code, setCode] = useState("");
const redeemMutation = useMutation({
mutationFn: (code: string) => redeemCode({ code }),
onSuccess: (response) => {
toast.success(response.data.message || t("redeemSuccess", "兑换成功"));
setCode("");
onSuccess?.();
},
onError: (error: any) => {
const errorMessage =
error?.response?.data?.message || t("redeemFailed", "兑换失败");
toast.error(errorMessage);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!code.trim()) {
toast.error(t("pleaseEnterCode", "请输入兑换码"));
return;
}
redeemMutation.mutate(code.trim());
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle>{t("redeemCode", "CDK 兑换")}</CardTitle>
<CardDescription>
{t("redeemCodeDescription", "输入兑换码即可兑换对应的订阅套餐")}
</CardDescription>
</CardHeader>
<CardContent>
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<div className="flex flex-col gap-2">
<Label htmlFor="redemption-code">
{t("redemptionCode", "兑换码")}
</Label>
<div className="flex gap-2">
<Input
id="redemption-code"
onChange={(e) => setCode(e.target.value)}
placeholder={t(
"enterRedemptionCode",
"请输入兑换码"
)}
value={code}
/>
<Button
disabled={redeemMutation.isPending || !code.trim()}
loading={redeemMutation.isPending}
type="submit"
>
{t("redeem", "兑换")}
</Button>
</div>
</div>
</form>
</CardContent>
</Card>
);
}

View File

@ -268,6 +268,24 @@ export async function updateUserRules(
); );
} }
/** Redeem Code POST /v1/public/redemption/ */
export async function redeemCode(
body: { code: string },
options?: { [key: string]: any }
) {
return request<API.Response & { data?: { message: string } }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/public/redemption/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Query User Subscribe GET /v1/public/user/subscribe */ /** Query User Subscribe GET /v1/public/user/subscribe */
export async function queryUserSubscribe(options?: { [key: string]: any }) { export async function queryUserSubscribe(options?: { [key: string]: any }) {
return request<API.Response & { data?: API.QueryUserSubscribeListResponse }>( return request<API.Response & { data?: API.QueryUserSubscribeListResponse }>(