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[]=husky
|
||||||
public-hoist-pattern[]=eslint
|
public-hoist-pattern[]=eslint
|
||||||
public-hoist-pattern[]=prettier
|
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
|
## Change Log
|
||||||
|
|
||||||
[CHANGELOG](./CHANGELOG.zh_CN.md)
|
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases/latest)
|
||||||
|
|
||||||
## How to contribute
|
## 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>
|
<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
|
## Contributor
|
||||||
|
|
||||||
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
|
||||||
|
@ -40,14 +40,14 @@
|
|||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"ant-design-vue": "^4.2.3",
|
"ant-design-vue": "^4.2.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"pinia": "2.2.1",
|
"pinia": "2.2.2",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
},
|
},
|
||||||
|
@ -40,10 +40,10 @@
|
|||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"element-plus": "^2.8.0",
|
"element-plus": "^2.8.0",
|
||||||
"pinia": "2.2.1",
|
"pinia": "2.2.2",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
},
|
},
|
||||||
|
@ -40,9 +40,9 @@
|
|||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"naive-ui": "^2.39.0",
|
"naive-ui": "^2.39.0",
|
||||||
"pinia": "2.2.1",
|
"pinia": "2.2.2",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-config-turbo": "^2.0.13",
|
"eslint-config-turbo": "^2.0.14",
|
||||||
"eslint-plugin-command": "^0.2.3",
|
"eslint-plugin-command": "^0.2.3",
|
||||||
"eslint-plugin-import-x": "^3.1.0"
|
"eslint-plugin-import-x": "^3.1.0"
|
||||||
},
|
},
|
||||||
@ -42,8 +42,8 @@
|
|||||||
"eslint-plugin-jsdoc": "^50.2.2",
|
"eslint-plugin-jsdoc": "^50.2.2",
|
||||||
"eslint-plugin-jsonc": "^2.16.0",
|
"eslint-plugin-jsonc": "^2.16.0",
|
||||||
"eslint-plugin-n": "^17.10.2",
|
"eslint-plugin-n": "^17.10.2",
|
||||||
"eslint-plugin-no-only-tests": "^3.1.0",
|
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||||
"eslint-plugin-perfectionist": "^3.1.3",
|
"eslint-plugin-perfectionist": "^3.2.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-regexp": "^2.6.0",
|
"eslint-plugin-regexp": "^2.6.0",
|
||||||
"eslint-plugin-unicorn": "^55.0.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-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-empty-function': [
|
'@typescript-eslint/no-empty-function': [
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"postcss-html": "^1.7.0",
|
"postcss-html": "^1.7.0",
|
||||||
"postcss-scss": "^4.0.9",
|
"postcss-scss": "^4.0.9",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"stylelint": "^16.8.1",
|
"stylelint": "^16.8.2",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^14.0.1",
|
||||||
"stylelint-config-recommended-scss": "^14.1.0",
|
"stylelint-config-recommended-scss": "^14.1.0",
|
||||||
"stylelint-config-recommended-vue": "^1.5.0",
|
"stylelint-config-recommended-vue": "^1.5.0",
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"execa": "^9.3.0",
|
"execa": "^9.3.1",
|
||||||
"find-up": "^7.0.0",
|
"find-up": "^7.0.0",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"ora": "^8.0.1",
|
"ora": "^8.0.1",
|
||||||
|
@ -20,6 +20,6 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
"rollup": "^4.20.0",
|
"rollup": "^4.20.0",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.1",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dts": "4.0.3",
|
"vite-plugin-dts": "4.0.3",
|
||||||
"vite-plugin-html": "^3.2.2"
|
"vite-plugin-html": "^3.2.2"
|
||||||
|
13
package.json
13
package.json
@ -82,10 +82,10 @@
|
|||||||
"lint-staged": "^15.2.9",
|
"lint-staged": "^15.2.9",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"turbo": "^2.0.13",
|
"turbo": "^2.0.14",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"unbuild": "^2.0.0",
|
"unbuild": "^2.0.0",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.1",
|
||||||
"vitest": "^2.0.5",
|
"vitest": "^2.0.5",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-tsc": "^2.0.29"
|
"vue-tsc": "^2.0.29"
|
||||||
@ -94,7 +94,7 @@
|
|||||||
"node": ">=20",
|
"node": ">=20",
|
||||||
"pnpm": ">=9"
|
"pnpm": ">=9"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.7.0",
|
"packageManager": "pnpm@9.7.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"peerDependencyRules": {
|
"peerDependencyRules": {
|
||||||
"allowedVersions": {
|
"allowedVersions": {
|
||||||
@ -102,9 +102,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@ctrl/tinycolor": "^4.1.0",
|
"@ctrl/tinycolor": "4.1.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "2.1.1",
|
||||||
"vue": "^3.4.37"
|
"pinia": "2.2.2",
|
||||||
|
"vue": "3.4.37"
|
||||||
},
|
},
|
||||||
"neverBuiltDependencies": [
|
"neverBuiltDependencies": [
|
||||||
"canvas",
|
"canvas",
|
||||||
|
@ -351,7 +351,7 @@
|
|||||||
--ring: 240 4.9% 83.9%;
|
--ring: 240 4.9% 83.9%;
|
||||||
--sidebar: 240 10% 3.9%;
|
--sidebar: 240 10% 3.9%;
|
||||||
--sidebar-deep: 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'],
|
.dark[data-theme='neutral'],
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/vue": "^4.1.2",
|
"@iconify/vue": "^4.1.2",
|
||||||
"lucide-vue-next": "^0.427.0",
|
"lucide-vue-next": "^0.428.0",
|
||||||
"vue": "^3.4.37"
|
"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>>
|
? MergeAll<Rest, Merge<R, F>>
|
||||||
: R;
|
: R;
|
||||||
|
|
||||||
export {
|
type EmitType = (name: Name, ...args: any[]) => void;
|
||||||
type AnyFunction,
|
|
||||||
type AnyNormalFunction,
|
export type {
|
||||||
type AnyPromiseFunction,
|
AnyFunction,
|
||||||
type DeepPartial,
|
AnyNormalFunction,
|
||||||
type DeepReadonly,
|
AnyPromiseFunction,
|
||||||
type IntervalHandle,
|
DeepPartial,
|
||||||
type MaybeComputedRef,
|
DeepReadonly,
|
||||||
type MaybeReadonlyRef,
|
EmitType,
|
||||||
type Merge,
|
IntervalHandle,
|
||||||
type MergeAll,
|
MaybeComputedRef,
|
||||||
type NonNullable,
|
MaybeReadonlyRef,
|
||||||
type Nullable,
|
Merge,
|
||||||
type ReadonlyRecordable,
|
MergeAll,
|
||||||
type Recordable,
|
NonNullable,
|
||||||
type TimeoutHandle,
|
Nullable,
|
||||||
|
ReadonlyRecordable,
|
||||||
|
Recordable,
|
||||||
|
TimeoutHandle,
|
||||||
};
|
};
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben-core/shared": "workspace:*",
|
"@vben-core/shared": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"radix-vue": "^1.9.4",
|
"radix-vue": "^1.9.4",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.2",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
||||||
@ -14,6 +14,7 @@ import { useCssVar, useDebounceFn } from '@vueuse/core';
|
|||||||
* @zh_CN content style
|
* @zh_CN content style
|
||||||
*/
|
*/
|
||||||
function useContentStyle() {
|
function useContentStyle() {
|
||||||
|
let resizeObserver: null | ResizeObserver = null;
|
||||||
const contentElement = ref<HTMLDivElement | null>(null);
|
const contentElement = ref<HTMLDivElement | null>(null);
|
||||||
const visibleDomRect = ref<null | VisibleDomRect>(null);
|
const visibleDomRect = ref<null | VisibleDomRect>(null);
|
||||||
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
||||||
@ -41,12 +42,15 @@ function useContentStyle() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
if (contentElement.value && !resizeObserver) {
|
||||||
if (contentElement.value) {
|
resizeObserver = new ResizeObserver(debouncedCalcHeight);
|
||||||
const observer = new ResizeObserver(debouncedCalcHeight);
|
resizeObserver.observe(contentElement.value);
|
||||||
observer.observe(contentElement.value);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { contentElement, overlayStyle, visibleDomRect };
|
return { contentElement, overlayStyle, visibleDomRect };
|
||||||
|
@ -39,7 +39,7 @@ describe('useSortable', () => {
|
|||||||
expect(Sortable.default.create).toHaveBeenCalledWith(
|
expect(Sortable.default.create).toHaveBeenCalledWith(
|
||||||
mockElement,
|
mockElement,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
animation: 100,
|
animation: 300,
|
||||||
delay: 400,
|
delay: 400,
|
||||||
delayOnTouchOnly: true,
|
delayOnTouchOnly: true,
|
||||||
...customOptions,
|
...customOptions,
|
||||||
|
@ -18,7 +18,7 @@ function useSortable<T extends HTMLElement>(
|
|||||||
// Sortable?.default?.mount?.(AutoScroll);
|
// Sortable?.default?.mount?.(AutoScroll);
|
||||||
|
|
||||||
const sortable = Sortable?.default?.create?.(sortableContainer, {
|
const sortable = Sortable?.default?.create?.(sortableContainer, {
|
||||||
animation: 100,
|
animation: 300,
|
||||||
delay: 400,
|
delay: 400,
|
||||||
delayOnTouchOnly: true,
|
delayOnTouchOnly: true,
|
||||||
...options,
|
...options,
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben-core/shared": "workspace:*",
|
"@vben-core/shared": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
"@vben-core/icons": "workspace:*",
|
"@vben-core/icons": "workspace:*",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben-core/shared": "workspace:*",
|
"@vben-core/shared": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,9 +46,9 @@
|
|||||||
"@vben-core/icons": "workspace:*",
|
"@vben-core/icons": "workspace:*",
|
||||||
"@vben-core/shared": "workspace:*",
|
"@vben-core/shared": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"lucide-vue-next": "^0.427.0",
|
"lucide-vue-next": "^0.428.0",
|
||||||
"radix-vue": "^1.9.4",
|
"radix-vue": "^1.9.4",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared';
|
||||||
|
|
||||||
@ -11,6 +11,10 @@ interface Props {
|
|||||||
scrollBarClass?: any;
|
scrollBarClass?: any;
|
||||||
shadow?: boolean;
|
shadow?: boolean;
|
||||||
shadowBorder?: boolean;
|
shadowBorder?: boolean;
|
||||||
|
shadowBottom?: boolean;
|
||||||
|
shadowLeft?: boolean;
|
||||||
|
shadowRight?: boolean;
|
||||||
|
shadowTop?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@ -18,29 +22,66 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
horizontal: false,
|
horizontal: false,
|
||||||
shadow: false,
|
shadow: false,
|
||||||
shadowBorder: 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 isAtTop = ref(true);
|
||||||
|
const isAtRight = ref(false);
|
||||||
const isAtBottom = 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) {
|
function handleScroll(event: Event) {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const scrollTop = target?.scrollTop ?? 0;
|
const scrollTop = target?.scrollTop ?? 0;
|
||||||
|
const scrollLeft = target?.scrollLeft ?? 0;
|
||||||
const offsetHeight = target?.offsetHeight ?? 0;
|
const offsetHeight = target?.offsetHeight ?? 0;
|
||||||
|
const offsetWidth = target?.offsetWidth ?? 0;
|
||||||
const scrollHeight = target?.scrollHeight ?? 0;
|
const scrollHeight = target?.scrollHeight ?? 0;
|
||||||
|
const scrollWidth = target?.scrollWidth ?? 0;
|
||||||
isAtTop.value = scrollTop <= 0;
|
isAtTop.value = scrollTop <= 0;
|
||||||
|
isAtLeft.value = scrollLeft <= 0;
|
||||||
isAtBottom.value = scrollTop + offsetHeight >= scrollHeight;
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
:class="[cn(props.class)]"
|
:class="[cn(props.class), computedShadowClasses]"
|
||||||
:on-scroll="handleScroll"
|
:on-scroll="handleScroll"
|
||||||
class="relative"
|
class="vben-scrollbar relative"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="shadow"
|
v-if="showShadowTop"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-100': !isAtTop,
|
'opacity-100': !isAtTop,
|
||||||
'border-border border-t': shadowBorder && !isAtTop,
|
'border-border border-t': shadowBorder && !isAtTop,
|
||||||
@ -49,7 +90,7 @@ function handleScroll(event: Event) {
|
|||||||
></div>
|
></div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<div
|
<div
|
||||||
v-if="shadow"
|
v-if="showShadowBottom"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-100': !isAtTop && !isAtBottom,
|
'opacity-100': !isAtTop && !isAtBottom,
|
||||||
'border-border border-b': shadowBorder && !isAtTop && !isAtBottom,
|
'border-border border-b': shadowBorder && !isAtTop && !isAtBottom,
|
||||||
@ -65,6 +106,31 @@ function handleScroll(event: Event) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.scrollbar-top-shadow {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
"@vben-core/icons": "workspace:*",
|
"@vben-core/icons": "workspace:*",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
|
"@vueuse/core": "^11.0.0",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
|
|||||||
|
|
||||||
import type { TabConfig, TabsProps } from '../../types';
|
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 { 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 {}
|
interface Props extends TabsProps {}
|
||||||
|
|
||||||
@ -20,17 +20,17 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
contentClass: 'vben-tabs-content',
|
contentClass: 'vben-tabs-content',
|
||||||
contextMenus: () => [],
|
contextMenus: () => [],
|
||||||
gap: 7,
|
gap: 7,
|
||||||
maxWidth: 150,
|
|
||||||
minWidth: 80,
|
|
||||||
tabs: () => [],
|
tabs: () => [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
|
const emit = defineEmits<{
|
||||||
|
close: [string];
|
||||||
|
unpin: [TabDefinition];
|
||||||
|
}>();
|
||||||
const active = defineModel<string>('active');
|
const active = defineModel<string>('active');
|
||||||
|
|
||||||
const contentRef = ref();
|
const contentRef = ref();
|
||||||
const tabRef = ref();
|
const tabRef = ref();
|
||||||
const tabWidth = ref<number>(props.maxWidth);
|
|
||||||
|
|
||||||
const style = computed(() => {
|
const style = computed(() => {
|
||||||
const { gap } = props;
|
const { gap } = props;
|
||||||
@ -53,51 +53,25 @@ 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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
|
<div
|
||||||
ref="contentRef"
|
ref="contentRef"
|
||||||
:class="contentClass"
|
:class="contentClass"
|
||||||
class="relative !flex h-full w-max"
|
:style="style"
|
||||||
|
class="tabs-chrome !flex h-full w-max pr-6"
|
||||||
>
|
>
|
||||||
<TransitionGroup name="slide-left">
|
<TransitionGroup name="slide-left">
|
||||||
<div
|
<div
|
||||||
v-for="(tab, i) in tabsView"
|
v-for="(tab, i) in tabsView"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
ref="tabRef"
|
ref="tabRef"
|
||||||
:class="[
|
:class="[{ 'is-active': tab.key === active, dragable: !tab.affixTab }]"
|
||||||
{ 'is-active': tab.key === active, dragable: !tab.affixTab },
|
|
||||||
]"
|
|
||||||
:data-active-tab="active"
|
:data-active-tab="active"
|
||||||
:data-index="i"
|
:data-index="i"
|
||||||
:style="{
|
class="tabs-chrome__item draggable group relative -mr-3 flex h-full select-none items-center"
|
||||||
width: `${tabWidth}px`,
|
data-tab-item="true"
|
||||||
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"
|
@click="active = tab.key"
|
||||||
>
|
>
|
||||||
<VbenContextMenu
|
<VbenContextMenu
|
||||||
@ -106,18 +80,18 @@ function scrollIntoView() {
|
|||||||
:modal="false"
|
:modal="false"
|
||||||
item-class="pr-6"
|
item-class="pr-6"
|
||||||
>
|
>
|
||||||
<div class="size-full">
|
<div class="relative size-full px-1">
|
||||||
<!-- divider -->
|
<!-- divider -->
|
||||||
<div
|
<div
|
||||||
v-if="i !== 0 && tab.key !== active"
|
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"
|
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>
|
></div>
|
||||||
<!-- background -->
|
<!-- background -->
|
||||||
<div
|
<div
|
||||||
class="tabs-chrome__background absolute z-[1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
|
class="tabs-chrome__background absolute z-[-1] size-full px-[calc(var(--gap)-1px)] py-0 transition-opacity duration-150"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
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>
|
></div>
|
||||||
<svg
|
<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"
|
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"
|
||||||
@ -137,37 +111,33 @@ function scrollIntoView() {
|
|||||||
|
|
||||||
<!-- extra -->
|
<!-- extra -->
|
||||||
<div
|
<div
|
||||||
class="tabs-chrome__extra absolute right-[calc(var(--gap)*1.5)] top-1/2 z-[3] size-4 translate-y-[-50%]"
|
class="tabs-chrome__extra absolute right-[var(--gap)] top-1/2 z-[3] size-4 translate-y-[-50%]"
|
||||||
>
|
>
|
||||||
<!-- close-icon -->
|
<!-- close-icon -->
|
||||||
<X
|
<X
|
||||||
v-show="
|
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
!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"
|
||||||
"
|
|
||||||
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)"
|
@click.stop="() => emit('close', tab.key)"
|
||||||
/>
|
/>
|
||||||
<MdiPin
|
<MdiPin
|
||||||
v-show="tab.affixTab && tabsView.length > 1 && tab.closable"
|
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"
|
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)"
|
@click.stop="() => emit('unpin', tab)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- tab-item-main -->
|
<!-- tab-item-main -->
|
||||||
<div
|
<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"
|
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
|
<VbenIcon
|
||||||
v-if="showIcon"
|
v-if="showIcon"
|
||||||
:icon="tab.icon"
|
:icon="tab.icon"
|
||||||
class="ml-[var(--gap)] flex size-4 items-center overflow-hidden"
|
class="mr-1 flex size-4 items-center overflow-hidden"
|
||||||
fallback
|
fallback
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
||||||
class="tabs-chrome__label ml-[var(--gap)] flex-1 overflow-hidden whitespace-nowrap text-sm"
|
|
||||||
>
|
|
||||||
{{ tab.title }}
|
{{ tab.title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -176,25 +146,25 @@ function scrollIntoView() {
|
|||||||
</div>
|
</div>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
<!-- footer -->
|
|
||||||
<!-- <div class="bg-background h-1"></div> -->
|
|
||||||
</VbenScrollbar>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.tabs-chrome {
|
.tabs-chrome {
|
||||||
.dragging {
|
/* .dragging { */
|
||||||
.tabs-chrome__item-main {
|
|
||||||
|
/* .tabs-chrome__item-main {
|
||||||
@apply pr-0;
|
@apply pr-0;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.tabs-chrome__extra {
|
/* .tabs-chrome__extra {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
} */
|
||||||
}
|
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
&__item:not(.dragging) {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
|
||||||
&__item {
|
|
||||||
&:hover:not(.is-active) {
|
&:hover:not(.is-active) {
|
||||||
& + .tabs-chrome__item {
|
& + .tabs-chrome__item {
|
||||||
.tabs-chrome__divider {
|
.tabs-chrome__divider {
|
||||||
@ -207,13 +177,10 @@ function scrollIntoView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tabs-chrome__background {
|
.tabs-chrome__background {
|
||||||
&-content {
|
@apply pb-[2px];
|
||||||
@apply bg-accent mx-1 rounded-md pb-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-before,
|
&-content {
|
||||||
&-after {
|
@apply bg-accent-hover mx-[2px] rounded-md;
|
||||||
@apply fill-primary/0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,30 +193,7 @@ function scrollIntoView() {
|
|||||||
@apply opacity-0 !important;
|
@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>
|
</style>
|
||||||
|
@ -3,10 +3,10 @@ import type { TabDefinition } from '@vben-core/typings';
|
|||||||
|
|
||||||
import type { TabConfig, TabsProps } from '../../types';
|
import type { TabConfig, TabsProps } from '../../types';
|
||||||
|
|
||||||
import { computed, watch } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { MdiPin, X } from '@vben-core/icons';
|
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 {}
|
interface Props extends TabsProps {}
|
||||||
|
|
||||||
@ -21,7 +21,10 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
tabs: () => [],
|
tabs: () => [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{ close: [string]; unpin: [TabDefinition] }>();
|
const emit = defineEmits<{
|
||||||
|
close: [string];
|
||||||
|
unpin: [TabDefinition];
|
||||||
|
}>();
|
||||||
const active = defineModel<string>('active');
|
const active = defineModel<string>('active');
|
||||||
|
|
||||||
const typeWithClass = computed(() => {
|
const typeWithClass = computed(() => {
|
||||||
@ -55,33 +58,12 @@ 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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
|
<div
|
||||||
:class="contentClass"
|
:class="contentClass"
|
||||||
class="relative !flex h-full w-max items-center"
|
class="relative !flex h-full w-max items-center pr-6"
|
||||||
>
|
>
|
||||||
<TransitionGroup name="slide-left">
|
<TransitionGroup name="slide-left">
|
||||||
<div
|
<div
|
||||||
@ -95,7 +77,8 @@ function scrollIntoView() {
|
|||||||
typeWithClass.content,
|
typeWithClass.content,
|
||||||
]"
|
]"
|
||||||
:data-index="i"
|
:data-index="i"
|
||||||
class="tabs-chrome__item [&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none transition-all duration-300"
|
class="tab-item [&:not(.is-active)]:hover:bg-accent group relative flex cursor-pointer select-none"
|
||||||
|
data-tab-item="true"
|
||||||
@click="active = tab.key"
|
@click="active = tab.key"
|
||||||
>
|
>
|
||||||
<VbenContextMenu
|
<VbenContextMenu
|
||||||
@ -111,9 +94,7 @@ function scrollIntoView() {
|
|||||||
>
|
>
|
||||||
<!-- close-icon -->
|
<!-- close-icon -->
|
||||||
<X
|
<X
|
||||||
v-show="
|
v-show="!tab.affixTab && tabsView.length > 1 && tab.closable"
|
||||||
!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"
|
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)"
|
@click.stop="() => emit('close', tab.key)"
|
||||||
/>
|
/>
|
||||||
@ -135,9 +116,7 @@ function scrollIntoView() {
|
|||||||
fallback
|
fallback
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span class="flex-1 overflow-hidden whitespace-nowrap text-sm">
|
||||||
class="flex-1 overflow-hidden whitespace-nowrap text-sm"
|
|
||||||
>
|
|
||||||
{{ tab.title }}
|
{{ tab.title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -146,17 +125,4 @@ function scrollIntoView() {
|
|||||||
</div>
|
</div>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</VbenScrollbar>
|
|
||||||
</div>
|
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import type { Sortable } from '@vben-core/composables';
|
import type { TabsEmits, TabsProps } from './types';
|
||||||
import type { TabDefinition } from '@vben-core/typings';
|
|
||||||
|
|
||||||
import type { TabsProps } from './types';
|
import { useForwardPropsEmits } from '@vben-core/composables';
|
||||||
|
|
||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import { useForwardPropsEmits, useSortable } from '@vben-core/composables';
|
|
||||||
import { ChevronLeft, ChevronRight } from '@vben-core/icons';
|
import { ChevronLeft, ChevronRight } from '@vben-core/icons';
|
||||||
|
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { Tabs, TabsChrome } from './components';
|
import { Tabs, TabsChrome } from './components';
|
||||||
|
import { useTabsDrag } from './use-tabs-drag';
|
||||||
import { useTabsViewScroll } from './use-tabs-view-scroll';
|
import { useTabsViewScroll } from './use-tabs-view-scroll';
|
||||||
|
|
||||||
interface Props extends TabsProps {}
|
interface Props extends TabsProps {}
|
||||||
@ -24,136 +21,69 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
styleType: 'chrome',
|
styleType: 'chrome',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<TabsEmits>();
|
||||||
close: [string];
|
|
||||||
sortTabs: [number, number];
|
|
||||||
unpin: [TabDefinition];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const forward = useForwardPropsEmits(props, emit);
|
const forward = useForwardPropsEmits(props, emit);
|
||||||
|
|
||||||
const { initScrollbar, scrollDirection } = useTabsViewScroll();
|
const {
|
||||||
|
handleScrollAt,
|
||||||
|
scrollbarRef,
|
||||||
|
scrollDirection,
|
||||||
|
scrollIsAtLeft,
|
||||||
|
scrollIsAtRight,
|
||||||
|
showScrollButton,
|
||||||
|
} = useTabsViewScroll(props);
|
||||||
|
|
||||||
const sortableInstance = ref<null | Sortable>(null);
|
useTabsDrag(props, emit);
|
||||||
|
|
||||||
// 可能会找到拖拽的子元素,这里需要确保拖拽的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();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="flex h-full flex-1 overflow-hidden">
|
||||||
:class="{
|
|
||||||
'overflow-hidden': styleType !== 'chrome',
|
|
||||||
}"
|
|
||||||
class="flex h-full flex-1"
|
|
||||||
>
|
|
||||||
<!-- 左侧滚动按钮 -->
|
<!-- 左侧滚动按钮 -->
|
||||||
<span
|
<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')"
|
@click="scrollDirection('left')"
|
||||||
>
|
>
|
||||||
<ChevronLeft class="size-4 h-full" />
|
<ChevronLeft class="size-4 h-full" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<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
|
<TabsChrome
|
||||||
v-if="styleType === 'chrome'"
|
v-if="styleType === 'chrome'"
|
||||||
v-bind="{ ...forward, ...$attrs, ...$props }"
|
v-bind="{ ...forward, ...$attrs, ...$props }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs v-else v-bind="{ ...forward, ...$attrs, ...$props }" />
|
<Tabs v-else v-bind="{ ...forward, ...$attrs, ...$props }" />
|
||||||
|
</VbenScrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 左侧滚动按钮 -->
|
<!-- 左侧滚动按钮 -->
|
||||||
<span
|
<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"
|
class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2"
|
||||||
@click="scrollDirection('right')"
|
@click="scrollDirection('right')"
|
||||||
>
|
>
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
import type { IContextMenuItem } from '@vben-core/shadcn-ui';
|
||||||
import type { TabDefinition, TabsStyleType } from '@vben-core/typings';
|
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
|
* @zh_CN content class
|
||||||
* @default tabs-chrome
|
* @default tabs-chrome
|
||||||
@ -48,12 +55,10 @@ interface TabsProps {
|
|||||||
tabs?: TabDefinition[];
|
tabs?: TabDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabConfig extends TabDefinition {
|
export interface TabConfig extends TabDefinition {
|
||||||
affixTab: boolean;
|
affixTab: boolean;
|
||||||
closable: boolean;
|
closable: boolean;
|
||||||
icon: string;
|
icon: string;
|
||||||
key: string;
|
key: string;
|
||||||
title: 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) {
|
import { VbenScrollbar } from '@vben-core/shadcn-ui';
|
||||||
const scrollbarEl = ref<El>(null);
|
|
||||||
const scrollViewportEl = ref<El>(null);
|
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() {
|
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;
|
const scrollViewWidth = scrollViewportEl.value.clientWidth;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -20,7 +33,7 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
|
|||||||
|
|
||||||
function scrollDirection(
|
function scrollDirection(
|
||||||
direction: 'left' | 'right',
|
direction: 'left' | 'right',
|
||||||
distance: number = scrollDistance,
|
distance: number = 150,
|
||||||
) {
|
) {
|
||||||
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
|
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
|
||||||
|
|
||||||
@ -39,21 +52,142 @@ export function useTabsViewScroll(scrollDistance: number = 150) {
|
|||||||
|
|
||||||
async function initScrollbar() {
|
async function initScrollbar() {
|
||||||
await nextTick();
|
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]',
|
'div[data-radix-scroll-area-viewport]',
|
||||||
);
|
);
|
||||||
|
|
||||||
scrollbarEl.value = barEl;
|
|
||||||
scrollViewportEl.value = viewportEl;
|
scrollViewportEl.value = viewportEl;
|
||||||
|
calcShowScrollbarButton();
|
||||||
|
|
||||||
const activeItem = viewportEl?.querySelector('.is-active');
|
await nextTick();
|
||||||
activeItem?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
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 {
|
return {
|
||||||
|
handleScrollAt,
|
||||||
initScrollbar,
|
initScrollbar,
|
||||||
|
scrollbarRef,
|
||||||
scrollDirection,
|
scrollDirection,
|
||||||
|
scrollIsAtLeft,
|
||||||
|
scrollIsAtRight,
|
||||||
|
showScrollButton,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben/preferences": "workspace:*",
|
"@vben/preferences": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
"@vben/icons": "workspace:*",
|
"@vben/icons": "workspace:*",
|
||||||
"@vben/locales": "workspace:*",
|
"@vben/locales": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vueuse/integrations": "^10.11.1",
|
"@vueuse/integrations": "^11.0.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
|
@ -27,6 +27,6 @@
|
|||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3",
|
"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/stores": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben-core/shared": "workspace:*",
|
"@vben-core/shared": "workspace:*",
|
||||||
"@vben-core/typings": "workspace:*",
|
"@vben-core/typings": "workspace:*",
|
||||||
"pinia": "2.2.1",
|
"pinia": "2.2.2",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
|
@ -55,7 +55,7 @@ describe('useAccessStore', () => {
|
|||||||
const updatedTab = { ...initialTab, query: { id: '1' } };
|
const updatedTab = { ...initialTab, query: { id: '1' } };
|
||||||
store.addTab(updatedTab);
|
store.addTab(updatedTab);
|
||||||
expect(store.tabs.length).toBe(1);
|
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 () => {
|
it('closes all tabs', async () => {
|
||||||
@ -67,7 +67,7 @@ describe('useAccessStore', () => {
|
|||||||
|
|
||||||
await store.closeAllTabs(router);
|
await store.closeAllTabs(router);
|
||||||
|
|
||||||
expect(store.tabs.length).toBe(0); // 假设没有固定的标签页
|
expect(store.tabs.length).toBe(1); // 假设没有固定的标签页
|
||||||
// expect(router.replace).toHaveBeenCalled();
|
// expect(router.replace).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -124,10 +124,21 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
|||||||
} else {
|
} else {
|
||||||
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
|
// 页面已经存在,不重复添加选项卡,只更新选项卡参数
|
||||||
const currentTab = toRaw(this.tabs)[tabIndex];
|
const currentTab = toRaw(this.tabs)[tabIndex];
|
||||||
const mergedTab = { ...currentTab, ...tab };
|
const mergedTab = {
|
||||||
if (currentTab && Reflect.has(currentTab.meta, 'affixTab')) {
|
...currentTab,
|
||||||
mergedTab.meta.affixTab = currentTab.meta.affixTab;
|
...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.tabs.splice(tabIndex, 1, mergedTab);
|
||||||
}
|
}
|
||||||
this.updateCacheTab();
|
this.updateCacheTab();
|
||||||
@ -136,7 +147,8 @@ export const useTabbarStore = defineStore('core-tabbar', {
|
|||||||
* @zh_CN 关闭所有标签页
|
* @zh_CN 关闭所有标签页
|
||||||
*/
|
*/
|
||||||
async closeAllTabs(router: Router) {
|
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);
|
await this._goToDefaultTab(router);
|
||||||
this.updateCacheTab();
|
this.updateCacheTab();
|
||||||
},
|
},
|
||||||
|
@ -40,10 +40,10 @@
|
|||||||
"@vben/styles": "workspace:*",
|
"@vben/styles": "workspace:*",
|
||||||
"@vben/types": "workspace:*",
|
"@vben/types": "workspace:*",
|
||||||
"@vben/utils": "workspace:*",
|
"@vben/utils": "workspace:*",
|
||||||
"@vueuse/core": "^10.11.1",
|
"@vueuse/core": "^11.0.0",
|
||||||
"ant-design-vue": "^4.2.3",
|
"ant-design-vue": "^4.2.3",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"pinia": "2.2.1",
|
"pinia": "2.2.2",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.4.37",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.4.3"
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
import(
|
import(
|
||||||
'#/views/demos/features/hide-menu-children/children.vue'
|
'#/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",
|
"cac": "^6.7.14",
|
||||||
"circular-dependency-scanner": "^2.2.2",
|
"circular-dependency-scanner": "^2.2.2",
|
||||||
"depcheck": "^1.4.7",
|
"depcheck": "^1.4.7",
|
||||||
"publint": "^0.2.9"
|
"publint": "^0.2.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user