mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-05-06 05:38:51 +00:00
Fix and update (#139)
* refactor(ui): 统一组件引用类型为ComponentRef 更新所有UI组件中的forwardRef类型,从ElementRef改为ComponentRef以保持一致性 迁移postcss配置至mjs格式并更新依赖版本 * refactor: 优化表单类型定义和验证逻辑 移除自定义的 asOptionalField 工具函数,直接使用 Zod 的 optional() 方法,并明确定义表单数据类型。 * style: 更新UI主题配置和样式变量 将主题风格从default切换为new-york,并重构CSS变量使用OKLCH色彩空间。同时添加tailwindcss-animate插件支持。 * style: 统一页面头部按钮组样式 优化多个页面头部按钮组的布局样式,增加响应式设计和flex-wrap支持 * fix(server): 修复对话框交互问题并优化SWR配置 修复对话框关闭逻辑并阻止外部交互,同时禁用SWR的自动重新验证功能以提升性能。 * feat: 添加日历组件及账单相关国际化 实现基于 react-day-picker 的日历组件,并添加账单管理相关的多语言支持 * style(components): 统一按钮样式并格式化代码 为删除和禁用按钮添加text-white类名,同时调整ServerCard组件中的代码缩进格式。 * perf(build): 优化Vite打包配置与代码分割策略 调整Vite构建配置,改进第三方依赖的分组逻辑并添加UUID支持到安装命令组件 * fix: 修正页面标题翻译不一致问题 将CronPage和ServicePage的标题从"Server"分别改为"Task"和"Service",并优化NotificationGroupPage的按钮组布局。 * fix(auth): 改进登录错误处理和国际化支持 优化登录错误提示,添加多语言支持并移除控制台错误日志。同时修复头部组件透明度样式问题。 * feat: 添加服务器操作下拉菜单 为服务器卡片添加统一的下拉菜单操作入口,整合终端、配置和安装命令功能。 * feat[alert-rule]: 优化告警规则组件性能 重构告警规则组件代码结构,提升渲染效率并减少内存占用。 * docs(i18n): 新增翻译字段 为界面添加"Add"、"Delete"、"AdvancedJSON"和"Save"等关键操作的翻译字段,支持中英文双语显示。 * perf(vite): 优化分包策略以提升构建性能 重构 manualChunks 逻辑,按功能类别分组依赖项,并增加大型库的独立分包规则。 * style: 统一危险操作按钮的文字颜色 在所有确认操作的弹窗按钮中添加白色文字样式,保持视觉一致性。 * fix(components): 调整下拉菜单对齐方式 根据菜单项状态动态设置下拉菜单的对齐方向和起始位置。 * fix(types): 修复在线用户API分页类型 添加ModelOnlineUserApi接口类型,包含分页信息,并移除index.ts中重复的类型定义。 * chore: auto-fix linting and formatting issues * feat(locales): 添加无过期相关翻译项 为英文和中文翻译文件添加"NoExpiry"、"SetNoExpiry"等无过期相关字段的翻译。 fix(components): 移除重复的图标按钮选项 从IconButton组件中删除重复的"more"图标选项。 * feat(ServerCard): 优化日期选择器并添加下拉提示 为日期选择器添加下拉布局和年份范围限制,并在公共笔记区域增加下拉项生效提示文本。 * chore: auto-fix linting and formatting issues * style: 优化多个组件的UI交互细节 统一按钮悬停样式并简化国际化文本调用,移除冗余的单位显示和空值判断逻辑。 * refactor(ServerCard): 移除网络路由相关代码 删除 ServerCard 组件中与 plan.networkRoute 相关的字段验证和错误显示逻辑。 * chore: auto-fix linting and formatting issues * feat(ui): 添加Switch组件并改进服务器表单交互 - 新增Radix UI Switch组件依赖及实现 - 将IPv4/IPv6输入改为开关控件,优化用户体验 - 添加"按量付费"选项和新的翻译字段 - 改进网络路由和备注输入的占位提示 - 修复暗黑模式下的按钮背景色 * style(components): 为禁止按钮添加白色文本样式 * chore: auto-fix linting and formatting issues * fix(ServerCard): 修复日期选择器样式和滚动问题 调整日期选择器的宽度和高度限制,添加滚动容器以解决内容溢出问题 * refactor(server-config): 简化复选框checked属性的布尔转换 使用!!操作符简化controllerField.value的布尔值转换,使代码更简洁 * feat(国际化): 添加告警规则和搜索框的国际化支持 为告警规则组件添加多语言支持,包括服务器监控选项、忽略提示和示例文本。同时将搜索框的占位文本替换为国际化字段。 * chore: auto-fix linting and formatting issues * fix(switch): 修正 Switch 组件 ref 类型定义错误 --------- Co-authored-by: Guccen <171530509+Chillln@users.noreply.github.com>
This commit is contained in:
+694
-18
@@ -1,5 +1,6 @@
|
||||
import { updateServer } from "@/api/server"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -21,7 +22,16 @@ import {
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
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 { IconButton } from "@/components/xui/icon-button"
|
||||
import { conv } from "@/lib/utils"
|
||||
@@ -49,7 +59,7 @@ const serverFormSchema = z.object({
|
||||
enable_ddns: asOptionalField(z.boolean()),
|
||||
ddns_profiles: asOptionalField(z.array(z.number())),
|
||||
ddns_profiles_raw: asOptionalField(z.string()),
|
||||
override_ddns_domains: asOptionalField(z.record(z.coerce.number().int(), z.array(z.string()))),
|
||||
override_ddns_domains: asOptionalField(z.record(z.string(), z.array(z.string()))),
|
||||
override_ddns_domains_raw: asOptionalField(
|
||||
z.string().refine(
|
||||
(val) => {
|
||||
@@ -69,8 +79,8 @@ const serverFormSchema = z.object({
|
||||
|
||||
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useForm<z.infer<typeof serverFormSchema>>({
|
||||
resolver: zodResolver(serverFormSchema),
|
||||
const form = useForm({
|
||||
resolver: zodResolver(serverFormSchema) as any,
|
||||
defaultValues: {
|
||||
...data,
|
||||
ddns_profiles_raw: data.ddns_profiles ? conv.arrToStr(data.ddns_profiles) : undefined,
|
||||
@@ -85,7 +95,131 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof serverFormSchema>) => {
|
||||
type PublicNote = {
|
||||
billingDataMod: {
|
||||
startDate: string
|
||||
endDate: string
|
||||
autoRenewal: string
|
||||
cycle: string
|
||||
amount: string
|
||||
}
|
||||
planDataMod: {
|
||||
bandwidth: string
|
||||
trafficVol: string
|
||||
trafficType: string
|
||||
IPv4: string
|
||||
IPv6: string
|
||||
networkRoute: string
|
||||
extra: string
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPublicNote: PublicNote = {
|
||||
billingDataMod: {
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
autoRenewal: "",
|
||||
cycle: "",
|
||||
amount: "",
|
||||
},
|
||||
planDataMod: {
|
||||
bandwidth: "",
|
||||
trafficVol: "",
|
||||
trafficType: "",
|
||||
IPv4: "0",
|
||||
IPv6: "0",
|
||||
networkRoute: "",
|
||||
extra: "",
|
||||
},
|
||||
}
|
||||
|
||||
const parsePublicNote = (s?: string): PublicNote => {
|
||||
if (!s) return defaultPublicNote
|
||||
try {
|
||||
const obj = JSON.parse(s)
|
||||
return {
|
||||
billingDataMod: {
|
||||
startDate: obj?.billingDataMod?.startDate ?? "",
|
||||
endDate: obj?.billingDataMod?.endDate ?? "",
|
||||
autoRenewal: obj?.billingDataMod?.autoRenewal ?? "",
|
||||
cycle: obj?.billingDataMod?.cycle ?? "",
|
||||
amount: obj?.billingDataMod?.amount ?? "",
|
||||
},
|
||||
planDataMod: {
|
||||
bandwidth: obj?.planDataMod?.bandwidth ?? "",
|
||||
trafficVol: obj?.planDataMod?.trafficVol ?? "",
|
||||
trafficType: obj?.planDataMod?.trafficType ?? "",
|
||||
IPv4: obj?.planDataMod?.IPv4 === "1" ? "1" : "0",
|
||||
IPv6: obj?.planDataMod?.IPv6 === "1" ? "1" : "0",
|
||||
networkRoute: obj?.planDataMod?.networkRoute ?? "",
|
||||
extra: obj?.planDataMod?.extra ?? "",
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
return defaultPublicNote
|
||||
}
|
||||
}
|
||||
|
||||
const [publicNoteObj, setPublicNoteObj] = useState<PublicNote>(
|
||||
parsePublicNote(data?.public_note),
|
||||
)
|
||||
const [publicNoteErrors, setPublicNoteErrors] = useState<
|
||||
Partial<
|
||||
Record<
|
||||
| "billing.startDate"
|
||||
| "billing.endDate"
|
||||
| "billing.autoRenewal"
|
||||
| "billing.cycle"
|
||||
| "billing.amount"
|
||||
| "plan.bandwidth"
|
||||
| "plan.trafficVol"
|
||||
| "plan.trafficType"
|
||||
| "plan.IPv4"
|
||||
| "plan.IPv6"
|
||||
| "plan.extra",
|
||||
string
|
||||
>
|
||||
>
|
||||
>({})
|
||||
|
||||
const isValidISOLike = (v: string) => {
|
||||
if (!v) return true
|
||||
// special marker for "no expiry"
|
||||
if (v === "0000-00-00T23:59:59+08:00") return true
|
||||
const d = new Date(v)
|
||||
return !isNaN(d.getTime())
|
||||
}
|
||||
|
||||
const validatePublicNote = (pn: PublicNote) => {
|
||||
const errs: Partial<Record<string, string>> = {}
|
||||
|
||||
if (pn.billingDataMod.startDate && !isValidISOLike(pn.billingDataMod.startDate)) {
|
||||
errs["billing.startDate"] = t("Validation.InvalidDate")
|
||||
}
|
||||
if (pn.billingDataMod.endDate && !isValidISOLike(pn.billingDataMod.endDate)) {
|
||||
errs["billing.endDate"] = t("Validation.InvalidDate")
|
||||
}
|
||||
if (pn.billingDataMod.autoRenewal && !/^(0|1)$/.test(pn.billingDataMod.autoRenewal)) {
|
||||
errs["billing.autoRenewal"] = t("Validation.MustBe0Or1")
|
||||
}
|
||||
if (pn.billingDataMod.cycle && !/^(Day|Week|Month|Year)$/i.test(pn.billingDataMod.cycle)) {
|
||||
errs["billing.cycle"] = t("Validation.MustBeDayWeekMonthYear")
|
||||
}
|
||||
// amount 允许任意非空字符串或空
|
||||
if (pn.planDataMod.trafficType && !/^(1|2)$/.test(pn.planDataMod.trafficType)) {
|
||||
errs["plan.trafficType"] = t("Validation.MustBe1Or2")
|
||||
}
|
||||
if (!/^(0|1)$/.test(pn.planDataMod.IPv4)) {
|
||||
errs["plan.IPv4"] = t("Validation.MustBe0Or1")
|
||||
}
|
||||
if (!/^(0|1)$/.test(pn.planDataMod.IPv6)) {
|
||||
errs["plan.IPv6"] = t("Validation.MustBe0Or1")
|
||||
}
|
||||
|
||||
return { errors: errs, valid: Object.keys(errs).length === 0 }
|
||||
}
|
||||
|
||||
const onSubmit = async (values: any) => {
|
||||
try {
|
||||
values.ddns_profiles = values.ddns_profiles_raw
|
||||
? conv.strToArr(values.ddns_profiles_raw).map(Number)
|
||||
@@ -93,6 +227,36 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
values.override_ddns_domains = values.override_ddns_domains_raw
|
||||
? JSON.parse(values.override_ddns_domains_raw)
|
||||
: undefined
|
||||
|
||||
// validate structured fields
|
||||
const { errors, valid } = validatePublicNote(publicNoteObj)
|
||||
if (!valid) {
|
||||
setPublicNoteErrors(errors)
|
||||
toast(t("Error"), { description: t("Validation.InvalidForm") })
|
||||
return
|
||||
}
|
||||
setPublicNoteErrors({})
|
||||
|
||||
// normalize datetime-local to ISO string if provided
|
||||
const normalizeISO = (v: string) => {
|
||||
if (!v) return v
|
||||
// keep special "no expiry" value as-is
|
||||
if (v === "0000-00-00T23:59:59+08:00") return v
|
||||
const date = new Date(v)
|
||||
return isNaN(date.getTime()) ? v : date.toISOString()
|
||||
}
|
||||
const pnNormalized: PublicNote = {
|
||||
billingDataMod: {
|
||||
...publicNoteObj.billingDataMod,
|
||||
startDate: normalizeISO(publicNoteObj.billingDataMod.startDate),
|
||||
endDate: normalizeISO(publicNoteObj.billingDataMod.endDate),
|
||||
// keep others as-is
|
||||
},
|
||||
planDataMod: { ...publicNoteObj.planDataMod },
|
||||
}
|
||||
|
||||
// serialize structured public note back to JSON string
|
||||
values.public_note = JSON.stringify(pnNormalized)
|
||||
await updateServer(data!.id!, values)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -111,7 +275,11 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
<DialogTrigger asChild>
|
||||
<IconButton variant="outline" icon="edit" />
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogContent
|
||||
className="sm:max-w-xl"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<ScrollArea className="max-h-[calc(100dvh-5rem)] p-3">
|
||||
<div className="items-center mx-1">
|
||||
<DialogHeader>
|
||||
@@ -236,19 +404,527 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="public_note"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Public") + t("Note")}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea className="resize-y" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* Structured Public Note fields */}
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>{t("Public") + t("Note")}</FormLabel>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("PublicNote.DropdownHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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
|
||||
setPublicNoteObj((prev) => {
|
||||
const prevDateStr =
|
||||
prev.billingDataMod
|
||||
.startDate
|
||||
if (prevDateStr) {
|
||||
const pd = new Date(
|
||||
prevDateStr,
|
||||
)
|
||||
// 仅在有效日期时复制时分秒
|
||||
if (
|
||||
!isNaN(pd.getTime())
|
||||
) {
|
||||
d.setHours(
|
||||
pd.getHours(),
|
||||
pd.getMinutes(),
|
||||
pd.getSeconds(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.billingDataMod,
|
||||
startDate:
|
||||
d.toISOString(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}}
|
||||
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={() =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.billingDataMod,
|
||||
endDate:
|
||||
prev.billingDataMod
|
||||
.endDate ===
|
||||
"0000-00-00T23:59:59+08:00"
|
||||
? ""
|
||||
: "0000-00-00T23:59:59+08:00",
|
||||
},
|
||||
}))
|
||||
}
|
||||
>
|
||||
{publicNoteObj.billingDataMod.endDate ===
|
||||
"0000-00-00T23:59:59+08:00"
|
||||
? t("PublicNote.CancelNoExpiry")
|
||||
: t("PublicNote.SetNoExpiry")}
|
||||
</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,
|
||||
).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,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
onSelect={(d) => {
|
||||
if (!d) return
|
||||
setPublicNoteObj((prev) => {
|
||||
const prevDateStr =
|
||||
prev.billingDataMod
|
||||
.endDate
|
||||
if (prevDateStr) {
|
||||
const pd = new Date(
|
||||
prevDateStr,
|
||||
)
|
||||
// 仅在有效日期时复制时分秒(特殊“不过期”值不会影响)
|
||||
if (
|
||||
!isNaN(pd.getTime())
|
||||
) {
|
||||
d.setHours(
|
||||
pd.getHours(),
|
||||
pd.getMinutes(),
|
||||
pd.getSeconds(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.billingDataMod,
|
||||
endDate:
|
||||
d.toISOString(),
|
||||
},
|
||||
}
|
||||
})
|
||||
}}
|
||||
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">
|
||||
<Label className="text-xs">
|
||||
{t("PublicNote.AutoRenewal")}
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(val) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.billingDataMod,
|
||||
autoRenewal: val,
|
||||
},
|
||||
}))
|
||||
}
|
||||
defaultValue={
|
||||
publicNoteObj.billingDataMod.autoRenewal ||
|
||||
"0"
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">
|
||||
{t("PublicNote.Enabled")}
|
||||
</SelectItem>
|
||||
<SelectItem value="0">
|
||||
{t("PublicNote.Disabled")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{publicNoteErrors["billing.autoRenewal"] && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
{publicNoteErrors["billing.autoRenewal"]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">
|
||||
{t("PublicNote.Cycle")}
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(val) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.billingDataMod,
|
||||
cycle: val,
|
||||
},
|
||||
}))
|
||||
}
|
||||
defaultValue={
|
||||
publicNoteObj.billingDataMod.cycle ||
|
||||
"Month"
|
||||
}
|
||||
>
|
||||
<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={() =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.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={() =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.billingDataMod,
|
||||
amount: "-1",
|
||||
},
|
||||
}))
|
||||
}
|
||||
>
|
||||
{t("PublicNote.PayAsYouGo")}
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
placeholder="200EUR"
|
||||
value={publicNoteObj.billingDataMod.amount}
|
||||
onChange={(e) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
billingDataMod: {
|
||||
...prev.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) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
planDataMod: {
|
||||
...prev.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) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
planDataMod: {
|
||||
...prev.planDataMod,
|
||||
trafficVol: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">
|
||||
{t("PublicNote.TrafficType")}
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={(val) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
planDataMod: {
|
||||
...prev.planDataMod,
|
||||
trafficType: val,
|
||||
},
|
||||
}))
|
||||
}
|
||||
defaultValue={
|
||||
publicNoteObj.planDataMod.trafficType || "2"
|
||||
}
|
||||
>
|
||||
<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) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
planDataMod: {
|
||||
...prev.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) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
planDataMod: {
|
||||
...prev.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) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
planDataMod: {
|
||||
...prev.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) =>
|
||||
setPublicNoteObj((prev) => ({
|
||||
...prev,
|
||||
planDataMod: {
|
||||
...prev.planDataMod,
|
||||
extra: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="justify-end">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" className="my-2" variant="secondary">
|
||||
|
||||
Reference in New Issue
Block a user