feat(web): add login and customer chat demos
Add a responsive login screen using the shared appearance controls and a browser-only customer-service simulator for the chat workspace. Register both routes and include mock media resolution, message sending, and earlier-history loading for interactive verification.
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
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"
|
||||
import { EyeIcon, EyeOffIcon } from "lucide-react"
|
||||
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 (
|
||||
<fieldset className="relative flex h-12 items-center rounded-lg border-2 border-border p-0 focus-within:border-primary focus-within:[&_label]:opacity-100">
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user