This commit is contained in:
2025-12-19 22:23:16 +08:00
parent 064f67a2b9
commit 5ece041672
13 changed files with 106 additions and 97 deletions

View File

@@ -1,4 +1,3 @@
import { useCallback } from "react"
import {
Alert,
AlertDescription,

View File

@@ -15,8 +15,6 @@ import {
import { useUserStore } from "@/store/useUserStore";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
// import { useUserMe } from "@/hooks/user/use-user-me";
// import { toast } from "sonner";
export default function ConsoleMenuLayout({
children,
@@ -30,7 +28,7 @@ export default function ConsoleMenuLayout({
if (userStore.initialized && !userStore.user) {
router.replace('/console/login')
}
}, [userStore])
}, [userStore, router])
return (
<SidebarProvider>

View File

@@ -3,17 +3,15 @@
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from "@/components/ui/field";
import { useUserStore } from "@/store/useUserStore";
import { Checkbox } from "@radix-ui/react-checkbox";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { ReactElement, useMemo, useState } from "react";
import { ReactElement, useState } from "react";
import {
Dialog,
DialogClose,

View File

@@ -27,6 +27,7 @@ import { AlertCircle } from "lucide-react";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { AdminAPI } from "@/lib/api/client";
import { UserEntity } from "@/lib/api/endpoints/admin.client";
export function UserInfoEditor({
onClose,
@@ -35,7 +36,7 @@ export function UserInfoEditor({
userId,
}: {
onClose: () => void,
onUserUpdate: (user: User) => void,
onUserUpdate: (user: UserEntity) => void,
onUserSoftDelete: (userId: string) => void,
userId: string
}) {

View File

@@ -6,19 +6,19 @@ import { TooltipContent, TooltipProvider, TooltipTrigger, Tooltip } from "@/comp
import { useUserList } from "@/hooks/admin/user/use-user-list";
import { useState } from "react";
import { UserInfoEditor } from "./components/user-info-editor";
import { User } from "@/lib/types/user";
import { CreateUserEditor } from "./components/create-user-editor";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { toast } from "sonner";
import { ApiError } from "next/dist/server/api-utils";
import { AdminAPI } from "@/lib/api/client";
import { UserEntity } from "@/lib/api/endpoints/admin.client";
export default function Page() {
const { users, isLoading, error, mutate, refresh } = useUserList();
const [editorUserId, setEditorUserId] = useState("");
const handleUserUpdateLocal = async (newUser: User) => {
const handleUserUpdateLocal = async (newUser: UserEntity) => {
await mutate(
(data) => {
if (!data) return data;

View File

@@ -1,67 +1,67 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
import { useState, useCallback } from "react";
import { toast } from "sonner";
import LoginHeader from "./LoginHeader";
import { SendCodeFormData } from "./types";
import { Label } from "@/components/ui/label";
// import { Button } from "@/components/ui/button";
// import { Input } from "@/components/ui/input";
// import { InputOTP, InputOTPGroup, InputOTPSlot } from "@/components/ui/input-otp";
// import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp";
// import { useState, useCallback } from "react";
// import { toast } from "sonner";
// import LoginHeader from "./LoginHeader";
// import { SendCodeFormData } from "./types";
// import { Label } from "@/components/ui/label";
export default function EmailLoginMode({ onSendCode }: { onSendCode: (data: SendCodeFormData) => Promise<boolean> }) {
const [email, setEmail] = useState("");
const handleSendCode = useCallback(() => {
if (!email.trim().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
toast.error('请输入正确的邮箱地址');
return;
}
onSendCode({
type: 'email',
email,
})
}, [email, onSendCode]);
// export default function EmailLoginMode({ onSendCode }: { onSendCode: (data: SendCodeFormData) => Promise<boolean> }) {
// const [email, setEmail] = useState("");
// const handleSendCode = useCallback(() => {
// if (!email.trim().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
// toast.error('请输入正确的邮箱地址');
// return;
// }
// onSendCode({
// type: 'email',
// email,
// })
// }, [email, onSendCode]);
return (
<>
<LoginHeader />
<div className="grid gap-3">
<Label htmlFor="email"></Label>
<Input
id="email-login-mode-email"
name="email"
type="text"
placeholder="电子邮箱"
value={email}
onChange={(e) => setEmail(e.target.value)}
required />
</div>
<div className="grid gap-3">
<div className="flex items-center h-4">
<Label htmlFor="code"></Label>
</div>
<div className="flex gap-5">
<InputOTP
id="email-login-mode-code"
name="code"
maxLength={6}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
required
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<Button type="button" variant="secondary" onClick={handleSendCode}></Button>
</div>
</div>
<Button type="submit" className="w-full">
</Button>
</>
)
}
// return (
// <>
// <LoginHeader />
// <div className="grid gap-3">
// <Label htmlFor="email">电子邮箱</Label>
// <Input
// id="email-login-mode-email"
// name="email"
// type="text"
// placeholder="电子邮箱"
// value={email}
// onChange={(e) => setEmail(e.target.value)}
// required />
// </div>
// <div className="grid gap-3">
// <div className="flex items-center h-4">
// <Label htmlFor="code">验证码</Label>
// </div>
// <div className="flex gap-5">
// <InputOTP
// id="email-login-mode-code"
// name="code"
// maxLength={6}
// pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
// required
// >
// <InputOTPGroup>
// <InputOTPSlot index={0} />
// <InputOTPSlot index={1} />
// <InputOTPSlot index={2} />
// <InputOTPSlot index={3} />
// <InputOTPSlot index={4} />
// <InputOTPSlot index={5} />
// </InputOTPGroup>
// </InputOTP>
// <Button type="button" variant="secondary" onClick={handleSendCode}>获取验证码</Button>
// </div>
// </div>
// <Button type="submit" className="w-full">
// 注册并登录
// </Button>
// </>
// )
// }

View File

@@ -27,7 +27,7 @@ export default function Login() {
if (userStore.user) {
router.replace('/console')
}
}, [userStore])
}, [userStore, router])
return (
<>
@@ -41,7 +41,7 @@ export default function Login() {
e.preventDefault();
const formData = new FormData(e.currentTarget);
let handler = (await (async () => {
const handler = (await (async () => {
if (loginMode === 'password') {
return import('./components/PasswordLoginMode');
} else if (loginMode === 'sms') {

View File

@@ -8,7 +8,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { ReactNode, useEffect, useState } from "react"
import { ReactNode, useCallback, useEffect, useState } from "react"
interface HumanVerificationProps {
open?: boolean;
@@ -18,19 +18,19 @@ interface HumanVerificationProps {
children: ReactNode;
}
export function HumanVerification({ open, onOpenChange, onSuccess, onFail, children }: HumanVerificationProps) {
export function HumanVerification({ open, onOpenChange, onSuccess, children }: HumanVerificationProps) {
const [i_open, i_setOpen] = useState(false);
const setOpen = (o: boolean) => {
const setOpen = useCallback((o: boolean) => {
i_setOpen(o);
onOpenChange?.(o);
};
}, [onOpenChange]);
useEffect(() => {
if (i_open) {
setOpen(false);
onSuccess?.();
}
}, [i_open]);
}, [i_open, onSuccess, setOpen]);
return (
<Dialog open={open ?? i_open} onOpenChange={setOpen}>

View File

@@ -1,10 +1,10 @@
import { AdminAPI } from "@/lib/api/client";
import { User } from "@/lib/types/user";
import { UserEntity } from "@/lib/api/endpoints/admin.client";
import { toast } from "sonner";
import useSWR from "swr";
export function useUser(userId: string) {
const { data, error, isLoading, mutate } = useSWR<User>(
const { data, error, isLoading, mutate } = useSWR<UserEntity>(
['/api/admin/user', userId],
() => AdminAPI.getUser(userId),
{

View File

@@ -2,7 +2,7 @@ import { APIResponse, HttpMethod, normalizeAPIError } from './common';
interface ClientFetchRequestOptions extends RequestInit {
method?: HttpMethod;
body?: any;
body?: string;
}
export async function clientFetch<T = unknown>(

View File

@@ -2,7 +2,19 @@ import { Resource } from "@/lib/types/resource";
import { clientFetch } from "../client";
import { Blog } from "@/lib/types/blog";
import { BlogPermission } from "@/lib/types/Blog.Permission.enum";
import { User } from "@/lib/types/user";
import { Role } from "@/lib/types/role";
export interface UserEntity {
userId: string;
username: string;
nickname: string;
email?: string;
phone?: string;
avatar?: string;
createdAt: string;
deletedAt: string | null;
roles: Role[];
}
// ======== Resource ========
export async function listResources() {
@@ -24,7 +36,7 @@ export async function createResource(data: CreateResourceParams) {
data.description = data.description.trim();
data.imageUrl = data.imageUrl.trim();
data.link = data.link.trim();
for (let tag of data.tags) {
for (const tag of data.tags) {
tag.name = tag.name.trim();
}
@@ -59,7 +71,7 @@ export async function updateResource(id: string, data: UpdateResourceParams) {
data.description = data.description.trim();
data.imageUrl = data.imageUrl.trim();
data.link = data.link.trim();
for (let tag of data.tags) {
for (const tag of data.tags) {
tag.name = tag.name.trim();
}
@@ -139,18 +151,18 @@ interface CreateUserParams {
}
export async function createUser(data: CreateUserParams) {
type Keys = keyof CreateUserParams;
for (let key in data) {
for (const key in data) {
data[key as Keys] = data[key as Keys]?.trim() || null;
}
return clientFetch<User>("/api/admin/user", {
return clientFetch<UserEntity>("/api/admin/user", {
method: "POST",
body: JSON.stringify(data),
});
}
export function getUser(id: string) {
return clientFetch<User>(`/api/admin/user/${id}`);
return clientFetch<UserEntity>(`/api/admin/user/${id}`);
}
export interface UserListParams {
@@ -158,7 +170,7 @@ export interface UserListParams {
pageSize?: number
}
export interface UserListResponse {
items: User[],
items: UserEntity[],
total: number
page: number
pageSize: number
@@ -201,7 +213,7 @@ export async function updateUser(userId: string, user: updateUser) {
user.email = user.email?.trim() || null;
user.phone = user.phone?.trim() || null;
return clientFetch<User>(`/api/admin/user/${userId}`, {
return clientFetch<UserEntity>(`/api/admin/user/${userId}`, {
body: JSON.stringify(user),
method: "PUT",
});

View File

@@ -1,7 +1,7 @@
import { User } from "@/lib/types/user";
import { clientFetch } from "../client";
import { APIError } from "../common";
import { PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON } from "@simplewebauthn/browser";
import { AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON } from "@simplewebauthn/browser";
export async function loginByPassword(identifier: string, password: string) {
identifier = identifier.trim();
@@ -85,7 +85,8 @@ export async function getLoginByPasskeyOptions() {
})
}
export async function loginByPasskey(credentialResponse: any) {
/** @lint-ignore */
export async function loginByPasskey(credentialResponse: AuthenticationResponseJSON) {
return clientFetch<{ user: User }>('/api/auth/passkey/login', {
method: 'POST',
body: JSON.stringify({

View File

@@ -3,7 +3,7 @@ import { APIResponse, HttpMethod, normalizeAPIError } from './common';
interface ServerFetchRequestOptions extends RequestInit {
method?: HttpMethod;
body?: any;
body?: string;
}
export async function serverFetch<T = unknown>(