修复动静态ip标题跳转问题 & 添加admin页面

This commit is contained in:
Eamon
2026-08-18 13:33:22 +08:00
parent 1336a550fe
commit fc034dc971
35 changed files with 2516 additions and 524 deletions

View File

@@ -1,5 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Eye, EyeOff } from "lucide-react"
import { useState } from "react"
import { useEffect, useState } from "react"
import { Controller, useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Dialog,
@@ -7,45 +10,70 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { FieldError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export type EditInfoValues = {
username: string
taobao: string
password: string
email: string
wechat: string
qq: string
}
const editInfoSchema = z.object({
username: z.string().trim().min(1, "请输入用户名"),
taobao: z.string(),
password: z.string().min(1, "请输入密码"),
email: z
.string()
.refine(
v => !v || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
"请输入正确的邮箱",
),
wechat: z.string(),
qq: z.string(),
})
interface EditInfoDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
initialData?: {
username: string
taobao: string
password: string
email: string
wechat: string
qq: string
}
onSave?: (data: any) => void
initialData?: EditInfoValues
onSave?: (data: EditInfoValues) => void
}
const DEFAULT_INITIAL_DATA: EditInfoValues = {
username: "191****1234",
taobao: "191****1234",
password: "********",
email: "123456123@qq.com",
wechat: "",
qq: "",
}
export function EditInfoDialog({
open,
onOpenChange,
initialData = {
username: "191****1234",
taobao: "191****1234",
password: "********",
email: "123456123@qq.com",
wechat: "",
qq: "",
},
initialData = DEFAULT_INITIAL_DATA,
onSave,
}: EditInfoDialogProps) {
const [formData, setFormData] = useState(initialData)
const [showPassword, setShowPassword] = useState(false)
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const form = useForm<EditInfoValues>({
resolver: zodResolver(editInfoSchema),
defaultValues: initialData,
})
const handleSubmit = () => {
onSave?.(formData)
useEffect(() => {
if (open) {
form.reset(initialData)
}
}, [open, initialData, form])
const onSubmit = (data: EditInfoValues) => {
onSave?.(data)
onOpenChange(false)
}
@@ -58,91 +86,123 @@ export function EditInfoDialog({
</DialogTitle>
</DialogHeader>
<div className="space-y-4 mt-2">
{/* 用户名 */}
<form className="space-y-4 mt-2" onSubmit={form.handleSubmit(onSubmit)}>
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">用户名</Label>
<Input
value={formData.username}
onChange={e => handleChange("username", e.target.value)}
className="h-11 bg-gray-50 border-gray-200"
<Controller
name="username"
control={form.control}
render={({ field, fieldState }) => (
<>
<Input
{...field}
className="h-11 bg-gray-50 border-gray-200"
/>
<FieldError>{fieldState.error?.message}</FieldError>
</>
)}
/>
</div>
{/* 淘宝会员名 */}
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">淘宝会员名</Label>
<Input
value={formData.taobao}
onChange={e => handleChange("taobao", e.target.value)}
className="h-11 bg-gray-50 border-gray-200"
<Controller
name="taobao"
control={form.control}
render={({ field }) => (
<Input
{...field}
className="h-11 bg-gray-50 border-gray-200"
/>
)}
/>
</div>
{/* 密码 */}
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">密码</Label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={e => handleChange("password", e.target.value)}
className="h-11 bg-gray-50 border-gray-200 pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
<Controller
name="password"
control={form.control}
render={({ field, fieldState }) => (
<>
<div className="relative">
<Input
{...field}
type={showPassword ? "text" : "password"}
className="h-11 bg-gray-50 border-gray-200 pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
<FieldError>{fieldState.error?.message}</FieldError>
</>
)}
/>
</div>
{/* 邮箱 */}
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">邮箱</Label>
<Input
value={formData.email}
onChange={e => handleChange("email", e.target.value)}
className="h-11 bg-gray-50 border-gray-200"
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<>
<Input
{...field}
className="h-11 bg-gray-50 border-gray-200"
/>
<FieldError>{fieldState.error?.message}</FieldError>
</>
)}
/>
</div>
{/* 微信 */}
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">微信</Label>
<Input
value={formData.wechat}
onChange={e => handleChange("wechat", e.target.value)}
placeholder="请输入"
className="h-11 bg-gray-50 border-gray-200"
<Controller
name="wechat"
control={form.control}
render={({ field }) => (
<Input
{...field}
placeholder="请输入"
className="h-11 bg-gray-50 border-gray-200"
/>
)}
/>
</div>
{/* QQ */}
<div className="space-y-1.5">
<Label className="text-sm text-gray-600">QQ</Label>
<Input
value={formData.qq}
onChange={e => handleChange("qq", e.target.value)}
placeholder="请输入"
className="h-11 bg-gray-50 border-gray-200"
<Controller
name="qq"
control={form.control}
render={({ field }) => (
<Input
{...field}
placeholder="请输入"
className="h-11 bg-gray-50 border-gray-200"
/>
)}
/>
</div>
{/* 保存按钮 */}
<Button
onClick={handleSubmit}
type="submit"
className="w-full h-11 mt-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
>
保存
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)

View File

@@ -27,14 +27,12 @@ export type DataTableProps<T> = {
headRow?: string
dataRow?: string
}
/** 是否开启行选择复选框 */
enableSelection?: boolean
/** 受控选中状态 */
rowSelection?: Record<string, boolean>
/** 选中状态变化回调 */
onSelectionChange?: (selection: Record<string, boolean>) => void
/** 行唯一ID生成函数 */
getRowId?: (row: T, index: number) => string
stickyHeader?: boolean
maxHeight?: string
}
export default function DataTable<T extends Record<string, unknown>>(
@@ -112,8 +110,18 @@ export default function DataTable<T extends Record<string, unknown>>(
<>
{/* 数据表 */}
<div className="rounded-md relative bg-card">
<TableRoot>
<TableHeader>
<TableRoot
containerClassName={
props.stickyHeader
? cn("overflow-y-auto", props.maxHeight ?? "max-h-[600px]")
: undefined
}
>
<TableHeader
className={
props.stickyHeader ? "sticky top-0 z-10 bg-card" : undefined
}
>
{table.getHeaderGroups().map(group => (
<TableRow key={group.id} className={props.classNames?.headRow}>
{group.headers.map(header => (

View File

@@ -49,7 +49,7 @@ export default function ProductDropdown({
title="动静态IP"
desc="千万级动态短效IP,自动去重,高并发采集"
tag="热门"
onClick={() => handleClick("/product?tab=short")}
onClick={() => handleClick("/product")}
/>
<ProductCard
size={s}
@@ -87,7 +87,7 @@ export default function ProductDropdown({
>
<span className="text-xs font-medium text-gray-400">热门推荐:</span>
<button
onClick={() => handleClick("/product?tab=short")}
onClick={() => handleClick("/product")}
className={cn(
"text-blue-600 hover:underline whitespace-nowrap",
s.footerLink,

View File

@@ -2,11 +2,15 @@ import type * as React from "react"
import { cn } from "@/lib/utils/index"
function Table({ className, ...props }: React.ComponentProps<"table">) {
function Table({
className,
containerClassName,
...props
}: React.ComponentProps<"table"> & { containerClassName?: string }) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
className={cn("relative w-full overflow-x-auto", containerClassName)}
>
<table
data-slot="table"