mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-05-06 13:48:55 +00:00
Compare commits
3 Commits
825bcb08f4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2da8565e47 | |||
| 9720bc258f | |||
| 1e4fae5306 |
+459
-320
File diff suppressed because it is too large
Load Diff
+23
-9
@@ -1,30 +1,36 @@
|
|||||||
import { fetcher, FetcherMethod, swrFetcher } from './api' // 导入正确的 fetcher 函数和方法枚举
|
// 导入正确的 fetcher 函数和方法枚举
|
||||||
import type { Domain, BillingDataMod} from '@/types/api'
|
import type { BillingDataMod, Domain } from "@/types/domain"
|
||||||
|
|
||||||
|
import { FetcherMethod, fetcher } from "./api"
|
||||||
|
|
||||||
// --- GET 请求 (用于 SWR) ---
|
// --- GET 请求 (用于 SWR) ---
|
||||||
|
|
||||||
// 获取域名列表的函数,专门为 useSWR 设计
|
// 获取域名列表的函数,专门为 useSWR 设计
|
||||||
// swrFetcher 内部会调用 fetcher,这一部分是正确的
|
// swrFetcher 内部会调用 fetcher,这一部分是正确的
|
||||||
export const useDomainList = () => {
|
export const useDomainList = (url: string) => {
|
||||||
return swrFetcher<Domain[]>('/api/v1/domains')
|
return fetcher<Domain[]>(FetcherMethod.GET, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- POST, PUT, DELETE 请求 (使用 fetcher) ---
|
// --- POST, PUT, DELETE 请求 (使用 fetcher) ---
|
||||||
|
|
||||||
// 添加一个新的域名
|
// 添加一个新的域名
|
||||||
export const addDomain = (domain: string) => {
|
export const addDomain = (domain: string) => {
|
||||||
return fetcher<Domain>(FetcherMethod.POST, '/api/v1/domains', { domain })
|
return fetcher<Domain>(FetcherMethod.POST, "/api/v1/domains", { domain })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发域名验证
|
// 触发域名验证
|
||||||
export const verifyDomain = (id: number) => {
|
export const verifyDomain = (id: number) => {
|
||||||
return fetcher<{ success: boolean; message: string }>(FetcherMethod.POST, `/api/v1/domains/${id}/verify`)
|
return fetcher<{ success: boolean; message: string }>(
|
||||||
|
FetcherMethod.POST,
|
||||||
|
`/api/v1/domains/${id}/verify`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新域名的配置信息
|
// 更新域名的配置信息
|
||||||
export const updateDomainConfig = (id: number, billingData: BillingDataMod) => {
|
export const updateDomainConfig = (id: number, billingData: BillingDataMod) => {
|
||||||
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, { billing_data: billingData })
|
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, {
|
||||||
|
billing_data: billingData,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除一个域名
|
// 删除一个域名
|
||||||
@@ -34,7 +40,10 @@ export const deleteDomain = (id: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新一个域名(包括公开状态和配置信息)
|
// 更新一个域名(包括公开状态和配置信息)
|
||||||
export const updateDomain = (id: number, data: { is_public: boolean, billing_data: BillingDataMod }) => {
|
export const updateDomain = (
|
||||||
|
id: number,
|
||||||
|
data: { is_public: boolean; billing_data: BillingDataMod },
|
||||||
|
) => {
|
||||||
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, data)
|
return fetcher<Domain>(FetcherMethod.PUT, `/api/v1/domains/${id}`, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,3 +51,8 @@ export const updateDomain = (id: number, data: { is_public: boolean, billing_dat
|
|||||||
export const syncDomainWHOIS = (id: number) => {
|
export const syncDomainWHOIS = (id: number) => {
|
||||||
return fetcher<Domain>(FetcherMethod.POST, `/api/v1/domains/${id}/sync`)
|
return fetcher<Domain>(FetcherMethod.POST, `/api/v1/domains/${id}/sync`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步所有域名
|
||||||
|
export const syncAllDomains = () => {
|
||||||
|
return fetcher<any>(FetcherMethod.POST, "/api/v1/domains/sync-all")
|
||||||
|
}
|
||||||
|
|||||||
+14
-16
@@ -1,4 +1,6 @@
|
|||||||
import { ModeToggle } from "@/components/mode-toggle"
|
import { ModeToggle } from "@/components/mode-toggle"
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerClose,
|
DrawerClose,
|
||||||
@@ -9,12 +11,24 @@ import {
|
|||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerTrigger,
|
DrawerTrigger,
|
||||||
} from "@/components/ui/drawer"
|
} from "@/components/ui/drawer"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
NavigationMenuLink,
|
NavigationMenuLink,
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle,
|
||||||
} from "@/components/ui/navigation-menu"
|
} from "@/components/ui/navigation-menu"
|
||||||
|
import { IconButton } from "@/components/xui/icon-button"
|
||||||
|
import { NzNavigationMenuLink } from "@/components/xui/navigation-menu"
|
||||||
import { useAuth } from "@/hooks/useAuth"
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import { useMainStore } from "@/hooks/useMainStore"
|
import { useMainStore } from "@/hooks/useMainStore"
|
||||||
import { useMediaQuery } from "@/hooks/useMediaQuery"
|
import { useMediaQuery } from "@/hooks/useMediaQuery"
|
||||||
@@ -26,21 +40,6 @@ import { useEffect, useRef, useState } from "react"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom"
|
import { Link, useLocation, useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu"
|
|
||||||
import { IconButton } from "@/components/xui/icon-button"
|
|
||||||
import { NzNavigationMenuLink } from "@/components/xui/navigation-menu"
|
|
||||||
|
|
||||||
// =======================================================
|
// =======================================================
|
||||||
// vvvvvvvvvvv 1. 在这里为移动端菜单添加新页面 vvvvvvvvvvv
|
// vvvvvvvvvvv 1. 在这里为移动端菜单添加新页面 vvvvvvvvvvv
|
||||||
const pages = [
|
const pages = [
|
||||||
@@ -428,4 +427,3 @@ function Overview() {
|
|||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+67
-40
@@ -30,7 +30,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { IconButton } from "@/components/xui/icon-button"
|
import { IconButton } from "@/components/xui/icon-button"
|
||||||
import { asOptionalField } from "@/lib/utils"
|
|
||||||
import { ModelNotification } from "@/types"
|
import { ModelNotification } from "@/types"
|
||||||
import { nrequestMethods, nrequestTypes } from "@/types"
|
import { nrequestMethods, nrequestTypes } from "@/types"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
@@ -55,9 +54,9 @@ const notificationFormSchema = z.object({
|
|||||||
request_type: z.coerce.number().int().min(1).max(255),
|
request_type: z.coerce.number().int().min(1).max(255),
|
||||||
request_header: z.string(),
|
request_header: z.string(),
|
||||||
request_body: z.string(),
|
request_body: z.string(),
|
||||||
verify_tls: asOptionalField(z.boolean()),
|
verify_tls: z.boolean().default(false),
|
||||||
skip_check: asOptionalField(z.boolean()),
|
skip_check: z.boolean().default(false),
|
||||||
format_metric_units: asOptionalField(z.boolean()),
|
format_metric_units: z.boolean().default(false),
|
||||||
type: z.coerce.number().int().default(1),
|
type: z.coerce.number().int().default(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -75,9 +74,9 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
request_type: data.request_type ?? 1,
|
request_type: data.request_type ?? 1,
|
||||||
request_header: data.request_header ?? "",
|
request_header: data.request_header ?? "",
|
||||||
request_body: data.request_body ?? "",
|
request_body: data.request_body ?? "",
|
||||||
verify_tls: (data as any).verify_tls ?? false,
|
verify_tls: data.verify_tls ?? false,
|
||||||
skip_check: (data as any).skip_check ?? false,
|
skip_check: data.skip_check ?? false,
|
||||||
format_metric_units: (data as any).format_metric_units ?? false,
|
format_metric_units: data.format_metric_units ?? false,
|
||||||
type: data.type ?? 1,
|
type: data.type ?? 1,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -177,11 +176,21 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{form.watch("type") == 2 ? "SMTP Server (host:port)" :
|
{form.watch("type") == 2
|
||||||
form.watch("type") == 3 ? "Bot Token" : "URL"}
|
? "SMTP Server (host:port)"
|
||||||
|
: form.watch("type") == 3
|
||||||
|
? "Bot Token"
|
||||||
|
: "URL"}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder={form.watch("type") == 3 ? "123456:ABC-DEF" : ""} />
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={
|
||||||
|
form.watch("type") == 3
|
||||||
|
? "123456:ABC-DEF"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -234,11 +243,13 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{Object.entries(nrequestTypes).map(([k, v]) => (
|
{Object.entries(nrequestTypes).map(
|
||||||
|
([k, v]) => (
|
||||||
<SelectItem key={k} value={k}>
|
<SelectItem key={k} value={k}>
|
||||||
{v}
|
{v}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -253,15 +264,21 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{form.watch("type") == 2 ? "SMTP User:Pass" :
|
{form.watch("type") == 2
|
||||||
form.watch("type") == 3 ? "Chat ID" : t("RequestHeader")}
|
? "SMTP User:Pass"
|
||||||
|
: form.watch("type") == 3
|
||||||
|
? "Chat ID"
|
||||||
|
: t("RequestHeader")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="resize-y"
|
className="resize-y"
|
||||||
placeholder={
|
placeholder={
|
||||||
form.watch("type") == 2 ? "user:pass" :
|
form.watch("type") == 2
|
||||||
form.watch("type") == 3 ? "123456789" : '{"User-Agent":"Nezha-Agent"}'
|
? "user:pass"
|
||||||
|
: form.watch("type") == 3
|
||||||
|
? "123456789"
|
||||||
|
: '{"User-Agent":"Nezha-Agent"}'
|
||||||
}
|
}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@@ -276,11 +293,23 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
name="request_body"
|
name="request_body"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{form.watch("type") == 2 ? "Recipient Email" : t("RequestBody")}</FormLabel>
|
<FormLabel>
|
||||||
|
{form.watch("type") == 2
|
||||||
|
? "Recipient Email"
|
||||||
|
: t("RequestBody")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
className={form.watch("type") == 2 ? "resize-y" : "resize-y h-[240px]"}
|
className={
|
||||||
placeholder={form.watch("type") == 2 ? "target@example.com" : '...'}
|
form.watch("type") == 2
|
||||||
|
? "resize-y"
|
||||||
|
: "resize-y h-[240px]"
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
form.watch("type") == 2
|
||||||
|
? "target@example.com"
|
||||||
|
: "..."
|
||||||
|
}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -289,23 +318,25 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<div className="pt-4 border-t space-y-3">
|
||||||
|
<Label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||||
|
{t("AdvancedSettings")}
|
||||||
|
</Label>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="verify_tls"
|
name="verify_tls"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex items-center space-x-2 space-y-0 py-1">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
<Label className="text-sm">
|
|
||||||
{t("VerifyTLS")}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormLabel className="text-sm font-normal cursor-pointer">
|
||||||
|
{t("VerifyTLS")}
|
||||||
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -313,19 +344,16 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="skip_check"
|
name="skip_check"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex items-center space-x-2 space-y-0 py-1">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
<Label className="text-sm">
|
|
||||||
{t("DoNotSendTestMessage")}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormLabel className="text-sm font-normal cursor-pointer">
|
||||||
|
{t("DoNotSendTestMessage")}
|
||||||
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -333,22 +361,21 @@ export const NotifierCard: React.FC<NotifierCardProps> = ({ data, mutate }) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="format_metric_units"
|
name="format_metric_units"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex items-center space-x-2">
|
<FormItem className="flex items-center space-x-2 space-y-0 py-1">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
<Label className="text-sm">
|
|
||||||
{t("FormatMetricUnits")}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormLabel className="text-sm font-normal cursor-pointer">
|
||||||
|
{t("FormatMetricUnits")}
|
||||||
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -160,13 +160,17 @@ export const ServerConfigCard = ({ sid, menuItem = false, ...props }: ServerConf
|
|||||||
const onSubmit = async (values: any) => {
|
const onSubmit = async (values: any) => {
|
||||||
let resp: ModelServerTaskResponse = {}
|
let resp: ModelServerTaskResponse = {}
|
||||||
try {
|
try {
|
||||||
values.nic_allowlist = values.nic_allowlist_raw
|
const submitValues = { ...values }
|
||||||
? JSON.parse(values.nic_allowlist_raw)
|
submitValues.nic_allowlist = submitValues.nic_allowlist_raw
|
||||||
|
? JSON.parse(submitValues.nic_allowlist_raw)
|
||||||
: undefined
|
: undefined
|
||||||
values.hard_drive_partition_allowlist = values.hard_drive_partition_allowlist_raw
|
submitValues.hard_drive_partition_allowlist =
|
||||||
? JSON.parse(values.hard_drive_partition_allowlist_raw)
|
submitValues.hard_drive_partition_allowlist_raw
|
||||||
|
? JSON.parse(submitValues.hard_drive_partition_allowlist_raw)
|
||||||
: undefined
|
: undefined
|
||||||
resp = await setServerConfig({ config: JSON.stringify(values), servers: [sid] })
|
delete submitValues.nic_allowlist_raw
|
||||||
|
delete submitValues.hard_drive_partition_allowlist_raw
|
||||||
|
resp = await setServerConfig({ config: JSON.stringify(submitValues), servers: [sid] })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast(t("Error"), {
|
toast(t("Error"), {
|
||||||
|
|||||||
+53
-23
@@ -28,7 +28,7 @@ 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 { useState, useEffect } from "react"
|
import { useEffect, 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,11 +65,13 @@ const serverFormSchema = z.object({
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
billing_data: z.object({
|
billing_data: z
|
||||||
|
.object({
|
||||||
registrar: asOptionalField(z.string()),
|
registrar: asOptionalField(z.string()),
|
||||||
endDate: asOptionalField(z.string()),
|
endDate: asOptionalField(z.string()),
|
||||||
notes: asOptionalField(z.string()),
|
notes: asOptionalField(z.string()),
|
||||||
}).optional(),
|
})
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
||||||
@@ -92,16 +94,16 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMessage = (e: MessageEvent) => {
|
const handleMessage = (e: MessageEvent) => {
|
||||||
if (e.data?.type === 'NZCFG_JSON') {
|
if (e.data?.type === "NZCFG_JSON") {
|
||||||
if (e.data.target === 'public_note') {
|
if (e.data.target === "public_note") {
|
||||||
form.setValue('public_note', e.data.payload);
|
form.setValue("public_note", e.data.payload)
|
||||||
toast(t("Success"), { description: "配置已通过可视化构建器自动填入" });
|
toast(t("Success"), { description: "配置已通过可视化构建器自动填入" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
window.addEventListener('message', handleMessage);
|
window.addEventListener("message", handleMessage)
|
||||||
return () => window.removeEventListener('message', handleMessage);
|
return () => window.removeEventListener("message", handleMessage)
|
||||||
}, [form, t]);
|
}, [form, t])
|
||||||
|
|
||||||
const onSubmit = async (values: any) => {
|
const onSubmit = async (values: any) => {
|
||||||
try {
|
try {
|
||||||
@@ -255,7 +257,9 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="p-3 border rounded-md border-dashed space-y-2">
|
<div className="p-3 border rounded-md border-dashed space-y-2">
|
||||||
<Label className="text-xs text-muted-foreground uppercase font-bold">Billing & Expiry</Label>
|
<Label className="text-xs text-muted-foreground uppercase font-bold">
|
||||||
|
Billing & Expiry
|
||||||
|
</Label>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control as any}
|
control={form.control as any}
|
||||||
name="billing_data.registrar"
|
name="billing_data.registrar"
|
||||||
@@ -263,7 +267,10 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Registrar</FormLabel>
|
<FormLabel>Registrar</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="AWS / Azure /阿里云" {...field} />
|
<Input
|
||||||
|
placeholder="AWS / Azure /阿里云"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -276,7 +283,20 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Expiry Date</FormLabel>
|
<FormLabel>Expiry Date</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="date" {...field} value={field.value?.split('T')[0] || ''} onChange={(e) => field.onChange(e.target.value ? new Date(e.target.value).toISOString() : '')} />
|
<Input
|
||||||
|
type="date"
|
||||||
|
{...field}
|
||||||
|
value={field.value?.split("T")[0] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value
|
||||||
|
? new Date(
|
||||||
|
e.target.value,
|
||||||
|
).toISOString()
|
||||||
|
: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -290,18 +310,28 @@ export const ServerCard: React.FC<ServerCardProps> = ({ data, mutate }) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex justify-between items-center w-full">
|
<FormLabel className="flex justify-between items-center w-full">
|
||||||
<span>{t("Public") + t("Note")}</span>
|
<span>{t("Public") + t("Note")}</span>
|
||||||
<a href="/dashboard/nzcfg.html" target="_blank" className="text-blue-500 hover:text-blue-700 text-xs flex items-center gap-1" onClick={(e) => {
|
<a
|
||||||
e.preventDefault();
|
href="/dashboard/nzcfg.html"
|
||||||
const popup = window.open('/dashboard/nzcfg.html', 'nzcfg', 'width=1000,height=800');
|
target="_blank"
|
||||||
if(popup) {
|
className="text-blue-500 hover:text-blue-700 text-xs flex items-center gap-1"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const popup = window.open(
|
||||||
|
"/dashboard/nzcfg.html",
|
||||||
|
"nzcfg",
|
||||||
|
"width=1000,height=800",
|
||||||
|
)
|
||||||
|
if (popup) {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
if(popup.closed) {
|
if (popup.closed) {
|
||||||
clearInterval(timer);
|
clearInterval(timer)
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500)
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
可视化管理配置 <i className="fa-solid fa-up-right-from-square"></i>
|
>
|
||||||
|
可视化管理配置{" "}
|
||||||
|
<i className="fa-solid fa-up-right-from-square"></i>
|
||||||
</a>
|
</a>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from "react"
|
import { createContext, useContext, useEffect, useState } from "react"
|
||||||
|
import { DateTime } from "luxon"
|
||||||
|
|
||||||
export type Theme = "dark" | "light" | "system"
|
export type Theme = "dark" | "light" | "system"
|
||||||
|
|
||||||
@@ -30,22 +31,28 @@ export function ThemeProvider({
|
|||||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [hour, setHour] = useState(() => DateTime.now().hour)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setHour(DateTime.now().hour)
|
||||||
|
}, 60000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = window.document.documentElement
|
const root = window.document.documentElement
|
||||||
|
|
||||||
root.classList.remove("light", "dark")
|
root.classList.remove("light", "dark")
|
||||||
|
|
||||||
|
let effectiveTheme = theme
|
||||||
if (theme === "system") {
|
if (theme === "system") {
|
||||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
|
const isNight = hour >= 18 || hour < 6
|
||||||
? "dark"
|
effectiveTheme = isNight ? "dark" : "light"
|
||||||
: "light"
|
|
||||||
|
|
||||||
root.classList.add(systemTheme)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.classList.add(theme)
|
root.classList.add(effectiveTheme)
|
||||||
}, [theme])
|
}, [theme, hour])
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
theme,
|
theme,
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ const badgeVariants = cva(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends HTMLAttributes<HTMLDivElement>,
|
extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ const buttonVariants = cva(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||||
VariantProps<typeof buttonVariants> {
|
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ const multiSelectVariants = cva(
|
|||||||
* Props for MultiSelect component
|
* Props for MultiSelect component
|
||||||
*/
|
*/
|
||||||
interface MultiSelectProps
|
interface MultiSelectProps
|
||||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
extends
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
VariantProps<typeof multiSelectVariants> {
|
VariantProps<typeof multiSelectVariants> {
|
||||||
/**
|
/**
|
||||||
* An array of option objects to be displayed in the multi-select component.
|
* An array of option objects to be displayed in the multi-select component.
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ const sheetVariants = cva(
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface SheetContentProps
|
interface SheetContentProps
|
||||||
extends ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
extends
|
||||||
|
ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||||
VariantProps<typeof sheetVariants> {
|
VariantProps<typeof sheetVariants> {
|
||||||
setOpen: Dispatch<SetStateAction<boolean>>
|
setOpen: Dispatch<SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const n = (await getNotification()) || []
|
const n = (await getNotification()) || []
|
||||||
const nData = n.map(({ id, name }) => ({ id, name }))
|
const nData = n.map(({ id, name }) => ({ id: id!, name }))
|
||||||
setNotifier(nData)
|
setNotifier(nData)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast("NotificationProvider Error", {
|
toast("NotificationProvider Error", {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const ServerProvider: React.FC<ServerProviderProps> = ({
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const s = (await getServers()) || []
|
const s = (await getServers()) || []
|
||||||
const serverData = s.map(({ id, name }) => ({ id, name }))
|
const serverData = s.map(({ id, name }) => ({ id: id!, name }))
|
||||||
setServer(serverData)
|
setServer(serverData)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast("ServerProvider Error", {
|
toast("ServerProvider Error", {
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default function AlertRulePage() {
|
|||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteAlertRules,
|
fn: deleteAlertRules,
|
||||||
id: s.id,
|
id: s.id!,
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -147,7 +147,7 @@ export default function AlertRulePage() {
|
|||||||
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteAlertRules,
|
fn: deleteAlertRules,
|
||||||
id: selectedRows.map((r) => r.original.id),
|
id: selectedRows.map((r) => r.original.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+3
-3
@@ -164,7 +164,7 @@ export default function CronPage() {
|
|||||||
return (
|
return (
|
||||||
<ActionButtonGroup
|
<ActionButtonGroup
|
||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{ fn: deleteCron, id: s.id, mutate: mutate }}
|
delete={{ fn: deleteCron, id: s.id!, mutate: mutate }}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -172,7 +172,7 @@ export default function CronPage() {
|
|||||||
icon="play"
|
icon="play"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await runCron(s.id)
|
await runCron(s.id!)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast(t("Error"), {
|
toast(t("Error"), {
|
||||||
@@ -215,7 +215,7 @@ export default function CronPage() {
|
|||||||
className="flex gap-2 flex-wrap shrink-0"
|
className="flex gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteCron,
|
fn: deleteCron,
|
||||||
id: selectedRows.map((r) => r.original.id),
|
id: selectedRows.map((r) => r.original.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+2
-2
@@ -121,7 +121,7 @@ export default function DDNSPage() {
|
|||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteDDNSProfiles,
|
fn: deleteDDNSProfiles,
|
||||||
id: s.id,
|
id: s.id!,
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -152,7 +152,7 @@ export default function DDNSPage() {
|
|||||||
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteDDNSProfiles,
|
fn: deleteDDNSProfiles,
|
||||||
id: selectedRows.map((r) => r.original.id),
|
id: selectedRows.map((r) => r.original.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+271
-76
@@ -1,27 +1,58 @@
|
|||||||
// src/routes/domain.tsx (最终 Bug 修复版)
|
// src/routes/domain.tsx (最终 Bug 修复版)
|
||||||
|
import {
|
||||||
import { useState, useEffect } from 'react'
|
addDomain,
|
||||||
import { PlusCircle, RefreshCw, MoreVertical, Trash2, Edit, CheckCircle, RefreshCcw } from 'lucide-react'
|
deleteDomain,
|
||||||
|
syncAllDomains,
|
||||||
|
syncDomainWHOIS,
|
||||||
|
updateDomain,
|
||||||
|
useDomainList,
|
||||||
|
verifyDomain,
|
||||||
|
} from "@/api/domain"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
// 导入 shadcn/ui 组件
|
// 导入 shadcn/ui 组件
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
import {
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
Dialog,
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
DialogContent,
|
||||||
import { Badge } from '@/components/ui/badge'
|
DialogDescription,
|
||||||
import { Button } from '@/components/ui/button'
|
DialogFooter,
|
||||||
import { Input } from '@/components/ui/input'
|
DialogHeader,
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
DialogTitle,
|
||||||
import { Label } from '@/components/ui/label'
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import {
|
||||||
import { toast } from 'sonner'
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
// 导入 API 类型和函数
|
// 导入 API 类型和函数
|
||||||
import type { Domain, BillingDataMod } from '@/types/api'
|
import type { BillingDataMod, Domain } from "@/types/domain"
|
||||||
import { useDomainList, addDomain, verifyDomain, deleteDomain, updateDomain, syncDomainWHOIS } from '@/api/domain'
|
import {
|
||||||
import useSWR from 'swr'
|
CheckCircle,
|
||||||
|
Edit,
|
||||||
|
MoreVertical,
|
||||||
|
PlusCircle,
|
||||||
|
RefreshCcw,
|
||||||
|
RefreshCw,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
export default function DomainPage() {
|
export default function DomainPage() {
|
||||||
// --- React State Hooks ---
|
// --- React State Hooks ---
|
||||||
@@ -29,9 +60,9 @@ export default function DomainPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||||
const [newDomainName, setNewDomainName] = useState('')
|
const [newDomainName, setNewDomainName] = useState("")
|
||||||
|
|
||||||
const [verificationToken, setVerificationToken] = useState('')
|
const [verificationToken, setVerificationToken] = useState("")
|
||||||
const [isVerificationInfoModalOpen, setIsVerificationInfoModalOpen] = useState(false)
|
const [isVerificationInfoModalOpen, setIsVerificationInfoModalOpen] = useState(false)
|
||||||
|
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||||
@@ -39,7 +70,12 @@ export default function DomainPage() {
|
|||||||
const [editFormData, setEditFormData] = useState<Partial<BillingDataMod>>({})
|
const [editFormData, setEditFormData] = useState<Partial<BillingDataMod>>({})
|
||||||
|
|
||||||
// --- 数据获取 (使用 SWR) ---
|
// --- 数据获取 (使用 SWR) ---
|
||||||
const { data: domainData, error, mutate } = useSWR('/api/v1/domains', useDomainList, { revalidateOnFocus: false })
|
const {
|
||||||
|
data: domainData,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
isValidating,
|
||||||
|
} = useSWR("/api/v1/domains", useDomainList, { revalidateOnFocus: false })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (domainData) {
|
if (domainData) {
|
||||||
@@ -47,14 +83,24 @@ export default function DomainPage() {
|
|||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error('无法加载域名列表,请检查后端服务是否正常。')
|
toast.error("无法加载域名列表,请检查后端服务是否正常。")
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [domainData, error])
|
}, [domainData, error])
|
||||||
|
|
||||||
|
const handleRefreshAll = async () => {
|
||||||
|
try {
|
||||||
|
await syncAllDomains()
|
||||||
|
toast.success("刷新成功", { description: "已触发所有域名的状态同步。" })
|
||||||
|
mutate()
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("刷新失败", { description: (err as Error).message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddDomain = async () => {
|
const handleAddDomain = async () => {
|
||||||
if (!newDomainName) {
|
if (!newDomainName) {
|
||||||
toast.error('请输入域名')
|
toast.error("请输入域名")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -62,10 +108,10 @@ export default function DomainPage() {
|
|||||||
setVerificationToken(response.VerifyToken)
|
setVerificationToken(response.VerifyToken)
|
||||||
setIsAddModalOpen(false)
|
setIsAddModalOpen(false)
|
||||||
setIsVerificationInfoModalOpen(true)
|
setIsVerificationInfoModalOpen(true)
|
||||||
setNewDomainName('')
|
setNewDomainName("")
|
||||||
mutate()
|
mutate()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('添加失败', { description: (err as Error).message })
|
toast.error("添加失败", { description: (err as Error).message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,24 +119,24 @@ export default function DomainPage() {
|
|||||||
try {
|
try {
|
||||||
const response = await verifyDomain(domainId)
|
const response = await verifyDomain(domainId)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success('验证成功', { description: response.message })
|
toast.success("验证成功", { description: response.message })
|
||||||
} else {
|
} else {
|
||||||
toast.warning('验证失败', { description: response.message })
|
toast.warning("验证失败", { description: response.message })
|
||||||
}
|
}
|
||||||
setTimeout(() => mutate(), 2000)
|
setTimeout(() => mutate(), 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('操作失败', { description: (err as Error).message })
|
toast.error("操作失败", { description: (err as Error).message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSyncWhois = async (domainId: number) => {
|
const handleSyncWhois = async (domainId: number) => {
|
||||||
const loadingToast = toast.loading('正在同步 Whois 信息...')
|
const loadingToast = toast.loading("正在同步 Whois 信息...")
|
||||||
try {
|
try {
|
||||||
await syncDomainWHOIS(domainId)
|
await syncDomainWHOIS(domainId)
|
||||||
toast.success('同步成功', { id: loadingToast, description: '域名 Whois 信息已更新。' })
|
toast.success("同步成功", { id: loadingToast, description: "域名 Whois 信息已更新。" })
|
||||||
mutate()
|
mutate()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('同步失败', { id: loadingToast, description: (err as Error).message })
|
toast.error("同步失败", { id: loadingToast, description: (err as Error).message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +144,10 @@ export default function DomainPage() {
|
|||||||
if (window.confirm(`确定要删除域名 ${domainName} 吗?`)) {
|
if (window.confirm(`确定要删除域名 ${domainName} 吗?`)) {
|
||||||
try {
|
try {
|
||||||
await deleteDomain(domainId)
|
await deleteDomain(domainId)
|
||||||
toast.success('删除成功', { description: `域名 ${domainName} 已被删除。` })
|
toast.success("删除成功", { description: `域名 ${domainName} 已被删除。` })
|
||||||
mutate()
|
mutate()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('删除失败', { description: (err as Error).message })
|
toast.error("删除失败", { description: (err as Error).message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +161,7 @@ export default function DomainPage() {
|
|||||||
toast.success(`域名 ${domain.Domain} 的可见状态已更新`)
|
toast.success(`域名 ${domain.Domain} 的可见状态已更新`)
|
||||||
mutate()
|
mutate()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('更新失败', { description: (err as Error).message })
|
toast.error("更新失败", { description: (err as Error).message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,32 +181,40 @@ export default function DomainPage() {
|
|||||||
const handleUpdateDomain = async () => {
|
const handleUpdateDomain = async () => {
|
||||||
if (!currentDomain) return
|
if (!currentDomain) return
|
||||||
try {
|
try {
|
||||||
const dataToSend = { ...editFormData };
|
const dataToSend = { ...editFormData }
|
||||||
if (dataToSend.registeredDate) {
|
if (dataToSend.registeredDate) {
|
||||||
dataToSend.registeredDate = new Date(dataToSend.registeredDate).toISOString();
|
dataToSend.registeredDate = new Date(dataToSend.registeredDate).toISOString()
|
||||||
}
|
}
|
||||||
if (dataToSend.endDate) {
|
if (dataToSend.endDate) {
|
||||||
dataToSend.endDate = new Date(dataToSend.endDate).toISOString();
|
dataToSend.endDate = new Date(dataToSend.endDate).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateDomain(currentDomain.ID, {
|
await updateDomain(currentDomain.ID, {
|
||||||
is_public: currentDomain.IsPublic,
|
is_public: currentDomain.IsPublic,
|
||||||
billing_data: dataToSend as BillingDataMod
|
billing_data: dataToSend as BillingDataMod,
|
||||||
|
})
|
||||||
|
toast.success("更新成功", {
|
||||||
|
description: `域名 ${currentDomain.Domain} 的配置已保存。`,
|
||||||
})
|
})
|
||||||
toast.success('更新成功', { description: `域名 ${currentDomain.Domain} 的配置已保存。` })
|
|
||||||
setIsEditModalOpen(false)
|
setIsEditModalOpen(false)
|
||||||
mutate()
|
mutate()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('更新失败', { description: (err as Error).message })
|
toast.error("更新失败", { description: (err as Error).message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusVariant = (status: string): 'default' | 'secondary' | 'destructive' | 'outline' => {
|
const getStatusVariant = (
|
||||||
|
status: string,
|
||||||
|
): "default" | "secondary" | "destructive" | "outline" => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'verified': return 'default'
|
case "verified":
|
||||||
case 'pending': return 'secondary'
|
return "default"
|
||||||
case 'expired': return 'destructive'
|
case "pending":
|
||||||
default: return 'outline'
|
return "secondary"
|
||||||
|
case "expired":
|
||||||
|
return "destructive"
|
||||||
|
default:
|
||||||
|
return "outline"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,21 +228,43 @@ export default function DomainPage() {
|
|||||||
<CardDescription>管理并监控您的域名到期状态。</CardDescription>
|
<CardDescription>管理并监控您的域名到期状态。</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" size="icon" onClick={() => mutate()} disabled={isLoading}>
|
<Button
|
||||||
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleRefreshAll}
|
||||||
|
disabled={isValidating}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 ${isValidating ? "animate-spin" : ""}`} />
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
<Dialog open={isAddModalOpen} onOpenChange={setIsAddModalOpen}>
|
||||||
<DialogTrigger asChild><Button><PlusCircle className="mr-2 h-4 w-4" />添加域名</Button></DialogTrigger>
|
<DialogTrigger asChild>
|
||||||
|
<Button>
|
||||||
|
<PlusCircle className="mr-2 h-4 w-4" />
|
||||||
|
添加域名
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>添加新域名</DialogTitle>
|
<DialogTitle>添加新域名</DialogTitle>
|
||||||
<DialogDescription>输入您需要监控的域名,例如 "example.com"。</DialogDescription>
|
<DialogDescription>
|
||||||
|
输入您需要监控的域名,例如 "example.com"。
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<Input value={newDomainName} onChange={(e) => setNewDomainName(e.target.value)} placeholder="your-domain.com" onKeyUp={(e) => e.key === 'Enter' && handleAddDomain()} />
|
<Input
|
||||||
|
value={newDomainName}
|
||||||
|
onChange={(e) => setNewDomainName(e.target.value)}
|
||||||
|
placeholder="your-domain.com"
|
||||||
|
onKeyUp={(e) => e.key === "Enter" && handleAddDomain()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="secondary" onClick={() => setIsAddModalOpen(false)}>取消</Button>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setIsAddModalOpen(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
<Button onClick={handleAddDomain}>提交</Button>
|
<Button onClick={handleAddDomain}>提交</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -196,7 +272,9 @@ export default function DomainPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{isLoading ? ( <div className="text-center py-10 text-muted-foreground">加载中...</div> ) : (
|
{isLoading ? (
|
||||||
|
<div className="text-center py-10 text-muted-foreground">加载中...</div>
|
||||||
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -210,9 +288,15 @@ export default function DomainPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{domains.map((domain) => (
|
{domains.map((domain) => (
|
||||||
<TableRow key={domain.ID}>
|
<TableRow key={domain.ID}>
|
||||||
<TableCell className="font-medium">{domain.Domain}</TableCell>
|
<TableCell className="font-medium">
|
||||||
<TableCell><Badge variant={getStatusVariant(domain.Status)}>{domain.Status}</Badge></TableCell>
|
{domain.Domain}
|
||||||
<TableCell>{domain.expires_in_days ?? 'N/A'}</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={getStatusVariant(domain.Status)}>
|
||||||
|
{domain.Status}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{domain.expires_in_days ?? "N/A"}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Switch
|
<Switch
|
||||||
checked={domain.IsPublic}
|
checked={domain.IsPublic}
|
||||||
@@ -221,12 +305,43 @@ export default function DomainPage() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild><Button variant="ghost" size="icon"><MoreVertical className="h-4 w-4" /></Button></DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MoreVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{domain.Status === 'pending' && (<DropdownMenuItem onClick={() => handleVerify(domain.ID)}><CheckCircle className="mr-2 h-4 w-4" /> 验证</DropdownMenuItem>)}
|
{domain.Status === "pending" && (
|
||||||
{domain.Status === 'verified' && (<DropdownMenuItem onClick={() => handleSyncWhois(domain.ID)}><RefreshCcw className="mr-2 h-4 w-4" /> 同步 Whois</DropdownMenuItem>)}
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem onClick={() => handleEditClick(domain)}><Edit className="mr-2 h-4 w-4" /> 编辑</DropdownMenuItem>
|
onClick={() => handleVerify(domain.ID)}
|
||||||
<DropdownMenuItem className="text-red-600" onClick={() => handleDelete(domain.ID, domain.Domain)}><Trash2 className="mr-2 h-4 w-4" /> 删除</DropdownMenuItem>
|
>
|
||||||
|
<CheckCircle className="mr-2 h-4 w-4" />{" "}
|
||||||
|
验证
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{domain.Status === "verified" && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handleSyncWhois(domain.ID)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RefreshCcw className="mr-2 h-4 w-4" />{" "}
|
||||||
|
同步 Whois
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleEditClick(domain)}
|
||||||
|
>
|
||||||
|
<Edit className="mr-2 h-4 w-4" /> 编辑
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-red-600"
|
||||||
|
onClick={() =>
|
||||||
|
handleDelete(domain.ID, domain.Domain)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 h-4 w-4" /> 删除
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -239,23 +354,41 @@ export default function DomainPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 验证信息弹窗 */}
|
{/* 验证信息弹窗 */}
|
||||||
<Dialog open={isVerificationInfoModalOpen} onOpenChange={setIsVerificationInfoModalOpen}>
|
<Dialog
|
||||||
|
open={isVerificationInfoModalOpen}
|
||||||
|
onOpenChange={setIsVerificationInfoModalOpen}
|
||||||
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>请验证域名所有权</DialogTitle>
|
<DialogTitle>请验证域名所有权</DialogTitle>
|
||||||
<DialogDescription>为了开始监控,请为您的域名添加一条 DNS TXT 记录。</DialogDescription>
|
<DialogDescription>
|
||||||
|
为了开始监控,请为您的域名添加一条 DNS TXT 记录。
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="py-4 space-y-2">
|
<div className="py-4 space-y-2">
|
||||||
<p>请将以下内容添加到您的 DNS 解析记录中:</p>
|
<p>请将以下内容添加到您的 DNS 解析记录中:</p>
|
||||||
<div className="p-2 bg-muted rounded-md text-sm">
|
<div className="p-2 bg-muted rounded-md text-sm">
|
||||||
<p><span className="font-semibold">类型:</span> TXT</p>
|
<p>
|
||||||
<p><span className="font-semibold">主机/名称:</span> @</p>
|
<span className="font-semibold">类型:</span> TXT
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-semibold">主机/名称:</span> @
|
||||||
|
</p>
|
||||||
<p className="font-semibold">记录值:</p>
|
<p className="font-semibold">记录值:</p>
|
||||||
<p className="font-mono bg-background p-2 rounded">{verificationToken}</p>
|
<p className="font-mono bg-background p-2 rounded">
|
||||||
|
{verificationToken}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">DNS 记录生效可能需要几分钟到几小时不等。生效后,请回到域名列表点击“验证”。</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
|
DNS
|
||||||
|
记录生效可能需要几分钟到几小时不等。生效后,请回到域名列表点击“验证”。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter><Button onClick={() => setIsVerificationInfoModalOpen(false)}>我明白了</Button></DialogFooter>
|
<DialogFooter>
|
||||||
|
<Button onClick={() => setIsVerificationInfoModalOpen(false)}>
|
||||||
|
我明白了
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
@@ -264,17 +397,79 @@ export default function DomainPage() {
|
|||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>编辑域名信息</DialogTitle>
|
<DialogTitle>编辑域名信息</DialogTitle>
|
||||||
<DialogDescription>为 <span className="font-mono">{currentDomain?.Domain}</span> 添加或修改详细信息。</DialogDescription>
|
<DialogDescription>
|
||||||
|
为 <span className="font-mono">{currentDomain?.Domain}</span>{" "}
|
||||||
|
添加或修改详细信息。
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="registrar" className="text-right">注册商</Label><Input id="registrar" name="registrar" value={editFormData.registrar || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="registeredDate" className="text-right">注册日期</Label><Input id="registeredDate" name="registeredDate" type="date" value={editFormData.registeredDate?.split('T')[0] || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
|
<Label htmlFor="registrar" className="text-right">
|
||||||
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="endDate" className="text-right">到期日期</Label><Input id="endDate" name="endDate" type="date" value={editFormData.endDate?.split('T')[0] || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
|
注册商
|
||||||
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="renewalPrice" className="text-right">续费价格</Label><Input id="renewalPrice" name="renewalPrice" value={editFormData.renewalPrice || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
|
</Label>
|
||||||
<div className="grid grid-cols-4 items-center gap-4"><Label htmlFor="notes" className="text-right">备注</Label><Textarea id="notes" name="notes" value={editFormData.notes || ''} onChange={handleEditFormChange} className="col-span-3" /></div>
|
<Input
|
||||||
|
id="registrar"
|
||||||
|
name="registrar"
|
||||||
|
value={editFormData.registrar || ""}
|
||||||
|
onChange={handleEditFormChange}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="registeredDate" className="text-right">
|
||||||
|
注册日期
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="registeredDate"
|
||||||
|
name="registeredDate"
|
||||||
|
type="date"
|
||||||
|
value={editFormData.registeredDate?.split("T")[0] || ""}
|
||||||
|
onChange={handleEditFormChange}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="endDate" className="text-right">
|
||||||
|
到期日期
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="endDate"
|
||||||
|
name="endDate"
|
||||||
|
type="date"
|
||||||
|
value={editFormData.endDate?.split("T")[0] || ""}
|
||||||
|
onChange={handleEditFormChange}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="renewalPrice" className="text-right">
|
||||||
|
续费价格
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="renewalPrice"
|
||||||
|
name="renewalPrice"
|
||||||
|
value={editFormData.renewalPrice || ""}
|
||||||
|
onChange={handleEditFormChange}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label htmlFor="notes" className="text-right">
|
||||||
|
备注
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="notes"
|
||||||
|
name="notes"
|
||||||
|
value={editFormData.notes || ""}
|
||||||
|
onChange={handleEditFormChange}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="secondary" onClick={() => setIsEditModalOpen(false)}>取消</Button>
|
<Button variant="secondary" onClick={() => setIsEditModalOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
<Button onClick={handleUpdateDomain}>保存</Button>
|
<Button onClick={handleUpdateDomain}>保存</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
+2
-2
@@ -106,7 +106,7 @@ export default function NATPage() {
|
|||||||
return (
|
return (
|
||||||
<ActionButtonGroup
|
<ActionButtonGroup
|
||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{ fn: deleteNAT, id: s.id, mutate: mutate }}
|
delete={{ fn: deleteNAT, id: s.id!, mutate: mutate }}
|
||||||
>
|
>
|
||||||
<NATCard mutate={mutate} data={s} />
|
<NATCard mutate={mutate} data={s} />
|
||||||
</ActionButtonGroup>
|
</ActionButtonGroup>
|
||||||
@@ -135,7 +135,7 @@ export default function NATPage() {
|
|||||||
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteNAT,
|
fn: deleteNAT,
|
||||||
id: selectedRows.map((r) => r.original.id),
|
id: selectedRows.map((r) => r.original.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function NotificationGroupPage() {
|
|||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteNotificationGroups,
|
fn: deleteNotificationGroups,
|
||||||
id: s.group.id,
|
id: s.group.id!,
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -128,7 +128,7 @@ export default function NotificationGroupPage() {
|
|||||||
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteNotificationGroups,
|
fn: deleteNotificationGroups,
|
||||||
id: selectedRows.map((r) => r.original.group.id),
|
id: selectedRows.map((r) => r.original.group.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export default function NotificationPage() {
|
|||||||
accessorFn: (row) => {
|
accessorFn: (row) => {
|
||||||
return (
|
return (
|
||||||
notifierGroup
|
notifierGroup
|
||||||
?.filter((ng) => ng.notifications?.includes(row.id))
|
?.filter((ng) => ng.notifications?.includes(row.id!))
|
||||||
.map((ng) => ng.group.id) || []
|
.map((ng) => ng.group.id) || []
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -112,7 +112,7 @@ export default function NotificationPage() {
|
|||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteNotification,
|
fn: deleteNotification,
|
||||||
id: s.id,
|
id: s.id!,
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -143,7 +143,7 @@ export default function NotificationPage() {
|
|||||||
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteNotification,
|
fn: deleteNotification,
|
||||||
id: selectedRows.map((r) => r.original.id),
|
id: selectedRows.map((r) => r.original.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
import { useAuth } from "@/hooks/useAuth"
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import { ModelOnlineUser, ModelOnlineUserApi } from "@/types"
|
import { GithubComNezhahqNezhaModelPaginatedResponseArrayModelOnlineUserModelOnlineUser, ModelOnlineUser } from "@/types"
|
||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||||
import { useEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -40,7 +40,7 @@ export default function OnlineUserPage() {
|
|||||||
// 计算 offset
|
// 计算 offset
|
||||||
const offset = (page - 1) * pageSize
|
const offset = (page - 1) * pageSize
|
||||||
|
|
||||||
const { data, mutate, error, isLoading } = useSWR<ModelOnlineUserApi, Error>(
|
const { data, mutate, error, isLoading } = useSWR<GithubComNezhahqNezhaModelPaginatedResponseArrayModelOnlineUserModelOnlineUser, Error>(
|
||||||
`/api/v1/online-user?offset=${offset}&limit=${pageSize}`,
|
`/api/v1/online-user?offset=${offset}&limit=${pageSize}`,
|
||||||
swrFetcher,
|
swrFetcher,
|
||||||
)
|
)
|
||||||
@@ -94,7 +94,7 @@ export default function OnlineUserPage() {
|
|||||||
accessorFn: (row) => row.connected_at,
|
accessorFn: (row) => row.connected_at,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original
|
const s = row.original
|
||||||
const date = new Date(s.connected_at)
|
const date = new Date(s.connected_at!)
|
||||||
return <span>{date.toISOString()}</span>
|
return <span>{date.toISOString()}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -125,7 +125,7 @@ export default function OnlineUserPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dataCache = useMemo(() => {
|
const dataCache = useMemo(() => {
|
||||||
return data?.value ?? []
|
return data?.data?.value ?? []
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
const table = useReactTable<ModelOnlineUser>({
|
const table = useReactTable<ModelOnlineUser>({
|
||||||
@@ -137,9 +137,9 @@ export default function OnlineUserPage() {
|
|||||||
const selectedRows = table.getSelectedRowModel().rows
|
const selectedRows = table.getSelectedRowModel().rows
|
||||||
|
|
||||||
const renderPagination = () => {
|
const renderPagination = () => {
|
||||||
if (!data?.pagination) return null
|
if (!data?.data?.pagination) return null
|
||||||
|
|
||||||
const { total } = data.pagination
|
const { total = 0 } = data.data.pagination
|
||||||
const totalPages = Math.ceil(total / pageSize)
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
|
|||||||
+4
-2
@@ -36,8 +36,10 @@ export default function Root() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||||
<section className="text-sm mx-auto h-full flex flex-col justify-between">
|
<section
|
||||||
<div>
|
className="text-sm mx-auto h-full flex flex-col justify-between relative z-10 bg-background"
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default function ServerGroupPage() {
|
|||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteServerGroups,
|
fn: deleteServerGroups,
|
||||||
id: s.group.id,
|
id: s.group.id!,
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -128,7 +128,7 @@ export default function ServerGroupPage() {
|
|||||||
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
className="flex ml-auto self-end sm:self-auto gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteServerGroups,
|
fn: deleteServerGroups,
|
||||||
id: selectedRows.map((r) => r.original.group.id),
|
id: selectedRows.map((r) => r.original.group.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -212,7 +212,9 @@ export default function ServerPage() {
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<BatchMoveServerIcon serverIds={selectedRows.map((r) => r.original.id) as number[]} />
|
<BatchMoveServerIcon
|
||||||
|
serverIds={selectedRows.map((r) => r.original.id) as number[]}
|
||||||
|
/>
|
||||||
<ServerConfigCardBatch
|
<ServerConfigCardBatch
|
||||||
sid={selectedRows.map((r) => r.original.id) as number[]}
|
sid={selectedRows.map((r) => r.original.id) as number[]}
|
||||||
className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-yellow-600 text-white hover:bg-yellow-500 dark:hover:bg-yellow-700 rounded-lg"
|
className="shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] bg-yellow-600 text-white hover:bg-yellow-500 dark:hover:bg-yellow-700 rounded-lg"
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export default function ServicePage() {
|
|||||||
return (
|
return (
|
||||||
<ActionButtonGroup
|
<ActionButtonGroup
|
||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{ fn: deleteService, id: s.id, mutate: mutate }}
|
delete={{ fn: deleteService, id: s.id!, mutate: mutate }}
|
||||||
>
|
>
|
||||||
<ServiceCard mutate={mutate} data={s} />
|
<ServiceCard mutate={mutate} data={s} />
|
||||||
</ActionButtonGroup>
|
</ActionButtonGroup>
|
||||||
@@ -181,7 +181,7 @@ export default function ServicePage() {
|
|||||||
className="flex gap-2 flex-wrap shrink-0"
|
className="flex gap-2 flex-wrap shrink-0"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteService,
|
fn: deleteService,
|
||||||
id: selectedRows.map((r) => r.original.id),
|
id: selectedRows.map((r) => r.original.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+51
-7
@@ -3,6 +3,7 @@ import { SettingsTab } from "@/components/settings-tab"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { Combobox } from "@/components/ui/combobox"
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -21,9 +22,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { Combobox } from "@/components/ui/combobox"
|
|
||||||
import { useNotification } from "@/hooks/useNotfication"
|
|
||||||
import { useAuth } from "@/hooks/useAuth"
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
|
import { useNotification } from "@/hooks/useNotfication"
|
||||||
import useSetting from "@/hooks/useSetting"
|
import useSetting from "@/hooks/useSetting"
|
||||||
import { asOptionalField } from "@/lib/utils"
|
import { asOptionalField } from "@/lib/utils"
|
||||||
import { nezhaLang, settingCoverageTypes } from "@/types"
|
import { nezhaLang, settingCoverageTypes } from "@/types"
|
||||||
@@ -57,6 +57,8 @@ const settingFormSchema = z.object({
|
|||||||
custom_links: asOptionalField(z.string()),
|
custom_links: asOptionalField(z.string()),
|
||||||
background_image_day: asOptionalField(z.string()),
|
background_image_day: asOptionalField(z.string()),
|
||||||
background_image_night: asOptionalField(z.string()),
|
background_image_night: asOptionalField(z.string()),
|
||||||
|
telegram_bot_token: asOptionalField(z.string()),
|
||||||
|
telegram_admin_chat_id: asOptionalField(z.string()),
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -151,7 +153,11 @@ export default function SettingsPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Expiry Notification Group ID</FormLabel>
|
<FormLabel>Expiry Notification Group ID</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="Enter Group ID" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter Group ID"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -177,7 +183,10 @@ export default function SettingsPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Custom Logo URL</FormLabel>
|
<FormLabel>Custom Logo URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="https://example.com/logo.png" {...field} />
|
<Input
|
||||||
|
placeholder="https://example.com/logo.png"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -203,7 +212,10 @@ export default function SettingsPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Custom Links (JSON Array)</FormLabel>
|
<FormLabel>Custom Links (JSON Array)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder='[{"link":"https://loohui.com/","name":"Blog","blank":false}]' {...field} />
|
<Input
|
||||||
|
placeholder='[{"link":"https://loohui.com/","name":"Blog","blank":false}]'
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -216,7 +228,10 @@ export default function SettingsPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Background Image (Day)</FormLabel>
|
<FormLabel>Background Image (Day)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="https://example.com/day.jpg" {...field} />
|
<Input
|
||||||
|
placeholder="https://example.com/day.jpg"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -229,7 +244,36 @@ export default function SettingsPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Background Image (Night)</FormLabel>
|
<FormLabel>Background Image (Night)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="https://example.com/night.jpg" {...field} />
|
<Input
|
||||||
|
placeholder="https://example.com/night.jpg"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="telegram_bot_token"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Telegram Bot Token</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="123456789:ABCDEF..." {...field} value={field.value as string || ""} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="telegram_admin_chat_id"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Telegram Admin Chat ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="12345678" {...field} value={field.value as string || ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
+2
-2
@@ -90,7 +90,7 @@ export default function UserPage() {
|
|||||||
className="flex gap-2"
|
className="flex gap-2"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteUser,
|
fn: deleteUser,
|
||||||
id: s.id,
|
id: s.id!,
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -121,7 +121,7 @@ export default function UserPage() {
|
|||||||
className="flex-2 flex gap-2 ml-auto"
|
className="flex-2 flex gap-2 ml-auto"
|
||||||
delete={{
|
delete={{
|
||||||
fn: deleteUser,
|
fn: deleteUser,
|
||||||
id: selectedRows.map((r) => r.original.id),
|
id: selectedRows.map((r) => r.original.id!),
|
||||||
mutate: mutate,
|
mutate: mutate,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+3
-3
@@ -98,13 +98,13 @@ export default function WAFPage() {
|
|||||||
header: t("LastBlockReason"),
|
header: t("LastBlockReason"),
|
||||||
accessorKey: "lastBlockReason",
|
accessorKey: "lastBlockReason",
|
||||||
accessorFn: (row) => row.block_reason,
|
accessorFn: (row) => row.block_reason,
|
||||||
cell: ({ row }) => <span>{wafBlockReasons[row.original.block_reason] || ""}</span>,
|
cell: ({ row }) => <span>{wafBlockReasons[row.original.block_reason!] || ""}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: t("BlockIdentifier"),
|
header: t("BlockIdentifier"),
|
||||||
accessorKey: "BlockIdentifier",
|
accessorKey: "BlockIdentifier",
|
||||||
accessorFn: (row) => {
|
accessorFn: (row) => {
|
||||||
return wafBlockIdentifiers[row.block_identifier] || row.block_identifier
|
return wafBlockIdentifiers[row.block_identifier!] || row.block_identifier
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -158,7 +158,7 @@ export default function WAFPage() {
|
|||||||
const renderPagination = () => {
|
const renderPagination = () => {
|
||||||
if (!data?.pagination) return null
|
if (!data?.pagination) return null
|
||||||
|
|
||||||
const { total } = data.pagination
|
const { total = 0 } = data.pagination
|
||||||
const totalPages = Math.ceil(total / pageSize)
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
|
|||||||
+453
-444
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
export interface BillingDataMod {
|
||||||
|
registrar?: string;
|
||||||
|
registeredDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
renewalPrice?: string;
|
||||||
|
autoRenewal?: string;
|
||||||
|
notes?: string;
|
||||||
|
cycle?: string;
|
||||||
|
amount?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Domain {
|
||||||
|
ID: number;
|
||||||
|
Domain: string;
|
||||||
|
Status: string;
|
||||||
|
VerifyToken: string;
|
||||||
|
IsPublic: boolean;
|
||||||
|
BillingData: BillingDataMod | null;
|
||||||
|
expires_in_days?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
IPReportPeriod int `json:"ip_report_period"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
jsonData := `{"ip_report_period":30, "unknown_field": 123}`
|
||||||
|
var c Config
|
||||||
|
|
||||||
|
dec := json.NewDecoder(strings.NewReader(jsonData))
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
err := dec.Decode(&c)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Success")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user