diff --git a/package.json b/package.json index 61ba2c4..1999dcd 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@auth/prisma-adapter": "^2.10.0", "@hookform/resolvers": "^5.2.1", "@prisma/client": "^6.15.0", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", @@ -52,6 +53,5 @@ "tw-animate-css": "^1.3.7", "typescript": "^5", "zod": "^4.1.5" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } } \ No newline at end of file diff --git a/prisma/init.sql b/prisma/init.sql index 039da3c..70f3e1e 100644 --- a/prisma/init.sql +++ b/prisma/init.sql @@ -39,7 +39,6 @@ CREATE TABLE `users` ( `password` varchar(191) NOT NULL, `phone` varchar(191) NOT NULL, `updatedAt` datetime(3) NOT NULL, - `verifiedPhone` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `users_phone_key` (`phone`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/prisma/prisma.config.ts b/prisma/prisma.config.ts deleted file mode 100644 index 3c17c9d..0000000 --- a/prisma/prisma.config.ts +++ /dev/null @@ -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 -} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0464fe4..476a30f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -124,10 +124,9 @@ model Account { model User { id Int @id @default(autoincrement()) - phone String @unique + account String @unique password String name String? - verifiedPhone Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sessions Session[] diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index ed699e8..21466bd 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -8,16 +8,13 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' 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 { toast, Toaster } from 'sonner' const formSchema = z.object({ - phone: z.string() - .min(11, '手机号必须是11位') - .max(11, '手机号必须是11位') - .regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'), - password: z.string().min(6, '密码至少需要6个字符'), + account: z.string().min(3, '账号至少需要3个字符'), + password: z.string().min(6, '密码至少需要6个字符'), }) export default function LoginPage() { @@ -27,7 +24,7 @@ export default function LoginPage() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - phone: '', + account: '', password: '', }, }) @@ -78,18 +75,17 @@ export default function LoginPage() {
( - 手机号 + 账号
- +
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index a26ecc4..1d4d913 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -4,23 +4,22 @@ import { compare } from 'bcryptjs' import { z } from 'zod' const loginSchema = z.object({ - phone: z.string() - .min(11, '手机号必须是11位') - .max(11, '手机号必须是11位') - .regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'), + account: z.string().min(3, '账号至少需要3个字符'), password: z.string().min(6, '密码至少需要6个字符'), }) export async function POST(request: Request) { try { const body = await request.json() - const { phone, password } = loginSchema.parse(body) + const { account, password } = loginSchema.parse(body) - // 查找用户 - 使用正确的查询方式 - const user = await prisma.user.findUnique({ + const user = await prisma.user.findFirst({ where: { - phone: phone.trim() // 去除空格 + OR: [ + { account: account.trim() }, + { password: account.trim() } + ] }, }) @@ -57,7 +56,7 @@ export async function POST(request: Request) { success: true, user: { id: user.id, - phone: user.phone, + account: user.account, name: user.name } }) diff --git a/src/app/api/users/route.tsx b/src/app/api/users/route.tsx new file mode 100644 index 0000000..303031b --- /dev/null +++ b/src/app/api/users/route.tsx @@ -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 } + ) + } +} diff --git a/src/app/dashboard/components/settings.tsx b/src/app/dashboard/components/settings.tsx new file mode 100644 index 0000000..2de8dc4 --- /dev/null +++ b/src/app/dashboard/components/settings.tsx @@ -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([]) + const [searchTerm, setSearchTerm] = useState('') + const [isCreateMode, setIsCreateMode] = useState(false) + + const form = useForm>({ + 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) { + 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 ( +
+
+
+

用户管理

+ +
+ + {isCreateMode && ( + + + 添加用户账号 + 创建新的系统用户账户 + + + + + ( + + 账号 + +
+ + +
+
+ +
+ )} + /> + + ( + + 密码 + +
+ + +
+
+ +
+ )} + /> + + ( + + 确认密码 + +
+ + +
+
+ +
+ )} + /> + +
+ + +
+ + +
+
+ )} + + + + 用户列表 + + 管理系统中的所有用户账户 + +
+ + setSearchTerm(e.target.value)} + /> +
+
+ + + + + 账号 + 创建时间 + 操作 + + + + {filteredUsers.length === 0 ? ( + + + 暂无用户数据 + + + ) : ( + filteredUsers.map((user) => ( + + {user.account} + {new Date(user.createdAt).toLocaleDateString()} + +
+ +
+
+
+ )) + )} +
+
+
+
+
+ +
+ ) +} \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 8b9d94d..c211554 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,11 +1,12 @@ 'use client' import { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import Gatewayinfo from './components/gatewayinfo' import GatewayConfig from './components/gatewayConfig' import CityNodeStats from './components/cityNodeStats' import AllocationStatus from './components/allocationStatus' +import Settings from './components/settings' import Edge from './components/edge' import { LogOut } from 'lucide-react' @@ -14,24 +15,23 @@ const tabs = [ { id: 'gateway', label: '网关配置' }, { id: 'city', label: '城市信息' }, { id: 'allocation', label: '分配状态' }, - { id: 'edge', label: '节点信息' } + { id: 'edge', label: '节点信息' }, + { id: 'setting', label: '设置'} ] export default function Dashboard() { const [activeTab, setActiveTab] = useState('gatewayInfo') const [isLoading, setIsLoading] = useState(false) const router = useRouter() + const searchParams = useSearchParams() - // 从 URL 中获取 tab 参数 + // 监听URL参数变化 useEffect(() => { - if (typeof window !== 'undefined') { - const urlParams = new URLSearchParams(window.location.search) - const urlTab = urlParams.get('tab') - if (urlTab && tabs.some(tab => tab.id === urlTab)) { - setActiveTab(urlTab) - } + const urlTab = searchParams.get('tab') + if (urlTab && tabs.some(tab => tab.id === urlTab)) { + setActiveTab(urlTab) } - }, []) + }, [searchParams]) // 退出登录 const handleLogout = async () => { @@ -58,7 +58,9 @@ export default function Dashboard() { const handleTabClick = (tabId: string) => { setActiveTab(tabId) // 更新 URL 参数 - router.push(`/dashboard?tab=${tabId}`) + const params = new URLSearchParams(searchParams.toString()) + params.set('tab', tabId) + router.push(`/dashboard?${params.toString()}`) } return ( @@ -108,6 +110,7 @@ export default function Dashboard() { {activeTab === 'city' && } {activeTab === 'allocation' && } {activeTab === 'edge' && } + {activeTab === 'setting' && } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..d9ccec9 --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -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) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/src/types/auth.d.ts b/src/types/auth.d.ts deleted file mode 100644 index 7f5d47e..0000000 --- a/src/types/auth.d.ts +++ /dev/null @@ -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 -}