Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin
This commit is contained in:
2
.github/release-drafter.yml
vendored
2
.github/release-drafter.yml
vendored
@@ -42,9 +42,9 @@ version-resolver:
|
|||||||
minor:
|
minor:
|
||||||
labels:
|
labels:
|
||||||
- "minor"
|
- "minor"
|
||||||
- "feature"
|
|
||||||
patch:
|
patch:
|
||||||
labels:
|
labels:
|
||||||
|
- "feature"
|
||||||
- "patch"
|
- "patch"
|
||||||
- "bug"
|
- "bug"
|
||||||
- "maintenance"
|
- "maintenance"
|
||||||
|
@@ -2,6 +2,10 @@ import type { DefaultTheme, HeadConfig } from 'vitepress';
|
|||||||
|
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GitChangelog,
|
||||||
|
GitChangelogMarkdownSection,
|
||||||
|
} from '@nolebase/vitepress-plugin-git-changelog/vite';
|
||||||
import { type PwaOptions, withPwa } from '@vite-pwa/vitepress';
|
import { type PwaOptions, withPwa } from '@vite-pwa/vitepress';
|
||||||
import { defineConfigWithTheme } from 'vitepress';
|
import { defineConfigWithTheme } from 'vitepress';
|
||||||
|
|
||||||
@@ -98,6 +102,12 @@ export default withPwa(
|
|||||||
json: {
|
json: {
|
||||||
stringify: true,
|
stringify: true,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
GitChangelog({
|
||||||
|
repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
|
||||||
|
}),
|
||||||
|
GitChangelogMarkdownSection(),
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
fs: {
|
fs: {
|
||||||
allow: ['../..'],
|
allow: ['../..'],
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// https://vitepress.dev/guide/custom-theme
|
// https://vitepress.dev/guide/custom-theme
|
||||||
import type { Theme } from 'vitepress';
|
import type { Theme } from 'vitepress';
|
||||||
|
|
||||||
|
import { NolebaseGitChangelogPlugin } from '@nolebase/vitepress-plugin-git-changelog/client';
|
||||||
import DefaultTheme from 'vitepress/theme';
|
import DefaultTheme from 'vitepress/theme';
|
||||||
|
|
||||||
import SiteLayout from './components/site-layout.vue';
|
import SiteLayout from './components/site-layout.vue';
|
||||||
@@ -9,11 +10,13 @@ import { initHmPlugin } from './plugins/hm';
|
|||||||
|
|
||||||
import './styles';
|
import './styles';
|
||||||
|
|
||||||
|
import '@nolebase/vitepress-plugin-git-changelog/client/style.css';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
enhanceApp({ app }) {
|
enhanceApp({ app }) {
|
||||||
// ...
|
// ...
|
||||||
app.component('VbenContributors', VbenContributors);
|
app.component('VbenContributors', VbenContributors);
|
||||||
|
app.use(NolebaseGitChangelogPlugin);
|
||||||
// 百度统计
|
// 百度统计
|
||||||
initHmPlugin();
|
initHmPlugin();
|
||||||
},
|
},
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
"medium-zoom": "^1.1.0"
|
"medium-zoom": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nolebase/vitepress-plugin-git-changelog": "^2.4.0",
|
||||||
"@vite-pwa/vitepress": "^0.5.0",
|
"@vite-pwa/vitepress": "^0.5.0",
|
||||||
"vitepress": "^1.3.2",
|
"vitepress": "^1.3.2",
|
||||||
"vue": "^3.4.37"
|
"vue": "^3.4.37"
|
||||||
|
@@ -223,7 +223,7 @@ css 变量内的颜色,必须使用 `hsl` 格式,如 `0 0% 100%`,不需要
|
|||||||
|
|
||||||
你只需要在你的项目中覆盖你想要修改的 CSS 变量即可。例如,要更改默认卡片背景色,你可以在你的 CSS 文件中添加以下内容进行覆盖:
|
你只需要在你的项目中覆盖你想要修改的 CSS 变量即可。例如,要更改默认卡片背景色,你可以在你的 CSS 文件中添加以下内容进行覆盖:
|
||||||
|
|
||||||
### 默认主题下:
|
### 默认主题下
|
||||||
|
|
||||||
```css
|
```css
|
||||||
:root {
|
:root {
|
||||||
@@ -1222,7 +1222,7 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||||||
|
|
||||||
侧边栏颜色通过`--sidebar`变量来配置
|
侧边栏颜色通过`--sidebar`变量来配置
|
||||||
|
|
||||||
### 默认主题下:
|
### 默认主题下
|
||||||
|
|
||||||
```css
|
```css
|
||||||
:root {
|
:root {
|
||||||
@@ -1244,7 +1244,7 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||||||
|
|
||||||
侧边栏颜色通过`--header`变量来配置
|
侧边栏颜色通过`--header`变量来配置
|
||||||
|
|
||||||
### 默认主题下:
|
### 默认主题下
|
||||||
|
|
||||||
```css
|
```css
|
||||||
:root {
|
:root {
|
||||||
|
@@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-config-turbo": "^2.0.12",
|
"eslint-config-turbo": "^2.0.13",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
"eslint-plugin-jsdoc": "^50.2.0",
|
"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.1.0",
|
||||||
|
@@ -55,8 +55,8 @@
|
|||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"postcss-antd-fixes": "^0.2.0",
|
"postcss-antd-fixes": "^0.2.0",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.1",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.10",
|
||||||
"tailwindcss-animate": "^1.0.7"
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@@ -42,7 +42,7 @@
|
|||||||
"@types/html-minifier-terser": "^7.0.2",
|
"@types/html-minifier-terser": "^7.0.2",
|
||||||
"@vben/node-utils": "workspace:*",
|
"@vben/node-utils": "workspace:*",
|
||||||
"@vitejs/plugin-vue": "^5.1.2",
|
"@vitejs/plugin-vue": "^5.1.2",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"rollup": "^4.20.0",
|
"rollup": "^4.20.0",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.0",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"vite-plugin-compression": "^0.5.1",
|
||||||
"vite-plugin-dts": "4.0.2",
|
"vite-plugin-dts": "4.0.3",
|
||||||
"vite-plugin-html": "^3.2.2"
|
"vite-plugin-html": "^3.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ import { viteImportMapPlugin } from './importmap';
|
|||||||
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
|
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
|
||||||
import { viteMetadataPlugin } from './inject-metadata';
|
import { viteMetadataPlugin } from './inject-metadata';
|
||||||
import { viteLicensePlugin } from './license';
|
import { viteLicensePlugin } from './license';
|
||||||
import { viteNitroMockPlugin } from './nitor-mock';
|
import { viteNitroMockPlugin } from './nitro-mock';
|
||||||
import { vitePrintPlugin } from './print';
|
import { vitePrintPlugin } from './print';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -60,7 +60,7 @@
|
|||||||
"@changesets/cli": "^2.27.7",
|
"@changesets/cli": "^2.27.7",
|
||||||
"@ls-lint/ls-lint": "^2.2.3",
|
"@ls-lint/ls-lint": "^2.2.3",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^22.2.0",
|
"@types/node": "^22.3.0",
|
||||||
"@vben/commitlint-config": "workspace:*",
|
"@vben/commitlint-config": "workspace:*",
|
||||||
"@vben/eslint-config": "workspace:*",
|
"@vben/eslint-config": "workspace:*",
|
||||||
"@vben/prettier-config": "workspace:*",
|
"@vben/prettier-config": "workspace:*",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"@vben/vite-config": "workspace:*",
|
"@vben/vite-config": "workspace:*",
|
||||||
"@vben/vsh": "workspace:*",
|
"@vben/vsh": "workspace:*",
|
||||||
"@vitejs/plugin-vue": "^5.1.2",
|
"@vitejs/plugin-vue": "^5.1.2",
|
||||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
"@vitejs/plugin-vue-jsx": "^4.0.1",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
"jsdom": "^24.1.1",
|
"jsdom": "^24.1.1",
|
||||||
"lint-staged": "^15.2.9",
|
"lint-staged": "^15.2.9",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.10",
|
||||||
"turbo": "^2.0.12",
|
"turbo": "^2.0.13",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"unbuild": "^2.0.0",
|
"unbuild": "^2.0.0",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.0",
|
||||||
|
@@ -9,6 +9,7 @@ export {
|
|||||||
Bell,
|
Bell,
|
||||||
BookOpenText,
|
BookOpenText,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Copy,
|
Copy,
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
* @en_US Layout content height
|
* @en_US Layout content height
|
||||||
*/
|
*/
|
||||||
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
|
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
|
||||||
|
export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh_CN 默认命名空间
|
* @zh_CN 默认命名空间
|
||||||
|
@@ -1,140 +1,127 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { getElementVisibleHeight } from './dom'; // 假设函数位于 utils.ts 中
|
import { getElementVisibleRect } from './dom'; // 假设函数位于 utils.ts 中
|
||||||
|
|
||||||
describe('getElementVisibleHeight', () => {
|
describe('getElementVisibleRect', () => {
|
||||||
// Mocking the getBoundingClientRect method
|
// 设置浏览器视口尺寸的 mock
|
||||||
const mockGetBoundingClientRect = vi.fn();
|
beforeEach(() => {
|
||||||
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
vi.spyOn(document.documentElement, 'clientHeight', 'get').mockReturnValue(
|
||||||
|
800,
|
||||||
beforeAll(() => {
|
);
|
||||||
// Mock getBoundingClientRect method
|
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
|
||||||
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect;
|
vi.spyOn(document.documentElement, 'clientWidth', 'get').mockReturnValue(
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
it('should return default rect if element is undefined', () => {
|
||||||
// Restore original getBoundingClientRect method
|
expect(getElementVisibleRect()).toEqual({
|
||||||
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
bottom: 0,
|
||||||
});
|
height: 0,
|
||||||
|
|
||||||
it('should return 0 if the element is null or undefined', () => {
|
|
||||||
expect(getElementVisibleHeight(null)).toBe(0);
|
|
||||||
expect(getElementVisibleHeight()).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the visible height of the element', () => {
|
|
||||||
// Mock the getBoundingClientRect return value
|
|
||||||
mockGetBoundingClientRect.mockReturnValue({
|
|
||||||
bottom: 500,
|
|
||||||
height: 400,
|
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
toJSON: () => ({}),
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default rect if element is null', () => {
|
||||||
|
expect(getElementVisibleRect(null)).toEqual({
|
||||||
|
bottom: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct visible rect when element is fully visible', () => {
|
||||||
|
const element = {
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
bottom: 400,
|
||||||
|
height: 300,
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
width: 400,
|
||||||
|
}),
|
||||||
|
} as HTMLElement;
|
||||||
|
|
||||||
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 400,
|
||||||
|
height: 300,
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
top: 100,
|
top: 100,
|
||||||
width: 0,
|
width: 400,
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockElement = document.createElement('div');
|
|
||||||
document.body.append(mockElement);
|
|
||||||
|
|
||||||
// Mocking window.innerHeight and document.documentElement.clientHeight
|
|
||||||
const originalInnerHeight = window.innerHeight;
|
|
||||||
const originalClientHeight = document.documentElement.clientHeight;
|
|
||||||
|
|
||||||
Object.defineProperty(window, 'innerHeight', {
|
|
||||||
value: 600,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(document.documentElement, 'clientHeight', {
|
|
||||||
value: 600,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getElementVisibleHeight(mockElement)).toBe(400);
|
|
||||||
|
|
||||||
// Restore original values
|
|
||||||
Object.defineProperty(window, 'innerHeight', {
|
|
||||||
value: originalInnerHeight,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(document.documentElement, 'clientHeight', {
|
|
||||||
value: originalClientHeight,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
mockElement.remove();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the visible height when element is partially out of viewport', () => {
|
it('should return correct visible rect when element is partially off-screen at the top', () => {
|
||||||
// Mock the getBoundingClientRect return value
|
const element = {
|
||||||
mockGetBoundingClientRect.mockReturnValue({
|
getBoundingClientRect: () => ({
|
||||||
bottom: 300,
|
bottom: 200,
|
||||||
height: 400,
|
height: 250,
|
||||||
left: 0,
|
left: 100,
|
||||||
right: 0,
|
right: 500,
|
||||||
toJSON: () => ({}),
|
top: -50,
|
||||||
top: -100,
|
width: 400,
|
||||||
width: 0,
|
}),
|
||||||
x: 0,
|
} as HTMLElement;
|
||||||
y: 0,
|
|
||||||
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 200,
|
||||||
|
height: 200,
|
||||||
|
left: 100,
|
||||||
|
right: 500,
|
||||||
|
top: 0,
|
||||||
|
width: 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockElement = document.createElement('div');
|
|
||||||
document.body.append(mockElement);
|
|
||||||
|
|
||||||
// Mocking window.innerHeight and document.documentElement.clientHeight
|
|
||||||
const originalInnerHeight = window.innerHeight;
|
|
||||||
const originalClientHeight = document.documentElement.clientHeight;
|
|
||||||
|
|
||||||
Object.defineProperty(window, 'innerHeight', {
|
|
||||||
value: 600,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(document.documentElement, 'clientHeight', {
|
|
||||||
value: 600,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(getElementVisibleHeight(mockElement)).toBe(300);
|
|
||||||
|
|
||||||
// Restore original values
|
|
||||||
Object.defineProperty(window, 'innerHeight', {
|
|
||||||
value: originalInnerHeight,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.defineProperty(document.documentElement, 'clientHeight', {
|
|
||||||
value: originalClientHeight,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
mockElement.remove();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 0 if the element is completely out of viewport', () => {
|
it('should return correct visible rect when element is partially off-screen at the right', () => {
|
||||||
// Mock the getBoundingClientRect return value
|
const element = {
|
||||||
mockGetBoundingClientRect.mockReturnValue({
|
getBoundingClientRect: () => ({
|
||||||
bottom: -100,
|
bottom: 400,
|
||||||
height: 400,
|
height: 300,
|
||||||
left: 0,
|
left: 800,
|
||||||
right: 0,
|
right: 1200,
|
||||||
toJSON: () => ({}),
|
top: 100,
|
||||||
top: -500,
|
width: 400,
|
||||||
width: 0,
|
}),
|
||||||
x: 0,
|
} as HTMLElement;
|
||||||
y: 0,
|
|
||||||
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 400,
|
||||||
|
height: 300,
|
||||||
|
left: 800,
|
||||||
|
right: 1000,
|
||||||
|
top: 100,
|
||||||
|
width: 200,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const mockElement = document.createElement('div');
|
it('should return all zeros when element is completely off-screen', () => {
|
||||||
document.body.append(mockElement);
|
const element = {
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
bottom: 1200,
|
||||||
|
height: 300,
|
||||||
|
left: 1100,
|
||||||
|
right: 1400,
|
||||||
|
top: 900,
|
||||||
|
width: 300,
|
||||||
|
}),
|
||||||
|
} as HTMLElement;
|
||||||
|
|
||||||
expect(getElementVisibleHeight(mockElement)).toBe(0);
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 800,
|
||||||
mockElement.remove();
|
height: 0,
|
||||||
|
left: 1100,
|
||||||
|
right: 1000,
|
||||||
|
top: 900,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,12 +1,28 @@
|
|||||||
|
export interface VisibleDomRect {
|
||||||
|
bottom: number;
|
||||||
|
height: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取元素可见高度
|
* 获取元素可见信息
|
||||||
* @param element
|
* @param element
|
||||||
*/
|
*/
|
||||||
function getElementVisibleHeight(
|
export function getElementVisibleRect(
|
||||||
element?: HTMLElement | null | undefined,
|
element?: HTMLElement | null | undefined,
|
||||||
): number {
|
): VisibleDomRect {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return 0;
|
return {
|
||||||
|
bottom: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const viewHeight = Math.max(
|
const viewHeight = Math.max(
|
||||||
@@ -17,7 +33,20 @@ function getElementVisibleHeight(
|
|||||||
const top = Math.max(rect.top, 0);
|
const top = Math.max(rect.top, 0);
|
||||||
const bottom = Math.min(rect.bottom, viewHeight);
|
const bottom = Math.min(rect.bottom, viewHeight);
|
||||||
|
|
||||||
return Math.max(0, bottom - top);
|
const viewWidth = Math.max(
|
||||||
}
|
document.documentElement.clientWidth,
|
||||||
|
window.innerWidth,
|
||||||
|
);
|
||||||
|
|
||||||
export { getElementVisibleHeight };
|
const left = Math.max(rect.left, 0);
|
||||||
|
const right = Math.min(rect.right, viewWidth);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bottom,
|
||||||
|
height: Math.max(0, bottom - top),
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
width: Math.max(0, right - left),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
export * from './use-content-height';
|
export * from './use-content-style';
|
||||||
export * from './use-namespace';
|
export * from './use-namespace';
|
||||||
export * from './use-sortable';
|
export * from './use-sortable';
|
||||||
export {
|
export {
|
||||||
|
@@ -1,47 +0,0 @@
|
|||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
|
||||||
getElementVisibleHeight,
|
|
||||||
} from '@vben-core/shared';
|
|
||||||
|
|
||||||
import { useCssVar, useDebounceFn, useWindowSize } from '@vueuse/core';
|
|
||||||
/**
|
|
||||||
* @zh_CN 获取内容高度(可视区域,不包含滚动条)
|
|
||||||
*/
|
|
||||||
function useContentHeight() {
|
|
||||||
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
|
||||||
|
|
||||||
const contentStyles = computed(() => {
|
|
||||||
return {
|
|
||||||
height: `var(${CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT})`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return { contentHeight, contentStyles };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh_CN 创建内容高度监听
|
|
||||||
*/
|
|
||||||
function useContentHeightListener() {
|
|
||||||
const contentElement = ref<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const { height, width } = useWindowSize();
|
|
||||||
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
|
||||||
const debouncedCalcHeight = useDebounceFn(() => {
|
|
||||||
contentHeight.value = `${getElementVisibleHeight(contentElement.value)}px`;
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
watch([height, width], () => {
|
|
||||||
debouncedCalcHeight();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
debouncedCalcHeight();
|
|
||||||
});
|
|
||||||
|
|
||||||
return { contentElement };
|
|
||||||
}
|
|
||||||
|
|
||||||
export { useContentHeight, useContentHeightListener };
|
|
55
packages/@core/composables/src/use-content-style.ts
Normal file
55
packages/@core/composables/src/use-content-style.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
||||||
|
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
|
||||||
|
getElementVisibleRect,
|
||||||
|
type VisibleDomRect,
|
||||||
|
} from '@vben-core/shared';
|
||||||
|
|
||||||
|
import { useCssVar, useDebounceFn } from '@vueuse/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN content style
|
||||||
|
*/
|
||||||
|
function useContentStyle() {
|
||||||
|
const contentElement = ref<HTMLDivElement | null>(null);
|
||||||
|
const visibleDomRect = ref<null | VisibleDomRect>(null);
|
||||||
|
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
||||||
|
const contentWidth = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_WIDTH);
|
||||||
|
|
||||||
|
const overlayStyle = computed((): CSSProperties => {
|
||||||
|
const { height, left, top, width } = visibleDomRect.value ?? {};
|
||||||
|
return {
|
||||||
|
height: `${height}px`,
|
||||||
|
left: `${left}px`,
|
||||||
|
position: 'fixed',
|
||||||
|
top: `${top}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
zIndex: 1000,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const debouncedCalcHeight = useDebounceFn(
|
||||||
|
(_entries: ResizeObserverEntry[]) => {
|
||||||
|
visibleDomRect.value = getElementVisibleRect(contentElement.value);
|
||||||
|
contentHeight.value = `${visibleDomRect.value.height}px`;
|
||||||
|
contentWidth.value = `${visibleDomRect.value.width}px`;
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (contentElement.value) {
|
||||||
|
const observer = new ResizeObserver(debouncedCalcHeight);
|
||||||
|
observer.observe(contentElement.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { contentElement, overlayStyle, visibleDomRect };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useContentStyle };
|
@@ -4,7 +4,7 @@ import type { ContentCompactType } from '@vben-core/typings';
|
|||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { useContentHeightListener } from '@vben-core/composables';
|
import { useContentStyle } from '@vben-core/composables';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +24,7 @@ interface Props {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {});
|
const props = withDefaults(defineProps<Props>(), {});
|
||||||
|
|
||||||
const { contentElement } = useContentHeightListener();
|
const { contentElement, overlayStyle } = useContentStyle();
|
||||||
|
|
||||||
const style = computed((): CSSProperties => {
|
const style = computed((): CSSProperties => {
|
||||||
const {
|
const {
|
||||||
@@ -53,7 +53,9 @@ const style = computed((): CSSProperties => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main ref="contentElement" :style="style" class="bg-background-deep">
|
<main ref="contentElement" :style="style" class="bg-background-deep relative">
|
||||||
|
<!-- <BlurShadow :style="shadowStyle" /> -->
|
||||||
|
<slot :overlay-style="overlayStyle" name="overlay"></slot>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -519,6 +519,10 @@ function handleOpenMenu() {
|
|||||||
class="transition-[margin-top] duration-200"
|
class="transition-[margin-top] duration-200"
|
||||||
>
|
>
|
||||||
<slot name="content"></slot>
|
<slot name="content"></slot>
|
||||||
|
|
||||||
|
<template #overlay="{ overlayStyle }">
|
||||||
|
<slot :overlay-style="overlayStyle" name="content-overlay"></slot>
|
||||||
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
|
|
||||||
<LayoutFooter
|
<LayoutFooter
|
||||||
|
@@ -72,9 +72,10 @@ function scrollIntoView() {
|
|||||||
<template>
|
<template>
|
||||||
<div :style="style" class="tabs-chrome size-full flex-1 overflow-hidden pt-1">
|
<div :style="style" class="tabs-chrome size-full flex-1 overflow-hidden pt-1">
|
||||||
<VbenScrollbar
|
<VbenScrollbar
|
||||||
|
id="tabs-scrollbar"
|
||||||
class="tabs-chrome__scrollbar h-full"
|
class="tabs-chrome__scrollbar h-full"
|
||||||
horizontal
|
horizontal
|
||||||
scroll-bar-class="z-10"
|
scroll-bar-class="z-10 hidden"
|
||||||
>
|
>
|
||||||
<!-- footer -> 4px -->
|
<!-- footer -> 4px -->
|
||||||
<div
|
<div
|
||||||
|
@@ -72,11 +72,12 @@ function scrollIntoView() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex-1 overflow-hidden">
|
<div class="size-full flex-1 overflow-hidden">
|
||||||
<VbenScrollbar
|
<VbenScrollbar
|
||||||
|
id="tabs-scrollbar"
|
||||||
class="tabs-scrollbar h-full"
|
class="tabs-scrollbar h-full"
|
||||||
horizontal
|
horizontal
|
||||||
scroll-bar-class="z-10"
|
scroll-bar-class="z-10 hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:class="contentClass"
|
:class="contentClass"
|
||||||
|
@@ -7,8 +7,10 @@ import type { TabsProps } from './types';
|
|||||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { useForwardPropsEmits, useSortable } from '@vben-core/composables';
|
import { useForwardPropsEmits, useSortable } from '@vben-core/composables';
|
||||||
|
import { ChevronLeft, ChevronRight } from '@vben-core/icons';
|
||||||
|
|
||||||
import { Tabs, TabsChrome } from './components';
|
import { Tabs, TabsChrome } from './components';
|
||||||
|
import { useTabsViewScroll } from './use-tabs-view-scroll';
|
||||||
|
|
||||||
interface Props extends TabsProps {}
|
interface Props extends TabsProps {}
|
||||||
|
|
||||||
@@ -30,6 +32,8 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const forward = useForwardPropsEmits(props, emit);
|
const forward = useForwardPropsEmits(props, emit);
|
||||||
|
|
||||||
|
const { initScrollbar, scrollDirection } = useTabsViewScroll();
|
||||||
|
|
||||||
const sortableInstance = ref<null | Sortable>(null);
|
const sortableInstance = ref<null | Sortable>(null);
|
||||||
|
|
||||||
// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
|
// 可能会找到拖拽的子元素,这里需要确保拖拽的dom时tab元素
|
||||||
@@ -104,13 +108,21 @@ async function initTabsSortable() {
|
|||||||
sortableInstance.value = await initializeSortable();
|
sortableInstance.value = await initializeSortable();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(initTabsSortable);
|
async function init() {
|
||||||
|
await nextTick();
|
||||||
|
initTabsSortable();
|
||||||
|
initScrollbar();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.styleType,
|
() => props.styleType,
|
||||||
() => {
|
() => {
|
||||||
sortableInstance.value?.destroy();
|
sortableInstance.value?.destroy();
|
||||||
initTabsSortable();
|
init();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -120,6 +132,32 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TabsChrome v-if="styleType === 'chrome'" v-bind="forward" />
|
<div
|
||||||
<Tabs v-else v-bind="forward" />
|
:class="{
|
||||||
|
'overflow-hidden': styleType !== 'chrome',
|
||||||
|
}"
|
||||||
|
class="flex h-full flex-1"
|
||||||
|
>
|
||||||
|
<!-- 左侧滚动按钮 -->
|
||||||
|
<span
|
||||||
|
class="hover:bg-muted text-muted-foreground cursor-pointer 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 }" />
|
||||||
|
|
||||||
|
<!-- 左侧滚动按钮 -->
|
||||||
|
<span
|
||||||
|
class="hover:bg-muted text-muted-foreground cursor-pointer border-l px-2"
|
||||||
|
@click="scrollDirection('right')"
|
||||||
|
>
|
||||||
|
<ChevronRight class="size-4 h-full" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
59
packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts
Normal file
59
packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
type El = Element | null | undefined;
|
||||||
|
|
||||||
|
export function useTabsViewScroll(scrollDistance: number = 150) {
|
||||||
|
const scrollbarEl = ref<El>(null);
|
||||||
|
const scrollViewportEl = ref<El>(null);
|
||||||
|
|
||||||
|
function getScrollClientWidth() {
|
||||||
|
if (!scrollbarEl.value || !scrollViewportEl.value) return {};
|
||||||
|
|
||||||
|
const scrollbarWidth = scrollbarEl.value.clientWidth;
|
||||||
|
const scrollViewWidth = scrollViewportEl.value.clientWidth;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollbarWidth,
|
||||||
|
scrollViewWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollDirection(
|
||||||
|
direction: 'left' | 'right',
|
||||||
|
distance: number = scrollDistance,
|
||||||
|
) {
|
||||||
|
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
|
||||||
|
|
||||||
|
if (!scrollbarWidth || !scrollViewWidth) return;
|
||||||
|
|
||||||
|
if (scrollbarWidth > scrollViewWidth) return;
|
||||||
|
|
||||||
|
scrollViewportEl.value?.scrollBy({
|
||||||
|
behavior: 'smooth',
|
||||||
|
left:
|
||||||
|
direction === 'left'
|
||||||
|
? -(scrollbarWidth - distance)
|
||||||
|
: +(scrollbarWidth - distance),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initScrollbar() {
|
||||||
|
await nextTick();
|
||||||
|
const barEl = document.querySelector('#tabs-scrollbar');
|
||||||
|
|
||||||
|
const viewportEl = barEl?.querySelector(
|
||||||
|
'div[data-radix-scroll-area-viewport]',
|
||||||
|
);
|
||||||
|
|
||||||
|
scrollbarEl.value = barEl;
|
||||||
|
scrollViewportEl.value = viewportEl;
|
||||||
|
|
||||||
|
const activeItem = viewportEl?.querySelector('.is-active');
|
||||||
|
activeItem?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initScrollbar,
|
||||||
|
scrollDirection,
|
||||||
|
};
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
import { VbenSpinner } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
|
import { useContentSpinner } from './use-content-spinner';
|
||||||
|
|
||||||
|
defineOptions({ name: 'LayoutContentSpinner' });
|
||||||
|
|
||||||
|
defineProps<{ overlayStyle: CSSProperties }>();
|
||||||
|
|
||||||
|
const { spinning } = useContentSpinner();
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VbenSpinner :spinning="spinning" :style="overlayStyle" />
|
||||||
|
</template>
|
@@ -7,20 +7,15 @@ import type {
|
|||||||
import { type VNode } from 'vue';
|
import { type VNode } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
import { useContentHeight } from '@vben/hooks';
|
|
||||||
import { preferences, usePreferences } from '@vben/preferences';
|
import { preferences, usePreferences } from '@vben/preferences';
|
||||||
import { storeToRefs, useTabbarStore } from '@vben/stores';
|
import { storeToRefs, useTabbarStore } from '@vben/stores';
|
||||||
import { VbenSpinner } from '@vben-core/shadcn-ui';
|
|
||||||
|
|
||||||
import { IFrameRouterView } from '../../iframe';
|
import { IFrameRouterView } from '../../iframe';
|
||||||
import { useContentSpinner } from './use-content-spinner';
|
|
||||||
|
|
||||||
defineOptions({ name: 'LayoutContent' });
|
defineOptions({ name: 'LayoutContent' });
|
||||||
|
|
||||||
const tabbarStore = useTabbarStore();
|
const tabbarStore = useTabbarStore();
|
||||||
const { keepAlive } = usePreferences();
|
const { keepAlive } = usePreferences();
|
||||||
const { spinning } = useContentSpinner();
|
|
||||||
const { contentStyles } = useContentHeight();
|
|
||||||
|
|
||||||
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
|
const { getCachedTabs, getExcludeCachedTabs, renderRouteView } =
|
||||||
storeToRefs(tabbarStore);
|
storeToRefs(tabbarStore);
|
||||||
@@ -86,11 +81,6 @@ function transformComponent(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative h-full">
|
<div class="relative h-full">
|
||||||
<VbenSpinner
|
|
||||||
v-if="preferences.transition.loading"
|
|
||||||
:spinning="spinning"
|
|
||||||
:style="contentStyles"
|
|
||||||
/>
|
|
||||||
<IFrameRouterView />
|
<IFrameRouterView />
|
||||||
<RouterView v-slot="{ Component, route }">
|
<RouterView v-slot="{ Component, route }">
|
||||||
<Transition :name="getTransitionName(route)" appear mode="out-in">
|
<Transition :name="getTransitionName(route)" appear mode="out-in">
|
||||||
|
@@ -1 +1,2 @@
|
|||||||
export { default as LayoutContent } from './content.vue';
|
export { default as LayoutContent } from './content.vue';
|
||||||
|
export { default as LayoutContentSpinner } from './content-spinner.vue';
|
||||||
|
@@ -16,7 +16,7 @@ import { VbenAdminLayout } from '@vben-core/layout-ui';
|
|||||||
import { Toaster, VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
|
import { Toaster, VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { Breadcrumb, CheckUpdates, Preferences } from '../widgets';
|
import { Breadcrumb, CheckUpdates, Preferences } from '../widgets';
|
||||||
import { LayoutContent } from './content';
|
import { LayoutContent, LayoutContentSpinner } from './content';
|
||||||
import { Copyright } from './copyright';
|
import { Copyright } from './copyright';
|
||||||
import { LayoutFooter } from './footer';
|
import { LayoutFooter } from './footer';
|
||||||
import { LayoutHeader } from './header';
|
import { LayoutHeader } from './header';
|
||||||
@@ -297,6 +297,12 @@ const headerSlots = computed(() => {
|
|||||||
<template #content>
|
<template #content>
|
||||||
<LayoutContent />
|
<LayoutContent />
|
||||||
</template>
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="preferences.transition.loading"
|
||||||
|
#content-overlay="{ overlayStyle }"
|
||||||
|
>
|
||||||
|
<LayoutContentSpinner :overlay-style="overlayStyle" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<template v-if="preferences.footer.enable" #footer>
|
<template v-if="preferences.footer.enable" #footer>
|
||||||
|
@@ -0,0 +1,50 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
type El = HTMLElement | null | undefined;
|
||||||
|
|
||||||
|
export function useTabViewScroll(scrollDistance: number = 150) {
|
||||||
|
const scrollbarEl = ref<El>(null);
|
||||||
|
const scrollViewportEl = ref<El>(null);
|
||||||
|
|
||||||
|
function setScrollBarEl(el: El) {
|
||||||
|
scrollbarEl.value = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setScrollViewEl(el: El) {
|
||||||
|
scrollViewportEl.value = el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScrollClientWidth() {
|
||||||
|
if (!scrollbarEl.value || !scrollViewportEl.value) return {};
|
||||||
|
|
||||||
|
const scrollbarWidth = scrollbarEl.value.clientWidth;
|
||||||
|
const scrollViewWidth = scrollViewportEl.value.clientWidth;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollbarWidth,
|
||||||
|
scrollViewWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollDirection(
|
||||||
|
direction: 'left' | 'right',
|
||||||
|
distance: number = scrollDistance,
|
||||||
|
) {
|
||||||
|
const { scrollbarWidth, scrollViewWidth } = getScrollClientWidth();
|
||||||
|
|
||||||
|
if (!scrollbarWidth || !scrollViewWidth) return;
|
||||||
|
|
||||||
|
if (scrollbarWidth > scrollViewWidth) return;
|
||||||
|
|
||||||
|
scrollViewportEl.value?.scrollBy({
|
||||||
|
behavior: 'smooth',
|
||||||
|
left: direction === 'left' ? -distance : +distance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollDirection,
|
||||||
|
setScrollBarEl,
|
||||||
|
setScrollViewEl,
|
||||||
|
};
|
||||||
|
}
|
@@ -478,7 +478,7 @@ function cloneTab(route: TabDefinition): TabDefinition {
|
|||||||
if (!route) {
|
if (!route) {
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
const { matched, ...opt } = route;
|
const { matched, meta, ...opt } = route;
|
||||||
return {
|
return {
|
||||||
...opt,
|
...opt,
|
||||||
matched: (matched
|
matched: (matched
|
||||||
@@ -488,6 +488,10 @@ function cloneTab(route: TabDefinition): TabDefinition {
|
|||||||
path: item.path,
|
path: item.path,
|
||||||
}))
|
}))
|
||||||
: undefined) as RouteRecordNormalized[],
|
: undefined) as RouteRecordNormalized[],
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
newTabTitle: meta.newTabTitle,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ import { ref } from 'vue';
|
|||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { useClipboard } from '@vueuse/core';
|
import { useClipboard } from '@vueuse/core';
|
||||||
import { Button, Input } from 'ant-design-vue';
|
import { Button, Card, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
const source = ref('Hello');
|
const source = ref('Hello');
|
||||||
const { copy, text } = useClipboard({ source });
|
const { copy, text } = useClipboard({ source });
|
||||||
@@ -12,12 +12,14 @@ const { copy, text } = useClipboard({ source });
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page title="剪切板示例">
|
<Page title="剪切板示例">
|
||||||
<p class="mb-3">
|
<Card title="基本使用">
|
||||||
Current copied: <code>{{ text || 'none' }}</code>
|
<p class="mb-3">
|
||||||
</p>
|
Current copied: <code>{{ text || 'none' }}</code>
|
||||||
<Input.Group class="flex">
|
</p>
|
||||||
<Input v-model:value="source" placeholder="请输入" />
|
<div class="flex">
|
||||||
<Button type="primary" @click="copy(source)"> Copy </Button>
|
<Input class="mr-3 flex w-[200px]" />
|
||||||
</Input.Group>
|
<Button type="primary" @click="copy(source)"> Copy </Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -31,7 +31,7 @@ const { isFullscreen: isDomFullscreen, toggle: toggleDom } =
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="mt-3" title="Dom Full Screen">
|
<Card class="mt-5" title="Dom Full Screen">
|
||||||
<Button type="primary" @click="toggleDom"> Enter Dom Full Screen </Button>
|
<Button type="primary" @click="toggleDom"> Enter Dom Full Screen </Button>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
754
pnpm-lock.yaml
generated
754
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user