添加actions文件移除route文件
This commit is contained in:
115
src/actions/auth.ts
Normal file
115
src/actions/auth.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
'use server'
|
||||
import prisma, { User } from '@/lib/prisma' // 使用统一的prisma实例
|
||||
import { compare } from 'bcryptjs'
|
||||
import { cookies } from 'next/headers'
|
||||
import { z } from 'zod'
|
||||
|
||||
const loginSchema = z.object({
|
||||
account: z.string().min(3, '账号至少需要3个字符'),
|
||||
password: z.string().min(6, '密码至少需要6个字符'),
|
||||
})
|
||||
|
||||
export async function login(props: {
|
||||
account: string
|
||||
password: string
|
||||
}) {
|
||||
try {
|
||||
const result = loginSchema.parse(props)
|
||||
|
||||
const user: User | null = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ account: result.account.trim() },
|
||||
{ password: result.password.trim() },
|
||||
],
|
||||
},
|
||||
})
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户不存在或密码未设置',
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const passwordMatch = await compare(result.password, user.password || '')
|
||||
|
||||
if (!passwordMatch) {
|
||||
return {
|
||||
success: false,
|
||||
error: '密码错误',
|
||||
}
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
const sessionToken = crypto.randomUUID()
|
||||
await prisma.session.create({
|
||||
data: {
|
||||
id: sessionToken,
|
||||
userId: user.id,
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
})
|
||||
|
||||
// 设置cookie
|
||||
const response = {
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
account: user.account,
|
||||
name: user.name,
|
||||
},
|
||||
}
|
||||
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('session', sessionToken, {
|
||||
httpOnly: true,
|
||||
// secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: '服务器错误,请稍后重试',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
const sessionToken = cookieStore.get('session')?.value
|
||||
|
||||
// 删除数据库中的session(如果存在)
|
||||
if (sessionToken) {
|
||||
await prisma.session.deleteMany({
|
||||
where: { id: sessionToken },
|
||||
}).catch(() => {
|
||||
// 忽略删除错误,确保cookie被清除
|
||||
})
|
||||
}
|
||||
|
||||
// 清除cookie
|
||||
const response = { success: true }
|
||||
cookieStore.set('session', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0, // 立即过期
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
catch (error) {
|
||||
console.error('退出错误:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: '退出失败',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,196 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
'use server'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
// 处理 BigInt 序列化
|
||||
function safeSerialize(data: unknown) {
|
||||
return JSON.parse(JSON.stringify(data, (key, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value,
|
||||
))
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const reportType = searchParams.get('type')
|
||||
|
||||
switch (reportType) {
|
||||
case 'gateway_info':
|
||||
return await getGatewayInfo()
|
||||
case 'gateway_config':
|
||||
return await getGatewayConfig(request)
|
||||
case 'city_config_count':
|
||||
return await getCityConfigCount()
|
||||
case 'city_node_count':
|
||||
return await getCityNodeCount()
|
||||
case 'allocation_status':
|
||||
return await getAllocationStatus(request)
|
||||
case 'edge_nodes':
|
||||
return await getEdgeNodes(request)
|
||||
default:
|
||||
return NextResponse.json({ error: 'Invalid report type' }, { status: 400 })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('API Error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 获取网关基本信息
|
||||
async function getGatewayInfo() {
|
||||
try {
|
||||
const result = await prisma.$queryRaw`
|
||||
SELECT macaddr, inner_ip, setid, enable
|
||||
FROM token
|
||||
ORDER BY macaddr
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Gateway info query error:', error)
|
||||
return NextResponse.json({ error: '查询网关信息失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 网关配置
|
||||
async function getGatewayConfig(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const macAddress = searchParams.get('mac') || ''
|
||||
const offset = parseInt(searchParams.get('offset') || '0')
|
||||
const limit = parseInt(searchParams.get('limit') || '100')
|
||||
|
||||
// 定义类型接口
|
||||
interface GatewayRecord {
|
||||
edge: string | null
|
||||
city: string | null
|
||||
user: string | null
|
||||
public: string | null
|
||||
inner_ip: string | null
|
||||
ischange: boolean | number | null
|
||||
isonline: boolean | number | null
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
let totalCountQuery = ''
|
||||
let totalCountParams: (string | number)[] = []
|
||||
|
||||
if (macAddress) {
|
||||
totalCountQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM gateway
|
||||
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
|
||||
LEFT JOIN edge ON edge.macaddr = gateway.edge
|
||||
WHERE gateway.macaddr = ?
|
||||
`
|
||||
totalCountParams = [macAddress]
|
||||
}
|
||||
else {
|
||||
totalCountQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM gateway
|
||||
`
|
||||
}
|
||||
|
||||
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
|
||||
totalCountQuery,
|
||||
...totalCountParams,
|
||||
)
|
||||
const totalCount = Number(totalCountResult[0]?.total || 0)
|
||||
|
||||
// 获取分页数据
|
||||
let query = `
|
||||
select edge, city, user, public, inner_ip, ischange, isonline
|
||||
from
|
||||
gateway
|
||||
left join cityhash
|
||||
on cityhash.hash = gateway.cityhash
|
||||
left join edge
|
||||
on edge.macaddr = gateway.edge
|
||||
`
|
||||
let params: (string | number)[] = []
|
||||
|
||||
if (macAddress) {
|
||||
query += ' WHERE gateway.macaddr = ?'
|
||||
params = [macAddress]
|
||||
}
|
||||
else {
|
||||
query += ' LIMIT ? OFFSET ?'
|
||||
params.push(limit, offset)
|
||||
}
|
||||
|
||||
// 指定返回类型
|
||||
const result = await prisma.$queryRawUnsafe<GatewayRecord[]>(query, ...params)
|
||||
|
||||
return NextResponse.json({
|
||||
data: safeSerialize(result),
|
||||
totalCount: totalCount,
|
||||
currentPage: Math.floor(offset / limit) + 1,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Gateway config query error:', error)
|
||||
return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 城市节点配置数量统计
|
||||
async function getCityConfigCount() {
|
||||
try {
|
||||
const result = await prisma.$queryRaw`
|
||||
SELECT c.city, COUNT(e.id) as node_count
|
||||
FROM cityhash c
|
||||
LEFT JOIN edge e ON c.id = e.city_id
|
||||
GROUP BY c.city
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('City config count query error:', error)
|
||||
return NextResponse.json({ error: '查询城市配置失败' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// 城市节点数量分布
|
||||
async function getCityNodeCount() {
|
||||
try {
|
||||
const result = await prisma.$queryRaw`
|
||||
select c.city, c.hash, c.label, e.count, c.\`offset\`
|
||||
from
|
||||
cityhash c
|
||||
left join (
|
||||
select city_id, count(*) as count
|
||||
from
|
||||
edge
|
||||
where
|
||||
edge.active is true
|
||||
group by
|
||||
city_id
|
||||
) e
|
||||
on c.id = e.city_id
|
||||
group by
|
||||
c.hash
|
||||
order by
|
||||
count desc
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
}
|
||||
catch (error) {
|
||||
console.error('City node count query error:', error)
|
||||
return NextResponse.json({ error: '查询城市节点失败' }, { status: 500 })
|
||||
}
|
||||
export type AllocationStatus = {
|
||||
city: string
|
||||
count: bigint
|
||||
assigned: bigint
|
||||
}
|
||||
|
||||
// 城市分配状态
|
||||
async function getAllocationStatus(request: NextRequest) {
|
||||
export async function getAllocationStatus(hours: number) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const hours = searchParams.get('hours') || '24'
|
||||
const hoursNum = parseInt(hours) || 24
|
||||
|
||||
const hoursNum = hours || 24
|
||||
// 使用参数化查询防止SQL注入
|
||||
const result = await prisma.$queryRaw`
|
||||
const result: AllocationStatus[] = await prisma.$queryRaw`
|
||||
SELECT
|
||||
city,
|
||||
c1.count AS count,
|
||||
@@ -222,24 +44,185 @@ async function getAllocationStatus(request: NextRequest) {
|
||||
WHERE
|
||||
cityhash.macaddr IS NOT NULL;
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Allocation status query error:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
return NextResponse.json(
|
||||
{ error: '查询分配状态失败: ' + errorMessage },
|
||||
{ status: 500 },
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '查询分配状态失败: ' + errorMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type GatewayInfo = {
|
||||
macaddr: string
|
||||
inner_ip: string
|
||||
setid: string
|
||||
enable: number
|
||||
}
|
||||
|
||||
// 获取网关基本信息
|
||||
export async function getGatewayInfo() {
|
||||
try {
|
||||
const result: GatewayInfo[] = await prisma.$queryRaw`
|
||||
SELECT macaddr, inner_ip, setid, enable
|
||||
FROM token
|
||||
ORDER BY macaddr
|
||||
`
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Gateway info query error:', error)
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '查询网关信息失败',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type GatewayConfig = {
|
||||
city: string
|
||||
edge: string
|
||||
user: string
|
||||
public: string
|
||||
inner_ip: string
|
||||
ischange: number
|
||||
isonline: number
|
||||
}
|
||||
|
||||
// 网关配置
|
||||
export async function getGatewayConfig(off: number, itemsPerPage: number, mac?: string) {
|
||||
try {
|
||||
const offset = off || 0
|
||||
const limit = itemsPerPage || 100
|
||||
|
||||
// 获取总数
|
||||
let totalCountQuery = ''
|
||||
let totalCountParams: (string | number)[] = []
|
||||
|
||||
if (mac) {
|
||||
totalCountQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM gateway
|
||||
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
|
||||
LEFT JOIN edge ON edge.macaddr = gateway.edge
|
||||
WHERE gateway.macaddr = ?
|
||||
`
|
||||
totalCountParams = [mac]
|
||||
}
|
||||
else {
|
||||
totalCountQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM gateway
|
||||
`
|
||||
}
|
||||
|
||||
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
|
||||
totalCountQuery,
|
||||
...totalCountParams,
|
||||
)
|
||||
const totalCount = Number(totalCountResult[0]?.total || 0)
|
||||
|
||||
// 获取分页数据
|
||||
let query = `
|
||||
select edge, city, user, public, inner_ip, ischange, isonline
|
||||
from
|
||||
gateway
|
||||
left join cityhash
|
||||
on cityhash.hash = gateway.cityhash
|
||||
left join edge
|
||||
on edge.macaddr = gateway.edge
|
||||
`
|
||||
let params: (string | number)[] = []
|
||||
|
||||
if (mac) {
|
||||
query += ' WHERE gateway.macaddr = ?'
|
||||
params = [mac]
|
||||
}
|
||||
else {
|
||||
query += ' LIMIT ? OFFSET ?'
|
||||
params.push(limit, offset)
|
||||
}
|
||||
|
||||
// 指定返回类型
|
||||
const result: GatewayConfig[] = await prisma.$queryRawUnsafe<GatewayConfig[]>(query, ...params)
|
||||
|
||||
return {
|
||||
data: result,
|
||||
totalCount: totalCount,
|
||||
currentPage: Math.floor(offset / limit) + 1,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Gateway config query error:', error)
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '查询网关配置失败',
|
||||
}
|
||||
}
|
||||
}
|
||||
export type CityNode = {
|
||||
city: string
|
||||
count: number
|
||||
hash: string
|
||||
label: string
|
||||
offset: string
|
||||
}
|
||||
|
||||
// 城市节点数量分布
|
||||
export async function getCityNodeCount() {
|
||||
try {
|
||||
const result: CityNode[] = await prisma.$queryRaw`
|
||||
select c.city, c.hash, c.label, e.count, c.\`offset\`
|
||||
from
|
||||
cityhash c
|
||||
left join (
|
||||
select city_id, count(*) as count
|
||||
from
|
||||
edge
|
||||
where
|
||||
edge.active is true
|
||||
group by
|
||||
city_id
|
||||
) e
|
||||
on c.id = e.city_id
|
||||
group by
|
||||
c.hash
|
||||
order by
|
||||
count desc
|
||||
`
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('City node count query error:', error)
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '查询城市节点失败',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取节点信息
|
||||
async function getEdgeNodes(request: NextRequest) {
|
||||
export async function getEdgeNodes(off: number, itemsPerPage: number) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const offset = parseInt(searchParams.get('offset') || '0')
|
||||
const limit = parseInt(searchParams.get('limit') || '100')
|
||||
const offset = off || 0
|
||||
const limit = itemsPerPage || 100
|
||||
// 获取总数 - 使用类型断言
|
||||
const totalCountResult = await prisma.$queryRaw<[{ total: bigint }]>`
|
||||
SELECT COUNT(*) as total
|
||||
@@ -257,15 +240,19 @@ async function getEdgeNodes(request: NextRequest) {
|
||||
ORDER BY edge.id
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
return NextResponse.json({
|
||||
data: safeSerialize(result),
|
||||
return {
|
||||
data: result,
|
||||
totalCount: totalCount,
|
||||
currentPage: Math.floor(offset / limit) + 1,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Edge nodes query error:', error)
|
||||
return NextResponse.json({ error: '查询边缘节点失败' }, { status: 500 })
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '查询边缘节点失败',
|
||||
}
|
||||
}
|
||||
}
|
||||
145
src/actions/user.ts
Normal file
145
src/actions/user.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
'use server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { hash } from 'bcryptjs'
|
||||
|
||||
type User = {
|
||||
id: number
|
||||
account: string
|
||||
name: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
// 获取所有用户
|
||||
export async function findUsers(): Promise<{
|
||||
success: boolean
|
||||
data: User[]
|
||||
error: string
|
||||
}> {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
account: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: users,
|
||||
error: '',
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取用户列表错误:', error)
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '服务器错误,请稍后重试',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
export async function createUser(params: {
|
||||
account: string
|
||||
password: string
|
||||
name: string | ''
|
||||
}) {
|
||||
try {
|
||||
// 检查用户是否已存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { account: params.account },
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户账号已存在',
|
||||
}
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await hash(params.password, 10)
|
||||
|
||||
// 创建用户
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
account: params.account,
|
||||
password: hashedPassword,
|
||||
name: params.name || params.account,
|
||||
},
|
||||
})
|
||||
|
||||
// 不返回密码字段
|
||||
const { password: _, ...userWithoutPassword } = user
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: userWithoutPassword,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('创建用户错误:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: '服务器错误,请稍后重试',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export async function removeUser(id: number) {
|
||||
try {
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户ID不能为空',
|
||||
}
|
||||
}
|
||||
|
||||
// const userId = parseInt(id)
|
||||
if (isNaN(id)) {
|
||||
return {
|
||||
success: false,
|
||||
error: '无效的用户ID',
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { id: id },
|
||||
})
|
||||
|
||||
if (!existingUser) {
|
||||
return {
|
||||
status: 404,
|
||||
success: false,
|
||||
error: '用户不存在',
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
await prisma.user.delete({
|
||||
where: { id: id },
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
message: '用户删除成功',
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除用户错误:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: '服务器错误,请稍后重试',
|
||||
status: 500,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
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 } from '@/components/ui/card'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
import { Lock, User } from 'lucide-react'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { toast, Toaster } from 'sonner'
|
||||
|
||||
const formSchema = z.object({
|
||||
account: z.string().min(3, '账号至少需要3个字符'),
|
||||
password: z.string().min(6, '密码至少需要6个字符'),
|
||||
})
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const setAuth = useAuthStore(state => state.setAuth)
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
account: '',
|
||||
password: '',
|
||||
},
|
||||
})
|
||||
|
||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '登录失败')
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
toast.success('登录成功', {
|
||||
description: '正在跳转到仪表盘...',
|
||||
})
|
||||
setAuth(true)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
router.push('/dashboard')
|
||||
router.refresh()
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
toast.error('登录失败', {
|
||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-screen items-center justify-center bg-gray-50">
|
||||
<Card className="w-[350px] shadow-lg">
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-2xl font-bold text-center">登录</CardTitle>
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
size="lg"
|
||||
>
|
||||
{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>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma' // 使用统一的prisma实例
|
||||
import { compare } from 'bcryptjs'
|
||||
import { z } from 'zod'
|
||||
|
||||
const loginSchema = z.object({
|
||||
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 { account, password } = loginSchema.parse(body)
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ account: account.trim() },
|
||||
{ password: account.trim() },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户不存在' },
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const passwordMatch = await compare(password, user.password || '')
|
||||
|
||||
if (!passwordMatch) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密码错误',
|
||||
}, { status: 401 })
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
const sessionToken = crypto.randomUUID()
|
||||
await prisma.session.create({
|
||||
data: {
|
||||
id: sessionToken,
|
||||
userId: user.id,
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
})
|
||||
|
||||
// 设置cookie
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
account: user.account,
|
||||
name: user.name,
|
||||
},
|
||||
})
|
||||
|
||||
response.cookies.set('session', sessionToken, {
|
||||
httpOnly: true,
|
||||
// secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误,请稍后重试' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies } from 'next/headers'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
const sessionToken = cookieStore.get('session')?.value
|
||||
|
||||
// 删除数据库中的session(如果存在)
|
||||
if (sessionToken) {
|
||||
await prisma.session.deleteMany({
|
||||
where: { id: sessionToken },
|
||||
}).catch(() => {
|
||||
// 忽略删除错误,确保cookie被清除
|
||||
})
|
||||
}
|
||||
|
||||
// 清除cookie
|
||||
const response = NextResponse.json({ success: true })
|
||||
response.cookies.set('session', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0, // 立即过期
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
catch (error) {
|
||||
console.error('退出错误:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '退出失败' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { formatNumber, validateNumber } from '@/lib/formatters'
|
||||
import LoadingCard from '@/components/ui/loadingCard'
|
||||
import ErrorCard from '@/components/ui/errorCard'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
|
||||
interface AllocationStatus {
|
||||
city: string
|
||||
count: number
|
||||
assigned: number
|
||||
}
|
||||
|
||||
interface ApiAllocationStatus {
|
||||
city?: string
|
||||
count: number | string | bigint
|
||||
assigned: number | string | bigint
|
||||
unique_allocated_ips: number | string | bigint
|
||||
}
|
||||
import { getAllocationStatus, type AllocationStatus } from '@/actions/stats'
|
||||
|
||||
export default function AllocationStatus({ detailed = false }: { detailed?: boolean }) {
|
||||
const [data, setData] = useState<AllocationStatus[]>([])
|
||||
@@ -48,21 +35,16 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
setLoading(true)
|
||||
|
||||
const hours = getTimeHours()
|
||||
|
||||
const response = await fetch(`/api/stats?type=allocation_status&hours=${hours}`)
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
|
||||
const result = await response.json()
|
||||
const result = await getAllocationStatus(hours)
|
||||
|
||||
// 数据验证
|
||||
const validatedData = (result as ApiAllocationStatus[]).map(item => ({
|
||||
const validatedData = (result.data).map(item => ({
|
||||
city: item.city || '未知',
|
||||
count: validateNumber(item.count),
|
||||
assigned: validateNumber(item.assigned),
|
||||
count: item.count || BigInt(0),
|
||||
assigned: item.assigned || BigInt(0),
|
||||
}))
|
||||
|
||||
const sortedData = validatedData.sort((a, b) => b.count - a.count)
|
||||
const sortedData = validatedData.sort((a, b) => Number(b.count - a.count))
|
||||
|
||||
setData(sortedData)
|
||||
}
|
||||
@@ -140,18 +122,18 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, index) => {
|
||||
const overage = calculateOverage(item.assigned, item.count)
|
||||
const overage = calculateOverage(Number(item.assigned), Number(item.count))
|
||||
return (
|
||||
<TableRow
|
||||
key={index}
|
||||
className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}
|
||||
>
|
||||
<TableCell className="px-4 py-2">{item.city}</TableCell>
|
||||
<TableCell className="px-4 py-2">{formatNumber(item.count)}</TableCell>
|
||||
<TableCell className="px-4 py-2">{formatNumber(item.assigned)}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.count}</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.assigned}</TableCell>
|
||||
<TableCell className="px-4 py-2">
|
||||
<span className={overage > 0 ? 'text-red-600 font-medium' : ''}>
|
||||
{formatNumber(overage)}
|
||||
{overage}
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
|
||||
interface CityNode {
|
||||
city: string
|
||||
count: number
|
||||
hash: string
|
||||
label: string
|
||||
offset: string
|
||||
}
|
||||
import { getCityNodeCount, type CityNode } from '@/actions/stats'
|
||||
|
||||
export default function CityNodeStats() {
|
||||
const [data, setData] = useState<CityNode[]>([])
|
||||
@@ -21,9 +14,8 @@ export default function CityNodeStats() {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/stats?type=city_node_count')
|
||||
const result = await response.json()
|
||||
setData(result)
|
||||
const result = await getCityNodeCount()
|
||||
setData(result.data)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取城市节点数据失败:', error)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
|
||||
import { validateNumber } from '@/lib/formatters'
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
import { getEdgeNodes } from '@/actions/stats'
|
||||
|
||||
interface Edge {
|
||||
id: number
|
||||
@@ -39,10 +40,9 @@ export default function Edge() {
|
||||
// 计算偏移量
|
||||
const offset = (currentPage - 1) * itemsPerPage
|
||||
|
||||
const response = await fetch(`/api/stats?type=edge_nodes&offset=${offset}&limit=${itemsPerPage}`)
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
// if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
|
||||
const result = await response.json()
|
||||
const result = await getEdgeNodes(offset, itemsPerPage)
|
||||
type ResultEdge = {
|
||||
id: number
|
||||
macaddr: string
|
||||
|
||||
@@ -3,23 +3,17 @@ import { useEffect, useState, Suspense } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
import { getGatewayConfig, type GatewayConfig } from '@/actions/stats'
|
||||
|
||||
interface GatewayConfig {
|
||||
city: string
|
||||
edge: string
|
||||
user: string
|
||||
public: string
|
||||
inner_ip: string
|
||||
ischange: number
|
||||
isonline: number
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
data: GatewayConfig[]
|
||||
totalCount: number
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
}
|
||||
// interface GatewayConfig {
|
||||
// city: string
|
||||
// edge: string
|
||||
// user: string
|
||||
// public: string
|
||||
// inner_ip: string
|
||||
// ischange: number
|
||||
// isonline: number
|
||||
// }
|
||||
|
||||
function GatewayConfigContent() {
|
||||
const [data, setData] = useState<GatewayConfig[]>([])
|
||||
@@ -59,18 +53,7 @@ function GatewayConfigContent() {
|
||||
// 计算偏移量
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
// 构建API URL
|
||||
let apiUrl = `/api/stats?type=gateway_config&offset=${offset}&limit=${limit}`
|
||||
if (mac.trim()) {
|
||||
apiUrl += `&mac=${encodeURIComponent(mac)}`
|
||||
}
|
||||
|
||||
const response = await fetch(apiUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误! 状态: ${response.status}`)
|
||||
}
|
||||
|
||||
const result: ApiResponse = await response.json()
|
||||
const result = await getGatewayConfig(offset, limit, mac)
|
||||
|
||||
// 检查返回的数据是否有效
|
||||
if (!result.data || result.data.length === 0) {
|
||||
|
||||
@@ -8,13 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { z } from 'zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
|
||||
interface GatewayInfo {
|
||||
macaddr: string
|
||||
inner_ip: string
|
||||
setid: string
|
||||
enable: number
|
||||
}
|
||||
import { getGatewayInfo, type GatewayInfo } from '@/actions/stats'
|
||||
|
||||
const filterSchema = z.object({
|
||||
status: z.string(),
|
||||
@@ -75,13 +69,9 @@ export default function Gatewayinfo() {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
const response = await fetch('/api/stats?type=gateway_info')
|
||||
if (!response.ok) {
|
||||
throw new Error('获取网关信息失败')
|
||||
}
|
||||
const result = await response.json()
|
||||
// const sortedData = result.sort(( a, b) => Number(a.inner_ip) - Number(b.inner_ip))
|
||||
const sortedData = result.sort((a: GatewayInfo, b: GatewayInfo) =>
|
||||
const result = await getGatewayInfo()
|
||||
|
||||
const sortedData = result.data.sort((a: GatewayInfo, b: GatewayInfo) =>
|
||||
sortByIpAddress(a.inner_ip, b.inner_ip),
|
||||
)
|
||||
setData(sortedData)
|
||||
|
||||
@@ -10,12 +10,15 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
|
||||
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'
|
||||
import { findUsers, createUser, removeUser } from '@/actions/user'
|
||||
|
||||
// 用户类型定义
|
||||
interface UserData {
|
||||
id: string
|
||||
id: number
|
||||
account: string
|
||||
createdAt: string
|
||||
createdAt: Date
|
||||
name: string | null
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -45,15 +48,10 @@ export default function Settings() {
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/users')
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '获取用户列表失败')
|
||||
}
|
||||
const data = await findUsers()
|
||||
|
||||
if (data.success) {
|
||||
setUsers(data.users)
|
||||
setUsers(data.data)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -72,23 +70,12 @@ export default function Settings() {
|
||||
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({
|
||||
const data = await createUser({
|
||||
account: values.account,
|
||||
password: values.password,
|
||||
}),
|
||||
name: '',
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '创建用户失败')
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
toast.success('用户创建成功', {
|
||||
description: '新账户已成功添加',
|
||||
@@ -115,17 +102,7 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
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 || '删除用户失败')
|
||||
}
|
||||
|
||||
const data = await removeUser(userId)
|
||||
if (data.success) {
|
||||
toast.success('用户删除成功', {
|
||||
description: '用户账户已删除',
|
||||
|
||||
@@ -9,6 +9,8 @@ import AllocationStatus from './components/allocationStatus'
|
||||
import Settings from './components/settings'
|
||||
import Edge from './components/edge'
|
||||
import { LogOut } from 'lucide-react'
|
||||
import { logout } from '@/actions/auth'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const tabs = [
|
||||
{ id: 'gatewayInfo', label: '网关信息' },
|
||||
@@ -37,11 +39,9 @@ function DashboardContent() {
|
||||
const handleLogout = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
})
|
||||
const response = await logout()
|
||||
|
||||
if (response.ok) {
|
||||
if (response) {
|
||||
// 退出成功后跳转到登录页
|
||||
router.push('/login')
|
||||
router.refresh()
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
// 数字格式化工具函数
|
||||
export const formatNumber = (num: number | string): string => {
|
||||
const numberValue = typeof num === 'string' ? parseInt(num) || 0 : num
|
||||
return numberValue.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
export const formatLargeNumber = (num: number | string): string => {
|
||||
const numberValue = typeof num === 'string' ? parseInt(num) || 0 : num
|
||||
|
||||
if (numberValue > 1e9) return `${(numberValue / 1e9).toFixed(1)}亿`
|
||||
if (numberValue > 1e6) return `${(numberValue / 1e6).toFixed(1)}百万`
|
||||
if (numberValue > 1e4) return `${(numberValue / 1e4).toFixed(1)}万`
|
||||
if (numberValue > 1e3) return `${(numberValue / 1e3).toFixed(1)}千`
|
||||
|
||||
return numberValue.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 数据验证函数
|
||||
export const validateNumber = (value: unknown): number => {
|
||||
if (typeof value === 'number') return value
|
||||
|
||||
@@ -4,10 +4,11 @@ const globalForPrisma = global as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||
const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma
|
||||
}
|
||||
|
||||
export default prisma
|
||||
export * from '@/generated/prisma/client'
|
||||
|
||||
@@ -9,7 +9,6 @@ export const config = {
|
||||
|
||||
const isIgnored = [
|
||||
'/login',
|
||||
'/api/auth/login',
|
||||
]
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
|
||||
Reference in New Issue
Block a user