mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-05-06 05:38:51 +00:00
Integrate interactive visual configuration generator (nzcfg) into Server public note form
This commit is contained in:
+1209
File diff suppressed because it is too large
Load Diff
+46
-734
@@ -1,6 +1,5 @@
|
|||||||
import { updateServer } from "@/api/server"
|
import { updateServer } from "@/api/server"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Calendar } from "@/components/ui/calendar"
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -22,35 +21,14 @@ import {
|
|||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select"
|
|
||||||
import { Switch } from "@/components/ui/switch"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { IconButton } from "@/components/xui/icon-button"
|
import { IconButton } from "@/components/xui/icon-button"
|
||||||
import {
|
|
||||||
type PublicNote,
|
|
||||||
PublicNoteSchema,
|
|
||||||
applyPublicNoteDate,
|
|
||||||
applyPublicNotePatch,
|
|
||||||
detectPublicNoteMode,
|
|
||||||
normalizeISO,
|
|
||||||
parsePublicNote,
|
|
||||||
toggleEndNoExpiry,
|
|
||||||
validatePublicNote,
|
|
||||||
} from "@/lib/public-note"
|
|
||||||
import { conv } from "@/lib/utils"
|
import { conv } from "@/lib/utils"
|
||||||
import { asOptionalField } from "@/lib/utils"
|
import { asOptionalField } from "@/lib/utils"
|
||||||
import { ModelServer } from "@/types"
|
import { ModelServer } from "@/types"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { HelpCircle } from "lucide-react"
|
import { useState, useEffect } from "react"
|
||||||
import { useState } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
@@ -65,29 +43,13 @@ interface ServerCardProps {
|
|||||||
const serverFormSchema = z.object({
|
const serverFormSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
note: asOptionalField(z.string()),
|
note: asOptionalField(z.string()),
|
||||||
public_note: asOptionalField(
|
public_note: asOptionalField(z.string()),
|
||||||
z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
const s = (val ?? "").trim()
|
|
||||||
if (s.length === 0) return true
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(s)
|
|
||||||
if (typeof obj !== "object" || obj === null) return true
|
|
||||||
return PublicNoteSchema.safeParse(obj).success
|
|
||||||
} catch {
|
|
||||||
// skip check if not JSON
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ message: "Invalid Public Note JSON" },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
display_index: z.coerce.number().int(),
|
display_index: z.coerce.number().int(),
|
||||||
hide_for_guest: asOptionalField(z.boolean()),
|
hide_for_guest: asOptionalField(z.boolean()),
|
||||||
enable_ddns: asOptionalField(z.boolean()),
|
enable_ddns: asOptionalField(z.boolean()),
|
||||||
ddns_profiles: asOptionalField(z.array(z.number())),
|
ddns_profiles: asOptionalField(z.array(z.number())),
|
||||||
ddns_profiles_raw: asOptionalField(z.string()),
|
ddns_profiles_raw: asOptionalField(z.string()),
|
||||||
override_ddns_domains: asOptionalField(z.record(z.string(), z.array(z.string()))),
|
override_ddns_domains: asOptionalField(z.record(z.coerce.number().int(), z.array(z.string()))),
|
||||||
override_ddns_domains_raw: asOptionalField(
|
override_ddns_domains_raw: asOptionalField(
|
||||||
z.string().refine(
|
z.string().refine(
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -107,8 +69,8 @@ const serverFormSchema = z.object({
|
|||||||
|
|
||||||
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const form = useForm({
|
const form = useForm<z.infer<typeof serverFormSchema>>({
|
||||||
resolver: zodResolver(serverFormSchema) as any,
|
resolver: zodResolver(serverFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...data,
|
...data,
|
||||||
ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined,
|
ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined,
|
||||||
@@ -123,47 +85,20 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const [publicNoteObj, setPublicNoteObj] = useState<PublicNote>(
|
useEffect(() => {
|
||||||
parsePublicNote(data?.public_note),
|
const handleMessage = (e: MessageEvent) => {
|
||||||
)
|
if (e.data?.type === 'NZCFG_JSON') {
|
||||||
const [publicNoteErrors, setPublicNoteErrors] = useState<
|
if (e.data.target === 'public_note') {
|
||||||
Partial<
|
form.setValue('public_note', e.data.payload);
|
||||||
Record<
|
toast(t("Success"), { description: "配置已通过可视化构建器自动填入" });
|
||||||
| "billing.startDate"
|
}
|
||||||
| "billing.endDate"
|
}
|
||||||
| "billing.autoRenewal"
|
};
|
||||||
| "billing.cycle"
|
window.addEventListener('message', handleMessage);
|
||||||
| "billing.amount"
|
return () => window.removeEventListener('message', handleMessage);
|
||||||
| "plan.bandwidth"
|
}, [form, t]);
|
||||||
| "plan.trafficVol"
|
|
||||||
| "plan.trafficType"
|
|
||||||
| "plan.IPv4"
|
|
||||||
| "plan.IPv6"
|
|
||||||
| "plan.extra",
|
|
||||||
string
|
|
||||||
>
|
|
||||||
>
|
|
||||||
>({})
|
|
||||||
|
|
||||||
const [publicNoteMode, setPublicNoteMode] = useState<"structured" | "raw">(
|
const onSubmit = async (values: z.infer<typeof serverFormSchema>) => {
|
||||||
detectPublicNoteMode(data?.public_note),
|
|
||||||
)
|
|
||||||
const [publicNoteRaw, setPublicNoteRaw] = useState<string>(data?.public_note ?? "")
|
|
||||||
|
|
||||||
const patchPublicNote = (path: string, value: string | undefined) => {
|
|
||||||
setPublicNoteObj((prev) => applyPublicNotePatch(prev, path, value))
|
|
||||||
}
|
|
||||||
const patchPublicNoteDate = (
|
|
||||||
path: "billingDataMod.startDate" | "billingDataMod.endDate",
|
|
||||||
d: Date,
|
|
||||||
) => {
|
|
||||||
setPublicNoteObj((prev) => applyPublicNoteDate(prev, path, d))
|
|
||||||
}
|
|
||||||
const toggleEndNoExpiryLocal = () => {
|
|
||||||
setPublicNoteObj((prev) => toggleEndNoExpiry(prev))
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async (values: any) => {
|
|
||||||
try {
|
try {
|
||||||
values.ddns_profiles = values.ddns_profiles_raw
|
values.ddns_profiles = values.ddns_profiles_raw
|
||||||
? conv.strToArr(values.ddns_profiles_raw).map(Number)
|
? conv.strToArr(values.ddns_profiles_raw).map(Number)
|
||||||
@@ -171,37 +106,6 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
values.override_ddns_domains = values.override_ddns_domains_raw
|
values.override_ddns_domains = values.override_ddns_domains_raw
|
||||||
? JSON.parse(values.override_ddns_domains_raw)
|
? JSON.parse(values.override_ddns_domains_raw)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
if (publicNoteMode === "raw") {
|
|
||||||
const raw = (publicNoteRaw ?? "").trim()
|
|
||||||
if (raw.length === 0) {
|
|
||||||
values.public_note = undefined
|
|
||||||
} else {
|
|
||||||
values.public_note = raw
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { errors, valid } = validatePublicNote(publicNoteObj)
|
|
||||||
if (!valid) {
|
|
||||||
setPublicNoteErrors(errors)
|
|
||||||
toast(t("Error"), { description: t("Validation.InvalidForm") })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setPublicNoteErrors({})
|
|
||||||
|
|
||||||
const bd = publicNoteObj.billingDataMod
|
|
||||||
const pd = publicNoteObj.planDataMod
|
|
||||||
const pnNormalized: PublicNote = {
|
|
||||||
billingDataMod: bd && {
|
|
||||||
...bd,
|
|
||||||
startDate: normalizeISO(bd.startDate),
|
|
||||||
endDate: normalizeISO(bd.endDate),
|
|
||||||
},
|
|
||||||
planDataMod: pd,
|
|
||||||
}
|
|
||||||
const jsonStr = JSON.stringify(pnNormalized)
|
|
||||||
values.public_note = jsonStr.length > 2 ? jsonStr : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateServer(data!.id!, values)
|
await updateServer(data!.id!, values)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -215,35 +119,12 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
form.reset()
|
form.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenChange = (v: boolean) => {
|
|
||||||
if (v) {
|
|
||||||
form.reset({
|
|
||||||
...data,
|
|
||||||
ddns_profiles_raw: data.ddns_profiles
|
|
||||||
? conv.arrToStr(data.ddns_profiles)
|
|
||||||
: undefined,
|
|
||||||
override_ddns_domains_raw: data.override_ddns_domains
|
|
||||||
? JSON.stringify(data.override_ddns_domains)
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
setPublicNoteObj(parsePublicNote(data?.public_note))
|
|
||||||
setPublicNoteRaw(data?.public_note ?? "")
|
|
||||||
setPublicNoteMode(detectPublicNoteMode(data?.public_note))
|
|
||||||
setPublicNoteErrors({})
|
|
||||||
}
|
|
||||||
setOpen(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<IconButton variant="outline" icon="edit" />
|
<IconButton variant="outline" icon="edit" />
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent className="sm:max-w-xl">
|
||||||
className="sm:max-w-xl"
|
|
||||||
onInteractOutside={(e) => e.preventDefault()}
|
|
||||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
|
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
|
||||||
<div className="items-center mx-1">
|
<div className="items-center mx-1">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -368,603 +249,34 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/* Public Note controls (optional + dual mode) */}
|
<FormField
|
||||||
<div className="space-y-3">
|
control={form.control}
|
||||||
<div className="space-y-1">
|
name="public_note"
|
||||||
<div className="flex items-center justify-between">
|
render={({ field }) => (
|
||||||
<div className="flex items-center gap-2">
|
<FormItem>
|
||||||
<FormLabel>{t("PublicNote.Label")}</FormLabel>
|
<FormLabel className="flex justify-between items-center w-full">
|
||||||
<a
|
<span>{t("Public") + t("Note")}</span>
|
||||||
href="https://nezha.wiki/guide/servers.html#%E5%85%AC%E5%BC%80%E5%A4%87%E6%B3%A8%E8%AE%BE%E7%BD%AE"
|
<a href="/nzcfg.html" target="_blank" className="text-blue-500 hover:text-blue-700 text-xs flex items-center gap-1" onClick={(e) => {
|
||||||
target="_blank"
|
e.preventDefault();
|
||||||
rel="noopener noreferrer"
|
const popup = window.open('/nzcfg.html', 'nzcfg', 'width=1000,height=800');
|
||||||
className="inline-flex items-center text-muted-foreground hover:text-foreground"
|
if(popup) {
|
||||||
>
|
const timer = setInterval(() => {
|
||||||
<HelpCircle className="h-4 w-4" />
|
if(popup.closed) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
可视化管理配置 <i className="fa-solid fa-up-right-from-square"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</FormLabel>
|
||||||
</div>
|
<FormControl>
|
||||||
</div>
|
<Textarea className="resize-y" {...field} />
|
||||||
|
</FormControl>
|
||||||
{/* Toggle: when disabled, hide edit controls and submit an empty value */}
|
<FormMessage />
|
||||||
<div className="flex items-center gap-4">
|
</FormItem>
|
||||||
{/* Mode switch: Raw text / Custom fields */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* Show 'structured' first, then 'raw' */}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={
|
|
||||||
publicNoteMode === "structured"
|
|
||||||
? "default"
|
|
||||||
: "outline"
|
|
||||||
}
|
|
||||||
className="text-xs h-7"
|
|
||||||
onClick={() => {
|
|
||||||
setPublicNoteMode("structured")
|
|
||||||
setPublicNoteObj(parsePublicNote(publicNoteRaw))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("PublicNote.CustomFields")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={
|
|
||||||
publicNoteMode === "raw" ? "default" : "outline"
|
|
||||||
}
|
|
||||||
className="text-xs h-7"
|
|
||||||
onClick={() => setPublicNoteMode("raw")}
|
|
||||||
>
|
|
||||||
{t("PublicNote.RawText")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Raw text mode: shown by default; submission uses this string */}
|
|
||||||
{publicNoteMode === "raw" && (
|
|
||||||
<div>
|
|
||||||
<Textarea
|
|
||||||
className="resize-y"
|
|
||||||
value={publicNoteRaw}
|
|
||||||
onChange={(e) => setPublicNoteRaw(e.target.value)}
|
|
||||||
rows={10}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
{/* Custom fields mode: keep structured editing; serialize to string on submit */}
|
|
||||||
{publicNoteMode === "structured" && (
|
|
||||||
<>
|
|
||||||
<div className="rounded-md border p-3 space-y-3">
|
|
||||||
<div className="text-sm font-medium opacity-80">
|
|
||||||
{t("PublicNote.Billing")}
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.StartDate")}
|
|
||||||
</Label>
|
|
||||||
{/* Add 'Clear' button to allow removing the date */}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700 ml-2"
|
|
||||||
onClick={() =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.startDate",
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("PublicNote.ClearDate") ?? "Clear"}
|
|
||||||
</Button>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-left font-normal"
|
|
||||||
>
|
|
||||||
{publicNoteObj.billingDataMod
|
|
||||||
?.startDate
|
|
||||||
? new Date(
|
|
||||||
publicNoteObj.billingDataMod!.startDate!,
|
|
||||||
).toLocaleDateString()
|
|
||||||
: "YYYY-MM-DD"}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="p-0 w-[300px] max-h-[60dvh] overflow-hidden"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<div className="max-h-[500px] overflow-y-auto">
|
|
||||||
<Calendar
|
|
||||||
className="w-full min-h-[320px]"
|
|
||||||
mode="single"
|
|
||||||
captionLayout="dropdown"
|
|
||||||
startMonth={
|
|
||||||
new Date(2000, 0)
|
|
||||||
}
|
|
||||||
endMonth={
|
|
||||||
new Date(2050, 11)
|
|
||||||
}
|
|
||||||
selected={
|
|
||||||
publicNoteObj
|
|
||||||
.billingDataMod
|
|
||||||
?.startDate
|
|
||||||
? new Date(
|
|
||||||
publicNoteObj.billingDataMod!.startDate!,
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onSelect={(d) => {
|
|
||||||
if (!d) return
|
|
||||||
patchPublicNoteDate(
|
|
||||||
"billingDataMod.startDate",
|
|
||||||
d,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
{publicNoteErrors["billing.startDate"] && (
|
|
||||||
<p className="text-xs text-destructive mt-1">
|
|
||||||
{
|
|
||||||
publicNoteErrors[
|
|
||||||
"billing.startDate"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.EndDate")}
|
|
||||||
</Label>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
|
|
||||||
onClick={toggleEndNoExpiryLocal}
|
|
||||||
>
|
|
||||||
{publicNoteObj.billingDataMod
|
|
||||||
?.endDate ===
|
|
||||||
"0000-00-00T23:59:59+08:00"
|
|
||||||
? t("PublicNote.CancelNoExpiry")
|
|
||||||
: t("PublicNote.SetNoExpiry")}
|
|
||||||
</Button>
|
|
||||||
{/* Add 'Clear' button to allow removing the date */}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
|
|
||||||
onClick={() =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.endDate",
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("PublicNote.ClearDate") ??
|
|
||||||
"Clear"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="w-full justify-start text-left font-normal"
|
|
||||||
>
|
|
||||||
{publicNoteObj.billingDataMod
|
|
||||||
?.endDate
|
|
||||||
? publicNoteObj
|
|
||||||
.billingDataMod
|
|
||||||
?.endDate ===
|
|
||||||
"0000-00-00T23:59:59+08:00"
|
|
||||||
? t(
|
|
||||||
"PublicNote.NoExpiry",
|
|
||||||
)
|
|
||||||
: new Date(
|
|
||||||
publicNoteObj
|
|
||||||
.billingDataMod
|
|
||||||
?.endDate as string,
|
|
||||||
).toLocaleDateString()
|
|
||||||
: "YYYY-MM-DD"}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="p-0 w-[300px] max-h-[60dvh] overflow-hidden"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<div className="max-h-[500px] overflow-y-auto">
|
|
||||||
<Calendar
|
|
||||||
className="w-full min-h-[320px]"
|
|
||||||
mode="single"
|
|
||||||
captionLayout="dropdown"
|
|
||||||
startMonth={
|
|
||||||
new Date(2000, 0)
|
|
||||||
}
|
|
||||||
endMonth={
|
|
||||||
new Date(2050, 11)
|
|
||||||
}
|
|
||||||
selected={
|
|
||||||
publicNoteObj
|
|
||||||
.billingDataMod
|
|
||||||
?.endDate &&
|
|
||||||
publicNoteObj
|
|
||||||
.billingDataMod
|
|
||||||
?.endDate !==
|
|
||||||
"0000-00-00T23:59:59+08:00"
|
|
||||||
? new Date(
|
|
||||||
publicNoteObj
|
|
||||||
.billingDataMod
|
|
||||||
?.endDate as string,
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onSelect={(d) => {
|
|
||||||
if (!d) return
|
|
||||||
patchPublicNoteDate(
|
|
||||||
"billingDataMod.endDate",
|
|
||||||
d,
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
{publicNoteErrors["billing.endDate"] && (
|
|
||||||
<p className="text-xs text-destructive mt-1">
|
|
||||||
{
|
|
||||||
publicNoteErrors[
|
|
||||||
"billing.endDate"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.AutoRenewal")}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 mt-3">
|
|
||||||
<span className="text-xs">
|
|
||||||
{t("PublicNote.Disabled")}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
checked={
|
|
||||||
publicNoteObj.billingDataMod
|
|
||||||
?.autoRenewal === "1"
|
|
||||||
}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.autoRenewal",
|
|
||||||
checked ? "1" : undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="text-xs">
|
|
||||||
{t("PublicNote.Enabled")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{publicNoteErrors[
|
|
||||||
"billing.autoRenewal"
|
|
||||||
] && (
|
|
||||||
<p className="text-xs text-destructive mt-1">
|
|
||||||
{
|
|
||||||
publicNoteErrors[
|
|
||||||
"billing.autoRenewal"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.Cycle")}
|
|
||||||
</Label>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
|
|
||||||
onClick={() =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.cycle",
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("PublicNote.Clear") ?? "Clear"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
onValueChange={(val) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.cycle",
|
|
||||||
val,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
publicNoteObj.billingDataMod?.cycle
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select cycle" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="Day">
|
|
||||||
{t("PublicNote.Day")}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="Week">
|
|
||||||
{t("PublicNote.Week")}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="Month">
|
|
||||||
{t("PublicNote.Month")}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="Year">
|
|
||||||
{t("PublicNote.Year")}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{publicNoteErrors["billing.cycle"] && (
|
|
||||||
<p className="text-xs text-destructive mt-1">
|
|
||||||
{publicNoteErrors["billing.cycle"]}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 sm:col-span-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.Amount")}
|
|
||||||
</Label>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
|
|
||||||
onClick={() =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.amount",
|
|
||||||
"0",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("PublicNote.Free")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
|
|
||||||
onClick={() =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.amount",
|
|
||||||
"-1",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("PublicNote.PayAsYouGo")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder="200EUR"
|
|
||||||
value={
|
|
||||||
publicNoteObj.billingDataMod?.amount
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"billingDataMod.amount",
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-md border p-3 space-y-3">
|
|
||||||
<div className="text-sm font-medium opacity-80">
|
|
||||||
{t("PublicNote.Plan")}
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.Bandwidth")}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="30Mbps"
|
|
||||||
value={
|
|
||||||
publicNoteObj.planDataMod?.bandwidth
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.bandwidth",
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.TrafficVolume")}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="1TB/Month"
|
|
||||||
value={
|
|
||||||
publicNoteObj.planDataMod
|
|
||||||
?.trafficVol
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.trafficVol",
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.TrafficType")}
|
|
||||||
</Label>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="text-xs px-2 py-0 h-auto bg-gray-200 dark:bg-gray-700"
|
|
||||||
onClick={() =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.trafficType",
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("PublicNote.Clear") ?? "Clear"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Select
|
|
||||||
onValueChange={(val) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.trafficType",
|
|
||||||
val,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
publicNoteObj.planDataMod
|
|
||||||
?.trafficType ?? ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="1">
|
|
||||||
{t("PublicNote.Inbound")}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="2">
|
|
||||||
{t("PublicNote.Both")}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{publicNoteErrors["plan.trafficType"] && (
|
|
||||||
<p className="text-xs text-destructive mt-1">
|
|
||||||
{
|
|
||||||
publicNoteErrors[
|
|
||||||
"plan.trafficType"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.IPv4")}
|
|
||||||
</Label>
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
|
||||||
<span className="text-xs">
|
|
||||||
{t("PublicNote.None")}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
checked={
|
|
||||||
publicNoteObj.planDataMod
|
|
||||||
?.IPv4 === "1"
|
|
||||||
}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.IPv4",
|
|
||||||
checked ? "1" : "0",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="text-xs">
|
|
||||||
{t("PublicNote.Has")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{publicNoteErrors["plan.IPv4"] && (
|
|
||||||
<p className="text-xs text-destructive mt-1">
|
|
||||||
{publicNoteErrors["plan.IPv4"]}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.IPv6")}
|
|
||||||
</Label>
|
|
||||||
<div className="flex items-center gap-2 mt-2">
|
|
||||||
<span className="text-xs">
|
|
||||||
{t("PublicNote.None")}
|
|
||||||
</span>
|
|
||||||
<Switch
|
|
||||||
checked={
|
|
||||||
publicNoteObj.planDataMod
|
|
||||||
?.IPv6 === "1"
|
|
||||||
}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.IPv6",
|
|
||||||
checked ? "1" : "0",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="text-xs">
|
|
||||||
{t("PublicNote.Has")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{publicNoteErrors["plan.IPv6"] && (
|
|
||||||
<p className="text-xs text-destructive mt-1">
|
|
||||||
{publicNoteErrors["plan.IPv6"]}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.NetworkRoute")}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder={t(
|
|
||||||
"PublicNote.CommaSeparated",
|
|
||||||
)}
|
|
||||||
value={
|
|
||||||
publicNoteObj.planDataMod
|
|
||||||
?.networkRoute ?? ""
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.networkRoute",
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 sm:col-span-2">
|
|
||||||
<Label className="text-xs">
|
|
||||||
{t("PublicNote.Extra")}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder={t(
|
|
||||||
"PublicNote.CommaSeparated",
|
|
||||||
)}
|
|
||||||
value={
|
|
||||||
publicNoteObj.planDataMod?.extra ??
|
|
||||||
""
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
patchPublicNote(
|
|
||||||
"planDataMod.extra",
|
|
||||||
e.target.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter className="justify-end">
|
<DialogFooter className="justify-end">
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" className="my-2" variant="secondary">
|
<Button type="button" className="my-2" variant="secondary">
|
||||||
|
|||||||
Reference in New Issue
Block a user