麵包屑功能完善
This commit is contained in:
@@ -17,6 +17,7 @@ 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";
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
@@ -26,7 +27,13 @@ interface MenuItem {
|
||||
children?: MenuItem[];
|
||||
}
|
||||
|
||||
export default function AuthenticatedLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function AuthenticatedLayout({
|
||||
children,
|
||||
breadcrumbs
|
||||
}: {
|
||||
children: React.ReactNode,
|
||||
breadcrumbs?: BreadcrumbItemType[]
|
||||
}) {
|
||||
const { url } = usePage();
|
||||
const [isCollapsed, setIsCollapsed] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -313,6 +320,11 @@ export default function AuthenticatedLayout({ children }: { children: React.Reac
|
||||
"pt-16" // Always allow space for header
|
||||
)}>
|
||||
<div className="relative">
|
||||
<div className="container mx-auto px-6 pt-6 max-w-7xl">
|
||||
{breadcrumbs && breadcrumbs.length > 1 && (
|
||||
<BreadcrumbNav items={breadcrumbs} className="mb-2" />
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
<Toaster richColors closeButton position="top-center" />
|
||||
|
||||
@@ -16,7 +16,7 @@ import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router } from "@inertiajs/react";
|
||||
import { debounce } from "lodash";
|
||||
import Pagination from "@/Components/shared/Pagination";
|
||||
import BreadcrumbNav from "@/Components/shared/BreadcrumbNav";
|
||||
import { getBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
@@ -173,19 +173,11 @@ export default function ProductManagement({ products, categories, filters }: Pag
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("products")}>
|
||||
<Head title="商品資料管理" />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<BreadcrumbNav
|
||||
items={[
|
||||
{ label: "首頁", href: "/" },
|
||||
{ label: "商品與庫存管理" },
|
||||
{ label: "商品資料管理", isPage: true },
|
||||
]}
|
||||
className="mb-2"
|
||||
/>
|
||||
<h1 className="mb-2">商品資料管理</h1>
|
||||
<p className="text-gray-600">管理小小冰室原物料與成品資料</p>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,6 @@ import type { PurchaseOrder, Supplier } from "@/types/purchase-order";
|
||||
import type { Warehouse } from "@/types/requester";
|
||||
import { usePurchaseOrderForm } from "@/hooks/usePurchaseOrderForm";
|
||||
import {
|
||||
validatePurchaseOrder,
|
||||
filterValidItems,
|
||||
calculateTotalAmount,
|
||||
getTodayDate,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
} from "@/utils/purchase-order";
|
||||
import { STATUS_OPTIONS } from "@/constants/purchase-order";
|
||||
import { toast } from "sonner";
|
||||
import { getCreateBreadcrumbs, getEditBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Props {
|
||||
order?: PurchaseOrder;
|
||||
@@ -152,7 +152,7 @@ export default function CreatePurchaseOrder({
|
||||
const hasSupplier = !!supplierId;
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={order ? getEditBreadcrumbs("purchaseOrders") : getCreateBreadcrumbs("purchaseOrders")}>
|
||||
<Head title={order ? "編輯採購單" : "建立採購單"} />
|
||||
<div className="container mx-auto p-6 max-w-5xl">
|
||||
{/* Header */}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { type DateRange } from "@/Components/PurchaseOrder/DateFilter";
|
||||
import type { PurchaseOrder } from "@/types/purchase-order";
|
||||
import { debounce } from "lodash";
|
||||
import Pagination from "@/Components/shared/Pagination";
|
||||
import { getBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Props {
|
||||
orders: {
|
||||
@@ -86,7 +87,7 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("purchaseOrders")}>
|
||||
<Head title="採購管理 - 管理採購單" />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
|
||||
@@ -11,6 +11,7 @@ import PurchaseOrderStatusBadge from "@/Components/PurchaseOrder/PurchaseOrderSt
|
||||
import CopyButton from "@/Components/shared/CopyButton";
|
||||
import type { PurchaseOrder } from "@/types/purchase-order";
|
||||
import { formatCurrency, formatDateTime } from "@/utils/format";
|
||||
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Props {
|
||||
order: PurchaseOrder;
|
||||
@@ -18,7 +19,7 @@ interface Props {
|
||||
|
||||
export default function ViewPurchaseOrderPage({ order }: Props) {
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getShowBreadcrumbs("purchaseOrders", `詳情 (#${order.poNumber})`)}>
|
||||
<Head title={`採購單詳情 - ${order.poNumber}`} />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* Header */}
|
||||
|
||||
3
resources/js/Pages/Vendor/Index.tsx
vendored
3
resources/js/Pages/Vendor/Index.tsx
vendored
@@ -7,6 +7,7 @@ import VendorDialog from "@/Components/Vendor/VendorDialog";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router } from "@inertiajs/react";
|
||||
import { debounce } from "lodash";
|
||||
import { getBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
export interface Vendor {
|
||||
id: number;
|
||||
@@ -124,7 +125,7 @@ export default function VendorManagement({ vendors, filters }: PageProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("vendors")}>
|
||||
<Head title="廠商資料管理" />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* Header */}
|
||||
|
||||
3
resources/js/Pages/Vendor/Show.tsx
vendored
3
resources/js/Pages/Vendor/Show.tsx
vendored
@@ -23,6 +23,7 @@ import AddSupplyProductDialog from "@/Components/Vendor/AddSupplyProductDialog";
|
||||
import EditSupplyProductDialog from "@/Components/Vendor/EditSupplyProductDialog";
|
||||
import type { Vendor } from "@/Pages/Vendor/Index";
|
||||
import type { SupplyProduct } from "@/types/vendor";
|
||||
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Pivot {
|
||||
last_price: number | null;
|
||||
@@ -103,7 +104,7 @@ export default function VendorShow({ vendor, products }: ShowProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getShowBreadcrumbs("vendors", `廠商詳情 (${vendor.name})`)}>
|
||||
<Head title={`廠商詳情 - ${vendor.name}`} />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* 返回按鈕 */}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Head, Link, router } from "@inertiajs/react";
|
||||
import { Warehouse, InboundItem, InboundReason } from "@/types/warehouse";
|
||||
import { getCurrentDateTime } from "@/utils/format";
|
||||
import { toast } from "sonner";
|
||||
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
@@ -151,7 +152,7 @@ export default function AddInventoryPage({ warehouse, products }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name, "手動入庫")}>
|
||||
<Head title={`新增庫存 - ${warehouse.name}`} />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* 頁面標題與導航 - 已於先前任務優化 */}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/Components/ui/alert-dialog";
|
||||
import TransactionTable, { Transaction } from "@/Components/Warehouse/Inventory/TransactionTable";
|
||||
import { getShowBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -72,7 +73,7 @@ export default function EditInventory({ warehouse, inventory, transactions = []
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getShowBreadcrumbs("warehouses", "修正庫存")}>
|
||||
<Head title={`編輯庫存 - ${inventory.productName} `} />
|
||||
<div className="container mx-auto p-6 max-w-4xl">
|
||||
{/* 頁面標題與麵包屑 */}
|
||||
|
||||
@@ -11,6 +11,7 @@ import WarehouseEmptyState from "@/Components/Warehouse/WarehouseEmptyState";
|
||||
import { Warehouse } from "@/types/warehouse";
|
||||
import Pagination from "@/Components/shared/Pagination";
|
||||
import { toast } from "sonner";
|
||||
import { getBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface PageProps {
|
||||
warehouses: {
|
||||
@@ -100,7 +101,7 @@ export default function WarehouseIndex({ warehouses, filters }: PageProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getBreadcrumbs("warehouses")}>
|
||||
<Head title="倉庫管理" />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* 頁面標題 */}
|
||||
|
||||
@@ -8,6 +8,7 @@ import InventoryToolbar from "@/Components/Warehouse/Inventory/InventoryToolbar"
|
||||
import InventoryTable from "@/Components/Warehouse/Inventory/InventoryTable";
|
||||
import { calculateLowStockCount } from "@/utils/inventory";
|
||||
import { toast } from "sonner";
|
||||
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -85,7 +86,7 @@ export default function WarehouseInventoryPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name)}>
|
||||
<Head title={`庫存管理 - ${warehouse.name}`} />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* 頁面標題與導航 */}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from "@/Components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Warehouse } from "@/types/warehouse";
|
||||
import TransactionTable, { Transaction } from "@/Components/Warehouse/Inventory/TransactionTable";
|
||||
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Props {
|
||||
warehouse: Warehouse;
|
||||
@@ -18,7 +19,7 @@ interface Props {
|
||||
|
||||
export default function InventoryHistory({ warehouse, inventory, transactions }: Props) {
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name, "庫存變動紀錄")}>
|
||||
<Head title={`庫存異動紀錄 - ${inventory.productName}`} />
|
||||
<div className="container mx-auto p-6 max-w-4xl">
|
||||
{/* Header */}
|
||||
|
||||
@@ -13,6 +13,7 @@ import SafetyStockList from "@/Components/Warehouse/SafetyStock/SafetyStockList"
|
||||
import AddSafetyStockDialog from "@/Components/Warehouse/SafetyStock/AddSafetyStockDialog";
|
||||
import EditSafetyStockDialog from "@/Components/Warehouse/SafetyStock/EditSafetyStockDialog";
|
||||
import { toast } from "sonner";
|
||||
import { getInventoryBreadcrumbs } from "@/utils/breadcrumb";
|
||||
|
||||
interface Props {
|
||||
warehouse: Warehouse;
|
||||
@@ -83,7 +84,7 @@ export default function SafetyStockPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout>
|
||||
<AuthenticatedLayout breadcrumbs={getInventoryBreadcrumbs(warehouse.id, warehouse.name, "安全庫存設定")}>
|
||||
<Head title={`安全庫存設定 - ${warehouse.name}`} />
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
{/* 頁面標題與導航 */}
|
||||
|
||||
100
resources/js/utils/breadcrumb.ts
Normal file
100
resources/js/utils/breadcrumb.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav";
|
||||
|
||||
/**
|
||||
* 麵包屑定義對應表
|
||||
* 根據側邊欄層級結構定義基礎麵包屑
|
||||
*/
|
||||
export const BREADCRUMB_MAP: Record<string, BreadcrumbItemType[]> = {
|
||||
dashboard: [
|
||||
{ label: "首頁", isPage: true }
|
||||
],
|
||||
products: [
|
||||
{ label: "首頁", href: "/" },
|
||||
{ label: "商品與庫存管理" },
|
||||
{ label: "商品資料管理", isPage: true }
|
||||
],
|
||||
warehouses: [
|
||||
{ label: "首頁", href: "/" },
|
||||
{ label: "商品與庫存管理" },
|
||||
{ label: "倉庫管理", isPage: true }
|
||||
],
|
||||
vendors: [
|
||||
{ label: "首頁", href: "/" },
|
||||
{ label: "廠商管理" },
|
||||
{ label: "廠商資料管理", isPage: true }
|
||||
],
|
||||
purchaseOrders: [
|
||||
{ label: "首頁", href: "/" },
|
||||
{ label: "採購管理" },
|
||||
{ label: "管理採購單", isPage: true }
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 組合麵包屑工具
|
||||
* @param base 基礎路徑名稱 (key of BREADCRUMB_MAP)
|
||||
* @param extra 額外的路徑項目 (例如編輯、詳情頁)
|
||||
*/
|
||||
export function getBreadcrumbs(base: keyof typeof BREADCRUMB_MAP, extra?: BreadcrumbItemType[]): BreadcrumbItemType[] {
|
||||
const baseItems = JSON.parse(JSON.stringify(BREADCRUMB_MAP[base] || []));
|
||||
|
||||
if (extra && extra.length > 0) {
|
||||
// 如果有額外路徑,基礎路徑的最後一項不應標記為 isPage
|
||||
if (baseItems.length > 0) {
|
||||
baseItems[baseItems.length - 1].isPage = false;
|
||||
}
|
||||
return [...baseItems, ...extra];
|
||||
}
|
||||
|
||||
return baseItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得「新增」操作的麵包屑
|
||||
*/
|
||||
export function getCreateBreadcrumbs(base: keyof typeof BREADCRUMB_MAP): BreadcrumbItemType[] {
|
||||
return getBreadcrumbs(base, [{ label: "新增", isPage: true }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得「編輯」操作的麵包屑
|
||||
*/
|
||||
export function getEditBreadcrumbs(base: keyof typeof BREADCRUMB_MAP): BreadcrumbItemType[] {
|
||||
return getBreadcrumbs(base, [{ label: "編輯", isPage: true }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得「詳情」操作的麵包屑
|
||||
*/
|
||||
export function getShowBreadcrumbs(base: keyof typeof BREADCRUMB_MAP, suffix: string = "詳情"): BreadcrumbItemType[] {
|
||||
return getBreadcrumbs(base, [{ label: suffix, isPage: true }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得「庫存管理」子頁面的麵包屑
|
||||
* 層級:倉庫管理 > 庫存詳情 (倉庫名) > [功能]
|
||||
*/
|
||||
export function getInventoryBreadcrumbs(warehouseId: string | number, warehouseName: string, subPageLabel?: string): BreadcrumbItemType[] {
|
||||
const baseItems: BreadcrumbItemType[] = [
|
||||
...JSON.parse(JSON.stringify(BREADCRUMB_MAP.warehouses || []))
|
||||
];
|
||||
|
||||
// 修改「倉庫管理」不作為最後一頁
|
||||
if (baseItems.length > 0) {
|
||||
baseItems[baseItems.length - 1].isPage = false;
|
||||
}
|
||||
|
||||
const inventoryDetailItem: BreadcrumbItemType = {
|
||||
label: `庫存管理 (${warehouseName})`,
|
||||
href: `/warehouses/${warehouseId}/inventory`,
|
||||
isPage: !subPageLabel
|
||||
};
|
||||
|
||||
const finalItems = [...baseItems, inventoryDetailItem];
|
||||
|
||||
if (subPageLabel) {
|
||||
finalItems.push({ label: subPageLabel, isPage: true });
|
||||
}
|
||||
|
||||
return finalItems;
|
||||
}
|
||||
Reference in New Issue
Block a user