登入驗證以及使用者按鈕
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 55s

This commit is contained in:
2026-01-07 14:44:01 +08:00
parent 19c60a6126
commit ef1fc47cff
10 changed files with 380 additions and 69 deletions

View File

@@ -0,0 +1,11 @@
import { ImgHTMLAttributes } from 'react';
export default function ApplicationLogo(props: ImgHTMLAttributes<HTMLImageElement>) {
return (
<img
{...props}
src="/logo.png"
alt="小小冰室 Logo"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
export default function InputError({ message, className = '', ...props }: HTMLAttributes<HTMLParagraphElement> & { message?: string }) {
return message ? (
<p {...props} className={cn('text-sm text-red-600', className)}>
{message}
</p>
) : null;
}

View File

@@ -1,5 +1,4 @@
import {
ChevronDown,
ChevronRight,
Package,
ShoppingCart,
@@ -11,13 +10,24 @@ import {
Warehouse,
Truck,
Contact2,
FileText
FileText,
LogOut,
User,
ChevronDown
} from "lucide-react";
import { Toaster } from "sonner";
import { useState, useEffect } from "react";
import { Link, usePage } from "@inertiajs/react";
import { cn } from "@/lib/utils";
import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
interface MenuItem {
id: string;
@@ -34,7 +44,9 @@ export default function AuthenticatedLayout({
children: React.ReactNode,
breadcrumbs?: BreadcrumbItemType[]
}) {
const { url } = usePage();
const { url, props } = usePage();
// @ts-ignore
const user = props.auth?.user || { name: 'Guest', username: 'guest' };
const [isCollapsed, setIsCollapsed] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("sidebar-collapsed") === "true";
@@ -243,6 +255,38 @@ export default function AuthenticatedLayout({
<span className="font-bold text-slate-900"> ERP</span>
</Link>
</div>
{/* User Menu */}
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="flex items-center gap-2 outline-none group">
<div className="hidden md:flex flex-col items-end mr-1">
<span className="text-sm font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
{user.name}
</span>
<span className="text-xs text-slate-500">
{user.username || 'Administrator'}
</span>
</div>
<div className="h-9 w-9 bg-slate-100 rounded-full flex items-center justify-center text-slate-600 group-hover:bg-primary-lightest group-hover:text-primary-main transition-all">
<User className="h-5 w-5" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 z-[100]" sideOffset={8}>
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href={route('logout')}
method="post"
as="button"
className="w-full flex items-center cursor-pointer text-red-600 focus:text-red-600 focus:bg-red-50"
>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
{/* Sidebar Desktop */}
@@ -281,15 +325,17 @@ export default function AuthenticatedLayout({
{isCollapsed ? <PanelLeftOpen className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
</button>
</div>
</aside>
</aside >
{/* Mobile Sidebar Overlay */}
{isMobileOpen && (
<div
className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[70] lg:hidden"
onClick={() => setIsMobileOpen(false)}
/>
)}
{
isMobileOpen && (
<div
className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[70] lg:hidden"
onClick={() => setIsMobileOpen(false)}
/>
)
}
{/* Mobile Sidebar Drawer */}
<aside className={cn(
@@ -329,6 +375,6 @@ export default function AuthenticatedLayout({
</div>
<Toaster richColors closeButton position="top-center" />
</main>
</div>
</div >
);
}

View File

@@ -0,0 +1,140 @@
import { Head, useForm } from "@inertiajs/react";
import { FormEventHandler, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import InputError from "../../Components/InputError";
import ApplicationLogo from "../../Components/ApplicationLogo";
export default function Login() {
const { data, setData, post, processing, errors, reset } = useForm({
username: localStorage.getItem("saved_username") || "",
password: "",
remember: false,
rememberUsername: localStorage.getItem("remember_username") === "true",
});
useEffect(() => {
return () => {
reset("password");
};
}, []);
const submit: FormEventHandler = (e) => {
e.preventDefault();
// 處理記住帳號邏輯
if (data.rememberUsername) {
localStorage.setItem("saved_username", data.username);
localStorage.setItem("remember_username", "true");
} else {
localStorage.removeItem("saved_username");
localStorage.setItem("remember_username", "false");
}
post(route("login"), {
onFinish: () => reset("password"),
});
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 relative overflow-hidden">
<Head title="登入" />
{/* 動態背景裝飾 */}
<div className="absolute top-0 -left-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
<div className="absolute top-0 -right-4 w-72 h-72 bg-yellow-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
<div className="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
<div className="w-full max-w-md p-8 relative z-10">
<div className="flex flex-col items-center mb-6">
<ApplicationLogo className="w-40 h-40 object-contain" />
</div>
<div className="glass-panel p-8 rounded-2xl shadow-xl bg-white/80 backdrop-blur-md border border-white/50">
<form onSubmit={submit} className="space-y-6">
<div>
<Label htmlFor="username"></Label>
<Input
id="username"
type="text"
className="mt-1 block w-full bg-white/50"
value={data.username}
onChange={(e) => setData("username", e.target.value)}
required
autoFocus
/>
<InputError message={errors.username} className="mt-2" />
</div>
<div>
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
className="mt-1 block w-full bg-white/50"
value={data.password}
onChange={(e) => setData("password", e.target.value)}
required
/>
<InputError message={errors.password} className="mt-2" />
</div>
<div className="flex items-center justify-between">
<label className="flex items-center cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only"
checked={data.rememberUsername}
onChange={(e) => setData("rememberUsername", e.target.checked)}
/>
<div className={cn(
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
data.rememberUsername ? "bg-[#01ab83]" : "bg-gray-300"
)}></div>
<div className={cn(
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
data.rememberUsername ? "translate-x-4" : "translate-x-0"
)}></div>
</div>
<span className="ml-2 text-sm text-gray-600 group-hover:text-gray-900 transition-colors"></span>
</label>
<label className="flex items-center cursor-pointer group">
<span className="mr-2 text-sm text-gray-600 group-hover:text-gray-900 transition-colors"></span>
<div className="relative">
<input
type="checkbox"
className="sr-only"
checked={data.remember}
onChange={(e) => setData("remember", e.target.checked)}
/>
<div className={cn(
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
data.remember ? "bg-[#01ab83]" : "bg-gray-300"
)}></div>
<div className={cn(
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
data.remember ? "translate-x-4" : "translate-x-0"
)}></div>
</div>
</label>
</div>
<Button
className="w-full h-11 text-base bg-[#01ab83] hover:bg-[#018a6a] transition-all shadow-lg hover:shadow-xl"
disabled={processing}
>
{processing ? "登入中..." : "登入系統"}
</Button>
</form>
</div>
<p className="text-center text-gray-400 text-sm mt-8">
&copy; 2026 . All rights reserved.
</p>
</div>
</div>
);
}