麵包屑功能完善

This commit is contained in:
2026-01-07 13:06:49 +08:00
parent d852d7b2ec
commit 8ea1ce1515
14 changed files with 137 additions and 23 deletions

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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 */}

View File

@@ -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">

View File

@@ -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 */}

View File

@@ -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 */}

View File

@@ -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">
{/* 返回按鈕 */}

View File

@@ -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">
{/* 頁面標題與導航 - 已於先前任務優化 */}

View File

@@ -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">
{/* 頁面標題與麵包屑 */}

View File

@@ -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">
{/* 頁面標題 */}

View File

@@ -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">
{/* 頁面標題與導航 */}

View File

@@ -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 */}

View File

@@ -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">
{/* 頁面標題與導航 */}

View 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;
}