2026-08-08 21:14:31 +08:00
|
|
|
import React, { useState } from "react"
|
|
|
|
|
import { useMutation } from "@tanstack/react-query"
|
|
|
|
|
import { Button } from "@workspace/ui/components/button"
|
|
|
|
|
import { Input } from "@workspace/ui/components/input"
|
2026-09-20 15:52:22 +08:00
|
|
|
import { EyeIcon, EyeOffIcon } from "@workspace/ui/components/icon"
|
2026-08-08 21:14:31 +08:00
|
|
|
import { cn } from "@workspace/ui/lib/utils"
|
|
|
|
|
import { ThemeToggleButton } from "@workspace/blocks/blocks/appearance"
|
|
|
|
|
import { Logo } from "@/components/logo"
|
|
|
|
|
|
|
|
|
|
export function LoginPage() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="isolate h-svh min-h-148 w-svw lg:min-h-164.5">
|
|
|
|
|
<header className="relative z-10 h-0">
|
|
|
|
|
<div className="flex h-14 items-center justify-between px-6">
|
|
|
|
|
<div className="size-6 opacity-0 lg:opacity-100">
|
|
|
|
|
<Logo />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<ThemeToggleButton />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
<main className="relative isolate flex size-full before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:bg-[url(https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/background/background-3-blur.webp)] before:bg-cover before:bg-center before:bg-no-repeat before:opacity-20">
|
|
|
|
|
<div className="pointer-events-none hidden w-120 flex-col justify-center gap-16 bg-linear-to-r from-secondary/10 to-muted/20 p-6 pt-18 select-none lg:flex">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<h2 className="text-3xl font-bold tracking-tight text-foreground">
|
|
|
|
|
欢迎回来 👋🏻
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="mt-4 text-muted-foreground">
|
|
|
|
|
让商品、订单与运营管理更简单高效。
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<img
|
|
|
|
|
src="https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/illustrations/illustration-dashboard.webp"
|
|
|
|
|
alt=""
|
|
|
|
|
className="aspect-4/3 w-full object-cover object-center"
|
|
|
|
|
/>
|
|
|
|
|
<Features />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-1 flex-col items-start gap-7 bg-sidebar/80 px-6 py-20 lg:justify-center lg:px-8">
|
|
|
|
|
<div className="mx-auto mb-10 w-full max-w-md lg:hidden">
|
|
|
|
|
<Logo className="size-12" />
|
|
|
|
|
</div>
|
|
|
|
|
<LoginForm />
|
|
|
|
|
<div className="lg:h-24" />
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const features = [
|
|
|
|
|
{
|
|
|
|
|
label: "Jwt",
|
|
|
|
|
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-jwt.svg",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Firebase",
|
|
|
|
|
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-firebase.svg",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Amplify",
|
|
|
|
|
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-amplify.svg",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Auth0",
|
|
|
|
|
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-auth0.svg",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Supabase",
|
|
|
|
|
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-supabase.svg",
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function Features() {
|
|
|
|
|
const [index, setIndex] = React.useState(0)
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
const timerId = setInterval(() => {
|
|
|
|
|
setIndex((currentIndex) => (currentIndex + 1) % features.length)
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
clearInterval(timerId)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<ul className="flex items-center justify-center gap-4">
|
|
|
|
|
{features.map((f, i) => (
|
|
|
|
|
<li
|
|
|
|
|
key={f.label}
|
|
|
|
|
className={cn(
|
|
|
|
|
"transition-[filter] duration-1000 ease-linear",
|
|
|
|
|
i === index ? "grayscale-0" : "grayscale-100"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<img className="size-8" alt={f.label} src={f.img} />
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LoginForm() {
|
|
|
|
|
// const navigate = useNavigate()
|
|
|
|
|
const [username, setUsername] = useState("")
|
|
|
|
|
const [password, setPassword] = useState("")
|
|
|
|
|
|
|
|
|
|
const mutation = useMutation({
|
|
|
|
|
// mutationFn: () => {
|
|
|
|
|
// return api.post<AdminAuthResponse>("/api/v1/admin/auth/login", {
|
|
|
|
|
// username,
|
|
|
|
|
// password,
|
|
|
|
|
// })
|
|
|
|
|
// },
|
|
|
|
|
// onSuccess: () => {
|
|
|
|
|
// login()
|
|
|
|
|
// Toast("登录成功", "success")
|
|
|
|
|
// navigate("/", { replace: true })
|
|
|
|
|
// },
|
|
|
|
|
// onError: (err: Error) => toast(err.message || "登录失败", "error"),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="mx-auto w-full max-w-md">
|
|
|
|
|
<h5 className="mb-3 text-2xl font-bold tracking-tight text-foreground">
|
|
|
|
|
账号登录
|
|
|
|
|
</h5>
|
|
|
|
|
<p className="lg:text-md text-sm text-muted-foreground">
|
|
|
|
|
<span className="text-muted-foreground">
|
|
|
|
|
请输入您的帐户信息以开始管理您的系统
|
|
|
|
|
</span>
|
|
|
|
|
</p>
|
|
|
|
|
<form
|
|
|
|
|
className="mt-8 space-y-8"
|
|
|
|
|
onSubmit={(e) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
mutation.mutate()
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Field
|
|
|
|
|
label="用户名"
|
|
|
|
|
inputId="username"
|
|
|
|
|
Component={UsernameInput}
|
|
|
|
|
value={username}
|
|
|
|
|
setValue={setUsername}
|
|
|
|
|
/>
|
|
|
|
|
<Field
|
|
|
|
|
label="密码"
|
|
|
|
|
inputId="password"
|
|
|
|
|
Component={PasswordInput}
|
|
|
|
|
value={password}
|
|
|
|
|
setValue={setPassword}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
className="mt-2 h-12 w-full rounded-lg"
|
|
|
|
|
disabled={mutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{mutation.isPending ? "登录中..." : "登录"}
|
|
|
|
|
</Button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type FieldInputProps = {
|
|
|
|
|
inputId: string
|
|
|
|
|
className?: string
|
|
|
|
|
value: string
|
|
|
|
|
setValue: React.Dispatch<React.SetStateAction<string>>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type FieldInputComponent = React.ComponentType<FieldInputProps>
|
|
|
|
|
|
|
|
|
|
function Field({
|
|
|
|
|
label,
|
|
|
|
|
Component,
|
|
|
|
|
...props
|
|
|
|
|
}: Omit<FieldInputProps, "className"> & {
|
|
|
|
|
label: string
|
|
|
|
|
Component: FieldInputComponent
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
2026-09-29 17:08:16 +08:00
|
|
|
<fieldset className="relative flex h-12 items-center rounded-lg border-2 border-border p-0 focus-within:border-accent focus-within:[&_label]:opacity-100">
|
2026-08-08 21:14:31 +08:00
|
|
|
<legend className="ml-2 flex h-0 px-0">
|
|
|
|
|
<label
|
|
|
|
|
className="h-fit -translate-y-1/2 px-1 text-sm opacity-50"
|
|
|
|
|
htmlFor={props.inputId}
|
|
|
|
|
>
|
|
|
|
|
{label}
|
|
|
|
|
</label>
|
|
|
|
|
</legend>
|
|
|
|
|
<Component
|
|
|
|
|
className="h-full rounded-none border-none bg-transparent outline-none autofill:bg-transparent focus-visible:border-none focus-visible:ring-0 [&:-webkit-autofill]:[-webkit-background-clip:text] [&:-webkit-autofill]:[-webkit-text-fill-color:var(--foreground)] [&:-webkit-autofill]:[transition:background-color_9999s_ease-out]"
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
</fieldset>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function UsernameInput({
|
|
|
|
|
inputId,
|
|
|
|
|
className,
|
|
|
|
|
value,
|
|
|
|
|
setValue,
|
|
|
|
|
}: FieldInputProps) {
|
|
|
|
|
return (
|
|
|
|
|
<Input
|
|
|
|
|
id={inputId}
|
|
|
|
|
value={value}
|
|
|
|
|
className={className}
|
|
|
|
|
onChange={(e) => setValue(e.target.value)}
|
|
|
|
|
placeholder="请输入用户名"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PasswordInput({
|
|
|
|
|
inputId,
|
|
|
|
|
className,
|
|
|
|
|
value,
|
|
|
|
|
setValue,
|
|
|
|
|
}: FieldInputProps) {
|
|
|
|
|
const [isVisible, setVisile] = React.useState(false)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Input
|
|
|
|
|
id={inputId}
|
|
|
|
|
type={isVisible ? "text" : "password"}
|
|
|
|
|
value={value}
|
|
|
|
|
className={cn("relative z-1 pe-24", className)}
|
|
|
|
|
onChange={(e) => setValue(e.target.value)}
|
|
|
|
|
placeholder="请输入登录密码"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute top-0 right-0 z-2 flex h-11 w-11 items-center justify-center">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
setVisile((v) => !v)
|
|
|
|
|
}}
|
|
|
|
|
className="rounded-sm"
|
|
|
|
|
>
|
|
|
|
|
{isVisible ? <EyeIcon /> : <EyeOffIcon />}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|