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 { import {
Alert, Alert,
AlertDescription, AlertDescription,

View File

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

View File

@@ -3,17 +3,15 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from "@/components/ui/field"; import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from "@/components/ui/field";
import { useUserStore } from "@/store/useUserStore"; import { useUserStore } from "@/store/useUserStore";
import { Checkbox } from "@radix-ui/react-checkbox";
import { import {
Table, Table,
TableBody, TableBody,
TableCaption,
TableCell, TableCell,
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { ReactElement, useMemo, useState } from "react"; import { ReactElement, useState } from "react";
import { import {
Dialog, Dialog,
DialogClose, 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 { 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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { AdminAPI } from "@/lib/api/client"; import { AdminAPI } from "@/lib/api/client";
import { UserEntity } from "@/lib/api/endpoints/admin.client";
export function UserInfoEditor({ export function UserInfoEditor({
onClose, onClose,
@@ -35,7 +36,7 @@ export function UserInfoEditor({
userId, userId,
}: { }: {
onClose: () => void, onClose: () => void,
onUserUpdate: (user: User) => void, onUserUpdate: (user: UserEntity) => void,
onUserSoftDelete: (userId: string) => void, onUserSoftDelete: (userId: string) => void,
userId: string 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 { useUserList } from "@/hooks/admin/user/use-user-list";
import { useState } from "react"; import { useState } from "react";
import { UserInfoEditor } from "./components/user-info-editor"; import { UserInfoEditor } from "./components/user-info-editor";
import { User } from "@/lib/types/user";
import { CreateUserEditor } from "./components/create-user-editor"; import { CreateUserEditor } from "./components/create-user-editor";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { toast } from "sonner"; import { toast } from "sonner";
import { ApiError } from "next/dist/server/api-utils"; import { ApiError } from "next/dist/server/api-utils";
import { AdminAPI } from "@/lib/api/client"; import { AdminAPI } from "@/lib/api/client";
import { UserEntity } from "@/lib/api/endpoints/admin.client";
export default function Page() { export default function Page() {
const { users, isLoading, error, mutate, refresh } = useUserList(); const { users, isLoading, error, mutate, refresh } = useUserList();
const [editorUserId, setEditorUserId] = useState(""); const [editorUserId, setEditorUserId] = useState("");
const handleUserUpdateLocal = async (newUser: User) => { const handleUserUpdateLocal = async (newUser: UserEntity) => {
await mutate( await mutate(
(data) => { (data) => {
if (!data) return data; if (!data) return data;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { User } from "@/lib/types/user"; import { User } from "@/lib/types/user";
import { clientFetch } from "../client"; import { clientFetch } from "../client";
import { APIError } from "../common"; 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) { export async function loginByPassword(identifier: string, password: string) {
identifier = identifier.trim(); 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', { return clientFetch<{ user: User }>('/api/auth/passkey/login', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({

View File

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