index.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. "use client";
  2. import type { DragEndEvent } from "@dnd-kit/core";
  3. import {
  4. closestCenter,
  5. DndContext,
  6. KeyboardSensor,
  7. PointerSensor,
  8. useSensor,
  9. useSensors,
  10. } from "@dnd-kit/core";
  11. import {
  12. horizontalListSortingStrategy,
  13. SortableContext,
  14. sortableKeyboardCoordinates,
  15. } from "@dnd-kit/sortable";
  16. import { theme } from "antd";
  17. import { usePathname } from "next/navigation";
  18. import type React from "react";
  19. import { useEffect, useRef } from "react";
  20. import { useMenuStore } from "@/stores/menuStore";
  21. import { useTabStore } from "@/stores/tabStore";
  22. import Tab from "./Tab";
  23. const TabBar: React.FC = () => {
  24. const pathname = usePathname();
  25. const tabs = useTabStore((state) => state.tabs);
  26. const activeTab = useTabStore((state) => state.activeTab);
  27. const addTab = useTabStore((state) => state.addTab);
  28. const setActiveTab = useTabStore((state) => state.setActiveTab);
  29. const getTabByPath = useTabStore((state) => state.getTabByPath);
  30. const getBreadcrumb = useMenuStore((state) => state.getBreadcrumb);
  31. const reorderTabs = useTabStore((state) => state.reorderTabs);
  32. const {
  33. token: { colorBorder },
  34. } = theme.useToken();
  35. const containerRef = useRef<HTMLDivElement>(null);
  36. // Setup sensors for drag and drop
  37. const sensors = useSensors(
  38. useSensor(PointerSensor, {
  39. activationConstraint: {
  40. distance: 8, // 8px movement required before drag starts
  41. },
  42. }),
  43. useSensor(KeyboardSensor, {
  44. coordinateGetter: sortableKeyboardCoordinates,
  45. }),
  46. );
  47. // Handle drag end
  48. const handleDragEnd = (event: DragEndEvent) => {
  49. const { active, over } = event;
  50. if (over && active.id !== over.id) {
  51. reorderTabs(active.id as string, over.id as string);
  52. }
  53. };
  54. // Sync tabs with route changes
  55. useEffect(() => {
  56. if (!pathname || pathname === "/") return;
  57. const existingTab = getTabByPath(pathname);
  58. if (existingTab) {
  59. // Tab exists, just activate it
  60. setActiveTab(existingTab.key);
  61. } else {
  62. // Create new tab
  63. const breadcrumb = getBreadcrumb(pathname);
  64. const label = breadcrumb[breadcrumb.length - 1] || "未命名";
  65. // Generate key from path
  66. const key = pathname.replace(/\//g, "-").slice(1) || "home";
  67. addTab({
  68. key,
  69. label,
  70. path: pathname,
  71. closable: pathname !== "/home",
  72. });
  73. }
  74. }, [pathname, getTabByPath, setActiveTab, addTab, getBreadcrumb]);
  75. // Auto scroll to active tab
  76. useEffect(() => {
  77. if (!activeTab || !containerRef.current) return;
  78. const container = containerRef.current;
  79. const activeElement = container.querySelector(
  80. ".tab-item.active",
  81. ) as HTMLElement;
  82. if (activeElement) {
  83. const containerRect = container.getBoundingClientRect();
  84. const elementRect = activeElement.getBoundingClientRect();
  85. // Check if element is outside viewport
  86. if (
  87. elementRect.left < containerRect.left ||
  88. elementRect.right > containerRect.right
  89. ) {
  90. activeElement.scrollIntoView({
  91. behavior: "smooth",
  92. block: "nearest",
  93. inline: "center",
  94. });
  95. }
  96. }
  97. }, [activeTab]);
  98. if (tabs.length === 0) {
  99. return null;
  100. }
  101. return (
  102. <DndContext
  103. sensors={sensors}
  104. collisionDetection={closestCenter}
  105. onDragEnd={handleDragEnd}
  106. >
  107. <SortableContext
  108. items={tabs.map((tab) => tab.key)}
  109. strategy={horizontalListSortingStrategy}
  110. >
  111. <div
  112. ref={containerRef}
  113. className="tab-bar flex items-center p-2 px-4"
  114. style={{
  115. borderBottom: `1px solid ${colorBorder}`,
  116. overflowX: "auto",
  117. overflowY: "hidden",
  118. whiteSpace: "nowrap",
  119. scrollBehavior: "smooth",
  120. }}
  121. >
  122. {tabs.map((tab) => (
  123. <Tab key={tab.key} tab={tab} active={activeTab?.key === tab.key} />
  124. ))}
  125. </div>
  126. </SortableContext>
  127. </DndContext>
  128. );
  129. };
  130. export default TabBar;