🔀 merge: main into develop
This commit is contained in:
@@ -44,6 +44,8 @@
|
||||
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
|
||||
"60004": "Unable to delete at the moment as the subscription has active users.",
|
||||
"60005": "Single subscription mode has exceeded user limit",
|
||||
"60006": "User quota limit has been reached, unable to continue.",
|
||||
"60007": "Insufficient inventory, please try again later or contact the administrator.",
|
||||
"70001": "Incorrect verification code, please re-enter.",
|
||||
"80001": "Task was not successfully queued, please try again later.",
|
||||
"90001": "Please disable DEBUG mode and try again.",
|
||||
@@ -61,5 +63,5 @@
|
||||
"system": "System",
|
||||
"toggle": "Toggle theme"
|
||||
},
|
||||
"unlimited": "unlimited"
|
||||
"unlimited": "Unlimited"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"discountPercent": "Discount Percentage",
|
||||
"Hour": "Hour",
|
||||
"inventory": "Subscription Limit",
|
||||
"unlimitedInventory": "Unlimited (enter -1)",
|
||||
"language": "Language",
|
||||
"languageDescription": "Leave empty for default without language restriction",
|
||||
"languagePlaceholder": "Language identifier for the subscription, e.g., en-US, zh-CN",
|
||||
@@ -56,6 +57,8 @@
|
||||
"resetOn1st": "Reset on the 1st",
|
||||
"selectResetCycle": "Please select a reset cycle",
|
||||
"selectUnitTime": "Please select a unit of time",
|
||||
"showOriginalPrice": "Show Original Price",
|
||||
"showOriginalPriceDescription": "When enabled, the subscription card will display both the original price and the discounted price to help users understand the discount amount",
|
||||
"speedLimit": "Speed Limit ",
|
||||
"traffic": "Traffic",
|
||||
"unitPrice": "Unit Price",
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"systemServices": "System Services",
|
||||
"update": "Update",
|
||||
"updateFailed": "Update failed",
|
||||
"updateServerDescription": "Are you sure you want to update the server version from V{{current}} to V{{latest}}?",
|
||||
"updateServerDescription": "Are you sure you want to update the server version from {{current}} to {{latest}}?",
|
||||
"updateSuccess": "Update completed successfully",
|
||||
"updateWebDescription": "Are you sure you want to update the web version from V{{current}} to V{{latest}}?",
|
||||
"updateWebDescription": "Are you sure you want to update the web version from {{current}} to {{latest}}?",
|
||||
"userUpdateSuccess": "User updated successfully",
|
||||
"webVersion": "Web Version"
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
"createSuccess": "Created successfully",
|
||||
"createUser": "Create User",
|
||||
"delete": "Delete",
|
||||
"deleted": "Deleted",
|
||||
"deleteDescription": "This action cannot be undone.",
|
||||
"deleteSubscriptionDescription": "This action cannot be undone.",
|
||||
"deleteSuccess": "Deleted successfully",
|
||||
"isDeleted": "Status",
|
||||
"deviceLimit": "Device Limit",
|
||||
"download": "Download",
|
||||
"downloadTraffic": "Download Traffic",
|
||||
@@ -51,6 +53,7 @@
|
||||
"loginStatus": "Login Status",
|
||||
"manager": "Administrator",
|
||||
"more": "More",
|
||||
"normal": "Normal",
|
||||
"notifySettingsTitle": "Notify Settings",
|
||||
"offline": "Offline",
|
||||
"online": "Online",
|
||||
@@ -80,6 +83,25 @@
|
||||
"toggleSubscriptionStatus": "Toggle Status",
|
||||
"toggleSubscriptionStatusDescription": "This will toggle the subscription status.",
|
||||
"resetTime": "Reset Time",
|
||||
"resetToken": "Reset Subscription Address",
|
||||
"resetTokenDescription": "This will reset the subscription address and regenerate a new token.",
|
||||
"resetTokenSuccess": "Subscription address reset successfully",
|
||||
"confirmResetToken": "Confirm Reset Subscription Address",
|
||||
"stopSubscribe": "Stop Subscription",
|
||||
"stopSubscribeDescription": "This will stop the subscription temporarily. User will not be able to use it.",
|
||||
"stopSubscribeSuccess": "Subscription stopped successfully",
|
||||
"confirmStopSubscribe": "Confirm Stop Subscription",
|
||||
"resumeSubscribe": "Resume Subscription",
|
||||
"resumeSubscribeDescription": "This will resume the subscription and allow the user to use it.",
|
||||
"resumeSubscribeSuccess": "Subscription resumed successfully",
|
||||
"confirmResumeSubscribe": "Confirm Resume Subscription",
|
||||
"status": "Status",
|
||||
"statusPending": "Pending",
|
||||
"statusActive": "Active",
|
||||
"statusFinished": "Finished",
|
||||
"statusExpired": "Expired",
|
||||
"statusDeducted": "Deducted",
|
||||
"statusStopped": "Stopped",
|
||||
"save": "Save",
|
||||
"speedLimit": "Speed Limit",
|
||||
"startTime": "startTime",
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
"60003": "检测到现有订阅,请先取消后再继续。",
|
||||
"60004": "由于订阅有活跃用户,暂时无法删除。",
|
||||
"60005": "单一订阅模式已超过用户限制",
|
||||
"60006": "用户配额已达到限制,无法继续操作。",
|
||||
"60007": "库存不足,请稍后再试或联系管理员。",
|
||||
"70001": "验证码不正确,请重新输入。",
|
||||
"80001": "任务未成功加入队列,请稍后再试。",
|
||||
"90001": "请禁用 DEBUG 模式后再试。",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"discountPercent": "折扣百分比",
|
||||
"Hour": "小时",
|
||||
"inventory": "订阅库存",
|
||||
"unlimitedInventory": "无限制(输入 -1)",
|
||||
"language": "语言",
|
||||
"languageDescription": "留空为默认无语言限制",
|
||||
"languagePlaceholder": "订阅的语言标识符,例如 en-US、zh-CN",
|
||||
@@ -56,6 +57,8 @@
|
||||
"resetOn1st": "每月1日重置",
|
||||
"selectResetCycle": "请选择重置周期",
|
||||
"selectUnitTime": "请选择时间单位",
|
||||
"showOriginalPrice": "显示原价",
|
||||
"showOriginalPriceDescription": "开启后,在订阅卡片上将会显示原价和折后价,帮助用户了解优惠幅度",
|
||||
"speedLimit": "速度限制",
|
||||
"traffic": "流量",
|
||||
"unitPrice": "单价",
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"systemServices": "系统服务",
|
||||
"update": "更新",
|
||||
"updateFailed": "更新失败",
|
||||
"updateServerDescription": "确定要将服务器版本从 V{{current}} 更新到 V{{latest}} 吗?",
|
||||
"updateServerDescription": "确定要将服务器版本从 {{current}} 更新到 {{latest}} 吗?",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateWebDescription": "确定要将前端版本从 V{{current}} 更新到 V{{latest}} 吗?",
|
||||
"updateWebDescription": "确定要将前端版本从 {{current}} 更新到 {{latest}} 吗?",
|
||||
"userUpdateSuccess": "用户端更新成功",
|
||||
"webVersion": "前端版本"
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@
|
||||
"createSuccess": "创建成功",
|
||||
"createUser": "创建用户",
|
||||
"delete": "删除",
|
||||
"deleted": "已删除",
|
||||
"deleteDescription": "此操作无法撤销。",
|
||||
"deleteSubscriptionDescription": "此操作无法撤销。",
|
||||
"deleteSuccess": "删除成功",
|
||||
"isDeleted": "状态",
|
||||
"deviceLimit": "IP限制",
|
||||
"download": "下载",
|
||||
"downloadTraffic": "下载流量",
|
||||
@@ -51,6 +53,7 @@
|
||||
"loginStatus": "登录状态",
|
||||
"manager": "管理员",
|
||||
"more": "更多",
|
||||
"normal": "正常",
|
||||
"notifySettingsTitle": "通知设置",
|
||||
"offline": "离线",
|
||||
"online": "在线",
|
||||
@@ -80,6 +83,25 @@
|
||||
"toggleSubscriptionStatus": "切换状态",
|
||||
"toggleSubscriptionStatusDescription": "将切换该订阅的启用/停用状态。",
|
||||
"resetTime": "重置时间",
|
||||
"resetToken": "重置订阅地址",
|
||||
"resetTokenDescription": "这将重置订阅地址并重新生成新的令牌。",
|
||||
"resetTokenSuccess": "订阅地址重置成功",
|
||||
"confirmResetToken": "确认重置订阅地址",
|
||||
"stopSubscribe": "暂停订阅",
|
||||
"stopSubscribeDescription": "这将暂时停止订阅。用户将无法使用。",
|
||||
"stopSubscribeSuccess": "订阅已暂停",
|
||||
"confirmStopSubscribe": "确认暂停订阅",
|
||||
"resumeSubscribe": "恢复订阅",
|
||||
"resumeSubscribeDescription": "这将恢复订阅,允许用户继续使用。",
|
||||
"resumeSubscribeSuccess": "订阅已恢复",
|
||||
"confirmResumeSubscribe": "确认恢复订阅",
|
||||
"status": "状态",
|
||||
"statusPending": "待处理",
|
||||
"statusActive": "活跃",
|
||||
"statusFinished": "已完成",
|
||||
"statusExpired": "已过期",
|
||||
"statusDeducted": "已扣除",
|
||||
"statusStopped": "已停止",
|
||||
"save": "保存",
|
||||
"speedLimit": "速度限制",
|
||||
"startTime": "开始时间",
|
||||
|
||||
@@ -28,7 +28,7 @@ export function Display<T extends number | undefined | null>({
|
||||
if (
|
||||
["traffic", "trafficSpeed", "number"].includes(type) &&
|
||||
unlimited &&
|
||||
!value
|
||||
(value === 0 || value === null || value === undefined)
|
||||
) {
|
||||
return t("unlimited");
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export function Display<T extends number | undefined | null>({
|
||||
}
|
||||
|
||||
if (type === "number") {
|
||||
return value ? value.toString() : "0";
|
||||
return value !== null && value !== undefined ? value.toString() : "0";
|
||||
}
|
||||
|
||||
return "0";
|
||||
|
||||
@@ -538,7 +538,7 @@ export default function EmailSettingsForm() {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<HTMLEditor
|
||||
onBlur={field.onChange}
|
||||
onChange={field.onChange}
|
||||
placeholder={t(
|
||||
"email.inputPlaceholder",
|
||||
"Please enter"
|
||||
@@ -642,7 +642,7 @@ export default function EmailSettingsForm() {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<HTMLEditor
|
||||
onBlur={field.onChange}
|
||||
onChange={field.onChange}
|
||||
placeholder={t(
|
||||
"email.inputPlaceholder",
|
||||
"Please enter"
|
||||
@@ -713,7 +713,7 @@ export default function EmailSettingsForm() {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<HTMLEditor
|
||||
onBlur={field.onChange}
|
||||
onChange={field.onChange}
|
||||
placeholder={t(
|
||||
"email.inputPlaceholder",
|
||||
"Please enter"
|
||||
@@ -795,7 +795,7 @@ export default function EmailSettingsForm() {
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<HTMLEditor
|
||||
onBlur={field.onChange}
|
||||
onChange={field.onChange}
|
||||
placeholder={t(
|
||||
"email.inputPlaceholder",
|
||||
"Please enter"
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function RegisterForm({
|
||||
/>
|
||||
<SendCode
|
||||
params={{
|
||||
...form.getValues(),
|
||||
email: form.watch("email"),
|
||||
type: 1,
|
||||
}}
|
||||
type="email"
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function ResetForm({
|
||||
/>
|
||||
<SendCode
|
||||
params={{
|
||||
...form.getValues(),
|
||||
email: form.watch("email"),
|
||||
type: 2,
|
||||
}}
|
||||
type="email"
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function Auth() {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
// navigate({ to: "/dashboard" });
|
||||
navigate({ to: "/dashboard" });
|
||||
}
|
||||
}, [navigate, user]);
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export default function SystemVersionCard() {
|
||||
queryFn: async () => {
|
||||
const { data } = await basicCheckServiceVersion(
|
||||
{
|
||||
service_name: "admin",
|
||||
service_name: "admin-web-with-api",
|
||||
secret: moduleConfig!.secret,
|
||||
},
|
||||
{ skipErrorHandler: true }
|
||||
@@ -105,13 +105,13 @@ export default function SystemVersionCard() {
|
||||
setIsUpdatingWeb(true);
|
||||
try {
|
||||
await basicUpdateService({
|
||||
service_name: "admin",
|
||||
service_name: "admin-web-with-api",
|
||||
secret: moduleConfig.secret,
|
||||
});
|
||||
toast.success(t("adminUpdateSuccess", "Admin updated successfully"));
|
||||
|
||||
await basicUpdateService({
|
||||
service_name: "user",
|
||||
service_name: "user-web-with-api",
|
||||
secret: moduleConfig.secret,
|
||||
});
|
||||
toast.success(t("userUpdateSuccess", "User updated successfully"));
|
||||
@@ -188,52 +188,52 @@ export default function SystemVersionCard() {
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge>V{packageJson.version}</Badge>
|
||||
{hasWebNewVersion && webVersionInfo && (
|
||||
<AlertDialog onOpenChange={setOpenUpdateWeb} open={openUpdateWeb}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={isUpdatingWeb}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
||||
{t("update", "Update")} V{webVersionInfo.latest_version}
|
||||
<AlertDialog onOpenChange={setOpenUpdateWeb} open={openUpdateWeb}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={!hasWebNewVersion || isUpdatingWeb}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
||||
{hasWebNewVersion && webVersionInfo
|
||||
? `${t("update", "Update")} ${webVersionInfo.latest_version}`
|
||||
: t("update", "Update")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{webVersionInfo
|
||||
? t(
|
||||
"updateWebDescription",
|
||||
"Are you sure you want to update the web version from {{current}} to {{latest}}?",
|
||||
{
|
||||
current: webVersionInfo.current_version,
|
||||
latest: webVersionInfo.latest_version,
|
||||
}
|
||||
)
|
||||
: t(
|
||||
"updateDescription",
|
||||
"Are you sure you want to update?"
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel", "Cancel")}</AlertDialogCancel>
|
||||
<Button disabled={isUpdatingWeb} onClick={handleUpdateWeb}>
|
||||
{isUpdatingWeb && (
|
||||
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
|
||||
)}
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
"updateWebDescription",
|
||||
"Are you sure you want to update the web version from V{{current}} to V{{latest}}?",
|
||||
{
|
||||
current: packageJson.version,
|
||||
latest: webVersionInfo.latest_version,
|
||||
}
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("cancel", "Cancel")}
|
||||
</AlertDialogCancel>
|
||||
<Button disabled={isUpdatingWeb} onClick={handleUpdateWeb}>
|
||||
{isUpdatingWeb && (
|
||||
<Icon
|
||||
className="mr-2 animate-spin"
|
||||
icon="mdi:loading"
|
||||
/>
|
||||
)}
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -251,62 +251,63 @@ export default function SystemVersionCard() {
|
||||
serverVersionInfo?.current_version ||
|
||||
"1.0.0"}
|
||||
</Badge>
|
||||
{hasServerNewVersion && serverVersionInfo && moduleConfig && (
|
||||
<AlertDialog
|
||||
onOpenChange={setOpenUpdateServer}
|
||||
open={openUpdateServer}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<AlertDialog
|
||||
onOpenChange={setOpenUpdateServer}
|
||||
open={openUpdateServer}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={!hasServerNewVersion || isUpdatingServer}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
||||
{hasServerNewVersion && serverVersionInfo
|
||||
? `${t("update", "Update")} ${serverVersionInfo.latest_version}`
|
||||
: t("update", "Update")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{serverVersionInfo && moduleConfig
|
||||
? t(
|
||||
"updateServerDescription",
|
||||
"Are you sure you want to update the server version from {{current}} to {{latest}}?",
|
||||
{
|
||||
current:
|
||||
moduleConfig.service_version ||
|
||||
serverVersionInfo.current_version,
|
||||
latest: serverVersionInfo.latest_version,
|
||||
}
|
||||
)
|
||||
: t(
|
||||
"updateDescription",
|
||||
"Are you sure you want to update?"
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel", "Cancel")}</AlertDialogCancel>
|
||||
<Button
|
||||
className="h-6 px-2 text-xs"
|
||||
disabled={isUpdatingServer}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={isUpdatingServer || !moduleConfig}
|
||||
onClick={() =>
|
||||
moduleConfig &&
|
||||
updateServerMutation.mutate(moduleConfig.service_name)
|
||||
}
|
||||
>
|
||||
<Icon className="mr-1 h-3 w-3" icon="mdi:download" />
|
||||
{t("update", "Update")} V{serverVersionInfo.latest_version}
|
||||
{isUpdatingServer && (
|
||||
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
|
||||
)}
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
"updateServerDescription",
|
||||
"Are you sure you want to update the server version from V{{current}} to V{{latest}}?",
|
||||
{
|
||||
current:
|
||||
moduleConfig.service_version ||
|
||||
serverVersionInfo.current_version,
|
||||
latest: serverVersionInfo.latest_version,
|
||||
}
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("cancel", "Cancel")}
|
||||
</AlertDialogCancel>
|
||||
<Button
|
||||
disabled={isUpdatingServer}
|
||||
onClick={() =>
|
||||
updateServerMutation.mutate(moduleConfig.service_name)
|
||||
}
|
||||
>
|
||||
{isUpdatingServer && (
|
||||
<Icon
|
||||
className="mr-2 animate-spin"
|
||||
icon="mdi:loading"
|
||||
/>
|
||||
)}
|
||||
{t("confirmUpdate", "Confirm Update")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -63,7 +63,7 @@ interface SubscribeFormProps<T> {
|
||||
}
|
||||
|
||||
const defaultValues = {
|
||||
inventory: 0,
|
||||
inventory: -1,
|
||||
speed_limit: 0,
|
||||
device_limit: 0,
|
||||
traffic: 0,
|
||||
@@ -278,7 +278,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
{trigger}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-[800px] max-w-full gap-0 md:max-w-screen-md">
|
||||
<SheetContent className="w-[800px] max-w-full gap-0 md:max-w-3xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</SheetHeader>
|
||||
@@ -446,11 +446,10 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
<FormLabel>{t("form.inventory")}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
min={0}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
placeholder={t("form.noLimit")}
|
||||
placeholder={t("form.unlimitedInventory")}
|
||||
step={1}
|
||||
type="number"
|
||||
value={field.value}
|
||||
@@ -901,6 +900,33 @@ export default function SubscribeForm<T extends Record<string, any>>({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="show_original_price"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
{t("form.showOriginalPrice")}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t("form.showOriginalPriceDescription")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={!!field.value}
|
||||
onCheckedChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -208,15 +208,14 @@ export default function SubscribeTable() {
|
||||
{
|
||||
accessorKey: "inventory",
|
||||
header: t("inventory"),
|
||||
cell: ({ row }) => (
|
||||
<Display
|
||||
type="number"
|
||||
unlimited
|
||||
value={
|
||||
row.getValue("inventory") === -1 ? 0 : row.getValue("inventory")
|
||||
}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const inventory = row.getValue("inventory") as number;
|
||||
return inventory === -1 ? (
|
||||
<Display type="number" unlimited value={0} />
|
||||
) : (
|
||||
<Display type="number" unlimited value={inventory} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "quota",
|
||||
|
||||
@@ -98,7 +98,7 @@ function DynamicField({
|
||||
field.generate ? (
|
||||
field.generate.functions &&
|
||||
field.generate.functions.length > 0 ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" type="button" variant="ghost">
|
||||
<Icon className="h-4 w-4" icon="mdi:key" />
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function User() {
|
||||
<Button variant="destructive">{t("delete", "Delete")}</Button>
|
||||
}
|
||||
/>,
|
||||
<DropdownMenu key="more">
|
||||
<DropdownMenu key="more" modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">{t("more", "More")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -176,6 +176,18 @@ export default function User() {
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
accessorKey: "deleted_at",
|
||||
header: t("isDeleted", "Deleted"),
|
||||
cell: ({ row }) => {
|
||||
const deletedAt = row.getValue("deleted_at") as number | undefined;
|
||||
return deletedAt ? (
|
||||
<Badge variant="destructive">{t("deleted", "Deleted")}</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">{t("normal", "Normal")}</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "auth_methods",
|
||||
header: t("userName", "Username"),
|
||||
@@ -371,16 +383,13 @@ function SubscriptionSheet({ userId }: { userId: number }) {
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="secondary">{t("subscription", "Subscription")}</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
className="w-[1000px] max-w-full md:max-w-screen-xl"
|
||||
side="right"
|
||||
>
|
||||
<SheetContent className="w-[1000px] max-w-full md:max-w-7xl" side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{t("subscriptionList", "Subscription List")} · ID: {userId}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 px-4">
|
||||
<UserSubscription userId={userId} />
|
||||
</div>
|
||||
</SheetContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
deleteUserSubscribe,
|
||||
getUserSubscribe,
|
||||
resetUserSubscribeToken,
|
||||
resetUserSubscribeTraffic,
|
||||
toggleUserSubscribeStatus,
|
||||
updateUserSubscribe,
|
||||
} from "@workspace/ui/services/admin/user";
|
||||
@@ -33,7 +33,6 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
||||
const { t } = useTranslation("user");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const ref = useRef<ProTableActions>(null);
|
||||
const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore();
|
||||
|
||||
return (
|
||||
<ProTable<API.UserSubscribe, Record<string, unknown>>
|
||||
@@ -59,100 +58,14 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
||||
title={t("editSubscription", "Edit Subscription")}
|
||||
trigger={t("edit", "Edit")}
|
||||
/>,
|
||||
<Button
|
||||
key="copy"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(
|
||||
getUserSubscribeUrls(row.token)[0] || ""
|
||||
);
|
||||
toast.success(t("copySuccess", "Copied successfully"));
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
{t("copySubscription", "Copy Subscription")}
|
||||
</Button>,
|
||||
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description={t(
|
||||
"resetSubscriptionTokenDescription",
|
||||
"This will reset the subscription token. Old links will become invalid."
|
||||
)}
|
||||
key="reset-token"
|
||||
onConfirm={async () => {
|
||||
await resetUserSubscribeToken({ user_subscribe_id: row.id });
|
||||
toast.success(t("resetSuccess", "Reset successfully"));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
title={t("resetSubscriptionToken", "Reset Token")}
|
||||
trigger={
|
||||
<Button variant="outline">
|
||||
{t("resetToken", "Reset Token")}
|
||||
</Button>
|
||||
}
|
||||
<RowMoreActions
|
||||
key="more"
|
||||
refresh={() => ref.current?.refresh()}
|
||||
row={row}
|
||||
token={row.token}
|
||||
userId={userId}
|
||||
/>
|
||||
/>,
|
||||
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description={t(
|
||||
"resetSubscriptionTrafficDescription",
|
||||
"This will reset the subscription traffic counters."
|
||||
)}
|
||||
key="reset-traffic"
|
||||
onConfirm={async () => {
|
||||
await resetUserSubscribeTraffic({ user_subscribe_id: row.id });
|
||||
toast.success(t("resetSuccess", "Reset successfully"));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
title={t("resetSubscriptionTraffic", "Reset Traffic")}
|
||||
trigger={
|
||||
<Button variant="outline">
|
||||
{t("resetTraffic", "Reset Traffic")}
|
||||
</Button>
|
||||
}
|
||||
/>,
|
||||
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description={t(
|
||||
"toggleSubscriptionStatusDescription",
|
||||
"This will toggle the subscription status."
|
||||
)}
|
||||
key="toggle-status"
|
||||
onConfirm={async () => {
|
||||
await toggleUserSubscribeStatus({ user_subscribe_id: row.id });
|
||||
toast.success(t("updateSuccess", "Updated successfully"));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
title={t("toggleSubscriptionStatus", "Toggle Status")}
|
||||
trigger={
|
||||
<Button variant="outline">
|
||||
{t("toggleStatus", "Toggle Status")}
|
||||
</Button>
|
||||
}
|
||||
/>,
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description={t(
|
||||
"deleteSubscriptionDescription",
|
||||
"This action cannot be undone."
|
||||
)}
|
||||
key="delete"
|
||||
onConfirm={async () => {
|
||||
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||
ref.current?.refresh();
|
||||
}}
|
||||
title={t("confirmDelete", "Confirm Delete")}
|
||||
trigger={
|
||||
<Button variant="destructive">{t("delete", "Delete")}</Button>
|
||||
}
|
||||
/>,
|
||||
<RowMoreActions key="more" subId={row.id} userId={userId} />,
|
||||
],
|
||||
}}
|
||||
columns={[
|
||||
@@ -165,6 +78,51 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
||||
header: t("subscriptionName", "Subscription Name"),
|
||||
cell: ({ row }) => row.original.subscribe.name,
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: t("status", "Status"),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue("status") as number;
|
||||
const expireTime = row.original.expire_time;
|
||||
|
||||
// 如果过期时间为0,说明是永久订阅,应该显示为激活状态
|
||||
const displayStatus = status === 3 && expireTime === 0 ? 1 : status;
|
||||
|
||||
const statusMap: Record<
|
||||
number,
|
||||
{
|
||||
label: string;
|
||||
variant: "default" | "secondary" | "destructive" | "outline";
|
||||
}
|
||||
> = {
|
||||
0: { label: t("statusPending", "Pending"), variant: "outline" },
|
||||
1: { label: t("statusActive", "Active"), variant: "default" },
|
||||
2: {
|
||||
label: t("statusFinished", "Finished"),
|
||||
variant: "secondary",
|
||||
},
|
||||
3: {
|
||||
label: t("statusExpired", "Expired"),
|
||||
variant: "destructive",
|
||||
},
|
||||
4: {
|
||||
label: t("statusDeducted", "Deducted"),
|
||||
variant: "secondary",
|
||||
},
|
||||
5: {
|
||||
label: t("statusStopped", "Stopped"),
|
||||
variant: "destructive",
|
||||
},
|
||||
};
|
||||
const statusInfo = statusMap[displayStatus] || {
|
||||
label: "Unknown",
|
||||
variant: "outline",
|
||||
};
|
||||
return (
|
||||
<Badge variant={statusInfo.variant}>{statusInfo.label}</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "upload",
|
||||
header: t("upload", "Upload"),
|
||||
@@ -216,10 +174,12 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
||||
{
|
||||
accessorKey: "expire_time",
|
||||
header: t("expireTime", "Expire Time"),
|
||||
cell: ({ row }) =>
|
||||
row.getValue("expire_time")
|
||||
? formatDate(row.getValue("expire_time"))
|
||||
: t("permanent", "Permanent"),
|
||||
cell: ({ row }) => {
|
||||
const expireTime = row.getValue("expire_time") as number;
|
||||
return expireTime && expireTime !== 0
|
||||
? formatDate(expireTime)
|
||||
: t("permanent", "Permanent");
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
@@ -263,19 +223,72 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
||||
function RowMoreActions({
|
||||
userId,
|
||||
row,
|
||||
token,
|
||||
refresh,
|
||||
}: {
|
||||
userId: number;
|
||||
row: API.UserSubscribe;
|
||||
token: string;
|
||||
refresh: () => void;
|
||||
}) {
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const resetTokenRef = useRef<HTMLButtonElement>(null);
|
||||
const toggleStatusRef = useRef<HTMLButtonElement>(null);
|
||||
const deleteRef = useRef<HTMLButtonElement>(null);
|
||||
const { t } = useTranslation("user");
|
||||
const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore();
|
||||
|
||||
return (
|
||||
<div className="inline-flex">
|
||||
<DropdownMenu>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">{t("more", "More")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onSelect={async (e) => {
|
||||
e.preventDefault();
|
||||
await navigator.clipboard.writeText(
|
||||
getUserSubscribeUrls(row.short, token)[0] || ""
|
||||
);
|
||||
toast.success(t("copySuccess", "Copied successfully"));
|
||||
}}
|
||||
>
|
||||
{t("copySubscription", "Copy Subscription")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
resetTokenRef.current?.click();
|
||||
}}
|
||||
>
|
||||
{t("resetToken", "Reset Subscription Address")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
toggleStatusRef.current?.click();
|
||||
}}
|
||||
>
|
||||
{row.status === 5
|
||||
? t("resumeSubscribe", "Resume Subscription")
|
||||
: t("stopSubscribe", "Stop Subscription")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
deleteRef.current?.click();
|
||||
}}
|
||||
>
|
||||
{t("delete", "Delete")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
||||
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||
to="/dashboard/log/subscribe"
|
||||
>
|
||||
{t("subscriptionLogs", "Subscription Logs")}
|
||||
@@ -283,7 +296,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
||||
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||
to="/dashboard/log/reset-subscribe"
|
||||
>
|
||||
{t("resetLogs", "Reset Logs")}
|
||||
@@ -291,7 +304,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
search={{ user_id: userId, user_subscribe_id: subId }}
|
||||
search={{ user_id: userId, user_subscribe_id: row.id }}
|
||||
to="/dashboard/log/subscribe-traffic"
|
||||
>
|
||||
{t("trafficStats", "Traffic Stats")}
|
||||
@@ -299,7 +312,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
search={{ user_id: userId, subscribe_id: subId }}
|
||||
search={{ user_id: userId, subscribe_id: row.id }}
|
||||
to="/dashboard/log/traffic-details"
|
||||
>
|
||||
{t("trafficDetails", "Traffic Details")}
|
||||
@@ -316,8 +329,78 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Hidden triggers for confirm dialogs */}
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description={t(
|
||||
"resetTokenDescription",
|
||||
"This will reset the subscription address and regenerate a new token."
|
||||
)}
|
||||
onConfirm={async () => {
|
||||
await resetUserSubscribeToken({
|
||||
user_subscribe_id: row.id,
|
||||
});
|
||||
toast.success(
|
||||
t("resetTokenSuccess", "Subscription address reset successfully")
|
||||
);
|
||||
refresh();
|
||||
}}
|
||||
title={t("confirmResetToken", "Confirm Reset Subscription Address")}
|
||||
trigger={<Button className="hidden" ref={resetTokenRef} />}
|
||||
/>
|
||||
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description={
|
||||
row.status === 5
|
||||
? t(
|
||||
"resumeSubscribeDescription",
|
||||
"This will resume the subscription and allow the user to use it."
|
||||
)
|
||||
: t(
|
||||
"stopSubscribeDescription",
|
||||
"This will stop the subscription temporarily. User will not be able to use it."
|
||||
)
|
||||
}
|
||||
onConfirm={async () => {
|
||||
await toggleUserSubscribeStatus({
|
||||
user_subscribe_id: row.id,
|
||||
});
|
||||
toast.success(
|
||||
row.status === 5
|
||||
? t("resumeSubscribeSuccess", "Subscription resumed successfully")
|
||||
: t("stopSubscribeSuccess", "Subscription stopped successfully")
|
||||
);
|
||||
refresh();
|
||||
}}
|
||||
title={
|
||||
row.status === 5
|
||||
? t("confirmResumeSubscribe", "Confirm Resume Subscription")
|
||||
: t("confirmStopSubscribe", "Confirm Stop Subscription")
|
||||
}
|
||||
trigger={<Button className="hidden" ref={toggleStatusRef} />}
|
||||
/>
|
||||
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
description={t(
|
||||
"deleteSubscriptionDescription",
|
||||
"This action cannot be undone."
|
||||
)}
|
||||
onConfirm={async () => {
|
||||
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||
refresh();
|
||||
}}
|
||||
title={t("confirmDelete", "Confirm Delete")}
|
||||
trigger={<Button className="hidden" ref={deleteRef} />}
|
||||
/>
|
||||
|
||||
<SubscriptionDetail
|
||||
subscriptionId={subId}
|
||||
subscriptionId={row.id}
|
||||
trigger={<Button className="hidden" ref={triggerRef} />}
|
||||
userId={userId}
|
||||
/>
|
||||
|
||||
@@ -95,145 +95,149 @@ export function SubscriptionForm({
|
||||
<SheetHeader>
|
||||
<SheetTitle>{title}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="mt-4 space-y-4"
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subscribe_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("subscription", "Subscription")}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox<number, false>
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
options={subscribes?.map((item) => ({
|
||||
value: item.id!,
|
||||
label: item.name!,
|
||||
}))}
|
||||
placeholder="Select Subscription"
|
||||
value={field.value}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traffic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("trafficLimit", "Traffic Limit")}</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t("unlimited", "Unlimited")}
|
||||
type="number"
|
||||
{...field}
|
||||
formatInput={(value) =>
|
||||
unitConversion("bytesToGb", value)
|
||||
}
|
||||
formatOutput={(value) =>
|
||||
unitConversion("gbToBytes", value)
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
suffix="GB"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="upload"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("uploadTraffic", "Upload Traffic")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder="0"
|
||||
type="number"
|
||||
{...field}
|
||||
formatInput={(value) =>
|
||||
unitConversion("bytesToGb", value)
|
||||
}
|
||||
formatOutput={(value) =>
|
||||
unitConversion("gbToBytes", value)
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
suffix="GB"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="download"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("downloadTraffic", "Download Traffic")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder="0"
|
||||
type="number"
|
||||
{...field}
|
||||
formatInput={(value) =>
|
||||
unitConversion("bytesToGb", value)
|
||||
}
|
||||
formatOutput={(value) =>
|
||||
unitConversion("gbToBytes", value)
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
suffix="GB"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expired_at"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("expiredAt", "Expired At")}</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
onChange={(value: number | null | undefined) => {
|
||||
if (value === field.value) {
|
||||
form.setValue(field.name, 0);
|
||||
} else {
|
||||
form.setValue(field.name, value!);
|
||||
<ScrollArea className="h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))] px-4">
|
||||
<div className="pr-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="mt-4 space-y-4"
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subscribe_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("subscription", "Subscription")}</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox<number, false>
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
}}
|
||||
options={subscribes?.map((item) => ({
|
||||
value: item.id!,
|
||||
label: item.name!,
|
||||
}))}
|
||||
placeholder="Select Subscription"
|
||||
value={field.value}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traffic"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("trafficLimit", "Traffic Limit")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder={t("unlimited", "Unlimited")}
|
||||
type="number"
|
||||
{...field}
|
||||
formatInput={(value) =>
|
||||
unitConversion("bytesToGb", value)
|
||||
}
|
||||
}}
|
||||
placeholder={t("permanent", "Permanent")}
|
||||
value={field.value ?? undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
formatOutput={(value) =>
|
||||
unitConversion("gbToBytes", value)
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
suffix="GB"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="upload"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("uploadTraffic", "Upload Traffic")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder="0"
|
||||
type="number"
|
||||
{...field}
|
||||
formatInput={(value) =>
|
||||
unitConversion("bytesToGb", value)
|
||||
}
|
||||
formatOutput={(value) =>
|
||||
unitConversion("gbToBytes", value)
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
suffix="GB"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="download"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("downloadTraffic", "Download Traffic")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<EnhancedInput
|
||||
placeholder="0"
|
||||
type="number"
|
||||
{...field}
|
||||
formatInput={(value) =>
|
||||
unitConversion("bytesToGb", value)
|
||||
}
|
||||
formatOutput={(value) =>
|
||||
unitConversion("gbToBytes", value)
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
form.setValue(field.name, value as number);
|
||||
}}
|
||||
suffix="GB"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="expired_at"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("expiredAt", "Expired At")}</FormLabel>
|
||||
<FormControl>
|
||||
<DatePicker
|
||||
onChange={(value: number | null | undefined) => {
|
||||
form.setValue(field.name, value || 0);
|
||||
}}
|
||||
placeholder={t("permanent", "Permanent")}
|
||||
value={
|
||||
field.value && field.value > 0
|
||||
? field.value
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<SheetFooter className="flex-row justify-end gap-2 pt-3">
|
||||
<Button
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface GlobalStore {
|
||||
setCommon: (common: Partial<API.GetGlobalConfigResponse>) => void;
|
||||
setUser: (user?: API.User) => void;
|
||||
getUserInfo: () => Promise<void>;
|
||||
getUserSubscribe: (uuid: string, type?: string) => string[];
|
||||
getUserSubscribe: (short: string, token: string, type?: string) => string[];
|
||||
getAppSubLink: (url: string, schema?: string) => string;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
console.error("Failed to refresh user:", error);
|
||||
}
|
||||
},
|
||||
getUserSubscribe: (uuid: string, type?: string) => {
|
||||
getUserSubscribe: (short: string, token: string, type?: string) => {
|
||||
const { pan_domain, subscribe_domain, subscribe_path } =
|
||||
get().common.subscribe || {};
|
||||
const domains = subscribe_domain
|
||||
@@ -129,12 +129,13 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
|
||||
return domains.map((domain) => {
|
||||
if (pan_domain) {
|
||||
if (type) return `https://${uuid}.${type}.${domain}`;
|
||||
return `https://${uuid}.${domain}`;
|
||||
if (type)
|
||||
return `https://${short}.${type}.${domain}${subscribe_path}?token=${token}&type=${type}`;
|
||||
return `https://${short}.${domain}${subscribe_path}?token=${token}`;
|
||||
}
|
||||
if (type)
|
||||
return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`;
|
||||
return `https://${domain}${subscribe_path}?token=${uuid}`;
|
||||
return `https://${domain}${subscribe_path}?token=${token}&type=${type}`;
|
||||
return `https://${domain}${subscribe_path}?token=${token}`;
|
||||
});
|
||||
},
|
||||
getAppSubLink: (url: string, schema?: string) => {
|
||||
|
||||
+32
-29
@@ -4,7 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
||||
import { devtools } from "@tanstack/devtools-vite";
|
||||
import { tanstackRouter } from "@tanstack/router-plugin/vite";
|
||||
import viteReact from "@vitejs/plugin-react";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import { defineConfig, loadEnv, type Plugin } from "vite";
|
||||
|
||||
// Plugin to generate version.lock file after build
|
||||
function versionLockPlugin(): Plugin {
|
||||
@@ -24,34 +24,37 @@ function versionLockPlugin(): Plugin {
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
plugins: [
|
||||
devtools({ eventBusConfig: { port: 42_070 } }),
|
||||
tanstackRouter({
|
||||
target: "react",
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
viteReact(),
|
||||
tailwindcss(),
|
||||
versionLockPlugin(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "https://api.ppanel.dev",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
|
||||
return {
|
||||
base: "./",
|
||||
plugins: [
|
||||
devtools({ eventBusConfig: { port: 42_070 } }),
|
||||
tanstackRouter({
|
||||
target: "react",
|
||||
autoCodeSplitting: true,
|
||||
}),
|
||||
viteReact(),
|
||||
tailwindcss(),
|
||||
versionLockPlugin(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
assetsDir: "static",
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: env.VITE_API_BASE_URL || "https://api.ppanel.dev",
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
assetsDir: "static",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user