应用 eslint 规则
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { NextConfig } from "next";
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
@@ -6,6 +6,6 @@ const nextConfig: NextConfig = {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
output: 'standalone',
|
||||
};
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
export default nextConfig
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
plugins: ['@tailwindcss/postcss'],
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
||||
@@ -20,7 +20,7 @@ const formSchema = z.object({
|
||||
export default function LoginPage() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const setAuth = useAuthStore((state) => state.setAuth)
|
||||
const setAuth = useAuthStore(state => state.setAuth)
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -47,19 +47,21 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
toast.success("登录成功", {
|
||||
description: "正在跳转到仪表盘...",
|
||||
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 : "服务器连接失败,请稍后重试",
|
||||
}
|
||||
catch (error) {
|
||||
toast.error('登录失败', {
|
||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||
})
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -115,12 +117,14 @@ export default function LoginPage() {
|
||||
disabled={loading}
|
||||
size="lg"
|
||||
>
|
||||
{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>
|
||||
|
||||
@@ -13,21 +13,19 @@ export async function POST(request: Request) {
|
||||
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() }
|
||||
]
|
||||
{ password: account.trim() },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户不存在' },
|
||||
{ status: 401 }
|
||||
{ status: 401 },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +35,7 @@ export async function POST(request: Request) {
|
||||
if (!passwordMatch) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: '密码错误'
|
||||
error: '密码错误',
|
||||
}, { status: 401 })
|
||||
}
|
||||
|
||||
@@ -47,8 +45,8 @@ export async function POST(request: Request) {
|
||||
data: {
|
||||
id: sessionToken,
|
||||
userId: user.id,
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
})
|
||||
|
||||
// 设置cookie
|
||||
@@ -57,23 +55,23 @@ export async function POST(request: Request) {
|
||||
user: {
|
||||
id: user.id,
|
||||
account: user.account,
|
||||
name: user.name
|
||||
}
|
||||
name: user.name,
|
||||
},
|
||||
})
|
||||
|
||||
response.cookies.set('session', sessionToken, {
|
||||
httpOnly: true,
|
||||
// secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 7
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误,请稍后重试' },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export async function POST() {
|
||||
// 删除数据库中的session(如果存在)
|
||||
if (sessionToken) {
|
||||
await prisma.session.deleteMany({
|
||||
where: { id: sessionToken }
|
||||
where: { id: sessionToken },
|
||||
}).catch(() => {
|
||||
// 忽略删除错误,确保cookie被清除
|
||||
})
|
||||
@@ -27,12 +27,12 @@ export async function POST() {
|
||||
})
|
||||
|
||||
return response
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('退出错误:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '退出失败' },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { prisma } from '@/lib/prisma'
|
||||
// 处理 BigInt 序列化
|
||||
function safeSerialize(data: unknown) {
|
||||
return JSON.parse(JSON.stringify(data, (key, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value
|
||||
typeof value === 'bigint' ? value.toString() : value,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ export async function GET(request: NextRequest) {
|
||||
default:
|
||||
return NextResponse.json({ error: 'Invalid report type' }, { status: 400 })
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('API Error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
@@ -44,7 +45,8 @@ async function getGatewayInfo() {
|
||||
ORDER BY macaddr
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Gateway info query error:', error)
|
||||
return NextResponse.json({ error: '查询网关信息失败' }, { status: 500 })
|
||||
}
|
||||
@@ -60,13 +62,13 @@ async function getGatewayConfig(request: NextRequest) {
|
||||
|
||||
// 定义类型接口
|
||||
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;
|
||||
edge: string | null
|
||||
city: string | null
|
||||
user: string | null
|
||||
public: string | null
|
||||
inner_ip: string | null
|
||||
ischange: boolean | number | null
|
||||
isonline: boolean | number | null
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
@@ -82,7 +84,8 @@ async function getGatewayConfig(request: NextRequest) {
|
||||
WHERE gateway.macaddr = ?
|
||||
`
|
||||
totalCountParams = [macAddress]
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
totalCountQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM gateway
|
||||
@@ -91,7 +94,7 @@ async function getGatewayConfig(request: NextRequest) {
|
||||
|
||||
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
|
||||
totalCountQuery,
|
||||
...totalCountParams
|
||||
...totalCountParams,
|
||||
)
|
||||
const totalCount = Number(totalCountResult[0]?.total || 0)
|
||||
|
||||
@@ -110,7 +113,8 @@ async function getGatewayConfig(request: NextRequest) {
|
||||
if (macAddress) {
|
||||
query += ' WHERE gateway.macaddr = ?'
|
||||
params = [macAddress]
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
query += ' LIMIT ? OFFSET ?'
|
||||
params.push(limit, offset)
|
||||
}
|
||||
@@ -122,9 +126,10 @@ async function getGatewayConfig(request: NextRequest) {
|
||||
data: safeSerialize(result),
|
||||
totalCount: totalCount,
|
||||
currentPage: Math.floor(offset / limit) + 1,
|
||||
totalPages: Math.ceil(totalCount / limit)
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
})
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Gateway config query error:', error)
|
||||
return NextResponse.json({ error: '查询网关配置失败' }, { status: 500 })
|
||||
}
|
||||
@@ -140,7 +145,8 @@ async function getCityConfigCount() {
|
||||
GROUP BY c.city
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('City config count query error:', error)
|
||||
return NextResponse.json({ error: '查询城市配置失败' }, { status: 500 })
|
||||
}
|
||||
@@ -169,7 +175,8 @@ async function getCityNodeCount() {
|
||||
count desc
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('City node count query error:', error)
|
||||
return NextResponse.json({ error: '查询城市节点失败' }, { status: 500 })
|
||||
}
|
||||
@@ -216,13 +223,13 @@ async function getAllocationStatus(request: NextRequest) {
|
||||
cityhash.macaddr IS NOT NULL;
|
||||
`
|
||||
return NextResponse.json(safeSerialize(result))
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
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 }
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -254,10 +261,10 @@ async function getEdgeNodes(request: NextRequest) {
|
||||
data: safeSerialize(result),
|
||||
totalCount: totalCount,
|
||||
currentPage: Math.floor(offset / limit) + 1,
|
||||
totalPages: Math.ceil(totalCount / limit)
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Edge nodes query error:', error)
|
||||
return NextResponse.json({ error: '查询边缘节点失败' }, { status: 500 })
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@ export async function GET() {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
users
|
||||
users,
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取用户列表错误:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误,请稍后重试' },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,13 @@ export async function POST(request: Request) {
|
||||
|
||||
// 检查用户是否已存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { account }
|
||||
where: { account },
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户账号已存在' },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -66,19 +66,18 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: userWithoutPassword
|
||||
user: userWithoutPassword,
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('创建用户错误:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误,请稍后重试' },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除用户
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
@@ -89,7 +88,7 @@ export async function DELETE(request: Request) {
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户ID不能为空' },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,36 +96,37 @@ export async function DELETE(request: Request) {
|
||||
if (isNaN(userId)) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '无效的用户ID' },
|
||||
{ status: 400 }
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { id: userId }
|
||||
where: { id: userId },
|
||||
})
|
||||
|
||||
if (!existingUser) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '用户不存在' },
|
||||
{ status: 404 }
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
await prisma.user.delete({
|
||||
where: { id: userId }
|
||||
where: { id: userId },
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '用户删除成功'
|
||||
message: '用户删除成功',
|
||||
})
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('删除用户错误:', error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '服务器错误,请稍后重试' },
|
||||
{ status: 500 }
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
|
||||
// 获取时间参数(小时数)
|
||||
const getTimeHours = useCallback(() => {
|
||||
|
||||
if (timeFilter === 'custom' && customHours) {
|
||||
const hours = parseInt(customHours)
|
||||
return isNaN(hours) ? 24 : Math.max(1, hours) // 默认24小时,最少1小时
|
||||
@@ -39,8 +38,8 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
|
||||
// 计算超额量
|
||||
const calculateOverage = (assigned: number, count: number) => {
|
||||
const overage = assigned - count;
|
||||
return Math.max(0, overage);
|
||||
const overage = assigned - count
|
||||
return Math.max(0, overage)
|
||||
}
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
@@ -57,7 +56,7 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
const result = await response.json()
|
||||
|
||||
// 数据验证
|
||||
const validatedData = (result as ApiAllocationStatus[]).map((item) => ({
|
||||
const validatedData = (result as ApiAllocationStatus[]).map(item => ({
|
||||
city: item.city || '未知',
|
||||
count: validateNumber(item.count),
|
||||
assigned: validateNumber(item.assigned),
|
||||
@@ -66,10 +65,12 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
const sortedData = validatedData.sort((a, b) => b.count - a.count)
|
||||
|
||||
setData(sortedData)
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch allocation status:', error)
|
||||
setError(error instanceof Error ? error.message : 'Unknown error')
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [getTimeHours])
|
||||
@@ -92,7 +93,7 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
<label className="font-medium">时间筛选:</label>
|
||||
<select
|
||||
value={timeFilter}
|
||||
onChange={(e) => setTimeFilter(e.target.value)}
|
||||
onChange={e => setTimeFilter(e.target.value)}
|
||||
className="border rounded p-2"
|
||||
>
|
||||
<option value="1">最近1小时</option>
|
||||
@@ -110,7 +111,7 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
min="1"
|
||||
max="720"
|
||||
value={customHours}
|
||||
onChange={(e) => setCustomHours(e.target.value)}
|
||||
onChange={e => setCustomHours(e.target.value)}
|
||||
placeholder="输入小时数"
|
||||
className="border rounded p-2 w-24"
|
||||
/>
|
||||
@@ -126,7 +127,7 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex gap-6 overflow-hidden">
|
||||
<div className="flex w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
||||
@@ -24,9 +24,11 @@ export default function CityNodeStats() {
|
||||
const response = await fetch('/api/stats?type=city_node_count')
|
||||
const result = await response.json()
|
||||
setData(result)
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取城市节点数据失败:', error)
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -50,7 +52,7 @@ export default function CityNodeStats() {
|
||||
</div>
|
||||
|
||||
<div className="flex overflow-hidden ">
|
||||
<div className='flex w-full'>
|
||||
<div className="flex w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
@@ -72,7 +74,8 @@ export default function CityNodeStats() {
|
||||
<TableCell className="px-4 py-2">{item.hash}</TableCell>
|
||||
<TableCell className="px-4 py-2">
|
||||
<span className="bg-gray-100 px-2 py-1 rounded text-gray-700">
|
||||
{item.label}</span>
|
||||
{item.label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-2">{item.offset}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function Edge() {
|
||||
online: number
|
||||
}
|
||||
|
||||
const validatedData = (result.data as ResultEdge[]).map((item) => ({
|
||||
const validatedData = (result.data as ResultEdge[]).map(item => ({
|
||||
id: validateNumber(item.id),
|
||||
macaddr: item.macaddr || '',
|
||||
city: item.city || '',
|
||||
@@ -64,21 +64,23 @@ export default function Edge() {
|
||||
single: item.single,
|
||||
sole: item.sole,
|
||||
arch: validateNumber(item.arch),
|
||||
online: validateNumber(item.online)
|
||||
online: validateNumber(item.online),
|
||||
}))
|
||||
|
||||
setData(validatedData)
|
||||
setTotalItems(result.totalCount || 0)
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch edge nodes:', error)
|
||||
setError(error instanceof Error ? error.message : '获取边缘节点数据失败')
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 多IP节点格式化
|
||||
const formatMultiIP = (value: number | boolean): string => {
|
||||
// 多IP节点格式化
|
||||
const formatMultiIP = (value: number | boolean): string => {
|
||||
if (typeof value === 'number') {
|
||||
switch (value) {
|
||||
case 1: return '是'
|
||||
@@ -88,18 +90,18 @@ const formatMultiIP = (value: number | boolean): string => {
|
||||
}
|
||||
}
|
||||
return value ? '是' : '否'
|
||||
}
|
||||
}
|
||||
|
||||
// 独享IP节点格式化
|
||||
const formatExclusiveIP = (value: number | boolean): string => {
|
||||
// 独享IP节点格式化
|
||||
const formatExclusiveIP = (value: number | boolean): string => {
|
||||
if (typeof value === 'number') {
|
||||
return value === 1 ? '是' : '否'
|
||||
}
|
||||
return value ? '是' : '否'
|
||||
}
|
||||
}
|
||||
|
||||
// 多IP节点颜色
|
||||
const getMultiIPColor = (value: number | boolean): string => {
|
||||
// 多IP节点颜色
|
||||
const getMultiIPColor = (value: number | boolean): string => {
|
||||
if (typeof value === 'number') {
|
||||
switch (value) {
|
||||
case 1: return 'bg-red-100 text-red-800'
|
||||
@@ -109,15 +111,15 @@ const getMultiIPColor = (value: number | boolean): string => {
|
||||
}
|
||||
}
|
||||
return value ? 'bg-red-100 text-red-800' : 'bg-green-100 text-green-800'
|
||||
}
|
||||
}
|
||||
|
||||
// 独享IP节点颜色
|
||||
const getExclusiveIPColor = (value: number | boolean): string => {
|
||||
// 独享IP节点颜色
|
||||
const getExclusiveIPColor = (value: number | boolean): string => {
|
||||
if (typeof value === 'number') {
|
||||
return value === 1 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
return value ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}
|
||||
}
|
||||
|
||||
const formatArchType = (arch: number): string => {
|
||||
switch (arch) {
|
||||
@@ -186,7 +188,7 @@ const getExclusiveIPColor = (value: number | boolean): string => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex gap-6 overflow-hidden">
|
||||
<div className="flex-3 w-full overflow-y-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -209,11 +211,15 @@ const getExclusiveIPColor = (value: number | boolean): string => {
|
||||
<TableCell className="px-4 py-3 text-sm font-mono text-green-600">{item.public}</TableCell>
|
||||
<TableCell className="px-4 py-3 text-sm text-gray-700">
|
||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||
item.isp === '移动' ? 'bg-blue-100 text-blue-800' :
|
||||
item.isp === '电信' ? 'bg-purple-100 text-purple-800' :
|
||||
item.isp === '联通' ? 'bg-red-100 text-red-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
item.isp === '移动'
|
||||
? 'bg-blue-100 text-blue-800'
|
||||
: item.isp === '电信'
|
||||
? 'bg-purple-100 text-purple-800'
|
||||
: item.isp === '联通'
|
||||
? 'bg-red-100 text-red-800'
|
||||
: 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{item.isp}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
@@ -43,7 +43,8 @@ function GatewayConfigContent() {
|
||||
setMacAddress(urlMac)
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
fetchData(urlMac, 1, itemsPerPage)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
setMacAddress('')
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
fetchData('', 1, itemsPerPage)
|
||||
@@ -75,7 +76,8 @@ function GatewayConfigContent() {
|
||||
if (!result.data || result.data.length === 0) {
|
||||
if (mac.trim()) {
|
||||
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
setError('未找到任何网关配置信息')
|
||||
}
|
||||
setData([])
|
||||
@@ -86,12 +88,14 @@ function GatewayConfigContent() {
|
||||
setData(result.data)
|
||||
|
||||
setTotalItems(result.totalCount || 0)
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取网关配置失败:', error)
|
||||
setError(error instanceof Error ? error.message : '获取网关配置失败')
|
||||
setData([])
|
||||
setTotalItems(0)
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +154,7 @@ function GatewayConfigContent() {
|
||||
<input
|
||||
type="text"
|
||||
value={macAddress}
|
||||
onChange={(e) => setMacAddress(e.target.value)}
|
||||
onChange={e => setMacAddress(e.target.value)}
|
||||
placeholder="输入MAC地址查询"
|
||||
className="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
@@ -181,7 +185,7 @@ function GatewayConfigContent() {
|
||||
</div>
|
||||
) : data.length > 0 ? (
|
||||
<>
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex gap-6 overflow-hidden">
|
||||
<div className="flex-3 w-full flex">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -268,14 +272,14 @@ function GatewayConfigContent() {
|
||||
|
||||
export default function GatewayConfig() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<Suspense fallback={(
|
||||
<div className="bg-white shadow rounded-lg p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">加载搜索参数...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
)}>
|
||||
<GatewayConfigContent />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
@@ -25,11 +25,11 @@ type FilterSchema = z.infer<typeof filterSchema>
|
||||
// IP地址排序函数
|
||||
const sortByIpAddress = (a: string, b: string): number => {
|
||||
const ipToNumber = (ip: string): number => {
|
||||
const parts = ip.split('.').map(part => parseInt(part, 10));
|
||||
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
||||
};
|
||||
const parts = ip.split('.').map(part => parseInt(part, 10))
|
||||
return (parts[0] << 24) + (parts[1] << 16) + (parts[2] << 8) + parts[3]
|
||||
}
|
||||
|
||||
return ipToNumber(a) - ipToNumber(b);
|
||||
return ipToNumber(a) - ipToNumber(b)
|
||||
}
|
||||
|
||||
export default function Gatewayinfo() {
|
||||
@@ -53,21 +53,23 @@ export default function Gatewayinfo() {
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!data.length) return
|
||||
|
||||
if (statusFilter === 'all') {
|
||||
setFilteredData(data)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
const enableValue = parseInt(statusFilter)
|
||||
// 添加 NaN 检查
|
||||
if (isNaN(enableValue)) {
|
||||
setFilteredData(data)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
setFilteredData(data.filter(item => item.enable === enableValue))
|
||||
}
|
||||
}
|
||||
}, [data, statusFilter])
|
||||
}, [data, statusFilter])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
@@ -80,14 +82,16 @@ useEffect(() => {
|
||||
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) =>
|
||||
sortByIpAddress(a.inner_ip, b.inner_ip)
|
||||
sortByIpAddress(a.inner_ip, b.inner_ip),
|
||||
)
|
||||
setData(sortedData)
|
||||
setFilteredData(sortedData) // 初始化时设置filteredData
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch gateway info:', error)
|
||||
setError(error instanceof Error ? error.message : '获取网关信息失败')
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -122,8 +126,8 @@ useEffect(() => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 overflow-hidden">
|
||||
<div className='flex gap-6'>
|
||||
<div className='flex flex-3 justify-between '>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex flex-3 justify-between ">
|
||||
<span className="text-lg pt-2 font-semibold mb-4">网关基本信息</span>
|
||||
<Form {...form}>
|
||||
<form className="flex items-center gap-4">
|
||||
@@ -153,10 +157,10 @@ useEffect(() => {
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<div className='flex flex-1'></div>
|
||||
<div className="flex flex-1"></div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-6 overflow-hidden'>
|
||||
<div className="flex gap-6 overflow-hidden">
|
||||
<div className="flex-3 w-full flex">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -176,7 +180,7 @@ useEffect(() => {
|
||||
<TableCell className="px-4 py-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push(`/dashboard?tab=gateway&mac=${item.macaddr}`);
|
||||
router.push(`/dashboard?tab=gateway&mac=${item.macaddr}`)
|
||||
}}
|
||||
className="font-mono text-blue-600 hover:text-blue-800 hover:underline cursor-pointer"
|
||||
>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
||||
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"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
|
||||
// 用户类型定义
|
||||
interface UserData {
|
||||
@@ -22,12 +22,11 @@ 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"],
|
||||
}).refine(data => data.password === data.confirmPassword, {
|
||||
message: '密码不匹配',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
|
||||
|
||||
export default function Settings() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [users, setUsers] = useState<UserData[]>([])
|
||||
@@ -43,7 +42,6 @@ export default function Settings() {
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
@@ -57,9 +55,10 @@ export default function Settings() {
|
||||
if (data.success) {
|
||||
setUsers(data.users)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("获取用户列表失败", {
|
||||
description: error instanceof Error ? error.message : "服务器连接失败,请稍后重试",
|
||||
}
|
||||
catch (error) {
|
||||
toast.error('获取用户列表失败', {
|
||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -91,25 +90,26 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
toast.success("用户创建成功", {
|
||||
description: "新账户已成功添加",
|
||||
toast.success('用户创建成功', {
|
||||
description: '新账户已成功添加',
|
||||
})
|
||||
form.reset()
|
||||
setIsCreateMode(false)
|
||||
fetchUsers() // 刷新用户列表
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("创建用户失败", {
|
||||
description: error instanceof Error ? error.message : "服务器连接失败,请稍后重试",
|
||||
}
|
||||
catch (error) {
|
||||
toast.error('创建用户失败', {
|
||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||
})
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDeleteUser = async (userId: number) => {
|
||||
|
||||
const handleDeleteUser = async (userId: number) => {
|
||||
if (!confirm('确定要删除这个用户吗?此操作不可恢复。')) {
|
||||
return
|
||||
}
|
||||
@@ -127,21 +127,22 @@ const handleDeleteUser = async (userId: number) => {
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
toast.success("用户删除成功", {
|
||||
description: "用户账户已删除",
|
||||
toast.success('用户删除成功', {
|
||||
description: '用户账户已删除',
|
||||
})
|
||||
fetchUsers() // 刷新用户列表
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("删除用户失败", {
|
||||
description: error instanceof Error ? error.message : "服务器连接失败,请稍后重试",
|
||||
}
|
||||
catch (error) {
|
||||
toast.error('删除用户失败', {
|
||||
description: error instanceof Error ? error.message : '服务器连接失败,请稍后重试',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤用户列表
|
||||
const filteredUsers = users.filter(user =>
|
||||
user.account.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
user.account.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -277,7 +278,7 @@ const handleDeleteUser = async (userId: number) => {
|
||||
placeholder="搜索用户..."
|
||||
className="pl-8"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onChange={e => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -298,7 +299,7 @@ const handleDeleteUser = async (userId: number) => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredUsers.map((user) => (
|
||||
filteredUsers.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">{user.account}</TableCell>
|
||||
<TableCell>{new Date(user.createdAt).toLocaleDateString()}</TableCell>
|
||||
@@ -309,7 +310,8 @@ const handleDeleteUser = async (userId: number) => {
|
||||
size="sm"
|
||||
className="h-5 border-0 hover:bg-transparent"
|
||||
onClick={() => handleDeleteUser(Number(user.id))}
|
||||
><Trash2 className="h-4 w-4" /></Button>
|
||||
><Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -16,7 +16,7 @@ const tabs = [
|
||||
{ id: 'city', label: '城市信息' },
|
||||
{ id: 'allocation', label: '分配状态' },
|
||||
{ id: 'edge', label: '节点信息' },
|
||||
{ id: 'setting', label: '设置'}
|
||||
{ id: 'setting', label: '设置' },
|
||||
]
|
||||
|
||||
function DashboardContent() {
|
||||
@@ -45,12 +45,15 @@ function DashboardContent() {
|
||||
// 退出成功后跳转到登录页
|
||||
router.push('/login')
|
||||
router.refresh()
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
console.error('退出失败')
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
console.error('退出错误:', error)
|
||||
} finally {
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -88,7 +91,7 @@ function DashboardContent() {
|
||||
<div className="flex flex-3 overflow-hidden px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="border-b border-gray-200 mb-6">
|
||||
<nav className="flex flex-col w-64 -mb-px space-x-8">
|
||||
{tabs.map((tab) => (
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
@@ -110,7 +113,7 @@ function DashboardContent() {
|
||||
{activeTab === 'city' && <CityNodeStats />}
|
||||
{activeTab === 'allocation' && <AllocationStatus detailed />}
|
||||
{activeTab === 'edge' && <Edge />}
|
||||
{activeTab === 'setting' && <Settings/>}
|
||||
{activeTab === 'setting' && <Settings />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,14 +122,14 @@ function DashboardContent() {
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<Suspense fallback={(
|
||||
<div className=" bg-gray-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
)}>
|
||||
<DashboardContent />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import { Toaster } from "sonner";
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import { Toaster } from 'sonner'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-Hans">
|
||||
<body
|
||||
className={`antialiased`}
|
||||
>
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
<Toaster richColors />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
default: 'bg-card text-card-foreground',
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
@@ -34,13 +34,13 @@ function Alert({
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -50,13 +50,13 @@ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
'text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
@@ -30,9 +30,9 @@ function Badge({
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
}: React.ComponentProps<'span'>
|
||||
& VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : 'span'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
@@ -41,11 +41,11 @@ function Button({
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
}: React.ComponentProps<'button'>
|
||||
& VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { XIcon } from 'lucide-react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
@@ -38,8 +38,8 @@ function DialogOverlay({
|
||||
<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
|
||||
'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}
|
||||
/>
|
||||
@@ -60,8 +60,8 @@ function DialogContent({
|
||||
<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
|
||||
'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}
|
||||
>
|
||||
@@ -80,23 +80,23 @@ function DialogContent({
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
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)}
|
||||
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
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
|
||||
'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -110,7 +110,7 @@ function DialogTitle({
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
className={cn('text-lg leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -123,7 +123,7 @@ function DialogDescription({
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default function ErrorCard({
|
||||
title,
|
||||
error,
|
||||
onRetry
|
||||
onRetry,
|
||||
}: {
|
||||
title: string
|
||||
error: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
} from 'react-hook-form'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
@@ -26,7 +26,7 @@ type FormFieldContextValue<
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
{} as FormFieldContextValue,
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
@@ -50,7 +50,7 @@ const useFormField = () => {
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
@@ -70,17 +70,17 @@ type FormItemContextValue = {
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
{} as FormItemContextValue,
|
||||
)
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function FormItem({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
className={cn('grid gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
@@ -97,7 +97,7 @@ function FormLabel({
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
className={cn('data-[error=true]:text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
@@ -122,22 +122,22 @@ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
const body = error ? String(error?.message ?? '') : props.children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
@@ -147,7 +147,7 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
className={cn('text-destructive text-sm', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Label({
|
||||
className,
|
||||
@@ -13,8 +13,8 @@ function Label({
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
import {useState, useEffect} from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
MoreHorizontalIcon,
|
||||
} from 'lucide-react'
|
||||
|
||||
import {cn} from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import {
|
||||
Select,
|
||||
@@ -53,7 +53,7 @@ function Pagination({
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// 总页数少于7,全部显示
|
||||
return Array.from({length: totalPages}, (_, i) => i + 1)
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
}
|
||||
|
||||
// 是否需要显示左边的省略号
|
||||
@@ -68,20 +68,20 @@ function Pagination({
|
||||
const rightSiblingIndex = Math.min(currentPage + SIBLINGS, totalPages)
|
||||
|
||||
return [1, DOTS, ...Array.from(
|
||||
{length: rightSiblingIndex - leftSiblingIndex + 1},
|
||||
{ length: rightSiblingIndex - leftSiblingIndex + 1 },
|
||||
(_, i) => leftSiblingIndex + i,
|
||||
), DOTS, totalPages]
|
||||
}
|
||||
|
||||
if (!showLeftDots && showRightDots) {
|
||||
// 只有右边有省略号
|
||||
return [...Array.from({length: 3 + SIBLINGS * 2}, (_, i) => i + 1), DOTS, totalPages]
|
||||
return [...Array.from({ length: 3 + SIBLINGS * 2 }, (_, i) => i + 1), DOTS, totalPages]
|
||||
}
|
||||
|
||||
if (showLeftDots && !showRightDots) {
|
||||
// 只有左边有省略号
|
||||
return [1, DOTS, ...Array.from(
|
||||
{length: 3 + SIBLINGS * 2},
|
||||
{ length: 3 + SIBLINGS * 2 },
|
||||
(_, i) => totalPages - (3 + SIBLINGS * 2) + i + 1,
|
||||
)]
|
||||
}
|
||||
@@ -119,7 +119,7 @@ function Pagination({
|
||||
onValueChange={handlePageSizeChange}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-20">
|
||||
<SelectValue/>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sizeOptions.map(option => (
|
||||
@@ -146,7 +146,7 @@ function Pagination({
|
||||
if (pageNum === -1) {
|
||||
return (
|
||||
<PaginationItem key={`dots-${index}`}>
|
||||
<PaginationEllipsis/>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
@@ -176,7 +176,7 @@ function Pagination({
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationLayout({className, ...props}: React.ComponentProps<'nav'>) {
|
||||
function PaginationLayout({ className, ...props }: React.ComponentProps<'nav'>) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
@@ -201,8 +201,8 @@ function PaginationContent({
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({...props}: React.ComponentProps<'li'>) {
|
||||
return <li data-slot="pagination-item" {...props}/>
|
||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
@@ -240,7 +240,7 @@ function PaginationPrevious({
|
||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon/>
|
||||
<ChevronLeftIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
@@ -255,7 +255,7 @@ function PaginationNext({
|
||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRightIcon/>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
@@ -271,7 +271,7 @@ function PaginationEllipsis({
|
||||
className={cn('flex size-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4"/>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import * as React from 'react'
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Select({
|
||||
...props
|
||||
@@ -26,19 +26,19 @@ function SelectValue({
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
size = 'default',
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
size?: 'sm' | 'default'
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -53,7 +53,7 @@ function SelectTrigger({
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
position = 'popper',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
@@ -61,10 +61,10 @@ function SelectContent({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
'bg-popover text-popover-foreground 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 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
position === 'popper'
|
||||
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
@@ -72,9 +72,9 @@ function SelectContent({
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
'p-1',
|
||||
position === 'popper'
|
||||
&& 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -92,7 +92,7 @@ function SelectLabel({
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -107,8 +107,8 @@ function SelectItem({
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -129,7 +129,7 @@ function SelectSeparator({
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -143,8 +143,8 @@ function SelectScrollUpButton({
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -161,8 +161,8 @@ function SelectScrollDownButton({
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
className={cn('bg-accent animate-pulse rounded-md', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Toaster as Sonner, ToasterProps } from 'sonner'
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
const { theme = 'system' } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
@@ -12,79 +12,79 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm ", className)}
|
||||
className={cn('w-full caption-bottom text-sm ', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b sticky top-0", className)}
|
||||
className={cn('[&_tr]:border-b sticky top-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors h-10",
|
||||
className
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors h-10',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 h-10 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'p-2 h-10 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -94,11 +94,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
}: React.ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
className={cn('text-muted-foreground mt-4 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import * as React from 'react'
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
@@ -12,7 +12,7 @@ function Tabs({
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
className={cn('flex flex-col gap-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -26,8 +26,8 @@ function TabsList({
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -42,8 +42,8 @@ function TabsTrigger({
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
'data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -57,7 +57,7 @@ function TabsContent({
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
className={cn('flex-1 outline-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PrismaClient } from "@/generated/prisma/client"
|
||||
import { PrismaClient } from '@/generated/prisma/client'
|
||||
|
||||
const globalForPrisma = global as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
|
||||
@@ -9,7 +9,7 @@ export const config = {
|
||||
|
||||
const isIgnored = [
|
||||
'/login',
|
||||
"/api/auth/login"
|
||||
'/api/auth/login',
|
||||
]
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
|
||||
@@ -8,12 +8,12 @@ interface AuthState {
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
set => ({
|
||||
isAuthenticated: false,
|
||||
setAuth: (state) => set({ isAuthenticated: state }),
|
||||
setAuth: state => set({ isAuthenticated: state }),
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user