Explorar el Código

feat: tabs adds a variety of style configurations

vben hace 8 meses
padre
commit
3a91a24e0d

+ 2 - 0
packages/@core/forward/preferences/src/config.ts

@@ -69,10 +69,12 @@ const defaultPreferences: Preferences = {
     width: 240,
   },
   tabbar: {
+    dragable: true,
     enable: true,
     keepAlive: true,
     persist: true,
     showIcon: true,
+    styleType: 'chrome',
   },
   theme: {
     builtinType: 'default',

+ 5 - 0
packages/@core/forward/preferences/src/types.ts

@@ -10,6 +10,7 @@ import type {
   NavigationStyleType,
   PageTransitionType,
   SupportedLanguagesType,
+  TabsStyleType,
   ThemeModeType,
 } from '@vben-core/typings';
 
@@ -135,6 +136,8 @@ interface ShortcutKeyPreferences {
 }
 
 interface TabbarPreferences {
+  /** 是否开启多标签页拖拽 */
+  dragable: boolean;
   /** 是否开启多标签页 */
   enable: boolean;
   /** 开启标签页缓存功能 */
@@ -143,6 +146,8 @@ interface TabbarPreferences {
   persist: boolean;
   /** 是否开启多标签页图标 */
   showIcon: boolean;
+  /** 标签页风格 */
+  styleType: TabsStyleType;
 }
 
 interface ThemePreferences {

+ 8 - 0
packages/@core/locales/src/langs/en-US.json

@@ -176,6 +176,14 @@
       "enable": "Enable Tab Bar",
       "icon": "Show Tabbar Icon",
       "persist": "Persist Tabs",
+      "dragable": "Enable Dragable Sort",
+      "styleType": {
+        "title": "Tabs Style",
+        "chrome": "Chrome",
+        "card": "Card",
+        "plain": "Plain",
+        "brisk": "Brisk"
+      },
       "contextMenu": {
         "reload": "Reload",
         "close": "Close",

+ 8 - 0
packages/@core/locales/src/langs/zh-CN.json

@@ -176,6 +176,14 @@
       "enable": "启用标签栏",
       "icon": "显示标签栏图标",
       "persist": "持久化标签页",
+      "dragable": "启动拖拽排序",
+      "styleType": {
+        "title": "标签页风格",
+        "chrome": "谷歌",
+        "card": "卡片",
+        "plain": "朴素",
+        "brisk": "轻快"
+      },
       "contextMenu": {
         "reload": "重新加载",
         "close": "关闭",

+ 4 - 1
packages/@core/shared/hooks/src/use-sortable.ts

@@ -1,4 +1,5 @@
 import type { SortableOptions } from 'sortablejs';
+import type Sortable from 'sortablejs';
 
 function useSortable<T extends HTMLElement>(
   sortableContainer: T,
@@ -22,7 +23,7 @@ function useSortable<T extends HTMLElement>(
       delayOnTouchOnly: true,
       ...options,
     });
-    return sortable;
+    return sortable as Sortable;
   };
 
   return {
@@ -31,3 +32,5 @@ function useSortable<T extends HTMLElement>(
 }
 
 export { useSortable };
+
+export type { Sortable };

+ 3 - 0
packages/@core/shared/typings/src/app.d.ts

@@ -44,6 +44,8 @@ type AccessModeType = 'allow-all' | 'backend' | 'frontend';
 
 type NavigationStyleType = 'plain' | 'rounded';
 
+type TabsStyleType = 'brisk' | 'card' | 'chrome' | 'plain';
+
 type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
 
 type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
@@ -60,5 +62,6 @@ export type {
   NavigationStyleType,
   PageTransitionType,
   SupportedLanguagesType,
+  TabsStyleType,
   ThemeModeType,
 };

+ 2 - 2
packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue

@@ -267,7 +267,7 @@ function handleMouseleave() {
     <div v-if="slots.logo" :style="headerStyle">
       <slot name="logo"></slot>
     </div>
-    <VbenScrollbar :style="contentStyle">
+    <VbenScrollbar :style="contentStyle" shadow>
       <slot></slot>
     </VbenScrollbar>
 
@@ -297,7 +297,7 @@ function handleMouseleave() {
       <div v-if="!extraCollapse" :style="extraTitleStyle">
         <slot name="extra-title"></slot>
       </div>
-      <VbenScrollbar :style="extraContentStyle" class="py-4">
+      <VbenScrollbar :style="extraContentStyle" class="py-4" shadow>
         <slot name="extra"></slot>
       </VbenScrollbar>
     </div>

+ 0 - 3
packages/@core/ui-kit/layout-ui/src/components/layout-tabbar.vue

@@ -39,8 +39,5 @@ const style = computed((): CSSProperties => {
 <template>
   <section :style="style" class="border-border flex w-full">
     <slot></slot>
-    <div class="flex items-center">
-      <slot name="toolbar"></slot>
-    </div>
   </section>
 </template>

+ 11 - 1
packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue

@@ -2,15 +2,22 @@
 import type { HTMLAttributes } from 'vue';
 import { ref } from 'vue';
 
-import { ScrollArea } from '@vben-core/shadcn-ui/components/ui/scroll-area';
+import {
+  ScrollArea,
+  ScrollBar,
+} from '@vben-core/shadcn-ui/components/ui/scroll-area';
 import { cn } from '@vben-core/toolkit';
 
 interface Props {
   class?: HTMLAttributes['class'];
+  horizontal?: boolean;
+  shadow?: boolean;
 }
 
 const props = withDefaults(defineProps<Props>(), {
   class: '',
+  horizontal: false,
+  shadow: false,
 });
 
 const isAtTop = ref(true);
@@ -33,6 +40,7 @@ function handleScroll(event: Event) {
     class="relative"
   >
     <div
+      v-if="shadow"
       :class="{
         'opacity-100': !isAtTop,
       }"
@@ -40,11 +48,13 @@ function handleScroll(event: Event) {
     ></div>
     <slot></slot>
     <div
+      v-if="shadow"
       :class="{
         'opacity-100': !isAtTop && !isAtBottom,
       }"
       class="scrollbar-bottom-shadow pointer-events-none absolute bottom-0 z-10 h-16 w-full opacity-0 transition-opacity duration-1000 ease-in-out will-change-[opacity]"
     ></div>
+    <ScrollBar v-if="horizontal" orientation="horizontal" />
   </ScrollArea>
 </template>
 

+ 10 - 12
packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue

@@ -5,13 +5,13 @@ import type { TabConfig, TabsProps } from '../../types';
 
 import { computed, nextTick, onMounted, ref, watch } from 'vue';
 
-import { MdiPin } from '@vben-core/iconify';
+import { IcRoundClose, MdiPin } from '@vben-core/iconify';
 import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
 
 interface Props extends TabsProps {}
 
 defineOptions({
-  name: 'TabsChrome',
+  name: 'VbenTabsChrome',
   // eslint-disable-next-line perfectionist/sort-objects
   inheritAttrs: false,
 });
@@ -94,7 +94,10 @@ function handleUnpinTab(tab: TabConfig) {
 </script>
 
 <template>
-  <div :style="style" class="tabs-chrome bg-accent size-full pt-1">
+  <div
+    :style="style"
+    class="tabs-chrome bg-accent size-full flex-1 overflow-hidden pt-1"
+  >
     <!-- footer -> 4px -->
     <div
       ref="contentRef"
@@ -157,16 +160,11 @@ function handleUnpinTab(tab: TabConfig) {
                 class="tabs-chrome__extra absolute right-[calc(var(--gap)*2)] top-1/2 z-[3] size-4 translate-y-[-50%] opacity-0 transition-opacity group-hover:opacity-100"
               >
                 <!-- close-icon -->
-                <svg
+                <IcRoundClose
                   v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
-                  class="hover:bg-accent hover:stroke-accent-foreground size-full cursor-pointer rounded-full transition-all"
-                  height="12"
-                  stroke="#595959"
-                  width="12"
+                  class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all"
                   @click.stop="handleClose(tab.key)"
-                >
-                  <path d="M 4 4 L 12 12 M 12 4 L 4 12" />
-                </svg>
+                />
                 <MdiPin
                   v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
                   class="hover:bg-accent hover:stroke-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
@@ -186,7 +184,7 @@ function handleUnpinTab(tab: TabConfig) {
                 />
 
                 <span
-                  class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap"
+                  class="tabs-chrome__label text-accent-foreground ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap"
                 >
                   {{ tab.title }}
                 </span>

+ 134 - 4
packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue

@@ -1,11 +1,141 @@
 <script lang="ts" setup>
-import { VbenScrollbar } from '@vben-core/shadcn-ui';
+import type { TabConfig, TabsProps } from '../../types';
+
+import { computed } from 'vue';
+
+import { IcRoundClose, MdiPin } from '@vben-core/iconify';
+import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
+import { TabDefinition } from '@vben-core/typings';
+
+interface Props extends TabsProps {}
+
+defineOptions({
+  name: 'VbenTabs',
+  // eslint-disable-next-line perfectionist/sort-objects
+  inheritAttrs: false,
+});
+const props = withDefaults(defineProps<Props>(), {
+  contentClass: 'vben-tabs-content',
+  contextMenus: () => [],
+  tabs: () => [],
+});
+
+const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
+const active = defineModel<string>('active');
+
+const typeWithClass = computed(() => {
+  const typeClasses: Record<string, { content: string }> = {
+    brisk: {
+      content: `h-full  after:content-['']  after:absolute after:bottom-0 after:left-0 after:w-full after:h-[1.5px] after:bg-primary after:scale-x-0 after:transition-[transform] after:ease-out after:duration-300 hover:after:scale-x-100 after:origin-left [&.is-active]:after:scale-x-100`,
+    },
+    card: {
+      content: 'h-[90%] rounded-md mr-1',
+    },
+    plain: {
+      content: 'h-full',
+    },
+  };
+
+  return typeClasses[props.styleType || 'plain'];
+});
+
+const tabsView = computed((): TabConfig[] => {
+  return props.tabs.map((tab) => {
+    return {
+      ...tab,
+      affixTab: !!tab.meta?.affixTab,
+      closable: Reflect.has(tab.meta, 'tabClosable')
+        ? !!tab.meta.tabClosable
+        : true,
+      icon: tab.meta.icon as string,
+      key: tab.fullPath || tab.path,
+      title: (tab.meta?.title || tab.name) as string,
+    };
+  });
+});
+
+function handleClose(key: string) {
+  emit('close', key);
+}
+
+function handleUnpinTab(tab: TabConfig) {
+  emit('unpin', tab);
+}
 </script>
 
 <template>
-  <div class="bg-accent size-full">
-    <VbenScrollbar>
-      <slot></slot>
+  <div class="bg-accent h-full flex-1 overflow-hidden">
+    <VbenScrollbar class="h-full" horizontal>
+      <div
+        :class="contentClass"
+        class="relative !flex h-full w-max items-center"
+      >
+        <TransitionGroup name="slide-down">
+          <div
+            v-for="(tab, i) in tabsView"
+            :key="tab.key"
+            :class="[
+              {
+                'is-active bg-background': tab.key === active,
+                dragable: !tab.affixTab,
+              },
+              typeWithClass.content,
+            ]"
+            :data-index="i"
+            class="[&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none transition-all duration-300"
+            @click="active = tab.key"
+          >
+            <VbenContextMenu
+              :handler-data="tab"
+              :menus="contextMenus"
+              :modal="false"
+              item-class="pr-6"
+            >
+              <div class="relative flex size-full items-center">
+                <!-- extra -->
+                <div
+                  class="absolute right-1.5 top-1/2 z-[3] translate-y-[-50%] overflow-hidden opacity-0 transition-opacity group-hover:opacity-100 group-[.is-active]:opacity-100"
+                >
+                  <!-- close-icon -->
+                  <IcRoundClose
+                    v-show="
+                      !tab.affixTab && tabsView.length > 1 && tab.closable
+                    "
+                    class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground size-3 cursor-pointer rounded-full transition-all"
+                    @click.stop="handleClose(tab.key)"
+                  />
+                  <MdiPin
+                    v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
+                    class="hover:bg-accent hover:stroke-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
+                    @click.stop="handleUnpinTab(tab)"
+                  />
+                </div>
+
+                <!-- tab-item-main -->
+                <div
+                  class="mx-3 mr-3 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
+                >
+                  <!-- <div
+                  class="mx-3 ml-3 mr-2 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] transition-all duration-300 group-hover:mr-2 group-hover:pr-4 group-[.is-active]:pr-4"
+                > -->
+                  <VbenIcon
+                    v-if="showIcon"
+                    :icon="tab.icon"
+                    class="mr-2 flex size-4 items-center overflow-hidden"
+                    fallback
+                  />
+
+                  <span
+                    class="text-accent-foreground flex-1 overflow-hidden whitespace-nowrap"
+                  >
+                    {{ tab.title }}
+                  </span>
+                </div>
+              </div>
+            </VbenContextMenu>
+          </div>
+        </TransitionGroup>
+      </div>
     </VbenScrollbar>
   </div>
 </template>

+ 16 - 7
packages/@core/ui-kit/tabs-ui/src/tabs-view.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
+import type { Sortable } from '@vben-core/hooks';
 import type { TabDefinition } from '@vben-core/typings';
 
-import { nextTick, onMounted } from 'vue';
+import { nextTick, onMounted, onUnmounted, ref } from 'vue';
 
 import { useForwardPropsEmits, useSortable } from '@vben-core/hooks';
 
-import { TabsChrome } from './components';
+import { Tabs, TabsChrome } from './components';
 import { TabsProps } from './types';
 
 interface Props extends TabsProps {}
@@ -17,6 +18,7 @@ defineOptions({
 const props = withDefaults(defineProps<Props>(), {
   contentClass: 'vben-tabs-content',
   dragable: true,
+  styleType: 'chrome',
 });
 
 const emit = defineEmits<{
@@ -27,13 +29,15 @@ const emit = defineEmits<{
 
 const forward = useForwardPropsEmits(props, emit);
 
+const sortableInstance = ref<Sortable | null>(null);
+
 // 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
-const findParentElement = (element: HTMLElement) => {
+function findParentElement(element: HTMLElement) {
   const parentCls = 'group';
   return element.classList.contains(parentCls)
     ? element
     : element.closest(`.${parentCls}`);
-};
+}
 
 async function initTabsSortable() {
   await nextTick();
@@ -80,7 +84,7 @@ async function initTabsSortable() {
     },
     onMove(evt) {
       const parent = findParentElement(evt.related);
-      return parent?.classList.contains('dragable');
+      return parent?.classList.contains('dragable') && props.dragable;
     },
     onStart: () => {
       el.style.cursor = 'grabbing';
@@ -88,12 +92,17 @@ async function initTabsSortable() {
     },
   });
 
-  await initializeSortable();
+  sortableInstance.value = await initializeSortable();
 }
 
 onMounted(initTabsSortable);
+
+onUnmounted(() => {
+  sortableInstance.value?.destroy();
+});
 </script>
 
 <template>
-  <TabsChrome v-bind="forward" />
+  <TabsChrome v-if="styleType === 'chrome'" v-bind="forward" />
+  <Tabs v-else v-bind="forward" />
 </template>

+ 6 - 2
packages/@core/ui-kit/tabs-ui/src/types.ts

@@ -1,5 +1,5 @@
 import type { IContextMenuItem } from '@vben-core/shadcn-ui';
-import type { TabDefinition } from '@vben-core/typings';
+import type { TabDefinition, TabsStyleType } from '@vben-core/typings';
 
 interface TabsProps {
   /**
@@ -21,7 +21,6 @@ interface TabsProps {
    * 仅限 tabs-chrome
    */
   gap?: number;
-
   /**
    * @zh_CN tab 最大宽度
    * 仅限 tabs-chrome
@@ -33,10 +32,15 @@ interface TabsProps {
    * 仅限 tabs-chrome
    */
   minWidth?: number;
+
   /**
    * @zh_CN 是否显示图标
    */
   showIcon?: boolean;
+  /**
+   * @zh_CN 标签页风格
+   */
+  styleType?: TabsStyleType;
 
   /**
    * @zh_CN 选项卡数据

+ 2 - 0
packages/effects/layouts/src/basic/tabbar/tabbar.vue

@@ -41,7 +41,9 @@ if (!preferences.tabbar.persist) {
   <TabsView
     :active="currentActive"
     :context-menus="createContextMenus"
+    :dragable="preferences.tabbar.dragable"
     :show-icon="showIcon"
+    :style-type="preferences.tabbar.styleType"
     :tabs="currentTabs"
     @close="handleClose"
     @sort-tabs="coreTabbarStore.sortTabs"

+ 32 - 0
packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue

@@ -1,6 +1,10 @@
 <script setup lang="ts">
+import { computed } from 'vue';
+
 import { $t } from '@vben-core/locales';
+import { SelectOption } from '@vben-core/typings';
 
+import SelectItem from '../select-item.vue';
 import SwitchItem from '../switch-item.vue';
 
 defineOptions({
@@ -12,6 +16,28 @@ defineProps<{ disabled?: boolean }>();
 const tabbarEnable = defineModel<boolean>('tabbarEnable');
 const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
 const tabbarPersist = defineModel<boolean>('tabbarPersist');
+const tabbarDragable = defineModel<boolean>('tabbarDragable');
+const tabbarStyleType = defineModel<string>('tabbarStyleType');
+
+const styleItems = computed((): SelectOption[] => [
+  {
+    label: $t('preferences.tabbar.styleType.chrome'),
+    value: 'chrome',
+  },
+  {
+    label: $t('preferences.tabbar.styleType.plain'),
+    value: 'plain',
+  },
+  {
+    label: $t('preferences.tabbar.styleType.card'),
+    value: 'card',
+  },
+
+  {
+    label: $t('preferences.tabbar.styleType.brisk'),
+    value: 'brisk',
+  },
+]);
 </script>
 
 <template>
@@ -24,4 +50,10 @@ const tabbarPersist = defineModel<boolean>('tabbarPersist');
   <SwitchItem v-model="tabbarPersist" :disabled="!tabbarEnable">
     {{ $t('preferences.tabbar.persist') }}
   </SwitchItem>
+  <SwitchItem v-model="tabbarDragable" :disabled="!tabbarEnable">
+    {{ $t('preferences.tabbar.dragable') }}
+  </SwitchItem>
+  <SelectItem v-model="tabbarStyleType" :items="styleItems">
+    {{ $t('preferences.tabbar.styleType.title') }}
+  </SelectItem>
 </template>

+ 4 - 0
packages/effects/layouts/src/widgets/preferences/preferences-sheet.vue

@@ -95,6 +95,8 @@ const breadcrumbHideOnlyOne = defineModel<boolean>('breadcrumbHideOnlyOne');
 const tabbarEnable = defineModel<boolean>('tabbarEnable');
 const tabbarShowIcon = defineModel<boolean>('tabbarShowIcon');
 const tabbarPersist = defineModel<boolean>('tabbarPersist');
+const tabbarDragable = defineModel<boolean>('tabbarDragable');
+const tabbarStyleType = defineModel<string>('tabbarStyleType');
 
 const navigationStyleType = defineModel<NavigationStyleType>(
   'navigationStyleType',
@@ -346,9 +348,11 @@ async function handleReset() {
 
             <Block :title="$t('preferences.tabbar.title')">
               <Tabbar
+                v-model:tabbar-dragable="tabbarDragable"
                 v-model:tabbar-enable="tabbarEnable"
                 v-model:tabbar-persist="tabbarPersist"
                 v-model:tabbar-show-icon="tabbarShowIcon"
+                v-model:tabbar-style-type="tabbarStyleType"
               />
             </Block>
             <Block :title="$t('preferences.widget.title')">