feat 使用 shadcn-admin

This commit is contained in:
PoRi
2026-04-25 17:09:20 +08:00
parent 88fa2666d2
commit 6cf1681a42
159 changed files with 11459 additions and 2989 deletions
+43
View File
@@ -0,0 +1,43 @@
/**
* Cookie utility functions using manual document.cookie approach
* Replaces js-cookie dependency for better consistency
*/
const DEFAULT_MAX_AGE = 60 * 60 * 24 * 7 // 7 days
/**
* Get a cookie value by name
*/
export function getCookie(name: string): string | undefined {
if (typeof document === 'undefined') return undefined
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) {
const cookieValue = parts.pop()?.split(';').shift()
return cookieValue
}
return undefined
}
/**
* Set a cookie with name, value, and optional max age
*/
export function setCookie(
name: string,
value: string,
maxAge: number = DEFAULT_MAX_AGE
): void {
if (typeof document === 'undefined') return
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`
}
/**
* Remove a cookie by setting its max age to 0
*/
export function removeCookie(name: string): void {
if (typeof document === 'undefined') return
document.cookie = `${name}=; path=/; max-age=0`
}
+29
View File
@@ -0,0 +1,29 @@
import { AxiosError } from 'axios'
import { toast } from 'sonner'
export function handleServerError(error: unknown) {
if (import.meta.env.DEV) {
// eslint-disable-next-line no-console
console.log(error)
}
let errMsg = 'Something went wrong!'
if (
error &&
typeof error === 'object' &&
'status' in error &&
Number(error.status) === 204
) {
errMsg = 'No content.'
}
if (error instanceof AxiosError) {
const title = error.response?.data?.title
if (typeof title === 'string' && title.length > 0) {
errMsg = title
}
}
toast.error(errMsg)
}
+14
View File
@@ -0,0 +1,14 @@
import { toast } from 'sonner'
export function showSubmittedData(
data: unknown,
title: string = 'You submitted the following values:'
) {
toast.message(title, {
description: (
<pre className='mt-2 w-full overflow-x-auto rounded-md bg-slate-950 p-4'>
<code className='text-white'>{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
+71 -2
View File
@@ -1,6 +1,75 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function sleep(ms: number = 1000) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Generates page numbers for pagination with ellipsis
* @param currentPage - Current page number (1-based)
* @param totalPages - Total number of pages
* @returns Array of page numbers and ellipsis strings
*
* Examples:
* - Small dataset (≤5 pages): [1, 2, 3, 4, 5]
* - Near beginning: [1, 2, 3, 4, '...', 10]
* - In middle: [1, '...', 4, 5, 6, '...', 10]
* - Near end: [1, '...', 7, 8, 9, 10]
*/
export function getPageNumbers(currentPage: number, totalPages: number) {
const maxVisiblePages = 5 // Maximum number of page buttons to show
const rangeWithDots = []
if (totalPages <= maxVisiblePages) {
// If total pages is 5 or less, show all pages
for (let i = 1; i <= totalPages; i++) {
rangeWithDots.push(i)
}
} else {
// Always show first page
rangeWithDots.push(1)
if (currentPage <= 3) {
// Near the beginning: [1] [2] [3] [4] ... [10]
for (let i = 2; i <= 4; i++) {
rangeWithDots.push(i)
}
rangeWithDots.push('...', totalPages)
} else if (currentPage >= totalPages - 2) {
// Near the end: [1] ... [7] [8] [9] [10]
rangeWithDots.push('...')
for (let i = totalPages - 3; i <= totalPages; i++) {
rangeWithDots.push(i)
}
} else {
// In the middle: [1] ... [4] [5] [6] ... [10]
rangeWithDots.push('...')
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
rangeWithDots.push(i)
}
rangeWithDots.push('...', totalPages)
}
}
return rangeWithDots
}
/**
* Initials from a display name: first character of the first word + first
* character of the last word. One word only: first two characters. Empty: `?`.
*/
export function getDisplayNameInitials(displayName: string): string {
const parts = displayName.trim().split(/\s+/).filter(Boolean)
if (parts.length === 0) return '?'
if (parts.length === 1) {
return parts[0].slice(0, 2).toUpperCase()
}
const first = parts[0][0] ?? ''
const last = parts[parts.length - 1]?.[0] ?? ''
return (first + last).toUpperCase()
}