Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin
This commit is contained in:
commit
5c13428bc9
2
.npmrc
2
.npmrc
@ -1,4 +1,4 @@
|
||||
# registry = "https://registry.npmmirror.com"
|
||||
registry = "https://registry.npmmirror.com"
|
||||
public-hoist-pattern[]=husky
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
|
@ -78,7 +78,7 @@ pnpm build
|
||||
|
||||
## 変更ログ
|
||||
|
||||
[CHANGELOG](./CHANGELOG.zh_CN.md)
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases/latest)
|
||||
|
||||
## 貢献方法
|
||||
|
||||
|
@ -77,7 +77,7 @@ pnpm build
|
||||
|
||||
## Change Log
|
||||
|
||||
[CHANGELOG](./CHANGELOG.zh_CN.md)
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases/latest)
|
||||
|
||||
## How to contribute
|
||||
|
||||
|
@ -126,6 +126,10 @@ pnpm build
|
||||
|
||||
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
|
||||
|
||||
## 更新日志
|
||||
|
||||
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases/latest)
|
||||
|
||||
## Contributor
|
||||
|
||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||
|
@ -40,14 +40,14 @@
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"ant-design-vue": "^4.2.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.12",
|
||||
"echarts": "^5.5.1",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "2.2.1",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
|
@ -40,10 +40,10 @@
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"dayjs": "^1.11.12",
|
||||
"element-plus": "^2.8.0",
|
||||
"pinia": "2.2.1",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
|
@ -40,9 +40,9 @@
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"naive-ui": "^2.39.0",
|
||||
"pinia": "2.2.1",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-config-turbo": "^2.0.13",
|
||||
"eslint-config-turbo": "^2.0.14",
|
||||
"eslint-plugin-command": "^0.2.3",
|
||||
"eslint-plugin-import-x": "^3.1.0"
|
||||
},
|
||||
@ -42,8 +42,8 @@
|
||||
"eslint-plugin-jsdoc": "^50.2.2",
|
||||
"eslint-plugin-jsonc": "^2.16.0",
|
||||
"eslint-plugin-n": "^17.10.2",
|
||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
||||
"eslint-plugin-perfectionist": "^3.1.3",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-perfectionist": "^3.2.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-regexp": "^2.6.0",
|
||||
"eslint-plugin-unicorn": "^55.0.0",
|
||||
|
@ -42,7 +42,8 @@ export async function typescript(): Promise<Linter.Config[]> {
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||
// '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': [
|
||||
|
@ -32,7 +32,7 @@
|
||||
"postcss-html": "^1.7.0",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "^3.3.3",
|
||||
"stylelint": "^16.8.1",
|
||||
"stylelint": "^16.8.2",
|
||||
"stylelint-config-recommended": "^14.0.1",
|
||||
"stylelint-config-recommended-scss": "^14.1.0",
|
||||
"stylelint-config-recommended-vue": "^1.5.0",
|
||||
|
@ -33,7 +33,7 @@
|
||||
"chalk": "^5.3.0",
|
||||
"consola": "^3.2.3",
|
||||
"dayjs": "^1.11.12",
|
||||
"execa": "^9.3.0",
|
||||
"execa": "^9.3.1",
|
||||
"find-up": "^7.0.0",
|
||||
"nanoid": "^5.0.7",
|
||||
"ora": "^8.0.1",
|
||||
|
@ -20,6 +20,6 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@vben/types": "workspace:*",
|
||||
"vite": "^5.4.0"
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@
|
||||
"rollup": "^4.20.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.8",
|
||||
"vite": "^5.4.0",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-dts": "4.0.3",
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
|
13
package.json
13
package.json
@ -82,10 +82,10 @@
|
||||
"lint-staged": "^15.2.9",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"turbo": "^2.0.13",
|
||||
"turbo": "^2.0.14",
|
||||
"typescript": "^5.5.4",
|
||||
"unbuild": "^2.0.0",
|
||||
"vite": "^5.4.0",
|
||||
"vite": "^5.4.1",
|
||||
"vitest": "^2.0.5",
|
||||
"vue": "^3.4.37",
|
||||
"vue-tsc": "^2.0.29"
|
||||
@ -94,7 +94,7 @@
|
||||
"node": ">=20",
|
||||
"pnpm": ">=9"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.0",
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"allowedVersions": {
|
||||
@ -102,9 +102,10 @@
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"vue": "^3.4.37"
|
||||
"@ctrl/tinycolor": "4.1.0",
|
||||
"clsx": "2.1.1",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "3.4.37"
|
||||
},
|
||||
"neverBuiltDependencies": [
|
||||
"canvas",
|
||||
|
@ -351,7 +351,7 @@
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--sidebar: 240 10% 3.9%;
|
||||
--sidebar-deep: 240 10% 3.9%;
|
||||
--header: 240 4.9% 83.9%;
|
||||
--header: 240 10% 3.9%;
|
||||
}
|
||||
|
||||
.dark[data-theme='neutral'],
|
||||
|
@ -35,7 +35,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"lucide-vue-next": "^0.427.0",
|
||||
"lucide-vue-next": "^0.428.0",
|
||||
"vue": "^3.4.37"
|
||||
}
|
||||
}
|
||||
|
35
packages/@core/base/typings/src/helper.d.ts
vendored
35
packages/@core/base/typings/src/helper.d.ts
vendored
@ -107,20 +107,23 @@ type MergeAll<
|
||||
? MergeAll<Rest, Merge<R, F>>
|
||||
: R;
|
||||
|
||||
export {
|
||||
type AnyFunction,
|
||||
type AnyNormalFunction,
|
||||
type AnyPromiseFunction,
|
||||
type DeepPartial,
|
||||
type DeepReadonly,
|
||||
type IntervalHandle,
|
||||
type MaybeComputedRef,
|
||||
type MaybeReadonlyRef,
|
||||
type Merge,
|
||||
type MergeAll,
|
||||
type NonNullable,
|
||||
type Nullable,
|
||||
type ReadonlyRecordable,
|
||||
type Recordable,
|
||||
type TimeoutHandle,
|
||||
type EmitType = (name: Name, ...args: any[]) => void;
|
||||
|
||||
export type {
|
||||
AnyFunction,
|
||||
AnyNormalFunction,
|
||||
AnyPromiseFunction,
|
||||
DeepPartial,
|
||||
DeepReadonly,
|
||||
EmitType,
|
||||
IntervalHandle,
|
||||
MaybeComputedRef,
|
||||
MaybeReadonlyRef,
|
||||
Merge,
|
||||
MergeAll,
|
||||
NonNullable,
|
||||
Nullable,
|
||||
ReadonlyRecordable,
|
||||
Recordable,
|
||||
TimeoutHandle,
|
||||
};
|
||||
|
@ -36,7 +36,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"radix-vue": "^1.9.4",
|
||||
"sortablejs": "^1.15.2",
|
||||
"vue": "^3.4.37"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
||||
@ -14,6 +14,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
|
||||
* @zh_CN content style
|
||||
*/
|
||||
function useContentStyle() {
|
||||
let resizeObserver: null | ResizeObserver = null;
|
||||
const contentElement = ref<HTMLDivElement | null>(null);
|
||||
const visibleDomRect = ref<null | VisibleDomRect>(null);
|
||||
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
||||
@ -41,12 +42,15 @@ function useContentStyle() {
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (contentElement.value) {
|
||||
const observer = new ResizeObserver(debouncedCalcHeight);
|
||||
observer.observe(contentElement.value);
|
||||
}
|
||||
});
|
||||
if (contentElement.value && !resizeObserver) {
|
||||
resizeObserver = new ResizeObserver(debouncedCalcHeight);
|
||||
resizeObserver.observe(contentElement.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
});
|
||||
|
||||
return { contentElement, overlayStyle, visibleDomRect };
|
||||
|
@ -39,7 +39,7 @@ describe('useSortable', () => {
|
||||
expect(Sortable.default.create).toHaveBeenCalledWith(
|
||||
mockElement,
|
||||
expect.objectContaining({
|
||||
animation: 100,
|
||||
animation: 300,
|
||||
delay: 400,
|
||||
delayOnTouchOnly: true,
|
||||
...customOptions,
|
||||
|
@ -18,7 +18,7 @@ function useSortable<T extends HTMLElement>(
|
||||
// Sortable?.default?.mount?.(AutoScroll);
|
||||
|
||||
const sortable = Sortable?.default?.create?.(sortableContainer, {
|
||||
animation: 100,
|
||||
animation: 300,
|
||||
delay: 400,
|
||||
delayOnTouchOnly: true,
|
||||
...options,
|
||||
|
@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"vue": "^3.4.37"
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
||||
"@vben-core/icons": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"vue": "^3.4.37"
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"vue": "^3.4.37"
|
||||
}
|
||||
}
|
||||
|
@ -46,9 +46,9 @@
|
||||
"@vben-core/icons": "workspace:*",
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"lucide-vue-next": "^0.427.0",
|
||||
"lucide-vue-next": "^0.428.0",
|
||||
"radix-vue": "^1.9.4",
|
||||
"vue": "^3.4.37"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { cn } from '@vben-core/shared';
|
||||
|
||||
@ -11,6 +11,10 @@ interface Props {
|
||||
scrollBarClass?: any;
|
||||
shadow?: boolean;
|
||||
shadowBorder?: boolean;
|
||||
shadowBottom?: boolean;
|
||||
shadowLeft?: boolean;
|
||||
shadowRight?: boolean;
|
||||
shadowTop?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@ -18,29 +22,66 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
horizontal: false,
|
||||
shadow: false,
|
||||
shadowBorder: false,
|
||||
shadowBottom: true,
|
||||
shadowLeft: false,
|
||||
shadowRight: false,
|
||||
shadowTop: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
scrollAt: [{ bottom: boolean; left: boolean; right: boolean; top: boolean }];
|
||||
}>();
|
||||
|
||||
const isAtTop = ref(true);
|
||||
const isAtRight = ref(false);
|
||||
const isAtBottom = ref(false);
|
||||
const isAtLeft = ref(true);
|
||||
|
||||
const showShadowTop = computed(() => props.shadow && props.shadowTop);
|
||||
const showShadowBottom = computed(() => props.shadow && props.shadowBottom);
|
||||
const showShadowLeft = computed(() => props.shadow && props.shadowLeft);
|
||||
const showShadowRight = computed(() => props.shadow && props.shadowRight);
|
||||
|
||||
const computedShadowClasses = computed(() => ({
|
||||
'shadow-both':
|
||||
!isAtLeft.value &&
|
||||
!isAtRight.value &&
|
||||
showShadowLeft.value &&
|
||||
showShadowRight.value,
|
||||
'shadow-left': !isAtLeft.value && showShadowLeft.value,
|
||||
'shadow-right': !isAtRight.value && showShadowRight.value,
|
||||
}));
|
||||
|
||||
function handleScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const scrollTop = target?.scrollTop ?? 0;
|
||||
const scrollLeft = target?.scrollLeft ?? 0;
|
||||
const offsetHeight = target?.offsetHeight ?? 0;
|
||||
const offsetWidth = target?.offsetWidth ?? 0;
|
||||
const scrollHeight = target?.scrollHeight ?? 0;
|
||||
const scrollWidth = target?.scrollWidth ?? 0;
|
||||
isAtTop.value = scrollTop <= 0;
|
||||
isAtLeft.value = scrollLeft <= 0;
|
||||
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
|
||||
isAtRight.value = scrollLeft + offsetWidth >= scrollWidth;
|
||||
|
||||
emit('scrollAt', {
|
||||
bottom: isAtBottom.value,
|
||||
left: isAtLeft.value,
|
||||
right: isAtRight.value,
|
||||
top: isAtTop.value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollArea
|
||||
:class="[cn(props.class)]"
|
||||
:class="[cn(props.class), computedShadowClasses]"
|
||||
:on-scroll="handleScroll"
|
||||
class="relative"
|
||||
class="vben-scrollbar relative"
|
||||
>
|
||||
<div
|
||||
v-if="shadow"
|
||||
v-if="showShadowTop"
|
||||
:class="{
|
||||
'opacity-100': !isAtTop,
|
||||
'border-border border-t': shadowBorder && !isAtTop,
|
||||
@ -49,7 +90,7 @@ function handleScroll(event: Event) {
|
||||
></div>
|
||||
<slot></slot>
|
||||
<div
|
||||
v-if="shadow"
|
||||
v-if="showShadowBottom"
|
||||
:class="{
|
||||
'opacity-100': !isAtTop && !isAtBottom,
|
||||
'border-border border-b': shadowBorder && !isAtTop && !isAtBottom,
|
||||
@ -65,6 +106,31 @@ function handleScroll(event: Event) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vben-scrollbar {
|
||||
&:not(.shadow-both).shadow-left {
|
||||
mask-image: linear-gradient(90deg, transparent, #000 16px);
|
||||
}
|
||||
|
||||
&:not(.shadow-both).shadow-right {
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
#000 calc(100% - 16px),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&.shadow-both {
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
#000 16px,
|
||||
#000 calc(100% - 16px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-top-shadow {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
|
@ -41,6 +41,7 @@
|
||||
"@vben-core/icons": "workspace:*",
|
||||
"@vben-core/shadcn-ui": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"vue": "^3.4.37"
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
|
||||
|
||||
import type { TabConfig, TabsProps } from '../../types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { MdiPin, X } from '@vben-core/icons';
|
||||
import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props extends TabsProps {}
|
||||
|
||||
@ -20,17 +20,17 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
contentClass: 'vben-tabs-content',
|
||||
contextMenus: () => [],
|
||||
gap: 7,
|
||||
maxWidth: 150,
|
||||
minWidth: 80,
|
||||
tabs: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
|
||||
const emit = defineEmits<{
|
||||
close: [string];
|
||||
unpin: [TabDefinition];
|
||||
}>();
|
||||
const active = defineModel<string>('active');
|
||||
|
||||
const contentRef = ref();
|
||||
const tabRef = ref();
|
||||
const tabWidth = ref<number>(props.maxWidth);
|
||||
|
||||
const style = computed(() => {
|
||||
const { gap } = props;
|
||||
@ -53,148 +53,118 @@ const tabsView = computed((): TabConfig[] => {
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
watch(active, () => {
|
||||
scrollIntoView();
|
||||
});
|
||||
|
||||
function scrollIntoView() {
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`.tabs-chrome__item.is-active`);
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :style="style" class="tabs-chrome size-full flex-1 overflow-hidden pt-1">
|
||||
<VbenScrollbar
|
||||
id="tabs-scrollbar"
|
||||
class="tabs-chrome__scrollbar h-full"
|
||||
horizontal
|
||||
scroll-bar-class="z-10 hidden"
|
||||
>
|
||||
<!-- footer -> 4px -->
|
||||
<div
|
||||
ref="contentRef"
|
||||
:class="contentClass"
|
||||
:style="style"
|
||||
class="tabs-chrome !flex h-full w-max pr-6"
|
||||
>
|
||||
<TransitionGroup name="slide-left">
|
||||
<div
|
||||
ref="contentRef"
|
||||
:class="contentClass"
|
||||
class="relative !flex h-full w-max"
|
||||
v-for="(tab, i) in tabsView"
|
||||
:key="tab.key"
|
||||
ref="tabRef"
|
||||
:class="[{ 'is-active': tab.key === active, dragable: !tab.affixTab }]"
|
||||
:data-active-tab="active"
|
||||
:data-index="i"
|
||||
class="tabs-chrome__item draggable group relative -mr-3 flex h-full select-none items-center"
|
||||
data-tab-item="true"
|
||||
@click="active = tab.key"
|
||||
>
|
||||
<TransitionGroup name="slide-left">
|
||||
<div
|
||||
v-for="(tab, i) in tabsView"
|
||||
:key="tab.key"
|
||||
ref="tabRef"
|
||||
:class="[
|
||||
{ 'is-active': tab.key === active, dragable: !tab.affixTab },
|
||||
]"
|
||||
:data-active-tab="active"
|
||||
:data-index="i"
|
||||
:style="{
|
||||
width: `${tabWidth}px`,
|
||||
left: `${(tabWidth - gap * 2) * i}px`,
|
||||
}"
|
||||
class="tabs-chrome__item group absolute flex h-full select-none items-center transition-all"
|
||||
@click="active = tab.key"
|
||||
>
|
||||
<VbenContextMenu
|
||||
:handler-data="tab"
|
||||
:menus="contextMenus"
|
||||
:modal="false"
|
||||
item-class="pr-6"
|
||||
<VbenContextMenu
|
||||
:handler-data="tab"
|
||||
:menus="contextMenus"
|
||||
:modal="false"
|
||||
item-class="pr-6"
|
||||
>
|
||||
<div class="relative size-full px-1">
|
||||
<!-- divider -->
|
||||
<div
|
||||
v-if="i !== 0 && tab.key !== active"
|
||||
class="tabs-chrome__divider bg-foreground/50 absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
|
||||
></div>
|
||||
<!-- background -->
|
||||
<div
|
||||
class="tabs-chrome__background absolute z-[-1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
|
||||
>
|
||||
<div class="size-full">
|
||||
<!-- divider -->
|
||||
<div
|
||||
v-if="i !== 0 && tab.key !== active"
|
||||
class="tabs-chrome__divider bg-foreground/60 absolute left-[var(--gap)] top-1/2 z-0 h-4 w-[1px] translate-y-[-50%] transition-all"
|
||||
></div>
|
||||
<!-- background -->
|
||||
<div
|
||||
class="tabs-chrome__background absolute z-[1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
|
||||
>
|
||||
<div
|
||||
class="tabs-chrome__background-content group-[.is-active]:bg-primary/15 dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
|
||||
></div>
|
||||
<svg
|
||||
class="tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150"
|
||||
height="7"
|
||||
width="7"
|
||||
>
|
||||
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" />
|
||||
</svg>
|
||||
<svg
|
||||
class="tabs-chrome__background-after group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 right-[-1px] fill-transparent transition-all duration-150"
|
||||
height="7"
|
||||
width="7"
|
||||
>
|
||||
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="tabs-chrome__background-content group-[.is-active]:bg-heavy dark:group-[.is-active]:bg-accent h-full rounded-tl-[var(--gap)] rounded-tr-[var(--gap)] duration-150"
|
||||
></div>
|
||||
<svg
|
||||
class="tabs-chrome__background-before group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 left-[-1px] fill-transparent transition-all duration-150"
|
||||
height="7"
|
||||
width="7"
|
||||
>
|
||||
<path d="M 0 7 A 7 7 0 0 0 7 0 L 7 7 Z" />
|
||||
</svg>
|
||||
<svg
|
||||
class="tabs-chrome__background-after group-[.is-active]:fill-primary/15 dark:group-[.is-active]:fill-accent absolute bottom-0 right-[-1px] fill-transparent transition-all duration-150"
|
||||
height="7"
|
||||
width="7"
|
||||
>
|
||||
<path d="M 0 0 A 7 7 0 0 0 7 7 L 0 7 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- extra -->
|
||||
<div
|
||||
class="tabs-chrome__extra absolute right-[calc(var(--gap)*1.5)] top-1/2 z-[3] size-4 translate-y-[-50%]"
|
||||
>
|
||||
<!-- close-icon -->
|
||||
<X
|
||||
v-show="
|
||||
!tab.affixTab && tabsView.length > 1 && tab.closable
|
||||
"
|
||||
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary mt-[2px] size-3 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('close', tab.key)"
|
||||
/>
|
||||
<MdiPin
|
||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('unpin', tab)"
|
||||
/>
|
||||
</div>
|
||||
<!-- extra -->
|
||||
<div
|
||||
class="tabs-chrome__extra absolute right-[var(--gap)] top-1/2 z-[3] size-4 translate-y-[-50%]"
|
||||
>
|
||||
<!-- close-icon -->
|
||||
<X
|
||||
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('close', tab.key)"
|
||||
/>
|
||||
<MdiPin
|
||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||
class="hover:text-accent-foreground text-accent-foreground/80 group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('unpin', tab)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- tab-item-main -->
|
||||
<div
|
||||
class="tabs-chrome__item-main group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground text-accent-foreground absolute left-0 right-0 z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-4 duration-150 group-hover:pr-3"
|
||||
>
|
||||
<VbenIcon
|
||||
v-if="showIcon"
|
||||
:icon="tab.icon"
|
||||
class="ml-[var(--gap)] flex size-4 items-center overflow-hidden"
|
||||
fallback
|
||||
/>
|
||||
<!-- tab-item-main -->
|
||||
<div
|
||||
class="tabs-chrome__item-main group-[.is-active]:text-accent-foreground dark:group-[.is-active]:text-accent-foreground text-accent-foreground z-[2] mx-[calc(var(--gap)*2)] my-0 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pl-2 pr-4 duration-150"
|
||||
>
|
||||
<VbenIcon
|
||||
v-if="showIcon"
|
||||
:icon="tab.icon"
|
||||
class="mr-1 flex size-4 items-center overflow-hidden"
|
||||
fallback
|
||||
/>
|
||||
|
||||
<span
|
||||
class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap text-sm"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VbenContextMenu>
|
||||
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</VbenContextMenu>
|
||||
</div>
|
||||
<!-- footer -->
|
||||
<!-- <div class="bg-background h-1"></div> -->
|
||||
</VbenScrollbar>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tabs-chrome {
|
||||
.dragging {
|
||||
.tabs-chrome__item-main {
|
||||
/* .dragging { */
|
||||
|
||||
/* .tabs-chrome__item-main {
|
||||
@apply pr-0;
|
||||
}
|
||||
} */
|
||||
|
||||
.tabs-chrome__extra {
|
||||
/* .tabs-chrome__extra {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/* } */
|
||||
|
||||
&__item:not(.dragging) {
|
||||
@apply cursor-pointer;
|
||||
|
||||
&__item {
|
||||
&:hover:not(.is-active) {
|
||||
& + .tabs-chrome__item {
|
||||
.tabs-chrome__divider {
|
||||
@ -207,13 +177,10 @@ function scrollIntoView() {
|
||||
}
|
||||
|
||||
.tabs-chrome__background {
|
||||
&-content {
|
||||
@apply bg-accent mx-1 rounded-md pb-2;
|
||||
}
|
||||
@apply pb-[2px];
|
||||
|
||||
&-before,
|
||||
&-after {
|
||||
@apply fill-primary/0;
|
||||
&-content {
|
||||
@apply bg-accent-hover mx-[2px] rounded-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -226,30 +193,7 @@ function scrollIntoView() {
|
||||
@apply opacity-0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-chrome__background {
|
||||
@apply opacity-100;
|
||||
|
||||
/* &-content {
|
||||
@apply bg-accent;
|
||||
}
|
||||
|
||||
&-before,
|
||||
&-after {
|
||||
@apply fill-heavy;
|
||||
} */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__scrollbar,
|
||||
&__label {
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
#000 calc(100% - 16px),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
|
||||
|
||||
import type { TabConfig, TabsProps } from '../../types';
|
||||
|
||||
import { computed, watch } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { MdiPin, X } from '@vben-core/icons';
|
||||
import { VbenContextMenu, VbenIcon, VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
import { VbenContextMenu, VbenIcon } from '@vben-core/shadcn-ui';
|
||||
|
||||
interface Props extends TabsProps {}
|
||||
|
||||
@ -21,7 +21,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
tabs: () => [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
|
||||
const emit = defineEmits<{
|
||||
close: [string];
|
||||
unpin: [TabDefinition];
|
||||
}>();
|
||||
const active = defineModel<string>('active');
|
||||
|
||||
const typeWithClass = computed(() => {
|
||||
@ -55,108 +58,71 @@ const tabsView = computed((): TabConfig[] => {
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
watch(active, () => {
|
||||
scrollIntoView();
|
||||
});
|
||||
|
||||
function scrollIntoView() {
|
||||
setTimeout(() => {
|
||||
const element = document.querySelector(`.tabs-chrome__item.is-active`);
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="size-full flex-1 overflow-hidden">
|
||||
<VbenScrollbar
|
||||
id="tabs-scrollbar"
|
||||
class="tabs-scrollbar h-full"
|
||||
horizontal
|
||||
scroll-bar-class="z-10 hidden"
|
||||
>
|
||||
<div
|
||||
:class="contentClass"
|
||||
class="relative !flex h-full w-max items-center pr-6"
|
||||
>
|
||||
<TransitionGroup name="slide-left">
|
||||
<div
|
||||
:class="contentClass"
|
||||
class="relative !flex h-full w-max items-center"
|
||||
v-for="(tab, i) in tabsView"
|
||||
:key="tab.key"
|
||||
:class="[
|
||||
{
|
||||
'is-active dark:bg-accent bg-primary/15': tab.key === active,
|
||||
dragable: !tab.affixTab,
|
||||
},
|
||||
typeWithClass.content,
|
||||
]"
|
||||
:data-index="i"
|
||||
class="tab-item [&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none"
|
||||
data-tab-item="true"
|
||||
@click="active = tab.key"
|
||||
>
|
||||
<TransitionGroup name="slide-left">
|
||||
<div
|
||||
v-for="(tab, i) in tabsView"
|
||||
:key="tab.key"
|
||||
:class="[
|
||||
{
|
||||
'is-active dark:bg-accent bg-primary/15': tab.key === active,
|
||||
dragable: !tab.affixTab,
|
||||
},
|
||||
typeWithClass.content,
|
||||
]"
|
||||
:data-index="i"
|
||||
class="tabs-chrome__item [&: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"
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<!-- close-icon -->
|
||||
<X
|
||||
v-show="
|
||||
!tab.affixTab && tabsView.length > 1 && tab.closable
|
||||
"
|
||||
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('close', tab.key)"
|
||||
/>
|
||||
<MdiPin
|
||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('unpin', tab)"
|
||||
/>
|
||||
</div>
|
||||
<!-- close-icon -->
|
||||
<X
|
||||
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||
class="hover:bg-accent stroke-accent-foreground/80 hover:stroke-accent-foreground dark:group-[.is-active]:text-accent-foreground group-[.is-active]:text-primary size-3 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('close', tab.key)"
|
||||
/>
|
||||
<MdiPin
|
||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||
class="hover:bg-accent hover:stroke-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mt-[2px] size-3.5 cursor-pointer rounded-full transition-all"
|
||||
@click.stop="() => emit('unpin', tab)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- tab-item-main -->
|
||||
<div
|
||||
class="text-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mx-3 mr-4 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
|
||||
>
|
||||
<VbenIcon
|
||||
v-if="showIcon"
|
||||
:icon="tab.icon"
|
||||
class="mr-2 flex size-4 items-center overflow-hidden"
|
||||
fallback
|
||||
/>
|
||||
<!-- tab-item-main -->
|
||||
<div
|
||||
class="text-accent-foreground group-[.is-active]:text-primary dark:group-[.is-active]:text-accent-foreground mx-3 mr-4 flex h-full items-center overflow-hidden rounded-tl-[5px] rounded-tr-[5px] pr-3 transition-all duration-300"
|
||||
>
|
||||
<VbenIcon
|
||||
v-if="showIcon"
|
||||
:icon="tab.icon"
|
||||
class="mr-2 flex size-4 items-center overflow-hidden"
|
||||
fallback
|
||||
/>
|
||||
|
||||
<span
|
||||
class="flex-1 overflow-hidden whitespace-nowrap text-sm"
|
||||
>
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</VbenContextMenu>
|
||||
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</VbenContextMenu>
|
||||
</div>
|
||||
</VbenScrollbar>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tabs-scrollbar {
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
#000 0%,
|
||||
#000 calc(100% - 16px),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,15 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { Sortable } from '@vben-core/composables';
|
||||
import type { TabDefinition } from '@vben-core/typings';
|
||||
import type { TabsEmits, TabsProps } from './types';
|
||||
|
||||
import type { TabsProps } from './types';
|
||||
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { useForwardPropsEmits, useSortable } from '@vben-core/composables';
|
||||
import { useForwardPropsEmits } from '@vben-core/composables';
|
||||
import { ChevronLeft, ChevronRight } from '@vben-core/icons';
|
||||
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { Tabs, TabsChrome } from './components';
|
||||
import { useTabsDrag } from './use-tabs-drag';
|
||||
import { useTabsViewScroll } from './use-tabs-view-scroll';
|
||||
|
||||
interface Props extends TabsProps {}
|
||||
@ -24,136 +21,69 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
styleType: 'chrome',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [string];
|
||||
sortTabs: [number, number];
|
||||
unpin: [TabDefinition];
|
||||
}>();
|
||||
const emit = defineEmits<TabsEmits>();
|
||||
|
||||
const forward = useForwardPropsEmits(props, emit);
|
||||
|
||||
const { initScrollbar, scrollDirection } = useTabsViewScroll();
|
||||
const {
|
||||
handleScrollAt,
|
||||
scrollbarRef,
|
||||
scrollDirection,
|
||||
scrollIsAtLeft,
|
||||
scrollIsAtRight,
|
||||
showScrollButton,
|
||||
} = useTabsViewScroll(props);
|
||||
|
||||
const sortableInstance = ref<null | Sortable>(null);
|
||||
|
||||
// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
|
||||
function findParentElement(element: HTMLElement) {
|
||||
const parentCls = 'group';
|
||||
return element?.classList?.contains(parentCls)
|
||||
? element
|
||||
: element?.closest(`.${parentCls}`);
|
||||
}
|
||||
|
||||
async function initTabsSortable() {
|
||||
await nextTick();
|
||||
const { contentClass } = props;
|
||||
|
||||
const el = document.querySelectorAll(`.${contentClass}`)?.[0] as HTMLElement;
|
||||
|
||||
const resetElState = () => {
|
||||
el.style.cursor = 'default';
|
||||
el.classList.remove('dragging');
|
||||
};
|
||||
|
||||
const { initializeSortable } = useSortable(el, {
|
||||
filter: (_evt, target: HTMLElement) => {
|
||||
const parent = findParentElement(target);
|
||||
const dragable = parent?.classList.contains('dragable');
|
||||
return !dragable || !props.dragable;
|
||||
},
|
||||
onEnd(evt) {
|
||||
const { newIndex, oldIndex } = evt;
|
||||
// const fromElement = evt.item;
|
||||
const { srcElement } = (evt as any).originalEvent;
|
||||
|
||||
if (!srcElement) {
|
||||
resetElState();
|
||||
return;
|
||||
}
|
||||
|
||||
const srcParent = findParentElement(srcElement);
|
||||
|
||||
if (!srcParent) {
|
||||
resetElState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!srcParent.classList.contains('dragable')) {
|
||||
resetElState();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
oldIndex !== undefined &&
|
||||
newIndex !== undefined &&
|
||||
!Number.isNaN(oldIndex) &&
|
||||
!Number.isNaN(newIndex) &&
|
||||
oldIndex !== newIndex
|
||||
) {
|
||||
emit('sortTabs', oldIndex, newIndex);
|
||||
}
|
||||
resetElState();
|
||||
},
|
||||
onMove(evt) {
|
||||
const parent = findParentElement(evt.related);
|
||||
return parent?.classList.contains('dragable') && props.dragable;
|
||||
},
|
||||
onStart: () => {
|
||||
el.style.cursor = 'grabbing';
|
||||
el.classList.add('dragging');
|
||||
},
|
||||
});
|
||||
|
||||
sortableInstance.value = await initializeSortable();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await nextTick();
|
||||
initTabsSortable();
|
||||
initScrollbar();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.styleType,
|
||||
() => {
|
||||
sortableInstance.value?.destroy();
|
||||
init();
|
||||
},
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
sortableInstance.value?.destroy();
|
||||
});
|
||||
useTabsDrag(props, emit);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'overflow-hidden': styleType !== 'chrome',
|
||||
}"
|
||||
class="flex h-full flex-1"
|
||||
>
|
||||
<div class="flex h-full flex-1 overflow-hidden">
|
||||
<!-- 左侧滚动按钮 -->
|
||||
<span
|
||||
class="hover:bg-muted text-muted-foreground cursor-pointer border-r px-2"
|
||||
v-show="showScrollButton"
|
||||
:class="{
|
||||
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtLeft,
|
||||
'pointer-events-none opacity-30': scrollIsAtLeft,
|
||||
}"
|
||||
class="border-r px-2"
|
||||
@click="scrollDirection('left')"
|
||||
>
|
||||
<ChevronLeft class="size-4 h-full" />
|
||||
</span>
|
||||
|
||||
<TabsChrome
|
||||
v-if="styleType === 'chrome'"
|
||||
v-bind="{ ...forward, ...$attrs, ...$props }"
|
||||
/>
|
||||
<Tabs v-else v-bind="{ ...forward, ...$attrs, ...$props }" />
|
||||
<div
|
||||
:class="{
|
||||
'pt-[3px]': styleType === 'chrome',
|
||||
}"
|
||||
class="size-full flex-1 overflow-hidden"
|
||||
>
|
||||
<VbenScrollbar
|
||||
ref="scrollbarRef"
|
||||
class="h-full"
|
||||
horizontal
|
||||
scroll-bar-class="z-10 hidden"
|
||||
shadow
|
||||
shadow-left
|
||||
shadow-right
|
||||
@scroll-at="handleScrollAt"
|
||||
>
|
||||
<TabsChrome
|
||||
v-if="styleType === 'chrome'"
|
||||
v-bind="{ ...forward, ...$attrs, ...$props }"
|
||||
/>
|
||||
|
||||
<Tabs v-else v-bind="{ ...forward, ...$attrs, ...$props }" />
|
||||
</VbenScrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 左侧滚动按钮 -->
|
||||
<span
|
||||
v-show="showScrollButton"
|
||||
:class="{
|
||||
'hover:bg-muted text-muted-foreground cursor-pointer': !scrollIsAtRight,
|
||||
'pointer-events-none opacity-30': scrollIsAtRight,
|
||||
}"
|
||||
class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2"
|
||||
@click="scrollDirection('right')"
|
||||
>
|
||||
|
@ -1,7 +1,14 @@
|
||||
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||
import type { TabDefinition, TabsStyleType } from '@vben-core/typings';
|
||||
|
||||
interface TabsProps {
|
||||
export type TabsEmits = {
|
||||
close: [string];
|
||||
sortTabs: [number, number];
|
||||
unpin: [TabDefinition];
|
||||
};
|
||||
|
||||
export interface TabsProps {
|
||||
active?: string;
|
||||
/**
|
||||
* @zh_CN content class
|
||||
* @default tabs-chrome
|
||||
@ -48,12 +55,10 @@ interface TabsProps {
|
||||
tabs?: TabDefinition[];
|
||||
}
|
||||
|
||||
interface TabConfig extends TabDefinition {
|
||||
export interface TabConfig extends TabDefinition {
|
||||
affixTab: boolean;
|
||||
closable: boolean;
|
||||
icon: string;
|
||||
key: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type { TabConfig, TabsProps };
|
||||
|
110
packages/@core/ui-kit/tabs-ui/src/use-tabs-drag.ts
Normal file
110
packages/@core/ui-kit/tabs-ui/src/use-tabs-drag.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import type { EmitType } from '@vben-core/typings';
|
||||
|
||||
import type { TabsProps } from './types';
|
||||
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
import { type Sortable, useSortable } from '@vben-core/composables';
|
||||
|
||||
// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
|
||||
function findParentElement(element: HTMLElement) {
|
||||
const parentCls = 'group';
|
||||
return element.classList.contains(parentCls)
|
||||
? element
|
||||
: element.closest(`.${parentCls}`);
|
||||
}
|
||||
|
||||
export function useTabsDrag(props: TabsProps, emit: EmitType) {
|
||||
const sortableInstance = ref<null | Sortable>(null);
|
||||
|
||||
async function initTabsSortable() {
|
||||
await nextTick();
|
||||
|
||||
const el = document.querySelectorAll(
|
||||
`.${props.contentClass}`,
|
||||
)?.[0] as HTMLElement;
|
||||
|
||||
if (!el) {
|
||||
console.warn('Element not found for sortable initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
const resetElState = async () => {
|
||||
el.style.cursor = 'default';
|
||||
el.classList.remove('dragging');
|
||||
el.querySelector('.draggable')?.classList.remove('dragging');
|
||||
};
|
||||
|
||||
const { initializeSortable } = useSortable(el, {
|
||||
filter: (_evt, target: HTMLElement) => {
|
||||
const parent = findParentElement(target);
|
||||
const dragable = parent?.classList.contains('dragable');
|
||||
return !dragable || !props.dragable;
|
||||
},
|
||||
onEnd(evt) {
|
||||
const { newIndex, oldIndex } = evt;
|
||||
// const fromElement = evt.item;
|
||||
const { srcElement } = (evt as any).originalEvent;
|
||||
|
||||
if (!srcElement) {
|
||||
resetElState();
|
||||
return;
|
||||
}
|
||||
|
||||
const srcParent = findParentElement(srcElement);
|
||||
|
||||
if (!srcParent) {
|
||||
resetElState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!srcParent.classList.contains('dragable')) {
|
||||
resetElState();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
oldIndex !== undefined &&
|
||||
newIndex !== undefined &&
|
||||
!Number.isNaN(oldIndex) &&
|
||||
!Number.isNaN(newIndex) &&
|
||||
oldIndex !== newIndex
|
||||
) {
|
||||
emit('sortTabs', oldIndex, newIndex);
|
||||
}
|
||||
resetElState();
|
||||
},
|
||||
onMove(evt) {
|
||||
const parent = findParentElement(evt.related);
|
||||
return parent?.classList.contains('dragable') && props.dragable;
|
||||
},
|
||||
onStart: () => {
|
||||
el.style.cursor = 'grabbing';
|
||||
el.querySelector('.draggable')?.classList.add('dragging');
|
||||
// el.classList.add('dragging');
|
||||
},
|
||||
});
|
||||
|
||||
sortableInstance.value = await initializeSortable();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await nextTick();
|
||||
initTabsSortable();
|
||||
}
|
||||
|
||||
onMounted(init);
|
||||
|
||||
watch(
|
||||
() => props.styleType,
|
||||
() => {
|
||||
sortableInstance.value?.destroy();
|
||||
init();
|
||||
},
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
sortableInstance.value?.destroy();
|
||||
});
|
||||
}
|
@ -1,15 +1,28 @@
|
||||
import { nextTick, ref } from 'vue';
|
||||
import type { TabsProps } from './types';
|
||||
|
||||
type El = Element | null | undefined;
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
export function useTabsViewScroll(scrollDistance: number = 150) {
|
||||
const scrollbarEl = ref<El>(null);
|
||||
const scrollViewportEl = ref<El>(null);
|
||||
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
|
||||
type DomElement = Element | null | undefined;
|
||||
|
||||
export function useTabsViewScroll(props: TabsProps) {
|
||||
let resizeObserver: null | ResizeObserver = null;
|
||||
let mutationObserver: MutationObserver | null = null;
|
||||
let tabItemCount = 0;
|
||||
const scrollbarRef = ref<InstanceType<typeof VbenScrollbar> | null>(null);
|
||||
const scrollViewportEl = ref<DomElement>(null);
|
||||
const showScrollButton = ref(false);
|
||||
const scrollIsAtLeft = ref(true);
|
||||
const scrollIsAtRight = ref(false);
|
||||
|
||||
function getScrollClientWidth() {
|
||||
if (!scrollbarEl.value || !scrollViewportEl.value) return {};
|
||||
const scrollbarEl = scrollbarRef.value?.$el;
|
||||
if (!scrollbarEl || !scrollViewportEl.value) return {};
|
||||
|
||||
const scrollbarWidth = scrollbarEl.value.clientWidth;
|
||||
const scrollbarWidth = scrollbarEl.clientWidth;
|
||||
const scrollViewWidth = scrollViewportEl.value.clientWidth;
|
||||
|
||||
return {
|
||||
@ -20,7 +33,7 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
|
||||
|
||||
function scrollDirection(
|
||||
direction: 'left' | 'right',
|
||||
distance: number = scrollDistance,
|
||||
distance: number = 150,
|
||||
) {
|
||||
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
|
||||
|
||||
@ -39,21 +52,142 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
|
||||
|
||||
async function initScrollbar() {
|
||||
await nextTick();
|
||||
const barEl = document.querySelector('#tabs-scrollbar');
|
||||
|
||||
const viewportEl = barEl?.querySelector(
|
||||
const scrollbarEl = scrollbarRef.value?.$el;
|
||||
if (!scrollbarEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportEl = scrollbarEl?.querySelector(
|
||||
'div[data-radix-scroll-area-viewport]',
|
||||
);
|
||||
|
||||
scrollbarEl.value = barEl;
|
||||
scrollViewportEl.value = viewportEl;
|
||||
calcShowScrollbarButton();
|
||||
|
||||
const activeItem = viewportEl?.querySelector('.is-active');
|
||||
activeItem?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
await nextTick();
|
||||
scrollToActiveIntoView();
|
||||
|
||||
// 监听大小变化
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = new ResizeObserver(
|
||||
useDebounceFn((_entries: ResizeObserverEntry[]) => {
|
||||
calcShowScrollbarButton();
|
||||
}, 100),
|
||||
);
|
||||
resizeObserver.observe(viewportEl);
|
||||
|
||||
tabItemCount = props.tabs?.length || 0;
|
||||
mutationObserver?.disconnect();
|
||||
// 使用 MutationObserver 仅监听子节点数量变化
|
||||
mutationObserver = new MutationObserver(() => {
|
||||
const count = viewportEl.querySelectorAll(
|
||||
`div[data-tab-item="true"]`,
|
||||
).length;
|
||||
|
||||
if (count > tabItemCount) {
|
||||
scrollToActiveIntoView();
|
||||
}
|
||||
|
||||
if (count !== tabItemCount) {
|
||||
calcShowScrollbarButton();
|
||||
tabItemCount = count;
|
||||
}
|
||||
});
|
||||
|
||||
// 配置为仅监听子节点的添加和移除
|
||||
mutationObserver.observe(viewportEl, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function scrollToActiveIntoView() {
|
||||
if (!scrollViewportEl.value) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
const viewportEl = scrollViewportEl.value;
|
||||
const { scrollbarWidth } = getScrollClientWidth();
|
||||
const { scrollWidth } = viewportEl;
|
||||
|
||||
if (scrollbarWidth >= scrollWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const activeItem = viewportEl?.querySelector('.is-active');
|
||||
activeItem?.scrollIntoView({ behavior: 'smooth', inline: 'start' });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算tabs 宽度,用于判断是否显示左右滚动按钮
|
||||
*/
|
||||
async function calcShowScrollbarButton() {
|
||||
if (!scrollViewportEl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollbarWidth } = getScrollClientWidth();
|
||||
|
||||
showScrollButton.value =
|
||||
scrollViewportEl.value.scrollWidth > scrollbarWidth;
|
||||
}
|
||||
|
||||
const handleScrollAt = useDebounceFn(({ left, right }) => {
|
||||
scrollIsAtLeft.value = left;
|
||||
scrollIsAtRight.value = right;
|
||||
}, 100);
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
async () => {
|
||||
// 200为了等待 tab 切换动画完成
|
||||
// setTimeout(() => {
|
||||
scrollToActiveIntoView();
|
||||
// }, 300);
|
||||
},
|
||||
{
|
||||
flush: 'post',
|
||||
},
|
||||
);
|
||||
|
||||
// watch(
|
||||
// () => props.tabs?.length,
|
||||
// async () => {
|
||||
// await nextTick();
|
||||
// calcShowScrollbarButton();
|
||||
// },
|
||||
// {
|
||||
// flush: 'post',
|
||||
// },
|
||||
// );
|
||||
|
||||
watch(
|
||||
() => props.styleType,
|
||||
() => {
|
||||
initScrollbar();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(initScrollbar);
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect();
|
||||
mutationObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
mutationObserver = null;
|
||||
});
|
||||
|
||||
return {
|
||||
handleScrollAt,
|
||||
initScrollbar,
|
||||
scrollbarRef,
|
||||
scrollDirection,
|
||||
scrollIsAtLeft,
|
||||
scrollIsAtRight,
|
||||
showScrollButton,
|
||||
};
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vben/preferences": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"echarts": "^5.5.1",
|
||||
"vue": "^3.4.37"
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
"@vben/icons": "workspace:*",
|
||||
"@vben/locales": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vueuse/integrations": "^10.11.1",
|
||||
"@vueuse/integrations": "^11.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
|
@ -27,6 +27,6 @@
|
||||
"@vben/utils": "workspace:*",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3",
|
||||
"watermark-js-plus": "^1.5.2"
|
||||
"watermark-js-plus": "^1.5.3"
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@
|
||||
"@vben/stores": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
"dependencies": {
|
||||
"@vben-core/shared": "workspace:*",
|
||||
"@vben-core/typings": "workspace:*",
|
||||
"pinia": "2.2.1",
|
||||
"pinia": "2.2.2",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
|
@ -55,7 +55,7 @@ describe('useAccessStore', () => {
|
||||
const updatedTab = { ...initialTab, query: { id: '1' } };
|
||||
store.addTab(updatedTab);
|
||||
expect(store.tabs.length).toBe(1);
|
||||
expect(store.tabs[0].query).toEqual({ id: '1' });
|
||||
expect(store.tabs[0]?.query).toEqual({ id: '1' });
|
||||
});
|
||||
|
||||
it('closes all tabs', async () => {
|
||||
@ -67,7 +67,7 @@ describe('useAccessStore', () => {
|
||||
|
||||
await store.closeAllTabs(router);
|
||||
|
||||
expect(store.tabs.length).toBe(0); // 假设没有固定的标签页
|
||||
expect(store.tabs.length).toBe(1); // 假设没有固定的标签页
|
||||
// expect(router.replace).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -124,10 +124,21 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
||||
} else {
|
||||
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
|
||||
const currentTab = toRaw(this.tabs)[tabIndex];
|
||||
const mergedTab = { ...currentTab, ...tab };
|
||||
if (currentTab && Reflect.has(currentTab.meta, 'affixTab')) {
|
||||
mergedTab.meta.affixTab = currentTab.meta.affixTab;
|
||||
const mergedTab = {
|
||||
...currentTab,
|
||||
...tab,
|
||||
meta: { ...currentTab?.meta, ...tab.meta },
|
||||
};
|
||||
if (currentTab) {
|
||||
const curMeta = currentTab.meta;
|
||||
if (Reflect.has(curMeta, 'affixTab')) {
|
||||
mergedTab.meta.affixTab = curMeta.affixTab;
|
||||
}
|
||||
if (Reflect.has(curMeta, 'newTabTitle')) {
|
||||
mergedTab.meta.newTabTitle = curMeta.newTabTitle;
|
||||
}
|
||||
}
|
||||
|
||||
this.tabs.splice(tabIndex, 1, mergedTab);
|
||||
}
|
||||
this.updateCacheTab();
|
||||
@ -136,7 +147,8 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
||||
* @zh_CN 关闭所有标签页
|
||||
*/
|
||||
async closeAllTabs(router: Router) {
|
||||
this.tabs = this.tabs.filter((tab) => isAffixTab(tab));
|
||||
const newTabs = this.tabs.filter((tab) => isAffixTab(tab));
|
||||
this.tabs = newTabs.length > 0 ? newTabs : [...this.tabs].splice(0, 1);
|
||||
await this._goToDefaultTab(router);
|
||||
this.updateCacheTab();
|
||||
},
|
||||
|
@ -40,10 +40,10 @@
|
||||
"@vben/styles": "workspace:*",
|
||||
"@vben/types": "workspace:*",
|
||||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"ant-design-vue": "^4.2.3",
|
||||
"dayjs": "^1.11.12",
|
||||
"pinia": "2.2.1",
|
||||
"pinia": "2.2.2",
|
||||
"vue": "^3.4.37",
|
||||
"vue-router": "^4.4.3"
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ const routes: RouteRecordRaw[] = [
|
||||
import(
|
||||
'#/views/demos/features/hide-menu-children/children.vue'
|
||||
),
|
||||
meta: { title: 'HideChildrenInMenuChildrenDemo' },
|
||||
meta: { title: $t('page.demos.features.hideChildrenInMenu') },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
1181
pnpm-lock.yaml
1181
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,6 @@
|
||||
"cac": "^6.7.14",
|
||||
"circular-dependency-scanner": "^2.2.2",
|
||||
"depcheck": "^1.4.7",
|
||||
"publint": "^0.2.9"
|
||||
"publint": "^0.2.10"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user