添加设置模块修复网关信息MAC地址跳转问题,删除冗余的文件和代码
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"@auth/prisma-adapter": "^2.10.0",
|
"@auth/prisma-adapter": "^2.10.0",
|
||||||
"@hookform/resolvers": "^5.2.1",
|
"@hookform/resolvers": "^5.2.1",
|
||||||
"@prisma/client": "^6.15.0",
|
"@prisma/client": "^6.15.0",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
@@ -52,6 +53,5 @@
|
|||||||
"tw-animate-css": "^1.3.7",
|
"tw-animate-css": "^1.3.7",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,6 @@ CREATE TABLE `users` (
|
|||||||
`password` varchar(191) NOT NULL,
|
`password` varchar(191) NOT NULL,
|
||||||
`phone` varchar(191) NOT NULL,
|
`phone` varchar(191) NOT NULL,
|
||||||
`updatedAt` datetime(3) NOT NULL,
|
`updatedAt` datetime(3) NOT NULL,
|
||||||
`verifiedPhone` tinyint(1) NOT NULL DEFAULT 0,
|
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `users_phone_key` (`phone`)
|
UNIQUE KEY `users_phone_key` (`phone`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var cachedPrisma: PrismaClient
|
|
||||||
}
|
|
||||||
|
|
||||||
export let prisma: PrismaClient
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
prisma = new PrismaClient()
|
|
||||||
} else {
|
|
||||||
if (!global.cachedPrisma) {
|
|
||||||
global.cachedPrisma = new PrismaClient()
|
|
||||||
}
|
|
||||||
prisma = global.cachedPrisma
|
|
||||||
}
|
|
||||||
@@ -124,10 +124,9 @@ model Account {
|
|||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
phone String @unique
|
account String @unique
|
||||||
password String
|
password String
|
||||||
name String?
|
name String?
|
||||||
verifiedPhone Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
|
|||||||
@@ -8,15 +8,12 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Lock, Phone } from 'lucide-react'
|
import { Lock, User } from 'lucide-react'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { toast, Toaster } from 'sonner'
|
import { toast, Toaster } from 'sonner'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
phone: z.string()
|
account: z.string().min(3, '账号至少需要3个字符'),
|
||||||
.min(11, '手机号必须是11位')
|
|
||||||
.max(11, '手机号必须是11位')
|
|
||||||
.regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'),
|
|
||||||
password: z.string().min(6, '密码至少需要6个字符'),
|
password: z.string().min(6, '密码至少需要6个字符'),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -27,7 +24,7 @@ export default function LoginPage() {
|
|||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
phone: '',
|
account: '',
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -78,18 +75,17 @@ export default function LoginPage() {
|
|||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="phone"
|
name="account"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>手机号</FormLabel>
|
<FormLabel>账号</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Phone className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
<User className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入手机号"
|
placeholder="请输入您的账号"
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
{...field}
|
{...field}
|
||||||
maxLength={11}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -4,23 +4,22 @@ import { compare } from 'bcryptjs'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
phone: z.string()
|
account: z.string().min(3, '账号至少需要3个字符'),
|
||||||
.min(11, '手机号必须是11位')
|
|
||||||
.max(11, '手机号必须是11位')
|
|
||||||
.regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'),
|
|
||||||
password: z.string().min(6, '密码至少需要6个字符'),
|
password: z.string().min(6, '密码至少需要6个字符'),
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { phone, password } = loginSchema.parse(body)
|
const { account, password } = loginSchema.parse(body)
|
||||||
|
|
||||||
|
|
||||||
// 查找用户 - 使用正确的查询方式
|
const user = await prisma.user.findFirst({
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
where: {
|
||||||
phone: phone.trim() // 去除空格
|
OR: [
|
||||||
|
{ account: account.trim() },
|
||||||
|
{ password: account.trim() }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ export async function POST(request: Request) {
|
|||||||
success: true,
|
success: true,
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
phone: user.phone,
|
account: user.account,
|
||||||
name: user.name
|
name: user.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
132
src/app/api/users/route.tsx
Normal file
132
src/app/api/users/route.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { prisma } from '@/lib/prisma'
|
||||||
|
import { hash } from 'bcryptjs'
|
||||||
|
|
||||||
|
// 获取所有用户
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
account: true,
|
||||||
|
name: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
users
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表错误:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '服务器错误,请稍后重试' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { account, password, name } = await request.json()
|
||||||
|
|
||||||
|
// 检查用户是否已存在
|
||||||
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
where: { account }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '用户账号已存在' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
const hashedPassword = await hash(password, 10)
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
const user = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
account,
|
||||||
|
password: hashedPassword,
|
||||||
|
name: name || account,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 不返回密码字段
|
||||||
|
const { password: _, ...userWithoutPassword } = user
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
user: userWithoutPassword
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建用户错误:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '服务器错误,请稍后重试' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
// 从URL中获取查询参数
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const id = url.searchParams.get('id')
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '用户ID不能为空' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = parseInt(id)
|
||||||
|
if (isNaN(userId)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '无效的用户ID' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否存在
|
||||||
|
const existingUser = await prisma.user.findUnique({
|
||||||
|
where: { id: userId }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '用户不存在' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: { id: userId }
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '用户删除成功'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除用户错误:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '服务器错误,请稍后重试' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
333
src/app/dashboard/components/settings.tsx
Normal file
333
src/app/dashboard/components/settings.tsx
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||||
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
|
import { User, Lock, Search, Trash2, Plus, X } from 'lucide-react'
|
||||||
|
import { toast, Toaster } from 'sonner'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
|
||||||
|
// 用户类型定义
|
||||||
|
interface UserData {
|
||||||
|
id: string
|
||||||
|
account: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
account: z.string().min(3, '账号至少需要3个字符'),
|
||||||
|
password: z.string().min(6, '密码至少需要6个字符'),
|
||||||
|
confirmPassword: z.string().min(6, '密码至少需要6个字符'),
|
||||||
|
}).refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: "密码不匹配",
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default function Settings() {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [users, setUsers] = useState<UserData[]>([])
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [isCreateMode, setIsCreateMode] = useState(false)
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
account: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/users')
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || '获取用户列表失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setUsers(data.users)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("获取用户列表失败", {
|
||||||
|
description: error instanceof Error ? error.message : "服务器连接失败,请稍后重试",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载用户列表
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
account: values.account,
|
||||||
|
password: values.password,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || '创建用户失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
toast.success("用户创建成功", {
|
||||||
|
description: "新账户已成功添加",
|
||||||
|
})
|
||||||
|
form.reset()
|
||||||
|
setIsCreateMode(false)
|
||||||
|
fetchUsers() // 刷新用户列表
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("创建用户失败", {
|
||||||
|
description: error instanceof Error ? error.message : "服务器连接失败,请稍后重试",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const handleDeleteUser = async (userId: number) => {
|
||||||
|
|
||||||
|
if (!confirm('确定要删除这个用户吗?此操作不可恢复。')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用查询参数传递ID
|
||||||
|
const response = await fetch(`/api/users?id=${userId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || '删除用户失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
toast.success("用户删除成功", {
|
||||||
|
description: "用户账户已删除",
|
||||||
|
})
|
||||||
|
fetchUsers() // 刷新用户列表
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("删除用户失败", {
|
||||||
|
description: error instanceof Error ? error.message : "服务器连接失败,请稍后重试",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤用户列表
|
||||||
|
const filteredUsers = users.filter(user =>
|
||||||
|
user.account.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white p-4 md:p-8">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">用户管理</h1>
|
||||||
|
<Button onClick={() => setIsCreateMode(!isCreateMode)}>
|
||||||
|
{isCreateMode ? (
|
||||||
|
<>
|
||||||
|
<X className="mr-2 h-4 w-4" /> 取消
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Plus className="mr-2 h-4 w-4" /> 添加用户
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isCreateMode && (
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">添加用户账号</CardTitle>
|
||||||
|
<CardDescription>创建新的系统用户账户</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="account"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>账号</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="请输入需要添加的账号"
|
||||||
|
className="pl-8"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>密码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
className="pl-8"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirmPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>确认密码</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
className="pl-8"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
|
创建中...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'创建用户'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsCreateMode(false)}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>用户列表</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
管理系统中的所有用户账户
|
||||||
|
</CardDescription>
|
||||||
|
<div className="relative mt-4 max-w-sm">
|
||||||
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索用户..."
|
||||||
|
className="pl-8"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>账号</TableHead>
|
||||||
|
<TableHead>创建时间</TableHead>
|
||||||
|
<TableHead className="text-right">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredUsers.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center py-4">
|
||||||
|
暂无用户数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredUsers.map((user) => (
|
||||||
|
<TableRow key={user.id}>
|
||||||
|
<TableCell className="font-medium">{user.account}</TableCell>
|
||||||
|
<TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteUser(Number(user.id))}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Toaster richColors />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import Gatewayinfo from './components/gatewayinfo'
|
import Gatewayinfo from './components/gatewayinfo'
|
||||||
import GatewayConfig from './components/gatewayConfig'
|
import GatewayConfig from './components/gatewayConfig'
|
||||||
import CityNodeStats from './components/cityNodeStats'
|
import CityNodeStats from './components/cityNodeStats'
|
||||||
import AllocationStatus from './components/allocationStatus'
|
import AllocationStatus from './components/allocationStatus'
|
||||||
|
import Settings from './components/settings'
|
||||||
import Edge from './components/edge'
|
import Edge from './components/edge'
|
||||||
import { LogOut } from 'lucide-react'
|
import { LogOut } from 'lucide-react'
|
||||||
|
|
||||||
@@ -14,24 +15,23 @@ const tabs = [
|
|||||||
{ id: 'gateway', label: '网关配置' },
|
{ id: 'gateway', label: '网关配置' },
|
||||||
{ id: 'city', label: '城市信息' },
|
{ id: 'city', label: '城市信息' },
|
||||||
{ id: 'allocation', label: '分配状态' },
|
{ id: 'allocation', label: '分配状态' },
|
||||||
{ id: 'edge', label: '节点信息' }
|
{ id: 'edge', label: '节点信息' },
|
||||||
|
{ id: 'setting', label: '设置'}
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [activeTab, setActiveTab] = useState('gatewayInfo')
|
const [activeTab, setActiveTab] = useState('gatewayInfo')
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
// 从 URL 中获取 tab 参数
|
// 监听URL参数变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
const urlTab = searchParams.get('tab')
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
|
||||||
const urlTab = urlParams.get('tab')
|
|
||||||
if (urlTab && tabs.some(tab => tab.id === urlTab)) {
|
if (urlTab && tabs.some(tab => tab.id === urlTab)) {
|
||||||
setActiveTab(urlTab)
|
setActiveTab(urlTab)
|
||||||
}
|
}
|
||||||
}
|
}, [searchParams])
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
@@ -58,7 +58,9 @@ export default function Dashboard() {
|
|||||||
const handleTabClick = (tabId: string) => {
|
const handleTabClick = (tabId: string) => {
|
||||||
setActiveTab(tabId)
|
setActiveTab(tabId)
|
||||||
// 更新 URL 参数
|
// 更新 URL 参数
|
||||||
router.push(`/dashboard?tab=${tabId}`)
|
const params = new URLSearchParams(searchParams.toString())
|
||||||
|
params.set('tab', tabId)
|
||||||
|
router.push(`/dashboard?${params.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,6 +110,7 @@ export default function Dashboard() {
|
|||||||
{activeTab === 'city' && <CityNodeStats />}
|
{activeTab === 'city' && <CityNodeStats />}
|
||||||
{activeTab === 'allocation' && <AllocationStatus detailed />}
|
{activeTab === 'allocation' && <AllocationStatus detailed />}
|
||||||
{activeTab === 'edge' && <Edge />}
|
{activeTab === 'edge' && <Edge />}
|
||||||
|
{activeTab === 'setting' && <Settings/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
27
src/types/auth.d.ts
vendored
27
src/types/auth.d.ts
vendored
@@ -1,27 +0,0 @@
|
|||||||
export interface User {
|
|
||||||
id: number
|
|
||||||
phone: string
|
|
||||||
name?: string | null
|
|
||||||
verifiedPhone: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Session {
|
|
||||||
id: string
|
|
||||||
userId: number
|
|
||||||
expires: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LoginResponse {
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
user?: {
|
|
||||||
id: number
|
|
||||||
phone: string
|
|
||||||
name?: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegisterResponse {
|
|
||||||
success: boolean
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user