This commit is contained in:
ryan
2026-06-08 08:19:22 +08:00
parent 4b72419a96
commit 9d0f9f0576
42 changed files with 1412 additions and 809 deletions
@@ -1,4 +1,4 @@
import {FilesMain} from "@/components/common/settings/files"
import {FilesMain} from "@/components/common/admin/files"
export default function FilesPage() {
return <FilesMain />
+8 -9
View File
@@ -46,12 +46,12 @@ export function LoginForm() {
const publicConfigQuery = useQuery({
queryKey: ["public-config"],
queryFn: services.config.getPublicConfig,
queryFn: () => services.config.getPublicConfig(),
})
const authSourcesQuery = useQuery({
queryKey: ["auth-sources"],
queryFn: services.auth.getAuthSources,
queryFn: () => services.auth.getAuthSources(),
enabled: publicConfigQuery.data?.oidc_login_enabled ?? true,
})
@@ -61,7 +61,7 @@ export function LoginForm() {
)
const loginMutation = useMutation({
mutationFn: services.auth.login,
mutationFn: (req: any) => services.auth.login(req),
onSuccess: (user) => {
setUser(user)
router.replace(redirectTarget)
@@ -72,7 +72,7 @@ export function LoginForm() {
})
const registerMutation = useMutation({
mutationFn: services.auth.register,
mutationFn: (req: any) => services.auth.register(req),
onSuccess: (user) => {
setUser(user)
router.replace(redirectTarget)
@@ -125,9 +125,6 @@ export function LoginForm() {
<h2 className="text-xl font-semibold tracking-tight text-foreground">
账号登录
</h2>
<p className="text-sm text-muted-foreground">
使用账号密码或第三方 OIDC 认证源登录
</p>
</div>
<Tabs value={mode} onValueChange={(value) => setMode(value as "login" | "register")}>
@@ -165,6 +162,7 @@ export function LoginForm() {
<Button
type="button"
className="w-full"
variant={"secondary"}
onClick={handlePasswordLogin}
disabled={!passwordLoginEnabled || loginMutation.isPending}
>
@@ -176,7 +174,7 @@ export function LoginForm() {
) : (
<>
<KeyRound className="mr-2 size-4" />
账号密码登录
登录
</>
)}
</Button>
@@ -216,6 +214,7 @@ export function LoginForm() {
<Button
type="button"
className="w-full"
variant="secondary"
onClick={handleRegister}
disabled={!registrationEnabled || registerMutation.isPending}
>
@@ -258,7 +257,7 @@ export function LoginForm() {
</div>
))
) : null}
</CardContent>
</Card>
)
+6 -12
View File
@@ -142,14 +142,12 @@ export function LoginPage() {
}}
className="relative z-10 w-full max-w-sm px-4"
>
<div className="text-center mb-8 space-y-2">
<h1 className="text-3xl font-bold tracking-tight text-foreground">
LINUX DO <span className="font-serif italic text-primary">Credit</span>
</h1>
<p className="text-sm text-muted-foreground font-light">
简单、安全,专为社区设计
</p>
</div>
{/*Title Do Not Remove*/}
{/*<div className="text-center mb-8 space-y-2">*/}
{/* <h1 className="text-3xl font-bold tracking-tight text-foreground">*/}
{/* Refresh*/}
{/* </h1>*/}
{/*</div>*/}
<AnimatePresence mode="wait">
{isProcessingCallback || isCheckingSession ? (
@@ -209,10 +207,6 @@ export function LoginPage() {
</motion.div>
)}
</AnimatePresence>
<div className="mt-8 text-center text-xs text-muted-foreground">
&copy; {new Date().getFullYear()} LINUX DO Credit. 版权所有
</div>
</motion.div>
</AuroraBackground>
)
@@ -0,0 +1,160 @@
"use client"
import {useEffect, useState} from "react"
import {Button} from "@/components/ui/button"
import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from "@/components/ui/dialog"
import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {Switch} from "@/components/ui/switch"
import {useAdminUsers} from "@/contexts/admin-users-context"
import type {CreateUserRequest} from "@/lib/services/admin"
const emptyForm: CreateUserRequest = {
username: "",
password: "",
nickname: "",
is_active: true,
is_admin: false,
}
export function CreateUserModal({
isOpen,
onClose,
}: {
isOpen: boolean
onClose: () => void
}) {
const { createUser } = useAdminUsers()
const [form, setForm] = useState<CreateUserRequest>(emptyForm)
const [saving, setSaving] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
useEffect(() => {
if (isOpen) {
setForm(emptyForm)
setErrors({})
} else {
setSaving(false)
}
}, [isOpen])
const validate = () => {
const newErrors: Record<string, string> = {}
if (!form.username.trim()) {
newErrors.username = "用户名不能为空"
} else if (form.username.trim().length < 3) {
newErrors.username = "用户名长度不能少于 3 位"
}
if (!form.password) {
newErrors.password = "密码不能为空"
} else if (form.password.length < 8) {
newErrors.password = "密码长度不能少于 8 位"
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSave = async (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) return
setSaving(true)
try {
await createUser({
...form,
username: form.username.trim(),
nickname: form.nickname?.trim() || undefined,
})
onClose()
} catch {
// Errors are already handled by context toast notifications
} finally {
setSaving(false)
}
}
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>新增用户</DialogTitle>
<DialogDescription>
直接创建一个本地密码登录的新账户。
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSave} className="space-y-4 pt-2">
<div className="space-y-1.5">
<Label htmlFor="username">用户名</Label>
<Input
id="username"
value={form.username}
onChange={(e) => setForm((prev) => ({ ...prev, username: e.target.value }))}
placeholder="请输入用户名 (至少 3 位)"
/>
{errors.username && (
<p className="text-xs text-destructive">{errors.username}</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="nickname">昵称 (选填)</Label>
<Input
id="nickname"
value={form.nickname}
onChange={(e) => setForm((prev) => ({ ...prev, nickname: e.target.value }))}
placeholder="请输入昵称"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="password">密码</Label>
<Input
id="password"
type="password"
value={form.password}
onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}
placeholder="请输入登录密码 (至少 8 位)"
/>
{errors.password && (
<p className="text-xs text-destructive">{errors.password}</p>
)}
</div>
<div className="flex items-center justify-between rounded-lg border border-dashed p-3 bg-muted/10">
<div>
<div className="font-medium text-sm">启用账户</div>
<div className="text-xs text-muted-foreground">禁用后此账号将无法登录系统。</div>
</div>
<Switch
checked={form.is_active}
onCheckedChange={(checked) => setForm((prev) => ({ ...prev, is_active: checked }))}
/>
</div>
<div className="flex items-center justify-between rounded-lg border border-dashed p-3 bg-muted/10">
<div>
<div className="font-medium text-sm">管理员权限</div>
<div className="text-xs text-muted-foreground">开启后此账号将拥有后台管理权限。</div>
</div>
<Switch
checked={form.is_admin}
onCheckedChange={(checked) => setForm((prev) => ({ ...prev, is_admin: checked }))}
/>
</div>
<div className="flex justify-end gap-2 pt-2 border-t mt-2">
<Button variant="outline" type="button" onClick={onClose}>
取消
</Button>
<Button type="submit" disabled={saving} variant="secondary">
{saving ? "创建中..." : "创建"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}
@@ -147,7 +147,7 @@ export function FilesMain() {
文件管理
</h1>
<p className="text-sm text-muted-foreground">
管理您上传的所有文件,支持下载和批量操作
管理您上传的所有文件,支持下载 and 批量操作
</p>
</div>
</div>
+39 -22
View File
@@ -1,25 +1,38 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
import { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Sheet, SheetTitle, SheetContent } from "@/components/ui/sheet"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Layers, Search, UserX, UserCheck, Eye, ShieldCheck, Filter, X, ChevronDown, ChevronLeft, ChevronRight, Loader2 } from "lucide-react"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import {useEffect, useState} from "react"
import {Button} from "@/components/ui/button"
import {Switch} from "@/components/ui/switch"
import {Separator} from "@/components/ui/separator"
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
import {Sheet, SheetContent, SheetTitle} from "@/components/ui/sheet"
import {Badge} from "@/components/ui/badge"
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
Filter,
Layers,
Loader2,
Plus,
Search,
ShieldCheck,
UserCheck,
UserX,
X
} from "lucide-react"
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip"
import { AdminUser, AdminService } from "@/lib/services"
import { toast } from "sonner"
import { formatDateTime } from "@/lib/utils"
import { EmptyStateWithBorder } from "@/components/layout/empty"
import { LoadingStateWithBorder } from "@/components/layout/loading"
import { ErrorInline } from "@/components/layout/error"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { cn } from "@/lib/utils"
import { useAdminUsers } from "@/contexts/admin-users-context"
import {AdminUser} from "@/lib/services"
import {cn, formatDateTime} from "@/lib/utils"
import {EmptyStateWithBorder} from "@/components/layout/empty"
import {LoadingStateWithBorder} from "@/components/layout/loading"
import {ErrorInline} from "@/components/layout/error"
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
import {useAdminUsers} from "@/contexts/admin-users-context"
import {CreateUserModal} from "./create-user-modal"
export function UsersManager() {
const {
@@ -43,6 +56,7 @@ export function UsersManager() {
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
const [detailOpen, setDetailOpen] = useState(false)
const [createModalOpen, setCreateModalOpen] = useState(false)
useEffect(() => {
fetchUsers()
@@ -277,6 +291,10 @@ export function UsersManager() {
<div className="flex flex-col gap-1">
<div className="text-2xl font-semibold">用户管理</div>
</div>
<Button variant="secondary" size="sm" className="h-7 text-xs" onClick={() => setCreateModalOpen(true)}>
<Plus className="size-3.5 mr-1" />
新增用户
</Button>
</div>
{renderFilterBar()}
@@ -409,9 +427,6 @@ export function UsersManager() {
<Badge variant="secondary" className="h-4.5 px-1.5 text-[9px] uppercase font-medium">
UID: {selectedUser.id}
</Badge>
<Badge variant="secondary" className="h-4.5 px-1.5 text-[9px] uppercase font-medium">
Lv.{selectedUser.trust_level}
</Badge>
{selectedUser.is_admin && (
<Badge className="h-4.5 px-1.5 text-[9px] uppercase font-medium bg-primary text-primary-foreground">
Admin
@@ -482,6 +497,8 @@ export function UsersManager() {
)}
</SheetContent>
</Sheet>
<CreateUserModal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} />
</div>
)
}
@@ -24,11 +24,7 @@ export const termsSections: PolicySection[] = [
title: "2. 服务定义与性质界定",
content: (
<div className="space-y-4 text-sm leading-relaxed">
<p><strong>2.1 纯技术研发脚手架:</strong>本平台是一个开源/闭源授权的技术二次开发底座。我们仅提供用户注册、API 调用、安全配置维护等<strong>纯软件技术服务</strong>。</p>
<p><strong>2.2 非金融机构与无承兑申明:</strong></p>
<ul className="list-disc pl-4 md:pl-5 space-y-2">
<li><strong>非持牌金融或支付机构:</strong>本系统不是银行、商户收单或清算结算机构。</li>
<li><strong>不提供资金管理:</strong>系统无真实法币、加密货币或商业代金券充值、存储与兑现功能。若在二开中加入了积分等属性,其最终性质也应限制于虚拟软件积分。</li>
<li><strong>责任自担:</strong>关于用户对本系统进行二次开发并应用于其他生产环境产生的任何业务行为,由二开部署运营主体承担全部合规责任。</li>
</ul>
</div>
@@ -9,7 +9,7 @@ import {ArrowRight, ExternalLink, FileText, HelpCircle, Layers, Shield, ShieldCh
import Link from "next/link"
export function HomeMain() {
const { user, getTrustLevelLabel } = useUser()
const { user } = useUser()
const quickLinks = [
{
@@ -90,56 +90,56 @@ export function AuthSourceModal({
<div className="grid gap-4 md:grid-cols-2 pt-2">
<div className="space-y-2">
<Label>标识符 (英文名称)</Label>
<Input
value={form.name}
<Input
value={form.name}
disabled={!!source}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
placeholder="例如: github"
/>
</div>
<div className="space-y-2">
<Label>展示名称</Label>
<Input
value={form.display_name}
onChange={(e) => setForm((prev) => ({ ...prev, display_name: e.target.value }))}
<Input
value={form.display_name}
onChange={(e) => setForm((prev) => ({ ...prev, display_name: e.target.value }))}
placeholder="例如: GitHub 登录"
/>
</div>
<div className="space-y-2">
<Label>Client ID</Label>
<Input
value={form.client_id}
onChange={(e) => setForm((prev) => ({ ...prev, client_id: e.target.value }))}
<Input
value={form.client_id}
onChange={(e) => setForm((prev) => ({ ...prev, client_id: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>Client Secret</Label>
<Input
value={form.client_secret}
onChange={(e) => setForm((prev) => ({ ...prev, client_secret: e.target.value }))}
placeholder={source ? "留空则保留原值" : ""}
<Input
value={form.client_secret}
onChange={(e) => setForm((prev) => ({ ...prev, client_secret: e.target.value }))}
placeholder={source ? "留空则保留原值" : ""}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label>Discovery URL (OIDC 发行方 URL)</Label>
<Input
value={form.openid_discovery_url}
onChange={(e) => setForm((prev) => ({ ...prev, openid_discovery_url: e.target.value }))}
<Input
value={form.openid_discovery_url}
onChange={(e) => setForm((prev) => ({ ...prev, openid_discovery_url: e.target.value }))}
placeholder="https://..."
/>
</div>
<div className="space-y-2">
<Label>Scopes</Label>
<Input
value={form.scopes}
onChange={(e) => setForm((prev) => ({ ...prev, scopes: e.target.value }))}
<Input
value={form.scopes}
onChange={(e) => setForm((prev) => ({ ...prev, scopes: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label>图标 URL (可选)</Label>
<Input
value={form.icon_url}
onChange={(e) => setForm((prev) => ({ ...prev, icon_url: e.target.value }))}
<Input
value={form.icon_url}
onChange={(e) => setForm((prev) => ({ ...prev, icon_url: e.target.value }))}
placeholder="https://... 或留空"
/>
</div>
@@ -154,7 +154,7 @@ export function AuthSourceModal({
<Button variant="outline" type="button" onClick={onClose}>
取消
</Button>
<Button type="button" onClick={saveSource} disabled={saving} className="bg-indigo-600 hover:bg-indigo-700 text-white shadow-md shadow-indigo-600/10">
<Button type="button" onClick={saveSource} disabled={saving} variant="secondary">
{saving ? "保存中..." : "保存"}
</Button>
</div>
+104 -6
View File
@@ -14,18 +14,53 @@ import {
BreadcrumbSeparator
} from "@/components/ui/breadcrumb"
import {useUser} from "@/contexts/user-context"
import {ArrowRight, Link2, Loader2, Shield, Unlink} from "lucide-react"
import {ArrowRight, Info, Link2, Loader2, Lock, Shield, Unlink} from "lucide-react"
import {Button} from "@/components/ui/button"
import {Input} from "@/components/ui/input"
import {Separator} from "@/components/ui/separator"
import {AuthService} from "@/lib/services"
import {toast} from "sonner"
export function ProfileMain() {
const { user, loading, getTrustLevelLabel } = useUser()
const { user, loading, refetch } = useUser()
const controls = useAnimation()
const isAnimatingRef = React.useRef(false)
const queryClient = useQueryClient()
const [oldPassword, setOldPassword] = React.useState("")
const [newPassword, setNewPassword] = React.useState("")
const [confirmPassword, setConfirmPassword] = React.useState("")
const changePasswordMutation = useMutation({
mutationFn: (req: any) => AuthService.changePassword(req),
onSuccess: () => {
toast.success("密码修改成功")
setOldPassword("")
setNewPassword("")
setConfirmPassword("")
void refetch()
},
onError: (error: Error) => {
toast.error(error.message || "修改密码失败,请重试")
},
})
const handlePasswordChange = (e: React.FormEvent) => {
e.preventDefault()
if (newPassword !== confirmPassword) {
toast.error("两次输入的新密码不一致")
return
}
if (newPassword.length < 8) {
toast.error("新密码长度不能少于 8 位")
return
}
changePasswordMutation.mutate({
old_password: oldPassword,
new_password: newPassword,
})
}
const externalAccountBindingsQuery = useQuery({
queryKey: ["auth", "external-accounts"],
queryFn: () => AuthService.getExternalAccountBindings(),
@@ -135,10 +170,7 @@ export function ProfileMain() {
<div className="text-sm font-mono font-semibold">{user.id}</div>
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">信任等级</div>
<div className="text-sm font-semibold">{getTrustLevelLabel(user.trust_level)}</div>
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">管理员身份</div>
@@ -158,6 +190,72 @@ export function ProfileMain() {
</div>
</div>
{/* 修改密码面板 */}
<div className="space-y-6 bg-card border border-dashed rounded-lg p-6">
<div className="border-b pb-4 flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-amber-500/10 text-amber-500">
<Lock className="size-4" />
</div>
<div>
<h2 className="text-lg font-semibold tracking-tight">修改密码</h2>
<p className="text-xs text-muted-foreground">更改您的账号密码以确保安全。密码长度不能少于 8 位。</p>
</div>
</div>
{user.need_change_password && (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-3.5 py-3 text-xs text-amber-500 flex items-start gap-2.5">
<Info className="size-4 shrink-0 mt-0.5" />
<div>
<p className="font-semibold">密码风险提示</p>
<p className="mt-0.5 text-amber-500/80 leading-relaxed font-normal">
为了账号安全,您必须修改初始密码。
</p>
</div>
</div>
)}
<form onSubmit={handlePasswordChange} className="space-y-4 pt-2 max-w-md">
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">当前密码</label>
<Input
type="password"
placeholder="请输入当前密码"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">新密码</label>
<Input
type="password"
placeholder="新密码(至少 8 位)"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-muted-foreground">确认新密码</label>
<Input
type="password"
placeholder="确认新密码"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
<Button
type="submit"
className="w-full sm:w-auto"
disabled={changePasswordMutation.isPending}
>
{changePasswordMutation.isPending ? "提交中..." : "确认修改"}
</Button>
</form>
</div>
{/* 账号绑定面板 */}
<div className="space-y-6 bg-card border border-dashed rounded-lg p-6">
<div className="border-b pb-4 flex items-center gap-2">
+28 -12
View File
@@ -1,17 +1,17 @@
"use client"
import { useState, useEffect, memo } from "react"
import { AnimatePresence, motion } from "motion/react"
import { Button } from "@/components/ui/button"
import { Bell, Plus, Settings, Search, Moon, Sun, Maximize2, Minimize2 } from "lucide-react"
import { useUser } from "@/contexts/user-context"
import { useBellRing } from "@/contexts/bell-ring-context"
import { useNotificationSettings } from "@/contexts/notification-settings-context"
import { SidebarTrigger } from "@/components/ui/sidebar"
import { useTheme } from "next-themes"
import { useRouter } from "next/navigation"
import { SearchDialog } from "@/components/layout/search-dialog"
import { Kbd } from "@/components/ui/kbd"
import {memo, useEffect, useState} from "react"
import {AnimatePresence, motion} from "motion/react"
import {Button} from "@/components/ui/button"
import {Bell, Info, Maximize2, Minimize2, Moon, Search, Settings, Sun} from "lucide-react"
import {useUser} from "@/contexts/user-context"
import {useBellRing} from "@/contexts/bell-ring-context"
import {useNotificationSettings} from "@/contexts/notification-settings-context"
import {SidebarTrigger} from "@/components/ui/sidebar"
import {useTheme} from "next-themes"
import {useRouter} from "next/navigation"
import {SearchDialog} from "@/components/layout/search-dialog"
import {Kbd} from "@/components/ui/kbd"
/**
@@ -75,6 +75,22 @@ export function SiteHeader({ isFullWidth = false, onToggleFullWidth }: { isFullW
return (
<div className="flex flex-col w-full">
<AppBanner />
{user?.need_change_password && (
<div className="bg-amber-500/10 border-b border-amber-500/20 px-4 py-3 text-amber-500 text-xs sm:text-sm flex items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Info className="size-4 shrink-0 text-amber-500" />
<span>为了您的账号安全,请立即修改密码!</span>
</div>
<Button
variant="outline"
size="sm"
className="text-amber-500 hover:text-amber-600 border-amber-500/30 hover:border-amber-500/50 bg-transparent shrink-0 h-8 px-3 text-xs"
onClick={() => router.push("/settings/profile")}
>
去修改密码
</Button>
</div>
)}
<header className="flex h-(--header-height) shrink-0 items-center bg-background px-4 md:px-0">
<div className="flex w-full items-center justify-between md:hidden">
<div className="flex items-center gap-2">
+4 -4
View File
@@ -65,13 +65,13 @@ import {useUser} from "@/contexts/user-context"
const data = {
navMain: [
{ title: "首页", url: "/home", icon: Home },
{ title: "文件管理", url: "/settings/files", icon: FolderOpen },
],
admin: [
{ title: "系统设置", url: "/admin/settings", icon: Settings },
{ title: "系统配置", url: "/admin/system", icon: ShieldCheck },
{ title: "用户管理", url: "/admin/users", icon: UserRound },
{ title: "任务管理", url: "/admin/tasks", icon: Layers },
{ title: "文件管理", url: "/admin/files", icon: FolderOpen },
],
document: [
{ title: "接口文档", url: "/docs/api", icon: CreditCard, external: true },
@@ -92,7 +92,7 @@ const data = {
*/
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const { toggleSidebar, state, isMobile, setOpenMobile } = useSidebar()
const { user, getTrustLevelLabel, logout } = useUser()
const { user, logout } = useUser()
const [showLogoutDialog, setShowLogoutDialog] = React.useState(false)
const [isLoggingOut, setIsLoggingOut] = React.useState(false)
const pathname = usePathname()
@@ -185,7 +185,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
{user?.nickname || user?.username || "Unknown User"}
</span>
<span className="text-[11px] font-medium text-muted-foreground/100 truncate w-full text-left ml-2">
{user ? getTrustLevelLabel(user.trust_level) : "Trust Level Unknown"}
{user?.is_admin ? "系统管理员" : "普通用户"}
</span>
</div>
<ChevronDown className="size-4 text-muted-foreground ml-auto group-data-[collapsible=icon]:hidden" />
@@ -226,7 +226,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
{user?.nickname || user?.username || "Unknown User"}
</span>
<span className="text-xs font-base text-muted-foreground">
{user ? getTrustLevelLabel(user.trust_level) : "Trust Level Unknown"}
{user?.is_admin ? "系统管理员" : "普通用户"}
</span>
</div>
</DropdownMenuLabel>
+26 -9
View File
@@ -1,9 +1,9 @@
"use client"
import * as React from "react"
import { createContext, useContext, useCallback, useState, useRef, useEffect } from "react"
import { toast } from "sonner"
import { AdminService, AdminUser, ListUsersRequest } from "@/lib/services"
import {createContext, useCallback, useContext, useEffect, useRef, useState} from "react"
import {toast} from "sonner"
import {AdminService, AdminUser, CreateUserRequest, ListUsersRequest} from "@/lib/services"
/** 用户列表查询参数 */
export interface UserQueryParams {
@@ -38,6 +38,7 @@ interface AdminUsersContextState {
fetchUsers: (force?: boolean) => Promise<void>
refresh: () => Promise<void>
updateUserStatus: (user: AdminUser) => Promise<void>
createUser: (req: CreateUserRequest) => Promise<AdminUser>
}
const CACHE_DURATION = 5 * 60 * 1000 // 5 minutes cache
@@ -107,17 +108,17 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
const requestId = ++latestRequestIdRef.current
try {
// Current API doesn't support status filter in listUsers?
// The previous implementation did client-side filtering.
// Current API doesn't support status filter in listUsers?
// The previous implementation did client-side filtering.
// Ideally backend supports it. If not, we fetch and filter?
// "AdminService.listUsers" in previous code only took page, page_size, username.
// So status filter was client side.
// So status filter was client side.
// However, caching client-filtered result is tricky if we don't have all data.
// But previous implementation fetched *paged* data then filtered? No, that would be wrong (filtering 20 items might leave 0).
// Let's check previous implementation:
// Let's check previous implementation:
// "const data = await AdminService.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..."
// This means filtering happens ONLY on the current page of results! This is technically buggy if the user wants "all inactive users".
// But preserving that behavior for now.
// But preserving that behavior for now.
const requestParams: ListUsersRequest = {
page,
@@ -188,6 +189,21 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
}
}
const createUser = async (req: CreateUserRequest) => {
try {
const newUser = await AdminService.createUser(req)
setUsers(prev => [newUser, ...prev])
setTotal(prev => prev + 1)
// Clear cache because data changed
cacheRef.current = {}
toast.success(`已成功创建用户 ${ newUser.username }`)
return newUser
} catch (err) {
toast.error(err instanceof Error ? err.message : '创建用户失败')
throw err
}
}
const value = {
users,
total,
@@ -205,7 +221,8 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
setStatusFilter,
fetchUsers,
refresh,
updateUserStatus
updateUserStatus,
createUser
}
return (
+1 -28
View File
@@ -3,7 +3,7 @@
import {createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react'
import services from '@/lib/services'
import {PayLevel, TrustLevel, User} from '@/lib/services/auth/types'
import {User} from '@/lib/services/auth/types'
/** 用户状态接口 */
@@ -17,27 +17,10 @@ interface UserState {
interface UserContextValue extends UserState {
setUser: (user: User) => void
refetch: () => Promise<void>
getTrustLevelLabel: (trustLevel: TrustLevel) => string
getPayLevelLabel: (payLevel: PayLevel) => string
logout: () => Promise<void>
}
/** 信任等级映射 */
const TRUST_LEVEL_LABELS: Record<TrustLevel, string> = {
[TrustLevel.New]: '新用户',
[TrustLevel.Basic]: '基本用户',
[TrustLevel.Member]: '成员',
[TrustLevel.Regular]: '活跃用户',
[TrustLevel.Leader]: '领导者',
}
/** 支付等级映射 */
const PAY_LEVEL_LABELS: Record<PayLevel, string> = {
[PayLevel.BlackGold]: '黑金',
[PayLevel.WhiteGold]: '白金',
[PayLevel.Gold]: '黄金',
[PayLevel.Ordinary]: '普通',
}
/** 用户上下文 */
const UserContext = createContext<UserContextValue | undefined>(undefined)
@@ -65,15 +48,7 @@ export function UserProvider({ children }: { children: ReactNode }) {
const isMountedRef = useRef(true)
/** 获取信任等级标签 */
const getTrustLevelLabel = useCallback((trustLevel: TrustLevel): string => {
return TRUST_LEVEL_LABELS[trustLevel] || '未知'
}, [])
/** 获取支付等级标签 */
const getPayLevelLabel = useCallback((payLevel: PayLevel): string => {
return PAY_LEVEL_LABELS[payLevel] || '未知'
}, [])
/** 获取用户信息 */
const fetchUser = useCallback(async () => {
@@ -144,8 +119,6 @@ export function UserProvider({ children }: { children: ReactNode }) {
...state,
setUser,
refetch,
getTrustLevelLabel,
getPayLevelLabel,
logout,
}}
>
@@ -1,8 +1,10 @@
import {BaseService} from '../core/base.service';
import type {
AdminUser,
AuthSource,
AuthSourceRequest,
CreateSystemConfigRequest,
CreateUserRequest,
DispatchTaskRequest,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
@@ -307,4 +309,13 @@ export class AdminService extends BaseService {
): Promise<void> {
return this.put<void>(`/users/${ id }/status`, request);
}
/**
* 创建用户
* @param request - 创建用户请求参数
* @returns 创建成功的用户信息
*/
static async createUser(request: CreateUserRequest): Promise<AdminUser> {
return this.post<AdminUser>('/users', request);
}
}
+1
View File
@@ -23,6 +23,7 @@ export { AdminService } from './admin.service';
export type {
SystemConfig,
CreateSystemConfigRequest,
CreateUserRequest,
UpdateSystemConfigRequest,
AuthSource,
AuthSourceRequest,
+16 -14
View File
@@ -160,20 +160,6 @@ export interface AdminUser {
nickname: string;
/** 头像 URL */
avatar_url: string;
/** 信任等级 */
trust_level: number;
/** 支付积分 */
pay_score: number;
/** 累计收入 */
total_receive: string;
/** 累计支出 */
total_payment: string;
/** 累计社区积分 */
total_community: string;
/** 社区余额 */
community_balance: string;
/** 可用余额 */
available_balance: string;
/** 是否激活 */
is_active: boolean;
/** 是否管理员 */
@@ -218,6 +204,22 @@ export interface UpdateUserStatusRequest {
is_active: boolean;
}
/**
* 创建用户请求参数
*/
export interface CreateUserRequest {
/** 用户名 */
username: string;
/** 密码 */
password: string;
/** 昵称 */
nickname?: string;
/** 是否激活 */
is_active?: boolean;
/** 是否管理员 */
is_admin?: boolean;
}
/**
* 认证源信息
*/
@@ -1,6 +1,7 @@
import {BaseService} from '../core/base.service';
import type {
AuthSource,
ChangePasswordRequest,
ExternalAccountBinding,
LoginRequest,
OAuthAuthorizeResponse,
@@ -110,6 +111,10 @@ export class AuthService extends BaseService {
return this.post<User>('/user/register', request);
}
static async changePassword(request: ChangePasswordRequest): Promise<void> {
return this.post<void>('/user/change-password', request);
}
static async getExternalAccountBindings(): Promise<ExternalAccountBinding[]> {
return this.get<ExternalAccountBinding[]>('/oauth/external-accounts');
}
+1 -1
View File
@@ -26,7 +26,6 @@
*/
export { AuthService } from './auth.service';
export { TrustLevel, PayLevel } from './types';
export type {
User,
OAuthLoginUrlResponse,
@@ -37,4 +36,5 @@ export type {
OAuthCallbackResult,
AuthSource,
ExternalAccountBinding,
ChangePasswordRequest,
} from './types';
+7 -52
View File
@@ -1,33 +1,3 @@
/**
* 信任等级
*/
export enum TrustLevel {
/** 新用户 */
New = 0,
/** 基础用户 */
Basic = 1,
/** 成员 */
Member = 2,
/** 常规用户 */
Regular = 3,
/** 领导者 */
Leader = 4,
}
/**
* 支付等级
*/
export enum PayLevel {
/** 普通 */
Ordinary = 0,
/** 黄金 */
Gold = 1,
/** 白金 */
WhiteGold = 2,
/** 黑金 */
BlackGold = 3,
}
/**
* 用户基本信息
*/
@@ -38,32 +8,12 @@ export interface User {
username: string;
/** 昵称 */
nickname: string;
/** 信任等级 */
trust_level: TrustLevel;
/** 头像 URL */
avatar_url: string;
/** 总接收金额 */
total_receive: string;
/** 总支付金额 */
total_payment: string;
/** 总社区金额 */
total_community: string;
/** 社区余额 */
community_balance: string;
/** 可用余额 */
available_balance: string;
/** 在途资金(延迟到账中) */
pending_balance: string;
/** 支付分数 */
pay_score: number;
/** 是否为管理员 */
is_admin: boolean;
/** 当日剩余配额 */
remain_quota: string;
/** 支付等级 */
pay_level: PayLevel;
/** 每日限额 */
daily_limit: number | null;
/** 是否需要修改密码 */
need_change_password?: boolean;
}
/**
@@ -122,3 +72,8 @@ export interface ExternalAccountBinding {
email: string;
created_at: string;
}
export interface ChangePasswordRequest {
old_password: string;
new_password: string;
}
+3 -2
View File
@@ -81,8 +81,8 @@ export type {
// ==================== 业务服务导出 ====================
// 认证服务
export { AuthService, TrustLevel } from './auth';
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest, AuthSource, ExternalAccountBinding } from './auth';
export { AuthService } from './auth';
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest, AuthSource, ExternalAccountBinding, ChangePasswordRequest } from './auth';
// 配置服务
export { ConfigService } from './config';
@@ -93,6 +93,7 @@ export { AdminService } from './admin';
export type {
SystemConfig,
CreateSystemConfigRequest,
CreateUserRequest,
UpdateSystemConfigRequest,
TaskMeta,
TaskExecution,