@@ -0,0 +1,26 @@
|
||||
import {useEffect,useState} from "react"
|
||||
import {RefreshCw,Users} from "lucide-react"
|
||||
import {ApiError,getUsers,type User} from "@/lib/auth"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {Card,CardHeader,CardTitle,CardDescription,CardContent} from "@/components/ui/card"
|
||||
|
||||
export function UsersPanel({onSessionExpired}:{onSessionExpired:()=>void}) {
|
||||
const [users,setUsers]=useState<User[]>([])
|
||||
const [error,setError]=useState("")
|
||||
const [loading,setLoading]=useState(true)
|
||||
const [revision,setRevision]=useState(0)
|
||||
useEffect(()=>{
|
||||
const controller=new AbortController()
|
||||
setLoading(true)
|
||||
getUsers(controller.signal).then(data=>{if (!controller.signal.aborted) {setUsers(data);setError("")}}).catch(e=>{
|
||||
if (controller.signal.aborted) return
|
||||
if (e instanceof ApiError && e.status===401) {onSessionExpired();return}
|
||||
setError(e instanceof Error ? e.message : "无法读取用户。")
|
||||
}).finally(()=>{if (!controller.signal.aborted) setLoading(false)})
|
||||
return ()=>controller.abort()
|
||||
},[revision,onSessionExpired])
|
||||
return <Card><CardHeader><div className="flex items-center justify-between"><CardTitle>用户列表</CardTitle><Button variant="outline" size="sm" onClick={()=>setRevision(v=>v+1)} disabled={loading}><RefreshCw className={loading ? "animate-spin" : ""}/>刷新</Button></div><CardDescription>按创建时间显示最近 100 位 App 用户。</CardDescription></CardHeader><CardContent>
|
||||
{error ? <p role="alert" className="py-8 text-sm text-destructive">{error}</p> : loading ? <p className="py-8 text-sm text-muted-foreground">正在加载用户…</p> : users.length===0 ? <div className="flex flex-col items-center py-14"><Users size={28} className="mb-4 text-muted-foreground"/><p className="text-sm text-muted-foreground">暂时没有 App 用户</p></div> : <div className="overflow-x-auto"><table className="w-full text-left text-sm"><thead><tr className="border-b text-muted-foreground"><th className="py-3 font-normal">用户</th><th className="py-3 font-normal">状态</th><th className="py-3 font-normal">创建时间</th></tr></thead><tbody>{users.map(user=><tr key={user.id} className="border-b last:border-0"><td className="py-4 pr-6"><p className="font-medium">{user.display_name}</p><p className="mt-1 font-mono text-[11px] text-muted-foreground">{user.id}</p></td><td className="py-4 pr-6"><Badge variant={user.status==="ACTIVE" ? "default" : "secondary"}>{user.status==="ACTIVE" ? "正常" : "已停用"}</Badge></td><td className="whitespace-nowrap py-4 text-muted-foreground">{new Date(user.created_at).toLocaleString("zh-CN")}</td></tr>)}</tbody></table></div>}
|
||||
</CardContent></Card>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import {cn} from "@/lib/utils"
|
||||
const badgeVariants = cva("inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-medium", { variants: {variant: {default: "border-transparent bg-accent text-accent-foreground", secondary:"border-transparent bg-secondary text-secondary-foreground", outline:"text-muted-foreground"}}, defaultVariants:{variant:"default"} })
|
||||
function Badge({className,variant,...props}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) { return <span data-slot="badge" className={cn(badgeVariants({variant}),className)} {...props}/> }
|
||||
export {Badge}
|
||||
@@ -0,0 +1,17 @@
|
||||
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"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
{ variants: {
|
||||
variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", outline: "border border-input bg-card hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground" },
|
||||
size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", icon: "h-10 w-10" },
|
||||
}, defaultVariants: { variant: "default", size: "default" } },
|
||||
)
|
||||
function Button({ className, variant, size, asChild = false, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
|
||||
}
|
||||
export { Button, buttonVariants }
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
function Card({className, ...props}: React.ComponentProps<"div">) { return <div data-slot="card" className={cn("rounded-xl border bg-card text-card-foreground shadow-sm",className)} {...props} /> }
|
||||
function CardHeader({className, ...props}: React.ComponentProps<"div">) { return <div data-slot="card-header" className={cn("flex flex-col gap-1.5 p-6",className)} {...props} /> }
|
||||
function CardTitle({className, ...props}: React.ComponentProps<"h2">) { return <h2 data-slot="card-title" className={cn("text-base font-semibold leading-none",className)} {...props} /> }
|
||||
function CardDescription({className, ...props}: React.ComponentProps<"p">) { return <p data-slot="card-description" className={cn("text-sm text-muted-foreground",className)} {...props} /> }
|
||||
function CardContent({className, ...props}: React.ComponentProps<"div">) { return <div data-slot="card-content" className={cn("p-6 pt-0",className)} {...props} /> }
|
||||
export {Card, CardHeader, CardTitle, CardDescription, CardContent}
|
||||
@@ -0,0 +1,6 @@
|
||||
import * as React from "react"
|
||||
import {cn} from "@/lib/utils"
|
||||
function Input({className,type,...props}:React.ComponentProps<"input">) {
|
||||
return <input data-slot="input" type={type} className={cn("flex h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",className)} {...props}/>
|
||||
}
|
||||
export {Input}
|
||||
Reference in New Issue
Block a user