feat: 統一全系統頁面標題樣式、優化側邊欄與實作角色成員查看功能
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
||||
Warehouse,
|
||||
Truck,
|
||||
Contact2,
|
||||
FileText,
|
||||
LogOut,
|
||||
User,
|
||||
ChevronDown,
|
||||
@@ -20,7 +19,7 @@ import {
|
||||
Users
|
||||
} from "lucide-react";
|
||||
import { toast, Toaster } from "sonner";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { Link, usePage } from "@inertiajs/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav";
|
||||
@@ -32,6 +31,8 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/Components/ui/dropdown-menu";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import ApplicationLogo from "@/Components/ApplicationLogo";
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
@@ -39,6 +40,7 @@ interface MenuItem {
|
||||
icon?: React.ReactNode;
|
||||
route?: string;
|
||||
children?: MenuItem[];
|
||||
permission?: string | string[]; // 所需權限(單一或多個,滿足任一即可)
|
||||
}
|
||||
|
||||
export default function AuthenticatedLayout({
|
||||
@@ -51,6 +53,7 @@ export default function AuthenticatedLayout({
|
||||
const { url, props } = usePage();
|
||||
// @ts-ignore
|
||||
const user = props.auth?.user || { name: 'Guest', username: 'guest' };
|
||||
const { can, canAny } = usePermission();
|
||||
const [isCollapsed, setIsCollapsed] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("sidebar-collapsed") === "true";
|
||||
@@ -59,29 +62,34 @@ export default function AuthenticatedLayout({
|
||||
});
|
||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
// 完整的菜單定義(含權限配置)
|
||||
const allMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: "dashboard",
|
||||
label: "儀表板",
|
||||
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||
route: "/",
|
||||
// 儀表板無需特定權限,所有登入使用者皆可存取
|
||||
},
|
||||
{
|
||||
id: "inventory-management",
|
||||
label: "商品與庫存管理",
|
||||
icon: <Boxes className="h-5 w-5" />,
|
||||
permission: ["products.view", "warehouses.view"], // 滿足任一即可看到此群組
|
||||
children: [
|
||||
{
|
||||
id: "product-management",
|
||||
label: "商品資料管理",
|
||||
icon: <Package className="h-4 w-4" />,
|
||||
route: "/products",
|
||||
permission: "products.view",
|
||||
},
|
||||
{
|
||||
id: "warehouse-management",
|
||||
label: "倉庫管理",
|
||||
icon: <Warehouse className="h-4 w-4" />,
|
||||
route: "/warehouses",
|
||||
permission: "warehouses.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -89,12 +97,14 @@ export default function AuthenticatedLayout({
|
||||
id: "vendor-management",
|
||||
label: "廠商管理",
|
||||
icon: <Truck className="h-5 w-5" />,
|
||||
permission: "vendors.view",
|
||||
children: [
|
||||
{
|
||||
id: "vendor-list",
|
||||
label: "廠商資料管理",
|
||||
icon: <Contact2 className="h-4 w-4" />,
|
||||
route: "/vendors",
|
||||
permission: "vendors.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -102,12 +112,14 @@ export default function AuthenticatedLayout({
|
||||
id: "purchase-management",
|
||||
label: "採購管理",
|
||||
icon: <ShoppingCart className="h-5 w-5" />,
|
||||
permission: "purchase_orders.view",
|
||||
children: [
|
||||
{
|
||||
id: "purchase-order-list",
|
||||
label: "管理採購單",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
label: "採購單管理",
|
||||
icon: <ShoppingCart className="h-4 w-4" />,
|
||||
route: "/purchase-orders",
|
||||
permission: "purchase_orders.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -115,23 +127,53 @@ export default function AuthenticatedLayout({
|
||||
id: "system-management",
|
||||
label: "系統管理",
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
permission: ["users.view", "roles.view"],
|
||||
children: [
|
||||
{
|
||||
id: "user-management",
|
||||
label: "使用者管理",
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
route: "/admin/users",
|
||||
permission: "users.view",
|
||||
},
|
||||
{
|
||||
id: "role-management",
|
||||
label: "角色與權限",
|
||||
icon: <Shield className="h-4 w-4" />,
|
||||
route: "/admin/roles",
|
||||
permission: "roles.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 檢查單一項目是否有權限
|
||||
const hasPermissionForItem = (item: MenuItem): boolean => {
|
||||
if (!item.permission) return true; // 無指定權限則預設有權限
|
||||
if (Array.isArray(item.permission)) {
|
||||
return canAny(item.permission);
|
||||
}
|
||||
return can(item.permission);
|
||||
};
|
||||
|
||||
// 過濾菜單:移除無權限的項目,若父層所有子項目都無權限則隱藏父層
|
||||
const menuItems = useMemo(() => {
|
||||
return allMenuItems
|
||||
.map((item) => {
|
||||
// 如果有子項目,先過濾子項目
|
||||
if (item.children && item.children.length > 0) {
|
||||
const filteredChildren = item.children.filter(hasPermissionForItem);
|
||||
// 若所有子項目都無權限,則隱藏整個群組
|
||||
if (filteredChildren.length === 0) return null;
|
||||
return { ...item, children: filteredChildren };
|
||||
}
|
||||
// 無子項目的單一選單,直接檢查權限
|
||||
if (!hasPermissionForItem(item)) return null;
|
||||
return item;
|
||||
})
|
||||
.filter((item): item is MenuItem => item !== null);
|
||||
}, [can, canAny]);
|
||||
|
||||
// 初始化狀態:優先讀取 localStorage
|
||||
const [expandedItems, setExpandedItems] = useState<string[]>(() => {
|
||||
try {
|
||||
@@ -296,7 +338,7 @@ export default function AuthenticatedLayout({
|
||||
{isMobileOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
||||
</button>
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary-main flex items-center justify-center text-white font-bold text-lg">K</div>
|
||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain" />
|
||||
<span className="font-bold text-slate-900">小小冰室 ERP</span>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -342,12 +384,14 @@ export default function AuthenticatedLayout({
|
||||
<div className="hidden h-16 items-center justify-between px-6 border-b border-slate-100">
|
||||
{!isCollapsed && (
|
||||
<Link href="/" className="flex items-center gap-2 group">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary-main flex items-center justify-center text-white font-bold text-lg group-hover:scale-110 transition-transform">K</div>
|
||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain group-hover:scale-110 transition-transform" />
|
||||
<span className="font-extrabold text-[#01ab83] text-lg tracking-tight">小小冰室 ERP</span>
|
||||
</Link>
|
||||
)}
|
||||
{isCollapsed && (
|
||||
<Link href="/" className="w-8 h-8 rounded-lg bg-primary-main flex items-center justify-center text-white font-bold text-lg mx-auto">K</Link>
|
||||
<Link href="/" className="w-8 h-8 mx-auto">
|
||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -389,7 +433,7 @@ export default function AuthenticatedLayout({
|
||||
)}>
|
||||
<div className="h-16 flex items-center justify-between px-6 border-b border-slate-100">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-primary-main flex items-center justify-center text-white font-bold text-lg">K</div>
|
||||
<ApplicationLogo className="w-8 h-8 rounded-lg object-contain" />
|
||||
<span className="font-extrabold text-[#01ab83] text-lg">小小冰室 ERP</span>
|
||||
</Link>
|
||||
<button onClick={() => setIsMobileOpen(false)} className="p-2 text-slate-400">
|
||||
|
||||
Reference in New Issue
Block a user