feat: add frontend auth shell

This commit is contained in:
hailin
2026-07-01 14:15:26 +08:00
parent 953f126277
commit 4925285cf3
12 changed files with 664 additions and 40 deletions

View File

@@ -0,0 +1,92 @@
import * as React from 'react'
import * as authApi from './api'
import type { CurrentUser } from './types'
type AuthContextValue = {
user: CurrentUser | null
loading: boolean
authenticated: boolean
login: (username: string, password: string) => Promise<void>
logout: () => Promise<void>
refresh: () => Promise<void>
}
const AuthContext = React.createContext<AuthContextValue | null>(null)
type AuthProviderProps = {
children: React.ReactNode
}
export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = React.useState<CurrentUser | null>(null)
const [loading, setLoading] = React.useState(true)
const refresh = React.useCallback(async () => {
try {
const currentUser = await authApi.getCurrentUser()
setUser(currentUser)
} catch {
setUser(null)
}
}, [])
React.useEffect(() => {
let mounted = true
authApi
.getCurrentUser()
.then((currentUser) => {
if (mounted) {
setUser(currentUser)
}
})
.catch(() => {
if (mounted) {
setUser(null)
}
})
.finally(() => {
if (mounted) {
setLoading(false)
}
})
return () => {
mounted = false
}
}, [])
const handleLogin = React.useCallback(async (username: string, password: string) => {
const currentUser = await authApi.login(username, password)
setUser(currentUser)
}, [])
const handleLogout = React.useCallback(async () => {
await authApi.logout()
setUser(null)
window.location.hash = '/'
}, [])
const value = React.useMemo<AuthContextValue>(
() => ({
user,
loading,
authenticated: Boolean(user),
login: handleLogin,
logout: handleLogout,
refresh
}),
[handleLogin, handleLogout, loading, refresh, user]
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = React.useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}

View File

@@ -0,0 +1,111 @@
import * as React from 'react'
import { AlertCircle, LockKeyhole, UserRound } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { useAuth } from './AuthProvider'
export function LoginPage() {
const { login } = useAuth()
const [username, setUsername] = React.useState('admin')
const [password, setPassword] = React.useState('admin123')
const [error, setError] = React.useState('')
const [submitting, setSubmitting] = React.useState(false)
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setError('')
setSubmitting(true)
try {
await login(username, password)
} catch {
setError('登录失败,请检查账号、密码或后端服务状态。')
} finally {
setSubmitting(false)
}
}
return (
<main className="min-h-screen bg-background text-foreground">
<div className="grid min-h-screen lg:grid-cols-[minmax(0,1fr)_440px]">
<section className="hidden border-r border-border bg-surface px-10 py-8 lg:flex lg:flex-col lg:justify-between">
<div>
<div className="inline-flex h-10 items-center rounded-md border border-border bg-background px-3 text-sm font-semibold">
AI 工单平台
</div>
<div className="mt-20 max-w-2xl">
<p className="text-sm font-medium text-primary">企业后台治理基座</p>
<h1 className="mt-4 text-4xl font-semibold leading-tight tracking-normal text-foreground">
统一登录、菜单权限与工单运营入口
</h1>
<p className="mt-4 max-w-xl text-base leading-7 text-muted-foreground">
面向企业技术支持团队,先建立稳定的管理后台体验,再承接工单、知识库和智能处理能力。
</p>
</div>
</div>
<div className="grid max-w-xl grid-cols-3 gap-3 text-sm">
{['用户与角色', '菜单权限', '工单治理'].map((item) => (
<div key={item} className="rounded-md border border-border bg-background px-3 py-2 text-muted-foreground">
{item}
</div>
))}
</div>
</section>
<section className="flex min-h-screen items-center justify-center px-5 py-8">
<Card className="w-full max-w-[400px]">
<CardHeader>
<CardTitle>登录平台</CardTitle>
<CardDescription>使用管理员账号进入企业后台</CardDescription>
</CardHeader>
<CardContent>
<form className="grid gap-4" onSubmit={handleSubmit}>
{error ? (
<Alert variant="destructive">
<AlertCircle className="size-4" />
<AlertTitle>无法登录</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
<div className="grid gap-2">
<Label htmlFor="username">账号</Label>
<div className="relative">
<UserRound className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="username"
autoComplete="username"
className="pl-9"
value={username}
onChange={(event) => setUsername(event.target.value)}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="password">密码</Label>
<div className="relative">
<LockKeyhole className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="password"
type="password"
autoComplete="current-password"
className="pl-9"
value={password}
onChange={(event) => setPassword(event.target.value)}
/>
</div>
</div>
<Button type="submit" className="mt-1 w-full" disabled={submitting}>
{submitting ? '登录中...' : '登录'}
</Button>
</form>
</CardContent>
</Card>
</section>
</div>
</main>
)
}

View File

@@ -0,0 +1,20 @@
import { apiRequest } from '@/lib/api'
import type { CurrentUser } from './types'
export async function getCurrentUser() {
return apiRequest<CurrentUser>('/accounts/auth/me')
}
export async function login(username: string, password: string) {
return apiRequest<CurrentUser>('/accounts/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
})
}
export async function logout() {
await apiRequest<{ status: string }>('/accounts/auth/logout', {
method: 'POST'
})
}

View File

@@ -0,0 +1,38 @@
export type AuthRole = {
id: number
name: string
}
export type AuthUserGroup = {
id: number
name: string
code: string
description: string
is_active: boolean
created_at?: string | null
updated_at?: string | null
}
export type AuthMenu = {
id: number
code: string
title: string
path: string
icon: string
permission_code: string
is_visible: boolean
is_enabled: boolean
children: AuthMenu[]
}
export type CurrentUser = {
id: number
username: string
display_name: string
email: string
is_superuser: boolean
roles: AuthRole[]
user_groups: AuthUserGroup[]
permissions: string[]
menus: AuthMenu[]
}