mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-22 22:16:18 +08:00
feat: add modal and drawer components and examples (#4229)
* feat: add modal component * feat: add drawer component * feat: apply new modal and drawer components to the layout * chore: typo * feat: add some unit tests
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -168,9 +168,10 @@
|
|||||||
|
|
||||||
"i18n-ally.localesPaths": [
|
"i18n-ally.localesPaths": [
|
||||||
"packages/locales/src/langs",
|
"packages/locales/src/langs",
|
||||||
"playground/src/langs",
|
"playground/src/locales/langs",
|
||||||
"apps/*/src/locales/langs"
|
"apps/*/src/locales/langs"
|
||||||
],
|
],
|
||||||
|
"i18n-ally.pathMatcher": "{locale}.json",
|
||||||
"i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"],
|
"i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"],
|
||||||
"i18n-ally.sourceLanguage": "en",
|
"i18n-ally.sourceLanguage": "en",
|
||||||
"i18n-ally.displayLanguage": "zh-CN",
|
"i18n-ally.displayLanguage": "zh-CN",
|
||||||
|
@@ -14,7 +14,7 @@ export interface UserPayload extends UserInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateAccessToken(user: UserInfo) {
|
export function generateAccessToken(user: UserInfo) {
|
||||||
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '1d' });
|
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateRefreshToken(user: UserInfo) {
|
export function generateRefreshToken(user: UserInfo) {
|
||||||
|
@@ -37,6 +37,7 @@
|
|||||||
"astro",
|
"astro",
|
||||||
"ui-kit",
|
"ui-kit",
|
||||||
"styl",
|
"styl",
|
||||||
|
"vnode",
|
||||||
"nocheck",
|
"nocheck",
|
||||||
"prefixs",
|
"prefixs",
|
||||||
"vitepress",
|
"vitepress",
|
||||||
@@ -53,6 +54,9 @@
|
|||||||
"**/*-dist/**",
|
"**/*-dist/**",
|
||||||
"**/icons/**",
|
"**/icons/**",
|
||||||
"pnpm-lock.yaml",
|
"pnpm-lock.yaml",
|
||||||
"**/*.log"
|
"**/*.log",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/__tests__/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { generatorContentHash } from './hash';
|
import { generatorContentHash } from '../hash';
|
||||||
|
|
||||||
describe('generatorContentHash', () => {
|
describe('generatorContentHash', () => {
|
||||||
it('should generate an MD5 hash for the content', () => {
|
it('should generate an MD5 hash for the content', () => {
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { toPosixPath } from './path';
|
import { toPosixPath } from '../path';
|
||||||
|
|
||||||
describe('toPosixPath', () => {
|
describe('toPosixPath', () => {
|
||||||
// 测试 Windows 风格路径到 POSIX 风格路径的转换
|
// 测试 Windows 风格路径到 POSIX 风格路径的转换
|
@@ -34,13 +34,6 @@
|
|||||||
transition: all 0.6s ease-out;
|
transition: all 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading .dots {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 98px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading .title {
|
.loading .title {
|
||||||
margin-top: 36px;
|
margin-top: 36px;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
@@ -109,6 +102,6 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="loading" id="__app-loading__">
|
<div class="loading" id="__app-loading__">
|
||||||
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
|
<span class="dot"><i></i><i></i><i></i><i></i></span>
|
||||||
<div class="title"><%= VITE_APP_TITLE %></div>
|
<div class="title"><%= VITE_APP_TITLE %></div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -20,12 +20,14 @@ export {
|
|||||||
CornerDownLeft,
|
CornerDownLeft,
|
||||||
Disc as IconDefault,
|
Disc as IconDefault,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
|
Expand,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
FoldHorizontal,
|
FoldHorizontal,
|
||||||
Fullscreen,
|
Fullscreen,
|
||||||
Github,
|
Github,
|
||||||
|
Info,
|
||||||
InspectionPanel,
|
InspectionPanel,
|
||||||
Languages,
|
Languages,
|
||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
@@ -46,6 +48,7 @@ export {
|
|||||||
Search,
|
Search,
|
||||||
SearchX,
|
SearchX,
|
||||||
Settings,
|
Settings,
|
||||||
|
Shrink,
|
||||||
Sun,
|
Sun,
|
||||||
SunMoon,
|
SunMoon,
|
||||||
SwatchBook,
|
SwatchBook,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { StorageManager } from './storage-manager';
|
import { StorageManager } from '../storage-manager';
|
||||||
|
|
||||||
describe('storageManager', () => {
|
describe('storageManager', () => {
|
||||||
let storageManager: StorageManager;
|
let storageManager: StorageManager;
|
@@ -5,7 +5,7 @@ import {
|
|||||||
convertToHslCssVar,
|
convertToHslCssVar,
|
||||||
convertToRgb,
|
convertToRgb,
|
||||||
isValidColor,
|
isValidColor,
|
||||||
} from './convert';
|
} from '../convert';
|
||||||
|
|
||||||
describe('color conversion functions', () => {
|
describe('color conversion functions', () => {
|
||||||
it('should correctly convert color to HSL format', () => {
|
it('should correctly convert color to HSL format', () => {
|
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { diff } from './diff';
|
import { diff } from '../diff';
|
||||||
|
|
||||||
describe('diff function', () => {
|
describe('diff function', () => {
|
||||||
it('should return an empty object when comparing identical objects', () => {
|
it('should return an empty object when comparing identical objects', () => {
|
@@ -1,6 +1,6 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { getElementVisibleRect } from './dom'; // 假设函数位于 utils.ts 中
|
import { getElementVisibleRect } from '../dom'; // 假设函数位于 utils.ts 中
|
||||||
|
|
||||||
describe('getElementVisibleRect', () => {
|
describe('getElementVisibleRect', () => {
|
||||||
// 设置浏览器视口尺寸的 mock
|
// 设置浏览器视口尺寸的 mock
|
@@ -1,12 +1,13 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getFirstNonNullOrUndefined,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
isHttpUrl,
|
isHttpUrl,
|
||||||
isObject,
|
isObject,
|
||||||
isUndefined,
|
isUndefined,
|
||||||
isWindow,
|
isWindow,
|
||||||
} from './inference';
|
} from '../inference';
|
||||||
|
|
||||||
describe('isHttpUrl', () => {
|
describe('isHttpUrl', () => {
|
||||||
it("should return true when given 'http://example.com'", () => {
|
it("should return true when given 'http://example.com'", () => {
|
||||||
@@ -103,7 +104,6 @@ describe('isObject', () => {
|
|||||||
|
|
||||||
it('should return false for non-objects', () => {
|
it('should return false for non-objects', () => {
|
||||||
expect(isObject(null)).toBe(false);
|
expect(isObject(null)).toBe(false);
|
||||||
expect(isObject()).toBe(false);
|
|
||||||
expect(isObject(42)).toBe(false);
|
expect(isObject(42)).toBe(false);
|
||||||
expect(isObject('string')).toBe(false);
|
expect(isObject('string')).toBe(false);
|
||||||
expect(isObject(true)).toBe(false);
|
expect(isObject(true)).toBe(false);
|
||||||
@@ -112,3 +112,56 @@ describe('isObject', () => {
|
|||||||
expect(isObject(/regex/)).toBe(true);
|
expect(isObject(/regex/)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getFirstNonNullOrUndefined', () => {
|
||||||
|
describe('getFirstNonNullOrUndefined', () => {
|
||||||
|
it('should return the first non-null and non-undefined value for a number array', () => {
|
||||||
|
expect(getFirstNonNullOrUndefined<number>(undefined, null, 0, 42)).toBe(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(getFirstNonNullOrUndefined<number>(null, undefined, 42, 123)).toBe(
|
||||||
|
42,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the first non-null and non-undefined value for a string array', () => {
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<string>(undefined, null, '', 'hello'),
|
||||||
|
).toBe('');
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<string>(null, undefined, 'test', 'world'),
|
||||||
|
).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if all values are null or undefined', () => {
|
||||||
|
expect(getFirstNonNullOrUndefined(undefined, null)).toBeUndefined();
|
||||||
|
expect(getFirstNonNullOrUndefined(null)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with a single value', () => {
|
||||||
|
expect(getFirstNonNullOrUndefined(42)).toBe(42);
|
||||||
|
expect(getFirstNonNullOrUndefined()).toBeUndefined();
|
||||||
|
expect(getFirstNonNullOrUndefined(null)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed types correctly', () => {
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<number | object | string>(
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
'test',
|
||||||
|
123,
|
||||||
|
{ key: 'value' },
|
||||||
|
),
|
||||||
|
).toBe('test');
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<number | object | string>(
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
[1, 2, 3],
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -2,9 +2,10 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
|
kebabToCamelCase,
|
||||||
toCamelCase,
|
toCamelCase,
|
||||||
toLowerCaseFirstLetter,
|
toLowerCaseFirstLetter,
|
||||||
} from './letter';
|
} from '../letter';
|
||||||
|
|
||||||
// 编写测试用例
|
// 编写测试用例
|
||||||
describe('capitalizeFirstLetter', () => {
|
describe('capitalizeFirstLetter', () => {
|
||||||
@@ -76,3 +77,41 @@ describe('toCamelCase', () => {
|
|||||||
expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');
|
expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('kebabToCamelCase', () => {
|
||||||
|
it('should convert kebab-case to camelCase correctly', () => {
|
||||||
|
expect(kebabToCamelCase('my-component-name')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple consecutive hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('my--component--name')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim leading and trailing hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('-my-component-name-')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve the case of the first word', () => {
|
||||||
|
expect(kebabToCamelCase('My-component-name')).toBe('MyComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert a single word correctly', () => {
|
||||||
|
expect(kebabToCamelCase('component')).toBe('component');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty string if input is empty', () => {
|
||||||
|
expect(kebabToCamelCase('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with no hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('mycomponentname')).toBe('mycomponentname');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with only hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('---')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed case inputs', () => {
|
||||||
|
expect(kebabToCamelCase('my-Component-Name')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
});
|
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { filterTree, mapTree, traverseTreeValues } from './tree';
|
import { filterTree, mapTree, traverseTreeValues } from '../tree';
|
||||||
|
|
||||||
describe('traverseTreeValues', () => {
|
describe('traverseTreeValues', () => {
|
||||||
interface Node {
|
interface Node {
|
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { uniqueByField } from './unique';
|
import { uniqueByField } from '../unique';
|
||||||
|
|
||||||
describe('uniqueByField', () => {
|
describe('uniqueByField', () => {
|
||||||
it('should return an array with unique items based on id field', () => {
|
it('should return an array with unique items based on id field', () => {
|
@@ -1,6 +1,6 @@
|
|||||||
import { expect, it } from 'vitest';
|
import { expect, it } from 'vitest';
|
||||||
|
|
||||||
import { updateCSSVariables } from './update-css-variables';
|
import { updateCSSVariables } from '../update-css-variables';
|
||||||
|
|
||||||
it('updateCSSVariables should update CSS variables in :root selector', () => {
|
it('updateCSSVariables should update CSS variables in :root selector', () => {
|
||||||
// 模拟初始的内联样式表内容
|
// 模拟初始的内联样式表内容
|
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { openWindow } from './window'; // 假设你的函数在 'openWindow' 文件中
|
import { openWindow } from '../window'; // 假设你的函数在 'openWindow' 文件中
|
||||||
|
|
||||||
describe('openWindow', () => {
|
describe('openWindow', () => {
|
||||||
// 保存原始的 window.open 函数
|
// 保存原始的 window.open 函数
|
@@ -24,7 +24,7 @@ function isUndefined(value?: unknown): value is undefined {
|
|||||||
* @param {T} value 要检查的值。
|
* @param {T} value 要检查的值。
|
||||||
* @returns {boolean} 如果值为空,返回true,否则返回false。
|
* @returns {boolean} 如果值为空,返回true,否则返回false。
|
||||||
*/
|
*/
|
||||||
function isEmpty<T = unknown>(value: T): value is T {
|
function isEmpty<T = unknown>(value?: T): value is T {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,42 @@ function isNumber(value: any): value is number {
|
|||||||
return typeof value === 'number' && Number.isFinite(value);
|
return typeof value === 'number' && Number.isFinite(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first value in the provided list that is neither `null` nor `undefined`.
|
||||||
|
*
|
||||||
|
* This function iterates over the input values and returns the first one that is
|
||||||
|
* not strictly equal to `null` or `undefined`. If all values are either `null` or
|
||||||
|
* `undefined`, it returns `undefined`.
|
||||||
|
*
|
||||||
|
* @template T - The type of the input values.
|
||||||
|
* @param {...(T | null | undefined)[]} values - A list of values to evaluate.
|
||||||
|
* @returns {T | undefined} - The first value that is not `null` or `undefined`, or `undefined` if none are found.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Returns 42 because it is the first non-null, non-undefined value.
|
||||||
|
* getFirstNonNullOrUndefined(undefined, null, 42, 'hello'); // 42
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Returns 'hello' because it is the first non-null, non-undefined value.
|
||||||
|
* getFirstNonNullOrUndefined(null, undefined, 'hello', 123); // 'hello'
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Returns undefined because all values are either null or undefined.
|
||||||
|
* getFirstNonNullOrUndefined(undefined, null); // undefined
|
||||||
|
*/
|
||||||
|
function getFirstNonNullOrUndefined<T>(
|
||||||
|
...values: (null | T | undefined)[]
|
||||||
|
): T | undefined {
|
||||||
|
for (const value of values) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
getFirstNonNullOrUndefined,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
isFunction,
|
isFunction,
|
||||||
isHttpUrl,
|
isHttpUrl,
|
||||||
|
@@ -29,4 +29,19 @@ function toCamelCase(key: string, parentKey: string): string {
|
|||||||
return parentKey + key.charAt(0).toUpperCase() + key.slice(1);
|
return parentKey + key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { capitalizeFirstLetter, toCamelCase, toLowerCaseFirstLetter };
|
function kebabToCamelCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.split('-')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((word, index) =>
|
||||||
|
index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
kebabToCamelCase,
|
||||||
|
toCamelCase,
|
||||||
|
toLowerCaseFirstLetter,
|
||||||
|
};
|
||||||
|
@@ -2,7 +2,7 @@ import type { SortableOptions } from 'sortablejs';
|
|||||||
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { useSortable } from './use-sortable';
|
import { useSortable } from '../use-sortable';
|
||||||
|
|
||||||
describe('useSortable', () => {
|
describe('useSortable', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -30,7 +30,6 @@ describe('useSortable', () => {
|
|||||||
|
|
||||||
// Import sortablejs to access the mocked create function
|
// Import sortablejs to access the mocked create function
|
||||||
const Sortable = await import(
|
const Sortable = await import(
|
||||||
// @ts-expect-error - This is a dynamic import
|
|
||||||
'sortablejs/modular/sortable.complete.esm.js'
|
'sortablejs/modular/sortable.complete.esm.js'
|
||||||
);
|
);
|
||||||
|
|
@@ -1,5 +1,6 @@
|
|||||||
export * from './use-content-style';
|
export * from './use-content-style';
|
||||||
export * from './use-namespace';
|
export * from './use-namespace';
|
||||||
|
export * from './use-priority-value';
|
||||||
export * from './use-sortable';
|
export * from './use-sortable';
|
||||||
export {
|
export {
|
||||||
useEmitAsProps,
|
useEmitAsProps,
|
||||||
|
47
packages/@core/composables/src/use-priority-value.ts
Normal file
47
packages/@core/composables/src/use-priority-value.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed, getCurrentInstance, useAttrs, useSlots } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFirstNonNullOrUndefined,
|
||||||
|
kebabToCamelCase,
|
||||||
|
} from '@vben-core/shared';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 依次从插槽、attrs、props、state 中获取值
|
||||||
|
* @param key
|
||||||
|
* @param props
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
export function usePriorityValue<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
S extends Record<string, any>,
|
||||||
|
K extends keyof T = keyof T,
|
||||||
|
>(key: K, props: T, state: Readonly<Ref<NoInfer<S>>> | undefined) {
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const slots = useSlots();
|
||||||
|
const attrs = useAttrs() as T;
|
||||||
|
|
||||||
|
const value = computed((): T[K] => {
|
||||||
|
// props不管有没有传,都会有默认值,会影响这里的顺序,
|
||||||
|
// 通过判断原始props是否有值来判断是否传入
|
||||||
|
const rawProps = (instance?.vnode?.props || {}) as T;
|
||||||
|
|
||||||
|
const standardRwaProps = {} as T;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rawProps)) {
|
||||||
|
standardRwaProps[kebabToCamelCase(key) as K] = value;
|
||||||
|
}
|
||||||
|
const propsKey =
|
||||||
|
standardRwaProps?.[key] === undefined ? undefined : props[key];
|
||||||
|
|
||||||
|
// slot可以关闭
|
||||||
|
return getFirstNonNullOrUndefined(
|
||||||
|
slots[key as string],
|
||||||
|
attrs[key],
|
||||||
|
propsKey,
|
||||||
|
state?.value?.[key as keyof S],
|
||||||
|
) as T[K];
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
@@ -149,14 +149,6 @@ function usePreferences() {
|
|||||||
return enable && globalLockScreen;
|
return enable && globalLockScreen;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @zh_CN 是否启用全局偏好设置快捷键
|
|
||||||
*/
|
|
||||||
const globalPreferencesShortcutKey = computed(() => {
|
|
||||||
const { enable, globalPreferences } = shortcutKeysPreferences.value;
|
|
||||||
return enable && globalPreferences;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authPanelCenter,
|
authPanelCenter,
|
||||||
authPanelLeft,
|
authPanelLeft,
|
||||||
@@ -165,7 +157,6 @@ function usePreferences() {
|
|||||||
diffPreference,
|
diffPreference,
|
||||||
globalLockScreenShortcutKey,
|
globalLockScreenShortcutKey,
|
||||||
globalLogoutShortcutKey,
|
globalLogoutShortcutKey,
|
||||||
globalPreferencesShortcutKey,
|
|
||||||
globalSearchShortcutKey,
|
globalSearchShortcutKey,
|
||||||
isDark,
|
isDark,
|
||||||
isFullContent,
|
isFullContent,
|
||||||
|
21
packages/@core/ui-kit/popup-ui/build.config.ts
Normal file
21
packages/@core/ui-kit/popup-ui/build.config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
builder: 'mkdist',
|
||||||
|
input: './src',
|
||||||
|
loaders: ['vue'],
|
||||||
|
pattern: ['**/*.vue'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
builder: 'mkdist',
|
||||||
|
format: 'esm',
|
||||||
|
input: './src',
|
||||||
|
loaders: ['js'],
|
||||||
|
pattern: ['**/*.ts'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
47
packages/@core/ui-kit/popup-ui/package.json
Normal file
47
packages/@core/ui-kit/popup-ui/package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/popup-ui",
|
||||||
|
"version": "5.1.1",
|
||||||
|
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||||
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||||
|
"directory": "packages/@vben-core/uikit/popup-ui"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm unbuild",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"**/*.css"
|
||||||
|
],
|
||||||
|
"main": "./dist/index.mjs",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vben-core/composables": "workspace:*",
|
||||||
|
"@vben-core/icons": "workspace:*",
|
||||||
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
|
"@vben-core/shared": "workspace:*",
|
||||||
|
"@vueuse/core": "^11.0.1",
|
||||||
|
"vue": "^3.4.38"
|
||||||
|
}
|
||||||
|
}
|
1
packages/@core/ui-kit/popup-ui/postcss.config.mjs
Normal file
1
packages/@core/ui-kit/popup-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config/postcss';
|
@@ -0,0 +1,113 @@
|
|||||||
|
import type { DrawerState } from '../drawer';
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { DrawerApi } from '../drawer-api';
|
||||||
|
|
||||||
|
// 模拟 Store 类
|
||||||
|
vi.mock('@vben-core/shared', () => {
|
||||||
|
return {
|
||||||
|
isFunction: (fn: any) => typeof fn === 'function',
|
||||||
|
Store: class {
|
||||||
|
private _state: DrawerState;
|
||||||
|
private options: any;
|
||||||
|
|
||||||
|
constructor(initialState: DrawerState, options: any) {
|
||||||
|
this._state = initialState;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch(cb: () => void) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(fn: (prev: DrawerState) => DrawerState) {
|
||||||
|
this._state = fn(this._state);
|
||||||
|
this.options.onUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('drawerApi', () => {
|
||||||
|
let drawerApi: DrawerApi;
|
||||||
|
let drawerState: DrawerState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
drawerApi = new DrawerApi();
|
||||||
|
drawerState = drawerApi.store.state;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize with default state', () => {
|
||||||
|
expect(drawerState.isOpen).toBe(false);
|
||||||
|
expect(drawerState.cancelText).toBe('取消');
|
||||||
|
expect(drawerState.confirmText).toBe('确定');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the drawer', () => {
|
||||||
|
drawerApi.open();
|
||||||
|
expect(drawerApi.store.state.isOpen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the drawer if onBeforeClose allows it', () => {
|
||||||
|
drawerApi.open();
|
||||||
|
drawerApi.close();
|
||||||
|
expect(drawerApi.store.state.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not close the drawer if onBeforeClose returns false', () => {
|
||||||
|
const onBeforeClose = vi.fn(() => false);
|
||||||
|
const drawerApiWithHook = new DrawerApi({ onBeforeClose });
|
||||||
|
drawerApiWithHook.open();
|
||||||
|
drawerApiWithHook.close();
|
||||||
|
expect(drawerApiWithHook.store.state.isOpen).toBe(true);
|
||||||
|
expect(onBeforeClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onCancel and keep drawer open if onCancel is provided', () => {
|
||||||
|
const onCancel = vi.fn();
|
||||||
|
const drawerApiWithHook = new DrawerApi({ onCancel });
|
||||||
|
drawerApiWithHook.open();
|
||||||
|
drawerApiWithHook.onCancel();
|
||||||
|
expect(onCancel).toHaveBeenCalled();
|
||||||
|
expect(drawerApiWithHook.store.state.isOpen).toBe(true); // 关闭逻辑不在 onCancel 内
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update shared data correctly', () => {
|
||||||
|
const testData = { key: 'value' };
|
||||||
|
drawerApi.setData(testData);
|
||||||
|
expect(drawerApi.getData()).toEqual(testData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set state correctly using an object', () => {
|
||||||
|
drawerApi.setState({ title: 'New Title' });
|
||||||
|
expect(drawerApi.store.state.title).toBe('New Title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set state correctly using a function', () => {
|
||||||
|
drawerApi.setState((prev) => ({ ...prev, confirmText: 'Yes' }));
|
||||||
|
expect(drawerApi.store.state.confirmText).toBe('Yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onOpenChange when state changes', () => {
|
||||||
|
const onOpenChange = vi.fn();
|
||||||
|
const drawerApiWithHook = new DrawerApi({ onOpenChange });
|
||||||
|
drawerApiWithHook.open();
|
||||||
|
expect(onOpenChange).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should batch state updates', () => {
|
||||||
|
const batchSpy = vi.spyOn(drawerApi.store, 'batch');
|
||||||
|
drawerApi.batchStore(() => {
|
||||||
|
drawerApi.setState({ title: 'Batch Title' });
|
||||||
|
drawerApi.setState({ confirmText: 'Batch Confirm' });
|
||||||
|
});
|
||||||
|
expect(batchSpy).toHaveBeenCalled();
|
||||||
|
expect(drawerApi.store.state.title).toBe('Batch Title');
|
||||||
|
expect(drawerApi.store.state.confirmText).toBe('Batch Confirm');
|
||||||
|
});
|
||||||
|
});
|
123
packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
Normal file
123
packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { DrawerApiOptions, DrawerState } from './drawer';
|
||||||
|
|
||||||
|
import { isFunction, Store } from '@vben-core/shared';
|
||||||
|
|
||||||
|
export class DrawerApi {
|
||||||
|
private api: Pick<
|
||||||
|
DrawerApiOptions,
|
||||||
|
'onBeforeClose' | 'onCancel' | 'onConfirm' | 'onOpenChange'
|
||||||
|
>;
|
||||||
|
// private prevState!: DrawerState;
|
||||||
|
private state!: DrawerState;
|
||||||
|
|
||||||
|
// 共享数据
|
||||||
|
public sharedData: Record<'payload', any> = {
|
||||||
|
payload: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
public store: Store<DrawerState>;
|
||||||
|
|
||||||
|
constructor(options: DrawerApiOptions = {}) {
|
||||||
|
const {
|
||||||
|
connectedComponent: _,
|
||||||
|
onBeforeClose,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
onOpenChange,
|
||||||
|
...storeState
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const defaultState: DrawerState = {
|
||||||
|
cancelText: '取消',
|
||||||
|
closable: true,
|
||||||
|
confirmLoading: false,
|
||||||
|
confirmText: '确定',
|
||||||
|
footer: true,
|
||||||
|
isOpen: false,
|
||||||
|
loading: false,
|
||||||
|
modal: true,
|
||||||
|
sharedData: {},
|
||||||
|
title: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.store = new Store<DrawerState>(
|
||||||
|
{
|
||||||
|
...defaultState,
|
||||||
|
...storeState,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onUpdate: () => {
|
||||||
|
const state = this.store.state;
|
||||||
|
if (state?.isOpen === this.state?.isOpen) {
|
||||||
|
this.state = state;
|
||||||
|
} else {
|
||||||
|
this.state = state;
|
||||||
|
this.api.onOpenChange?.(!!state?.isOpen);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.api = {
|
||||||
|
onBeforeClose,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
onOpenChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要多次更新状态,可以使用 batch 方法
|
||||||
|
batchStore(cb: () => void) {
|
||||||
|
this.store.batch(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭弹窗
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
// 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗
|
||||||
|
// 如果 onBeforeClose 返回 false,则不关闭弹窗
|
||||||
|
const allowClose = this.api.onBeforeClose?.() ?? true;
|
||||||
|
if (allowClose) {
|
||||||
|
this.store.setState((prev) => ({ ...prev, isOpen: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getData<T extends object = Record<string, any>>() {
|
||||||
|
return (this.sharedData?.payload ?? {}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消操作
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
this.api.onCancel?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认操作
|
||||||
|
*/
|
||||||
|
onConfirm() {
|
||||||
|
this.api.onConfirm?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.store.setState((prev) => ({ ...prev, isOpen: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setData<T>(payload: T) {
|
||||||
|
this.sharedData.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(
|
||||||
|
stateOrFn:
|
||||||
|
| ((prev: DrawerState) => Partial<DrawerState>)
|
||||||
|
| Partial<DrawerState>,
|
||||||
|
) {
|
||||||
|
if (isFunction(stateOrFn)) {
|
||||||
|
this.store.setState(stateOrFn);
|
||||||
|
} else {
|
||||||
|
this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
93
packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
Normal file
93
packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import type { DrawerApi } from './drawer-api';
|
||||||
|
|
||||||
|
import type { Component, Ref } from 'vue';
|
||||||
|
|
||||||
|
export interface DrawerProps {
|
||||||
|
/**
|
||||||
|
* 取消按钮文字
|
||||||
|
*/
|
||||||
|
cancelText?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示右上角的关闭按钮
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
closable?: boolean;
|
||||||
|
/**
|
||||||
|
* 确定按钮 loading
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
/**
|
||||||
|
* 确定按钮文字
|
||||||
|
*/
|
||||||
|
confirmText?: string;
|
||||||
|
/**
|
||||||
|
* 弹窗描述
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* 是否显示底部
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
footer?: boolean;
|
||||||
|
/**
|
||||||
|
* 弹窗是否显示
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
loading?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否显示遮罩
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
modal?: boolean;
|
||||||
|
/**
|
||||||
|
* 弹窗标题
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* 弹窗标题提示
|
||||||
|
*/
|
||||||
|
titleTooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DrawerState extends DrawerProps {
|
||||||
|
/** 弹窗打开状态 */
|
||||||
|
isOpen?: boolean;
|
||||||
|
/**
|
||||||
|
* 共享数据
|
||||||
|
*/
|
||||||
|
sharedData?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendedDrawerApi = {
|
||||||
|
useStore: <T = NoInfer<DrawerState>>(
|
||||||
|
selector?: (state: NoInfer<DrawerState>) => T,
|
||||||
|
) => Readonly<Ref<T>>;
|
||||||
|
} & DrawerApi;
|
||||||
|
|
||||||
|
export interface DrawerApiOptions extends DrawerState {
|
||||||
|
/**
|
||||||
|
* 独立的弹窗组件
|
||||||
|
*/
|
||||||
|
connectedComponent?: Component;
|
||||||
|
/**
|
||||||
|
* 关闭前的回调,返回 false 可以阻止关闭
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onBeforeClose?: () => void;
|
||||||
|
/**
|
||||||
|
* 点击取消按钮的回调
|
||||||
|
*/
|
||||||
|
onCancel?: () => void;
|
||||||
|
/**
|
||||||
|
* 点击确定按钮的回调
|
||||||
|
*/
|
||||||
|
onConfirm?: () => void;
|
||||||
|
/**
|
||||||
|
* 弹窗状态变化回调
|
||||||
|
* @param isOpen
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
}
|
141
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
Normal file
141
packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DrawerProps, ExtendedDrawerApi } from './drawer';
|
||||||
|
|
||||||
|
import { usePriorityValue } from '@vben-core/composables';
|
||||||
|
import { Info, X } from '@vben-core/icons';
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
VbenButton,
|
||||||
|
VbenIconButton,
|
||||||
|
VbenLoading,
|
||||||
|
VbenTooltip,
|
||||||
|
VisuallyHidden,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
import { cn } from '@vben-core/shared';
|
||||||
|
|
||||||
|
interface Props extends DrawerProps {
|
||||||
|
class?: string;
|
||||||
|
contentClass?: string;
|
||||||
|
drawerApi?: ExtendedDrawerApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
class: '',
|
||||||
|
contentClass: '',
|
||||||
|
drawerApi: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = props.drawerApi?.useStore?.();
|
||||||
|
|
||||||
|
const title = usePriorityValue('title', props, state);
|
||||||
|
const description = usePriorityValue('description', props, state);
|
||||||
|
const titleTooltip = usePriorityValue('titleTooltip', props, state);
|
||||||
|
const showFooter = usePriorityValue('footer', props, state);
|
||||||
|
const showLoading = usePriorityValue('loading', props, state);
|
||||||
|
const closable = usePriorityValue('closable', props, state);
|
||||||
|
const modal = usePriorityValue('modal', props, state);
|
||||||
|
const confirmLoading = usePriorityValue('confirmLoading', props, state);
|
||||||
|
const cancelText = usePriorityValue('cancelText', props, state);
|
||||||
|
const confirmText = usePriorityValue('confirmText', props, state);
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Sheet
|
||||||
|
:modal="modal"
|
||||||
|
:open="state?.isOpen"
|
||||||
|
@update:open="() => drawerApi?.close()"
|
||||||
|
>
|
||||||
|
<SheetContent :class="cn('flex w-[520px] flex-col', props.class, {})">
|
||||||
|
<SheetHeader
|
||||||
|
:class="
|
||||||
|
cn('!flex flex-row items-center justify-between border-b px-6 py-5', {
|
||||||
|
'px-4 py-3': closable,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<SheetTitle v-if="title">
|
||||||
|
<slot name="title">
|
||||||
|
{{ title }}
|
||||||
|
|
||||||
|
<VbenTooltip v-if="titleTooltip" side="right">
|
||||||
|
<template #trigger>
|
||||||
|
<Info class="inline-flex size-5 cursor-pointer pb-1" />
|
||||||
|
</template>
|
||||||
|
{{ titleTooltip }}
|
||||||
|
</VbenTooltip>
|
||||||
|
</slot>
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription v-if="description" class="mt-1 text-xs">
|
||||||
|
<slot name="description">
|
||||||
|
{{ description }}
|
||||||
|
</slot>
|
||||||
|
</SheetDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VisuallyHidden v-if="!title || !description">
|
||||||
|
<SheetTitle v-if="!title" />
|
||||||
|
<SheetDescription v-if="!description" />
|
||||||
|
</VisuallyHidden>
|
||||||
|
|
||||||
|
<div class="flex-center">
|
||||||
|
<slot name="extra"></slot>
|
||||||
|
<SheetClose
|
||||||
|
v-if="closable"
|
||||||
|
as-child
|
||||||
|
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<VbenIconButton>
|
||||||
|
<X class="size-4" />
|
||||||
|
</VbenIconButton>
|
||||||
|
</SheetClose>
|
||||||
|
</div>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('relative flex-1 p-3', contentClass, {
|
||||||
|
'overflow-y-auto': !showLoading,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<VbenLoading v-if="showLoading" class="size-full" spinning />
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SheetFooter
|
||||||
|
v-if="showFooter"
|
||||||
|
class="w-full items-center border-t p-2 px-3"
|
||||||
|
>
|
||||||
|
<slot name="prepend-footer"></slot>
|
||||||
|
<slot name="footer">
|
||||||
|
<VbenButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="() => drawerApi?.onCancel()"
|
||||||
|
>
|
||||||
|
<slot name="cancelText">
|
||||||
|
{{ cancelText }}
|
||||||
|
</slot>
|
||||||
|
</VbenButton>
|
||||||
|
<VbenButton
|
||||||
|
:loading="confirmLoading"
|
||||||
|
size="sm"
|
||||||
|
@click="() => drawerApi?.onConfirm()"
|
||||||
|
>
|
||||||
|
<slot name="confirmText">
|
||||||
|
{{ confirmText }}
|
||||||
|
</slot>
|
||||||
|
</VbenButton>
|
||||||
|
</slot>
|
||||||
|
<slot name="append-footer"></slot>
|
||||||
|
</SheetFooter>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</template>
|
3
packages/@core/ui-kit/popup-ui/src/drawer/index.ts
Normal file
3
packages/@core/ui-kit/popup-ui/src/drawer/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type * from './drawer';
|
||||||
|
export { default as VbenDrawer } from './drawer.vue';
|
||||||
|
export { useVbenDrawer } from './use-drawer';
|
105
packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts
Normal file
105
packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import type {
|
||||||
|
DrawerApiOptions,
|
||||||
|
DrawerProps,
|
||||||
|
ExtendedDrawerApi,
|
||||||
|
} from './drawer';
|
||||||
|
|
||||||
|
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
||||||
|
|
||||||
|
import { useStore } from '@vben-core/shared';
|
||||||
|
|
||||||
|
import VbenDrawer from './drawer.vue';
|
||||||
|
import { DrawerApi } from './drawer-api';
|
||||||
|
|
||||||
|
const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
|
||||||
|
|
||||||
|
export function useVbenDrawer<
|
||||||
|
TParentDrawerProps extends DrawerProps = DrawerProps,
|
||||||
|
>(options: DrawerApiOptions = {}) {
|
||||||
|
// Drawer一般会抽离出来,所以如果有传入 connectedComponent,则表示为外部调用,与内部组件进行连接
|
||||||
|
// 外部的Drawer通过provide/inject传递api
|
||||||
|
|
||||||
|
const { connectedComponent } = options;
|
||||||
|
if (connectedComponent) {
|
||||||
|
const extendedApi = reactive({});
|
||||||
|
const Drawer = defineComponent(
|
||||||
|
(props: TParentDrawerProps, { attrs, slots }) => {
|
||||||
|
provide(USER_DRAWER_INJECT_KEY, {
|
||||||
|
extendApi(api: ExtendedDrawerApi) {
|
||||||
|
// 不能直接给 reactive 赋值,会丢失响应
|
||||||
|
// 不能用 Object.assign,会丢失 api 的原型函数
|
||||||
|
Object.setPrototypeOf(extendedApi, api);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
checkProps(extendedApi as ExtendedDrawerApi, {
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
...slots,
|
||||||
|
});
|
||||||
|
return () => h(connectedComponent, { ...props, ...attrs }, slots);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inheritAttrs: false,
|
||||||
|
name: 'VbenParentDrawer',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return [Drawer, extendedApi as ExtendedDrawerApi] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectData = inject<any>(USER_DRAWER_INJECT_KEY, {});
|
||||||
|
|
||||||
|
const mergedOptions = {
|
||||||
|
...injectData.options,
|
||||||
|
...options,
|
||||||
|
} as DrawerApiOptions;
|
||||||
|
|
||||||
|
// mergedOptions.onOpenChange = (isOpen: boolean) => {
|
||||||
|
// options.onOpenChange?.(isOpen);
|
||||||
|
// injectData.options?.onOpenChange?.(isOpen);
|
||||||
|
// };
|
||||||
|
const api = new DrawerApi(mergedOptions);
|
||||||
|
|
||||||
|
const extendedApi: ExtendedDrawerApi = api as never;
|
||||||
|
|
||||||
|
extendedApi.useStore = (selector) => {
|
||||||
|
return useStore(api.store, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Drawer = defineComponent(
|
||||||
|
(props: DrawerProps, { attrs, slots }) => {
|
||||||
|
return () =>
|
||||||
|
h(VbenDrawer, { ...props, ...attrs, drawerApi: extendedApi }, slots);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inheritAttrs: false,
|
||||||
|
name: 'VbenDrawer',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
injectData.extendApi?.(extendedApi);
|
||||||
|
return [Drawer, extendedApi] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkProps(api: ExtendedDrawerApi, attrs: Record<string, any>) {
|
||||||
|
if (!attrs || Object.keys(attrs).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const state = api?.store?.state;
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateKeys = new Set(Object.keys(state));
|
||||||
|
|
||||||
|
for (const attr of Object.keys(attrs)) {
|
||||||
|
if (stateKeys.has(attr)) {
|
||||||
|
// connectedComponent存在时,不要传入Drawer的props,会造成复杂度提升,如果你需要修改Drawer的props,请使用 useVbenDrawer 或者api
|
||||||
|
console.warn(
|
||||||
|
`[Vben Drawer]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Drawer, please use useVbenDrawer or api.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
packages/@core/ui-kit/popup-ui/src/index.ts
Normal file
2
packages/@core/ui-kit/popup-ui/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './drawer';
|
||||||
|
export * from './modal';
|
@@ -0,0 +1,112 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { ModalApi } from '../modal-api'; // 假设 ModalApi 位于同一目录
|
||||||
|
import type { ModalState } from '../modal';
|
||||||
|
|
||||||
|
vi.mock('@vben-core/shared', () => {
|
||||||
|
return {
|
||||||
|
isFunction: (fn: any) => typeof fn === 'function',
|
||||||
|
Store: class {
|
||||||
|
private _state: ModalState;
|
||||||
|
private options: any;
|
||||||
|
|
||||||
|
constructor(initialState: ModalState, options: any) {
|
||||||
|
this._state = initialState;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch(cb: () => void) {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(fn: (prev: ModalState) => ModalState) {
|
||||||
|
this._state = fn(this._state);
|
||||||
|
this.options.onUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
get state() {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('modalApi', () => {
|
||||||
|
let modalApi: ModalApi;
|
||||||
|
// 使用 modalState 而不是 state
|
||||||
|
let modalState: ModalState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
modalApi = new ModalApi();
|
||||||
|
// 获取 modalApi 内的 state
|
||||||
|
modalState = modalApi.store.state;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize with default state', () => {
|
||||||
|
expect(modalState.isOpen).toBe(false);
|
||||||
|
expect(modalState.cancelText).toBe('取消');
|
||||||
|
expect(modalState.confirmText).toBe('确定');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the modal', () => {
|
||||||
|
modalApi.open();
|
||||||
|
expect(modalApi.store.state.isOpen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the modal if onBeforeClose allows it', () => {
|
||||||
|
modalApi.close();
|
||||||
|
expect(modalApi.store.state.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not close the modal if onBeforeClose returns false', () => {
|
||||||
|
const onBeforeClose = vi.fn(() => false);
|
||||||
|
const modalApiWithHook = new ModalApi({ onBeforeClose });
|
||||||
|
modalApiWithHook.open();
|
||||||
|
modalApiWithHook.close();
|
||||||
|
expect(modalApiWithHook.store.state.isOpen).toBe(true);
|
||||||
|
expect(onBeforeClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onCancel and close the modal if no onCancel hook is provided', () => {
|
||||||
|
const onCancel = vi.fn();
|
||||||
|
const modalApiWithHook = new ModalApi({ onCancel });
|
||||||
|
modalApiWithHook.open();
|
||||||
|
modalApiWithHook.onCancel();
|
||||||
|
expect(onCancel).toHaveBeenCalled();
|
||||||
|
expect(modalApiWithHook.store.state.isOpen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update shared data correctly', () => {
|
||||||
|
const testData = { key: 'value' };
|
||||||
|
modalApi.setData(testData);
|
||||||
|
expect(modalApi.getData()).toEqual(testData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set state correctly using an object', () => {
|
||||||
|
modalApi.setState({ title: 'New Title' });
|
||||||
|
expect(modalApi.store.state.title).toBe('New Title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set state correctly using a function', () => {
|
||||||
|
modalApi.setState((prev) => ({ ...prev, confirmText: 'Yes' }));
|
||||||
|
expect(modalApi.store.state.confirmText).toBe('Yes');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onOpenChange when state changes', () => {
|
||||||
|
const onOpenChange = vi.fn();
|
||||||
|
const modalApiWithHook = new ModalApi({ onOpenChange });
|
||||||
|
modalApiWithHook.open();
|
||||||
|
expect(onOpenChange).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should batch state updates', () => {
|
||||||
|
const batchSpy = vi.spyOn(modalApi.store, 'batch');
|
||||||
|
modalApi.batchStore(() => {
|
||||||
|
modalApi.setState({ title: 'Batch Title' });
|
||||||
|
modalApi.setState({ confirmText: 'Batch Confirm' });
|
||||||
|
});
|
||||||
|
expect(batchSpy).toHaveBeenCalled();
|
||||||
|
expect(modalApi.store.state.title).toBe('Batch Title');
|
||||||
|
expect(modalApi.store.state.confirmText).toBe('Batch Confirm');
|
||||||
|
});
|
||||||
|
});
|
3
packages/@core/ui-kit/popup-ui/src/modal/index.ts
Normal file
3
packages/@core/ui-kit/popup-ui/src/modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type * from './modal';
|
||||||
|
export { default as VbenModal } from './modal.vue';
|
||||||
|
export { useVbenModal } from './use-modal';
|
134
packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
Normal file
134
packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import type { ModalApiOptions, ModalState } from './modal';
|
||||||
|
|
||||||
|
import { isFunction, Store } from '@vben-core/shared';
|
||||||
|
|
||||||
|
export class ModalApi {
|
||||||
|
private api: Pick<
|
||||||
|
ModalApiOptions,
|
||||||
|
'onBeforeClose' | 'onCancel' | 'onConfirm' | 'onOpenChange'
|
||||||
|
>;
|
||||||
|
// private prevState!: ModalState;
|
||||||
|
private state!: ModalState;
|
||||||
|
|
||||||
|
// 共享数据
|
||||||
|
public sharedData: Record<'payload', any> = {
|
||||||
|
payload: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
public store: Store<ModalState>;
|
||||||
|
|
||||||
|
constructor(options: ModalApiOptions = {}) {
|
||||||
|
const {
|
||||||
|
connectedComponent: _,
|
||||||
|
onBeforeClose,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
onOpenChange,
|
||||||
|
...storeState
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const defaultState: ModalState = {
|
||||||
|
cancelText: '取消',
|
||||||
|
centered: false,
|
||||||
|
closeOnClickModal: true,
|
||||||
|
closeOnPressEscape: true,
|
||||||
|
confirmLoading: false,
|
||||||
|
confirmText: '确定',
|
||||||
|
draggable: false,
|
||||||
|
footer: true,
|
||||||
|
fullscreen: false,
|
||||||
|
fullscreenButton: true,
|
||||||
|
isOpen: false,
|
||||||
|
loading: false,
|
||||||
|
modal: true,
|
||||||
|
sharedData: {},
|
||||||
|
title: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.store = new Store<ModalState>(
|
||||||
|
{
|
||||||
|
...defaultState,
|
||||||
|
...storeState,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onUpdate: () => {
|
||||||
|
const state = this.store.state;
|
||||||
|
|
||||||
|
// 每次更新状态时,都会调用 onOpenChange 回调函数
|
||||||
|
if (state?.isOpen === this.state?.isOpen) {
|
||||||
|
this.state = state;
|
||||||
|
} else {
|
||||||
|
this.state = state;
|
||||||
|
this.api.onOpenChange?.(!!state?.isOpen);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.api = {
|
||||||
|
onBeforeClose,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
onOpenChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果需要多次更新状态,可以使用 batch 方法
|
||||||
|
batchStore(cb: () => void) {
|
||||||
|
this.store.batch(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭弹窗
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
// 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗
|
||||||
|
// 如果 onBeforeClose 返回 false,则不关闭弹窗
|
||||||
|
const allowClose = this.api.onBeforeClose?.() ?? true;
|
||||||
|
if (allowClose) {
|
||||||
|
this.store.setState((prev) => ({ ...prev, isOpen: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getData<T extends object = Record<string, any>>() {
|
||||||
|
return (this.sharedData?.payload ?? {}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消操作
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
if (this.api.onCancel) {
|
||||||
|
this.api.onCancel?.();
|
||||||
|
} else {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认操作
|
||||||
|
*/
|
||||||
|
onConfirm() {
|
||||||
|
this.api.onConfirm?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.store.setState((prev) => ({ ...prev, isOpen: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setData<T>(payload: T) {
|
||||||
|
this.sharedData.payload = payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(
|
||||||
|
stateOrFn:
|
||||||
|
| ((prev: ModalState) => Partial<ModalState>)
|
||||||
|
| Partial<ModalState>,
|
||||||
|
) {
|
||||||
|
if (isFunction(stateOrFn)) {
|
||||||
|
this.store.setState(stateOrFn);
|
||||||
|
} else {
|
||||||
|
this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
123
packages/@core/ui-kit/popup-ui/src/modal/modal.ts
Normal file
123
packages/@core/ui-kit/popup-ui/src/modal/modal.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import type { ModalApi } from './modal-api';
|
||||||
|
|
||||||
|
import type { Component, Ref } from 'vue';
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
/**
|
||||||
|
* 取消按钮文字
|
||||||
|
*/
|
||||||
|
cancelText?: string;
|
||||||
|
/**
|
||||||
|
* 是否居中
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
centered?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否显示右上角的关闭按钮
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
closable?: boolean;
|
||||||
|
/**
|
||||||
|
* 点击弹窗遮罩是否关闭弹窗
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
closeOnClickModal?: boolean;
|
||||||
|
/**
|
||||||
|
* 按下 ESC 键是否关闭弹窗
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
closeOnPressEscape?: boolean;
|
||||||
|
/**
|
||||||
|
* 确定按钮 loading
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
confirmLoading?: boolean;
|
||||||
|
/**
|
||||||
|
* 确定按钮文字
|
||||||
|
*/
|
||||||
|
confirmText?: string;
|
||||||
|
/**
|
||||||
|
* 弹窗描述
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* 是否可拖拽
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
draggable?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否显示底部
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
footer?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否全屏
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
fullscreen?: boolean;
|
||||||
|
/**
|
||||||
|
* 是否显示全屏按钮
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
fullscreenButton?: boolean;
|
||||||
|
/**
|
||||||
|
* 弹窗是否显示
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
loading?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示遮罩
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
modal?: boolean;
|
||||||
|
/**
|
||||||
|
* 弹窗标题
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* 弹窗标题提示
|
||||||
|
*/
|
||||||
|
titleTooltip?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalState extends ModalProps {
|
||||||
|
/** 弹窗打开状态 */
|
||||||
|
isOpen?: boolean;
|
||||||
|
/**
|
||||||
|
* 共享数据
|
||||||
|
*/
|
||||||
|
sharedData?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendedModalApi = {
|
||||||
|
useStore: <T = NoInfer<ModalState>>(
|
||||||
|
selector?: (state: NoInfer<ModalState>) => T,
|
||||||
|
) => Readonly<Ref<T>>;
|
||||||
|
} & ModalApi;
|
||||||
|
|
||||||
|
export interface ModalApiOptions extends ModalState {
|
||||||
|
/**
|
||||||
|
* 独立的弹窗组件
|
||||||
|
*/
|
||||||
|
connectedComponent?: Component;
|
||||||
|
/**
|
||||||
|
* 关闭前的回调,返回 false 可以阻止关闭
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onBeforeClose?: () => void;
|
||||||
|
/**
|
||||||
|
* 点击取消按钮的回调
|
||||||
|
*/
|
||||||
|
onCancel?: () => void;
|
||||||
|
/**
|
||||||
|
* 点击确定按钮的回调
|
||||||
|
*/
|
||||||
|
onConfirm?: () => void;
|
||||||
|
/**
|
||||||
|
* 弹窗状态变化回调
|
||||||
|
* @param isOpen
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
}
|
231
packages/@core/ui-kit/popup-ui/src/modal/modal.vue
Normal file
231
packages/@core/ui-kit/popup-ui/src/modal/modal.vue
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ExtendedModalApi, ModalProps } from './modal';
|
||||||
|
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { usePriorityValue } from '@vben-core/composables';
|
||||||
|
import { Expand, Info, Shrink } from '@vben-core/icons';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
VbenButton,
|
||||||
|
VbenIconButton,
|
||||||
|
VbenLoading,
|
||||||
|
VbenTooltip,
|
||||||
|
VisuallyHidden,
|
||||||
|
} from '@vben-core/shadcn-ui';
|
||||||
|
import { cn } from '@vben-core/shared';
|
||||||
|
|
||||||
|
// import { useElementSize } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { useModalDraggable } from './use-modal-draggable';
|
||||||
|
|
||||||
|
interface Props extends ModalProps {
|
||||||
|
class?: string;
|
||||||
|
contentClass?: string;
|
||||||
|
footerClass?: string;
|
||||||
|
headerClass?: string;
|
||||||
|
modalApi?: ExtendedModalApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
class: '',
|
||||||
|
contentClass: '',
|
||||||
|
footerClass: '',
|
||||||
|
headerClass: '',
|
||||||
|
modalApi: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentRef = ref();
|
||||||
|
const dialogRef = ref();
|
||||||
|
const headerRef = ref();
|
||||||
|
const footerRef = ref();
|
||||||
|
|
||||||
|
// const { height: headerHeight } = useElementSize(headerRef);
|
||||||
|
// const { height: footerHeight } = useElementSize(footerRef);
|
||||||
|
const state = props.modalApi?.useStore?.();
|
||||||
|
|
||||||
|
const title = usePriorityValue('title', props, state);
|
||||||
|
const fullscreen = usePriorityValue('fullscreen', props, state);
|
||||||
|
const description = usePriorityValue('description', props, state);
|
||||||
|
const titleTooltip = usePriorityValue('titleTooltip', props, state);
|
||||||
|
const showFooter = usePriorityValue('footer', props, state);
|
||||||
|
const showLoading = usePriorityValue('loading', props, state);
|
||||||
|
const closable = usePriorityValue('closable', props, state);
|
||||||
|
const modal = usePriorityValue('modal', props, state);
|
||||||
|
const centered = usePriorityValue('centered', props, state);
|
||||||
|
const confirmLoading = usePriorityValue('confirmLoading', props, state);
|
||||||
|
const cancelText = usePriorityValue('cancelText', props, state);
|
||||||
|
const confirmText = usePriorityValue('confirmText', props, state);
|
||||||
|
const draggable = usePriorityValue('draggable', props, state);
|
||||||
|
const fullscreenButton = usePriorityValue('fullscreenButton', props, state);
|
||||||
|
const closeOnClickModal = usePriorityValue('closeOnClickModal', props, state);
|
||||||
|
const closeOnPressEscape = usePriorityValue('closeOnPressEscape', props, state);
|
||||||
|
const shouldDraggable = computed(() => draggable.value && !fullscreen.value);
|
||||||
|
|
||||||
|
const { dragging } = useModalDraggable(dialogRef, headerRef, shouldDraggable);
|
||||||
|
|
||||||
|
// const loadingStyle = computed(() => {
|
||||||
|
// // py-5 4px*5*2
|
||||||
|
// const headerPadding = 40;
|
||||||
|
// // p-2 4px*2*2
|
||||||
|
// const footerPadding = 16;
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// bottom: `${footerHeight.value + footerPadding}px`,
|
||||||
|
// height: `calc(100% - ${footerHeight.value + headerHeight.value + headerPadding + footerPadding}px)`,
|
||||||
|
// top: `${headerHeight.value + headerPadding}px`,
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => state?.value?.isOpen,
|
||||||
|
async (v) => {
|
||||||
|
if (v) {
|
||||||
|
await nextTick();
|
||||||
|
if (contentRef.value) {
|
||||||
|
const innerContentRef = contentRef.value.getContentRef();
|
||||||
|
dialogRef.value = innerContentRef.$el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleFullscreen() {
|
||||||
|
props.modalApi?.setState((prev) => {
|
||||||
|
// if (prev.fullscreen) {
|
||||||
|
// resetPosition();
|
||||||
|
// }
|
||||||
|
return { ...prev, fullscreen: !fullscreen.value };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function interactOutside(e: Event) {
|
||||||
|
if (!closeOnClickModal.value) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function escapeKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!closeOnPressEscape.value) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:modal="modal"
|
||||||
|
:open="state?.isOpen"
|
||||||
|
@update:open="() => modalApi?.close()"
|
||||||
|
>
|
||||||
|
<DialogTrigger v-if="$slots.trigger" as-child>
|
||||||
|
<slot name="trigger"> </slot>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent
|
||||||
|
ref="contentRef"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'left-0 right-0 top-[10vh] mx-auto flex max-h-[80%] w-[520px] flex-col p-0',
|
||||||
|
props.class,
|
||||||
|
{
|
||||||
|
'left-0 top-0 size-full max-h-full !translate-x-0 !translate-y-0':
|
||||||
|
fullscreen,
|
||||||
|
'top-1/2 -translate-y-1/2': centered && !fullscreen,
|
||||||
|
'duration-300': !dragging,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:show-close="closable"
|
||||||
|
close-class="top-4"
|
||||||
|
@escape-key-down="escapeKeyDown"
|
||||||
|
@interact-outside="interactOutside"
|
||||||
|
>
|
||||||
|
<DialogHeader
|
||||||
|
ref="headerRef"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'border-b px-6 py-5',
|
||||||
|
{
|
||||||
|
'cursor-move select-none': shouldDraggable,
|
||||||
|
},
|
||||||
|
props.headerClass,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<DialogTitle v-if="title">
|
||||||
|
<slot name="title">
|
||||||
|
{{ title }}
|
||||||
|
|
||||||
|
<VbenTooltip v-if="titleTooltip" side="right">
|
||||||
|
<template #trigger>
|
||||||
|
<Info class="inline-flex size-5 cursor-pointer pb-1" />
|
||||||
|
</template>
|
||||||
|
{{ titleTooltip }}
|
||||||
|
</VbenTooltip>
|
||||||
|
</slot>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription v-if="description">
|
||||||
|
<slot name="description">
|
||||||
|
{{ description }}
|
||||||
|
</slot>
|
||||||
|
</DialogDescription>
|
||||||
|
<VisuallyHidden v-if="!title || !description">
|
||||||
|
<DialogTitle v-if="!title" />
|
||||||
|
<DialogDescription v-if="!description" />
|
||||||
|
</VisuallyHidden>
|
||||||
|
</DialogHeader>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('relative min-h-40 flex-1 p-3', contentClass, {
|
||||||
|
'overflow-y-auto': !showLoading,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<VbenLoading v-if="showLoading" class="size-full" spinning />
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VbenIconButton
|
||||||
|
v-if="fullscreenButton"
|
||||||
|
class="hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-10 top-4 size-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
||||||
|
@click="handleFullscreen"
|
||||||
|
>
|
||||||
|
<Shrink v-if="fullscreen" class="size-3.5" />
|
||||||
|
<Expand v-else class="size-3.5" />
|
||||||
|
</VbenIconButton>
|
||||||
|
|
||||||
|
<DialogFooter
|
||||||
|
v-if="showFooter"
|
||||||
|
ref="footerRef"
|
||||||
|
:class="cn('items-center border-t p-2', props.footerClass)"
|
||||||
|
>
|
||||||
|
<slot name="prepend-footer"></slot>
|
||||||
|
<slot name="footer">
|
||||||
|
<VbenButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
@click="() => modalApi?.onCancel()"
|
||||||
|
>
|
||||||
|
<slot name="cancelText">
|
||||||
|
{{ cancelText }}
|
||||||
|
</slot>
|
||||||
|
</VbenButton>
|
||||||
|
<VbenButton
|
||||||
|
:loading="confirmLoading"
|
||||||
|
size="sm"
|
||||||
|
@click="() => modalApi?.onConfirm()"
|
||||||
|
>
|
||||||
|
<slot name="confirmText">
|
||||||
|
{{ confirmText }}
|
||||||
|
</slot>
|
||||||
|
</VbenButton>
|
||||||
|
</slot>
|
||||||
|
<slot name="append-footer"></slot>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
148
packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts
Normal file
148
packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* @copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-draggable/index.ts
|
||||||
|
* 调整部分细节
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watchEffect } from 'vue';
|
||||||
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
|
||||||
|
import { unrefElement } from '@vueuse/core';
|
||||||
|
|
||||||
|
export function useModalDraggable(
|
||||||
|
targetRef: Ref<HTMLElement | undefined>,
|
||||||
|
dragRef: Ref<HTMLElement | undefined>,
|
||||||
|
draggable: ComputedRef<boolean>,
|
||||||
|
) {
|
||||||
|
let transform = {
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragging = ref(false);
|
||||||
|
|
||||||
|
// let isFirstDrag = true;
|
||||||
|
// let initialX = 0;
|
||||||
|
// let initialY = 0;
|
||||||
|
const onMousedown = (e: MouseEvent) => {
|
||||||
|
const downX = e.clientX;
|
||||||
|
const downY = e.clientY;
|
||||||
|
|
||||||
|
if (!targetRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (isFirstDrag) {
|
||||||
|
// const { x, y } = getInitialTransform(targetRef.value);
|
||||||
|
// initialX = x;
|
||||||
|
// initialY = y;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const targetRect = targetRef.value.getBoundingClientRect();
|
||||||
|
|
||||||
|
const { offsetX, offsetY } = transform;
|
||||||
|
const targetLeft = targetRect.left;
|
||||||
|
const targetTop = targetRect.top;
|
||||||
|
const targetWidth = targetRect.width;
|
||||||
|
const targetHeight = targetRect.height;
|
||||||
|
const docElement = document.documentElement;
|
||||||
|
const clientWidth = docElement.clientWidth;
|
||||||
|
const clientHeight = docElement.clientHeight;
|
||||||
|
|
||||||
|
const minLeft = -targetLeft + offsetX;
|
||||||
|
const minTop = -targetTop + offsetY;
|
||||||
|
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX;
|
||||||
|
const maxTop = clientHeight - targetTop - targetHeight + offsetY;
|
||||||
|
|
||||||
|
const onMousemove = (e: MouseEvent) => {
|
||||||
|
let moveX = offsetX + e.clientX - downX;
|
||||||
|
let moveY = offsetY + e.clientY - downY;
|
||||||
|
// const x = isFirstDrag ? initialX : 0;
|
||||||
|
// const y = isFirstDrag ? initialY : 0;
|
||||||
|
moveX = Math.min(Math.max(moveX, minLeft), maxLeft);
|
||||||
|
// + x;
|
||||||
|
moveY = Math.min(Math.max(moveY, minTop), maxTop);
|
||||||
|
// + y;
|
||||||
|
|
||||||
|
transform = {
|
||||||
|
offsetX: moveX,
|
||||||
|
offsetY: moveY,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (targetRef.value) {
|
||||||
|
targetRef.value.style.transform = `translate(${moveX}px, ${moveY}px)`;
|
||||||
|
dragging.value = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseup = () => {
|
||||||
|
// isFirstDrag = false;
|
||||||
|
dragging.value = false;
|
||||||
|
document.removeEventListener('mousemove', onMousemove);
|
||||||
|
document.removeEventListener('mouseup', onMouseup);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMousemove);
|
||||||
|
document.addEventListener('mouseup', onMouseup);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDraggable = () => {
|
||||||
|
const dragDom = unrefElement(dragRef);
|
||||||
|
if (dragDom && targetRef.value) {
|
||||||
|
dragDom.addEventListener('mousedown', onMousedown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const offDraggable = () => {
|
||||||
|
const dragDom = unrefElement(dragRef);
|
||||||
|
if (dragDom && targetRef.value) {
|
||||||
|
dragDom.removeEventListener('mousedown', onMousedown);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetPosition = () => {
|
||||||
|
transform = {
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
};
|
||||||
|
const target = unrefElement(targetRef);
|
||||||
|
if (target) {
|
||||||
|
target.style.transform = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
watchEffect(() => {
|
||||||
|
if (draggable.value) {
|
||||||
|
onDraggable();
|
||||||
|
} else {
|
||||||
|
offDraggable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
offDraggable();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dragging,
|
||||||
|
resetPosition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// function getInitialTransform(target: HTMLElement) {
|
||||||
|
// let x = 0;
|
||||||
|
// let y = 0;
|
||||||
|
// const transformValue = window.getComputedStyle(target)?.transform;
|
||||||
|
// if (transformValue) {
|
||||||
|
// const match = transformValue.match(/matrix\(([^)]+)\)/);
|
||||||
|
// if (match) {
|
||||||
|
// const values = match[1]?.split(', ') ?? [];
|
||||||
|
// // 获取 translateX 值
|
||||||
|
// x = Number.parseFloat(`${values[4]}`);
|
||||||
|
// // 获取 translateY 值
|
||||||
|
// y = Number.parseFloat(`${values[5]}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return { x, y };
|
||||||
|
// }
|
101
packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts
Normal file
101
packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type { ExtendedModalApi, ModalApiOptions, ModalProps } from './modal';
|
||||||
|
|
||||||
|
import { defineComponent, h, inject, nextTick, provide, reactive } from 'vue';
|
||||||
|
|
||||||
|
import { useStore } from '@vben-core/shared';
|
||||||
|
|
||||||
|
import VbenModal from './modal.vue';
|
||||||
|
import { ModalApi } from './modal-api';
|
||||||
|
|
||||||
|
const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');
|
||||||
|
|
||||||
|
export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
|
||||||
|
options: ModalApiOptions = {},
|
||||||
|
) {
|
||||||
|
// Modal一般会抽离出来,所以如果有传入 connectedComponent,则表示为外部调用,与内部组件进行连接
|
||||||
|
// 外部的Modal通过provide/inject传递api
|
||||||
|
|
||||||
|
const { connectedComponent } = options;
|
||||||
|
if (connectedComponent) {
|
||||||
|
const extendedApi = reactive({});
|
||||||
|
const Modal = defineComponent(
|
||||||
|
(props: TParentModalProps, { attrs, slots }) => {
|
||||||
|
provide(USER_MODAL_INJECT_KEY, {
|
||||||
|
extendApi(api: ExtendedModalApi) {
|
||||||
|
// 不能直接给 reactive 赋值,会丢失响应
|
||||||
|
// 不能用 Object.assign,会丢失 api 的原型函数
|
||||||
|
Object.setPrototypeOf(extendedApi, api);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
checkProps(extendedApi as ExtendedModalApi, {
|
||||||
|
...props,
|
||||||
|
...attrs,
|
||||||
|
...slots,
|
||||||
|
});
|
||||||
|
return () => h(connectedComponent, { ...props, ...attrs }, slots);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inheritAttrs: false,
|
||||||
|
name: 'VbenParentModal',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return [Modal, extendedApi as ExtendedModalApi] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectData = inject<any>(USER_MODAL_INJECT_KEY, {});
|
||||||
|
|
||||||
|
const mergedOptions = {
|
||||||
|
...injectData.options,
|
||||||
|
...options,
|
||||||
|
} as ModalApiOptions;
|
||||||
|
|
||||||
|
// mergedOptions.onOpenChange = (isOpen: boolean) => {
|
||||||
|
// options.onOpenChange?.(isOpen);
|
||||||
|
// injectData.options?.onOpenChange?.(isOpen);
|
||||||
|
// };
|
||||||
|
const api = new ModalApi(mergedOptions);
|
||||||
|
|
||||||
|
const extendedApi: ExtendedModalApi = api as never;
|
||||||
|
|
||||||
|
extendedApi.useStore = (selector) => {
|
||||||
|
return useStore(api.store, selector);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Modal = defineComponent(
|
||||||
|
(props: ModalProps, { attrs, slots }) => {
|
||||||
|
return () =>
|
||||||
|
h(VbenModal, { ...props, ...attrs, modalApi: extendedApi }, slots);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
inheritAttrs: false,
|
||||||
|
name: 'VbenModal',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
injectData.extendApi?.(extendedApi);
|
||||||
|
return [Modal, extendedApi] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkProps(api: ExtendedModalApi, attrs: Record<string, any>) {
|
||||||
|
if (!attrs || Object.keys(attrs).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const state = api?.store?.state;
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateKeys = new Set(Object.keys(state));
|
||||||
|
|
||||||
|
for (const attr of Object.keys(attrs)) {
|
||||||
|
if (stateKeys.has(attr)) {
|
||||||
|
// connectedComponent存在时,不要传入Modal的props,会造成复杂度提升,如果你需要修改Modal的props,请使用 useModal 或者api
|
||||||
|
console.warn(
|
||||||
|
`[Vben Modal]: When 'connectedComponent' exists, do not set props or slots '${attr}', which will increase complexity. If you need to modify the props of Modal, please use useModal or api.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
packages/@core/ui-kit/popup-ui/tailwind.config.mjs
Normal file
1
packages/@core/ui-kit/popup-ui/tailwind.config.mjs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from '@vben/tailwind-config';
|
6
packages/@core/ui-kit/popup-ui/tsconfig.json
Normal file
6
packages/@core/ui-kit/popup-ui/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/web.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
@@ -1,62 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialog as AlertDialogRoot,
|
|
||||||
AlertDialogTitle,
|
|
||||||
} from '../ui/alert-dialog';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cancelText?: string;
|
|
||||||
content?: string;
|
|
||||||
submitText?: string;
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
cancelText: '取消',
|
|
||||||
submitText: '确认',
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
cancel: [];
|
|
||||||
submit: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const openModal = defineModel<boolean>('open');
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
emits('submit');
|
|
||||||
openModal.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCancel() {
|
|
||||||
emits('cancel');
|
|
||||||
openModal.value = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogRoot v-model:open="openModal">
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>{{ title }}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{{ content }}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel @click="handleCancel">
|
|
||||||
{{ cancelText }}
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction @click="handleSubmit">
|
|
||||||
{{ submitText }}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogRoot>
|
|
||||||
</template>
|
|
@@ -1 +0,0 @@
|
|||||||
export { default as VbenAlertDialog } from './alert-dialog.vue';
|
|
@@ -1,4 +1,3 @@
|
|||||||
export * from './alert-dialog';
|
|
||||||
export * from './avatar';
|
export * from './avatar';
|
||||||
export * from './back-top';
|
export * from './back-top';
|
||||||
export * from './breadcrumb';
|
export * from './breadcrumb';
|
||||||
@@ -20,11 +19,9 @@ export * from './popover';
|
|||||||
export * from './render-content';
|
export * from './render-content';
|
||||||
export * from './scrollbar';
|
export * from './scrollbar';
|
||||||
export * from './segmented';
|
export * from './segmented';
|
||||||
export * from './sheet';
|
|
||||||
export * from './spinner';
|
export * from './spinner';
|
||||||
export * from './swap';
|
export * from './swap';
|
||||||
export * from './tooltip';
|
export * from './tooltip';
|
||||||
export * from './ui/alert-dialog';
|
|
||||||
export * from './ui/avatar';
|
export * from './ui/avatar';
|
||||||
export * from './ui/badge';
|
export * from './ui/badge';
|
||||||
export * from './ui/breadcrumb';
|
export * from './ui/breadcrumb';
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
export { default as VbenSheet } from './sheet.vue';
|
|
@@ -1,113 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, useSlots } from 'vue';
|
|
||||||
|
|
||||||
import { X } from 'lucide-vue-next';
|
|
||||||
|
|
||||||
import { VbenButton, VbenIconButton } from '../button';
|
|
||||||
import { VbenScrollbar } from '../scrollbar';
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetFooter,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
SheetTrigger,
|
|
||||||
} from '../ui/sheet';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cancelText?: string;
|
|
||||||
description?: string;
|
|
||||||
showFooter?: boolean;
|
|
||||||
submitText?: string;
|
|
||||||
title?: string;
|
|
||||||
width?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
cancelText: '关闭',
|
|
||||||
description: '',
|
|
||||||
showFooter: false,
|
|
||||||
submitText: '确认',
|
|
||||||
title: '',
|
|
||||||
width: 400,
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
cancel: [];
|
|
||||||
submit: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const openModal = defineModel<boolean>('open');
|
|
||||||
|
|
||||||
const slots = useSlots();
|
|
||||||
|
|
||||||
const contentStyle = computed(() => {
|
|
||||||
return {
|
|
||||||
width: `${props.width}px`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function handlerSubmit() {
|
|
||||||
emits('submit');
|
|
||||||
openModal.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// function handleCancel() {
|
|
||||||
// emits('cancel');
|
|
||||||
// openModal.value = false;
|
|
||||||
// }
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Sheet v-model:open="openModal">
|
|
||||||
<SheetTrigger>
|
|
||||||
<slot name="trigger"></slot>
|
|
||||||
</SheetTrigger>
|
|
||||||
<SheetContent :style="contentStyle" class="!w-full pb-12 sm:rounded-l-lg">
|
|
||||||
<SheetHeader
|
|
||||||
:class="description ? 'h-16' : 'h-12'"
|
|
||||||
class="border-border flex flex-row items-center justify-between border-b pl-3 pr-3"
|
|
||||||
>
|
|
||||||
<div class="flex w-full items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<SheetTitle class="text-left text-lg">{{ title }}</SheetTitle>
|
|
||||||
<SheetDescription class="text-muted-foreground text-xs">
|
|
||||||
{{ description }}
|
|
||||||
</SheetDescription>
|
|
||||||
</div>
|
|
||||||
<slot v-if="slots.extra" name="extra"></slot>
|
|
||||||
</div>
|
|
||||||
<SheetClose
|
|
||||||
as-child
|
|
||||||
class="data-[state=open]:bg-secondary cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
|
||||||
>
|
|
||||||
<VbenIconButton>
|
|
||||||
<X class="size-4" />
|
|
||||||
</VbenIconButton>
|
|
||||||
</SheetClose>
|
|
||||||
</SheetHeader>
|
|
||||||
<div class="h-full pb-16">
|
|
||||||
<VbenScrollbar class="h-full" shadow>
|
|
||||||
<slot></slot>
|
|
||||||
</VbenScrollbar>
|
|
||||||
</div>
|
|
||||||
<SheetFooter v-if="showFooter || slots.footer" as-child>
|
|
||||||
<div
|
|
||||||
class="border-border absolute bottom-0 flex h-12 w-full items-center justify-end border-t"
|
|
||||||
>
|
|
||||||
<slot v-if="slots.footer" name="footer"></slot>
|
|
||||||
<template v-else>
|
|
||||||
<SheetClose as-child>
|
|
||||||
<VbenButton class="mr-2" variant="outline">
|
|
||||||
{{ cancelText }}
|
|
||||||
</VbenButton>
|
|
||||||
</SheetClose>
|
|
||||||
<VbenButton @click="handlerSubmit">{{ submitText }}</VbenButton>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</SheetFooter>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
</template>
|
|
@@ -1 +1,2 @@
|
|||||||
|
export { default as VbenLoading } from './loading.vue';
|
||||||
export { default as VbenSpinner } from './spinner.vue';
|
export { default as VbenSpinner } from './spinner.vue';
|
||||||
|
@@ -0,0 +1,137 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { cn } from '@vben-core/shared';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
/**
|
||||||
|
* @zh_CN 最小加载时间
|
||||||
|
* @en_US Minimum loading time
|
||||||
|
*/
|
||||||
|
minLoadingTime?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN loading状态开启
|
||||||
|
*/
|
||||||
|
spinning?: boolean;
|
||||||
|
/**
|
||||||
|
* @zh_CN 文字
|
||||||
|
*/
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'VbenLoading',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
minLoadingTime: 50,
|
||||||
|
text: '',
|
||||||
|
});
|
||||||
|
// const startTime = ref(0);
|
||||||
|
const showSpinner = ref(false);
|
||||||
|
const renderSpinner = ref(true);
|
||||||
|
const timer = ref<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.spinning,
|
||||||
|
(show) => {
|
||||||
|
if (!show) {
|
||||||
|
showSpinner.value = false;
|
||||||
|
clearTimeout(timer.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// startTime.value = performance.now();
|
||||||
|
timer.value = setTimeout(() => {
|
||||||
|
// const loadingTime = performance.now() - startTime.value;
|
||||||
|
|
||||||
|
showSpinner.value = true;
|
||||||
|
if (showSpinner.value) {
|
||||||
|
renderSpinner.value = true;
|
||||||
|
}
|
||||||
|
}, props.minLoadingTime);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function onTransitionEnd() {
|
||||||
|
if (!showSpinner.value) {
|
||||||
|
renderSpinner.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-overlay z-100 pointer-events-none absolute left-0 top-0 flex size-full flex-col items-center justify-center backdrop-blur-sm transition-all duration-500',
|
||||||
|
{
|
||||||
|
'invisible opacity-0': !showSpinner,
|
||||||
|
},
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@transitionend="onTransitionEnd"
|
||||||
|
>
|
||||||
|
<span class="dot relative inline-block size-9 text-3xl">
|
||||||
|
<i
|
||||||
|
v-for="index in 4"
|
||||||
|
:key="index"
|
||||||
|
class="bg-primary absolute block size-4 origin-[50%_50%] scale-75 rounded-full opacity-30"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div v-if="text" class="mt-4 text-xs">{{ text }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dot {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
animation: rotate-ani 1.2s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i {
|
||||||
|
animation: spin-move-ani 1s infinite linear alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(1) {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(2) {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(3) {
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
animation-delay: 0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot i:nth-child(4) {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate-ani {
|
||||||
|
to {
|
||||||
|
transform: rotate(405deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-move-ani {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,7 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { cn } from '@vben-core/shared';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
class?: string;
|
||||||
/**
|
/**
|
||||||
* @zh_CN 最小加载时间
|
* @zh_CN 最小加载时间
|
||||||
* @en_US Minimum loading time
|
* @en_US Minimum loading time
|
||||||
@@ -14,7 +17,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'Spinner',
|
name: 'VbenSpinner',
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -58,19 +61,34 @@ function onTransitionEnd() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="
|
||||||
'invisible opacity-0': !showSpinner,
|
cn(
|
||||||
}"
|
'flex-center bg-overlay z-100 absolute left-0 top-0 size-full backdrop-blur-sm transition-all duration-500',
|
||||||
class="flex-center bg-overlay z-100 absolute left-0 top-0 size-full backdrop-blur-sm transition-all duration-500"
|
{
|
||||||
|
'invisible opacity-0': !showSpinner,
|
||||||
|
},
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
@transitionend="onTransitionEnd"
|
@transitionend="onTransitionEnd"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="loader before:bg-primary/50 after:bg-primary relative h-12 w-12 before:absolute before:left-0 before:top-[60px] before:h-[5px] before:w-12 before:animate-[loader-shadow-ani_0.5s_linear_infinite] before:rounded-[50%] before:content-[''] after:absolute after:left-0 after:top-0 after:h-full after:w-full after:animate-[loader-jump-ani_0.5s_linear_infinite] after:rounded after:content-['']"
|
class="loader before:bg-primary/50 after:bg-primary relative size-12 before:absolute before:left-0 before:top-[60px] before:h-[5px] before:w-12 before:rounded-[50%] before:content-[''] after:absolute after:left-0 after:top-0 after:h-full after:w-full after:rounded after:content-['']"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
|
.loader {
|
||||||
|
&::before {
|
||||||
|
animation: loader-shadow-ani 0.5s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
animation: loader-jump-ani 0.5s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes loader-jump-ani {
|
@keyframes loader-jump-ani {
|
||||||
15% {
|
15% {
|
||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
|
@@ -1,19 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
type AlertDialogEmits,
|
|
||||||
type AlertDialogProps,
|
|
||||||
AlertDialogRoot,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from 'radix-vue';
|
|
||||||
|
|
||||||
const props = defineProps<AlertDialogProps>();
|
|
||||||
const emits = defineEmits<AlertDialogEmits>();
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(props, emits);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogRoot v-bind="forwarded">
|
|
||||||
<slot></slot>
|
|
||||||
</AlertDialogRoot>
|
|
||||||
</template>
|
|
@@ -1,28 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, type HTMLAttributes } from 'vue';
|
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
|
||||||
|
|
||||||
import { AlertDialogAction, type AlertDialogActionProps } from 'radix-vue';
|
|
||||||
|
|
||||||
import { buttonVariants } from '../button';
|
|
||||||
|
|
||||||
const props = defineProps<
|
|
||||||
{ class?: HTMLAttributes['class'] } & AlertDialogActionProps
|
|
||||||
>();
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props;
|
|
||||||
|
|
||||||
return delegated;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogAction
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="cn(buttonVariants(), props.class)"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</AlertDialogAction>
|
|
||||||
</template>
|
|
@@ -1,30 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, type HTMLAttributes } from 'vue';
|
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
|
||||||
|
|
||||||
import { AlertDialogCancel, type AlertDialogCancelProps } from 'radix-vue';
|
|
||||||
|
|
||||||
import { buttonVariants } from '../button';
|
|
||||||
|
|
||||||
const props = defineProps<
|
|
||||||
{ class?: HTMLAttributes['class'] } & AlertDialogCancelProps
|
|
||||||
>();
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props;
|
|
||||||
|
|
||||||
return delegated;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogCancel
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="
|
|
||||||
cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', props.class)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</AlertDialogCancel>
|
|
||||||
</template>
|
|
@@ -1,46 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, type HTMLAttributes } from 'vue';
|
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AlertDialogContent,
|
|
||||||
type AlertDialogContentEmits,
|
|
||||||
type AlertDialogContentProps,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogPortal,
|
|
||||||
useForwardPropsEmits,
|
|
||||||
} from 'radix-vue';
|
|
||||||
|
|
||||||
const props = defineProps<
|
|
||||||
{ class?: HTMLAttributes['class'] } & AlertDialogContentProps
|
|
||||||
>();
|
|
||||||
const emits = defineEmits<AlertDialogContentEmits>();
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props;
|
|
||||||
|
|
||||||
return delegated;
|
|
||||||
});
|
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogPortal>
|
|
||||||
<AlertDialogOverlay
|
|
||||||
class="bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[1000] backdrop-blur-sm"
|
|
||||||
/>
|
|
||||||
<AlertDialogContent
|
|
||||||
v-bind="forwarded"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-border fixed left-1/2 top-1/2 z-[1000] grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
|
||||||
props.class,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogPortal>
|
|
||||||
</template>
|
|
@@ -1,29 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, type HTMLAttributes } from 'vue';
|
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AlertDialogDescription,
|
|
||||||
type AlertDialogDescriptionProps,
|
|
||||||
} from 'radix-vue';
|
|
||||||
|
|
||||||
const props = defineProps<
|
|
||||||
{ class?: HTMLAttributes['class'] } & AlertDialogDescriptionProps
|
|
||||||
>();
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props;
|
|
||||||
|
|
||||||
return delegated;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogDescription
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</template>
|
|
@@ -1,22 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { HTMLAttributes } from 'vue';
|
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
class?: HTMLAttributes['class'];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
|
||||||
props.class,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@@ -1,17 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { HTMLAttributes } from 'vue';
|
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
class?: HTMLAttributes['class'];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@@ -1,26 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, type HTMLAttributes } from 'vue';
|
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
|
||||||
|
|
||||||
import { AlertDialogTitle, type AlertDialogTitleProps } from 'radix-vue';
|
|
||||||
|
|
||||||
const props = defineProps<
|
|
||||||
{ class?: HTMLAttributes['class'] } & AlertDialogTitleProps
|
|
||||||
>();
|
|
||||||
|
|
||||||
const delegatedProps = computed(() => {
|
|
||||||
const { class: _, ...delegated } = props;
|
|
||||||
|
|
||||||
return delegated;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogTitle
|
|
||||||
v-bind="delegatedProps"
|
|
||||||
:class="cn('text-lg font-semibold', props.class)"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</AlertDialogTitle>
|
|
||||||
</template>
|
|
@@ -1,11 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { AlertDialogTrigger, type AlertDialogTriggerProps } from 'radix-vue';
|
|
||||||
|
|
||||||
const props = defineProps<AlertDialogTriggerProps>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AlertDialogTrigger v-bind="props">
|
|
||||||
<slot></slot>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
</template>
|
|
@@ -1,9 +0,0 @@
|
|||||||
export { default as AlertDialog } from './AlertDialog.vue';
|
|
||||||
export { default as AlertDialogAction } from './AlertDialogAction.vue';
|
|
||||||
export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
|
|
||||||
export { default as AlertDialogContent } from './AlertDialogContent.vue';
|
|
||||||
export { default as AlertDialogDescription } from './AlertDialogDescription.vue';
|
|
||||||
export { default as AlertDialogFooter } from './AlertDialogFooter.vue';
|
|
||||||
export { default as AlertDialogHeader } from './AlertDialogHeader.vue';
|
|
||||||
export { default as AlertDialogTitle } from './AlertDialogTitle.vue';
|
|
||||||
export { default as AlertDialogTrigger } from './AlertDialogTrigger.vue';
|
|
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, type HTMLAttributes } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { cn } from '@vben-core/shared';
|
import { cn } from '@vben-core/shared';
|
||||||
|
|
||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<
|
defineProps<
|
||||||
{
|
{
|
||||||
class?: HTMLAttributes['class'];
|
class?: any;
|
||||||
|
closeClass?: any;
|
||||||
showClose?: boolean;
|
showClose?: boolean;
|
||||||
} & DialogContentProps
|
} & DialogContentProps
|
||||||
>(),
|
>(),
|
||||||
@@ -32,6 +33,12 @@ const delegatedProps = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
|
|
||||||
|
const contentRef = ref<InstanceType<typeof DialogContent> | null>(null);
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getContentRef: () => contentRef.value,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,10 +48,11 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||||||
@click="() => emits('close')"
|
@click="() => emits('close')"
|
||||||
/>
|
/>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
ref="contentRef"
|
||||||
v-bind="forwarded"
|
v-bind="forwarded"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-border fixed left-1/2 top-1/2 z-[1000] grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 border p-6 shadow-lg outline-none duration-300 sm:rounded-lg',
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed z-[1000] w-full p-6 shadow-lg outline-none sm:rounded-xl',
|
||||||
props.class,
|
props.class,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -53,7 +61,12 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||||||
|
|
||||||
<DialogClose
|
<DialogClose
|
||||||
v-if="showClose"
|
v-if="showClose"
|
||||||
class="data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
:class="
|
||||||
|
cn(
|
||||||
|
'data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none',
|
||||||
|
props.closeClass,
|
||||||
|
)
|
||||||
|
"
|
||||||
@click="() => emits('close')"
|
@click="() => emits('close')"
|
||||||
>
|
>
|
||||||
<Cross2Icon class="h-4 w-4" />
|
<Cross2Icon class="h-4 w-4" />
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { cva, type VariantProps } from 'class-variance-authority';
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
export const sheetVariants = cva(
|
export const sheetVariants = cva(
|
||||||
'fixed z-50 gap-4 bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
|
'fixed z-[1000] bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border',
|
||||||
{
|
{
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
side: 'right',
|
side: 'right',
|
||||||
@@ -10,9 +10,9 @@ export const sheetVariants = cva(
|
|||||||
side: {
|
side: {
|
||||||
bottom:
|
bottom:
|
||||||
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
'inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left ',
|
||||||
right:
|
right:
|
||||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
|
||||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -20,6 +20,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vben-core/popup-ui": "workspace:*",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben/constants": "workspace:*",
|
"@vben/constants": "workspace:*",
|
||||||
"@vben/hooks": "workspace:*",
|
"@vben/hooks": "workspace:*",
|
||||||
|
@@ -95,41 +95,43 @@ function handleExpand() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VbenTooltip
|
<div>
|
||||||
:content-style="{
|
<VbenTooltip
|
||||||
...tooltipOverlayStyle,
|
:content-style="{
|
||||||
maxWidth: `${defaultTooltipMaxWidth}px`,
|
...tooltipOverlayStyle,
|
||||||
fontSize: `${tooltipFontSize}px`,
|
maxWidth: `${defaultTooltipMaxWidth}px`,
|
||||||
color: tooltipColor,
|
fontSize: `${tooltipFontSize}px`,
|
||||||
backgroundColor: tooltipBackgroundColor,
|
color: tooltipColor,
|
||||||
}"
|
backgroundColor: tooltipBackgroundColor,
|
||||||
:disabled="!props.tooltip || isExpand"
|
}"
|
||||||
:side="placement"
|
:disabled="!props.tooltip || isExpand"
|
||||||
>
|
:side="placement"
|
||||||
<slot name="tooltip">
|
>
|
||||||
<slot></slot>
|
<slot name="tooltip">
|
||||||
</slot>
|
|
||||||
|
|
||||||
<template #trigger>
|
|
||||||
<div
|
|
||||||
ref="ellipsis"
|
|
||||||
:class="{
|
|
||||||
'!cursor-pointer': expand,
|
|
||||||
['inline-block truncate']: line === 1,
|
|
||||||
[$style.ellipsisMultiLine]: line > 1,
|
|
||||||
}"
|
|
||||||
:style="{
|
|
||||||
'-webkit-line-clamp': isExpand ? '' : line,
|
|
||||||
'max-width': textMaxWidth,
|
|
||||||
}"
|
|
||||||
class="cursor-text overflow-hidden"
|
|
||||||
@click="handleExpand"
|
|
||||||
v-bind="$attrs"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</slot>
|
||||||
</template>
|
|
||||||
</VbenTooltip>
|
<template #trigger>
|
||||||
|
<div
|
||||||
|
ref="ellipsis"
|
||||||
|
:class="{
|
||||||
|
'!cursor-pointer': expand,
|
||||||
|
['inline-block truncate']: line === 1,
|
||||||
|
[$style.ellipsisMultiLine]: line > 1,
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
'-webkit-line-clamp': isExpand ? '' : line,
|
||||||
|
'max-width': textMaxWidth,
|
||||||
|
}"
|
||||||
|
class="cursor-text overflow-hidden"
|
||||||
|
@click="handleExpand"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VbenTooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
|
@@ -1,2 +1,3 @@
|
|||||||
export * from './ellipsis-text';
|
export * from './ellipsis-text';
|
||||||
export * from './page';
|
export * from './page';
|
||||||
|
export * from '@vben-core/popup-ui';
|
||||||
|
@@ -1,15 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AuthenticationProps, LoginAndRegisterParams } from './types';
|
import type { AuthenticationProps, LoginAndRegisterParams } from './types';
|
||||||
|
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
import { useForwardPropsEmits } from '@vben/hooks';
|
import { useForwardPropsEmits } from '@vben/hooks';
|
||||||
import {
|
import { useVbenModal } from '@vben-core/popup-ui';
|
||||||
Dialog,
|
import { VbenAvatar } from '@vben-core/shadcn-ui';
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogTitle,
|
|
||||||
VbenAvatar,
|
|
||||||
VisuallyHidden,
|
|
||||||
} from '@vben-core/shadcn-ui';
|
|
||||||
|
|
||||||
import AuthenticationLogin from './login.vue';
|
import AuthenticationLogin from './login.vue';
|
||||||
|
|
||||||
@@ -32,32 +28,37 @@ const emit = defineEmits<{
|
|||||||
const open = defineModel<boolean>('open');
|
const open = defineModel<boolean>('open');
|
||||||
|
|
||||||
const forwarded = useForwardPropsEmits(props, emit);
|
const forwarded = useForwardPropsEmits(props, emit);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal();
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => open.value,
|
||||||
|
(val) => {
|
||||||
|
modalApi.setState({ isOpen: val });
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Dialog v-model:open="open">
|
<Modal
|
||||||
<DialogContent
|
:closable="false"
|
||||||
:show-close="false"
|
:close-on-click-modal="false"
|
||||||
class="top-1/2 h-full w-full translate-y-[-50%] border-none p-4 py-12 text-center shadow-xl sm:w-[600px] sm:rounded-2xl md:h-[unset] md:px-14 md:pt-12"
|
:close-on-press-escape="false"
|
||||||
@escape-key-down="(e) => e.preventDefault()"
|
:footer="false"
|
||||||
@interact-outside="(e) => e.preventDefault()"
|
:fullscreen-button="false"
|
||||||
>
|
class="border-none px-10 py-6 text-center shadow-xl sm:w-[600px] sm:rounded-2xl md:h-[unset]"
|
||||||
<DialogTitle>
|
header-class="hidden"
|
||||||
<VbenAvatar :src="avatar" class="mx-auto size-20" />
|
>
|
||||||
</DialogTitle>
|
<VbenAvatar :src="avatar" class="mx-auto mb-6 size-20" />
|
||||||
<VisuallyHidden>
|
<AuthenticationLogin
|
||||||
<DialogDescription />
|
v-bind="forwarded"
|
||||||
</VisuallyHidden>
|
:show-forget-password="false"
|
||||||
<AuthenticationLogin
|
:show-register="false"
|
||||||
v-bind="forwarded"
|
:show-remember-me="false"
|
||||||
:show-forget-password="false"
|
:sub-title="$t('authentication.loginAgainSubTitle')"
|
||||||
:show-register="false"
|
:title="$t('authentication.loginAgainTitle')"
|
||||||
:show-remember-me="false"
|
/>
|
||||||
:sub-title="$t('authentication.loginAgainSubTitle')"
|
</Modal>
|
||||||
:title="$t('authentication.loginAgainTitle')"
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -22,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vben-core/layout-ui": "workspace:*",
|
"@vben-core/layout-ui": "workspace:*",
|
||||||
"@vben-core/menu-ui": "workspace:*",
|
"@vben-core/menu-ui": "workspace:*",
|
||||||
|
"@vben-core/popup-ui": "workspace:*",
|
||||||
"@vben-core/shadcn-ui": "workspace:*",
|
"@vben-core/shadcn-ui": "workspace:*",
|
||||||
"@vben-core/tabs-ui": "workspace:*",
|
"@vben-core/tabs-ui": "workspace:*",
|
||||||
"@vben/constants": "workspace:*",
|
"@vben/constants": "workspace:*",
|
||||||
|
@@ -12,17 +12,9 @@ import {
|
|||||||
} from '@vben/icons';
|
} from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { isWindowsOs } from '@vben/utils';
|
import { isWindowsOs } from '@vben/utils';
|
||||||
import {
|
import { useVbenModal } from '@vben-core/popup-ui';
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@vben-core/shadcn-ui';
|
|
||||||
|
|
||||||
import { useMagicKeys, useToggle, whenever } from '@vueuse/core';
|
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||||
|
|
||||||
import SearchPanel from './search-panel.vue';
|
import SearchPanel from './search-panel.vue';
|
||||||
|
|
||||||
@@ -38,12 +30,18 @@ const props = withDefaults(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [open, toggleOpen] = useToggle();
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const open = modalApi.useStore((state) => state.isOpen);
|
||||||
|
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
const searchInputRef = ref<HTMLInputElement>();
|
const searchInputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
open.value = false;
|
modalApi.close();
|
||||||
keyword.value = '';
|
keyword.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +49,7 @@ const keys = useMagicKeys();
|
|||||||
const cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];
|
const cmd = isWindowsOs() ? keys['ctrl+k'] : keys['cmd+k'];
|
||||||
whenever(cmd!, () => {
|
whenever(cmd!, () => {
|
||||||
if (props.enableShortcutKey) {
|
if (props.enableShortcutKey) {
|
||||||
open.value = true;
|
modalApi.open();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,6 +73,10 @@ const toggleKeydownListener = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleOpen = () => {
|
||||||
|
open.value ? modalApi.close() : modalApi.open();
|
||||||
|
};
|
||||||
|
|
||||||
watch(() => props.enableShortcutKey, toggleKeydownListener);
|
watch(() => props.enableShortcutKey, toggleKeydownListener);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -88,67 +90,58 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<Dialog :open="open">
|
<Modal :fullscreen-button="false" class="w-[600px]" header-class="py-2">
|
||||||
<DialogTrigger as-child>
|
<template #title>
|
||||||
<div
|
<div class="flex items-center">
|
||||||
class="md:bg-accent group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-none"
|
<Search class="text-muted-foreground mr-2 size-4" />
|
||||||
@click="toggleOpen()"
|
<input
|
||||||
>
|
ref="searchInputRef"
|
||||||
<Search
|
v-model="keyword"
|
||||||
class="text-muted-foreground group-hover:text-foreground size-3 group-hover:opacity-100"
|
:placeholder="$t('widgets.search.searchNavigate')"
|
||||||
|
class="ring-none placeholder:text-muted-foreground w-[80%] rounded-md border border-none bg-transparent p-2 pl-0 text-sm font-normal outline-none ring-0 ring-offset-transparent focus-visible:ring-transparent"
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
class="text-muted-foreground group-hover:text-foreground hidden text-xs duration-300 md:block"
|
|
||||||
>
|
|
||||||
{{ $t('widgets.search.title') }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="enableShortcutKey"
|
|
||||||
class="bg-background border-foreground/60 text-muted-foreground group-hover:text-foreground relative hidden rounded-sm rounded-r-xl px-1.5 py-1 text-xs leading-none group-hover:opacity-100 md:block"
|
|
||||||
>
|
|
||||||
{{ isWindowsOs() ? 'Ctrl' : '⌘' }}
|
|
||||||
<kbd>K</kbd>
|
|
||||||
</span>
|
|
||||||
<span v-else></span>
|
|
||||||
</div>
|
</div>
|
||||||
</DialogTrigger>
|
</template>
|
||||||
<DialogContent
|
|
||||||
class="top-0 h-full w-full -translate-y-0 border-none p-0 shadow-xl sm:top-[10%] sm:h-[unset] sm:w-[600px] sm:rounded-2xl"
|
<SearchPanel :keyword="keyword" :menus="menus" @close="handleClose" />
|
||||||
@close="handleClose"
|
<template #footer>
|
||||||
>
|
<div class="flex w-full justify-start text-xs">
|
||||||
<DialogHeader>
|
<div class="mr-2 flex items-center">
|
||||||
<DialogTitle
|
|
||||||
class="border-border flex h-12 items-center gap-3 border-b px-5 font-normal"
|
|
||||||
>
|
|
||||||
<Search class="text-muted-foreground size-4" />
|
|
||||||
<input
|
|
||||||
ref="searchInputRef"
|
|
||||||
v-model="keyword"
|
|
||||||
:placeholder="$t('widgets.search.searchNavigate')"
|
|
||||||
class="ring-none placeholder:text-muted-foreground w-[80%] rounded-md border border-none bg-transparent p-2 pl-0 text-sm outline-none ring-0 ring-offset-transparent focus-visible:ring-transparent"
|
|
||||||
/>
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription />
|
|
||||||
</DialogHeader>
|
|
||||||
<SearchPanel :keyword="keyword" :menus="menus" @close="handleClose" />
|
|
||||||
<DialogFooter
|
|
||||||
class="text-muted-foreground border-border hidden flex-row rounded-b-2xl border-t px-4 py-2 text-xs sm:flex sm:justify-start sm:gap-x-4"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<CornerDownLeft class="mr-1 size-3" />
|
<CornerDownLeft class="mr-1 size-3" />
|
||||||
{{ $t('widgets.search.select') }}
|
{{ $t('widgets.search.select') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="mr-2 flex items-center">
|
||||||
<ArrowUp class="mr-2 size-3" />
|
<ArrowUp class="mr-1 size-3" />
|
||||||
<ArrowDown class="mr-2 size-3" />
|
<ArrowDown class="mr-1 size-3" />
|
||||||
{{ $t('widgets.search.navigate') }}
|
{{ $t('widgets.search.navigate') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<MdiKeyboardEsc class="mr-1 size-3" />
|
<MdiKeyboardEsc class="mr-1 size-3" />
|
||||||
{{ $t('widgets.search.close') }}
|
{{ $t('widgets.search.close') }}
|
||||||
</div>
|
</div>
|
||||||
</DialogFooter>
|
</div>
|
||||||
</DialogContent>
|
</template>
|
||||||
</Dialog>
|
</Modal>
|
||||||
|
<div
|
||||||
|
class="md:bg-accent group flex h-8 cursor-pointer items-center gap-3 rounded-2xl border-none bg-none px-2 py-0.5 outline-none"
|
||||||
|
@click="toggleOpen()"
|
||||||
|
>
|
||||||
|
<Search
|
||||||
|
class="text-muted-foreground group-hover:text-foreground size-3 group-hover:opacity-100"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-muted-foreground group-hover:text-foreground hidden text-xs duration-300 md:block"
|
||||||
|
>
|
||||||
|
{{ $t('widgets.search.title') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="enableShortcutKey"
|
||||||
|
class="bg-background border-foreground/60 text-muted-foreground group-hover:text-foreground relative hidden rounded-sm rounded-r-xl px-1.5 py-1 text-xs leading-none group-hover:opacity-100 md:block"
|
||||||
|
>
|
||||||
|
{{ isWindowsOs() ? 'Ctrl' : '⌘' }}
|
||||||
|
<kbd>K</kbd>
|
||||||
|
</span>
|
||||||
|
<span v-else></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -217,14 +217,14 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VbenScrollbar>
|
<VbenScrollbar>
|
||||||
<div class="!flex h-full justify-center px-4 sm:max-h-[450px]">
|
<div class="!flex h-full justify-center px-2 sm:max-h-[450px]">
|
||||||
<!-- 无搜索结果 -->
|
<!-- 无搜索结果 -->
|
||||||
<div
|
<div
|
||||||
v-if="keyword && searchResults.length === 0"
|
v-if="keyword && searchResults.length === 0"
|
||||||
class="text-muted-foreground text-center"
|
class="text-muted-foreground text-center"
|
||||||
>
|
>
|
||||||
<SearchX class="mx-auto size-12" />
|
<SearchX class="mx-auto mt-4 size-12" />
|
||||||
<p class="my-10 text-xs">
|
<p class="mb-10 mt-6 text-xs">
|
||||||
{{ $t('widgets.search.noResults') }}
|
{{ $t('widgets.search.noResults') }}
|
||||||
<span class="text-foreground text-sm font-medium">
|
<span class="text-foreground text-sm font-medium">
|
||||||
"{{ keyword }}"
|
"{{ keyword }}"
|
||||||
|
@@ -1,12 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben-core/popup-ui';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
VbenAvatar,
|
VbenAvatar,
|
||||||
VbenButton,
|
VbenButton,
|
||||||
VbenInputPassword,
|
VbenInputPassword,
|
||||||
@@ -28,28 +24,33 @@ interface RegisterEmits {
|
|||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'LockScreenModal',
|
name: 'LockScreenModal',
|
||||||
});
|
});
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
avatar: '',
|
avatar: '',
|
||||||
text: '',
|
text: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
submit: RegisterEmits['submit'];
|
submit: RegisterEmits['submit'];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const [Modal] = useVbenModal({
|
||||||
|
onConfirm() {
|
||||||
|
handleSubmit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const formState = reactive({
|
const formState = reactive({
|
||||||
lockScreenPassword: '',
|
lockScreenPassword: '',
|
||||||
submitted: false,
|
submitted: false,
|
||||||
});
|
});
|
||||||
const openModal = defineModel<boolean>('open');
|
|
||||||
const passwordStatus = computed(() => {
|
const passwordStatus = computed(() => {
|
||||||
return formState.submitted && !formState.lockScreenPassword
|
return formState.submitted && !formState.lockScreenPassword
|
||||||
? 'error'
|
? 'error'
|
||||||
: 'default';
|
: 'default';
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleClose() {
|
|
||||||
openModal.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
formState.submitted = true;
|
formState.submitted = true;
|
||||||
if (passwordStatus.value !== 'default') {
|
if (passwordStatus.value !== 'default') {
|
||||||
@@ -62,51 +63,40 @@ function handleSubmit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<Modal
|
||||||
<Dialog :open="openModal">
|
:footer="false"
|
||||||
<DialogContent
|
:fullscreen-button="false"
|
||||||
class="top-0 h-full w-full -translate-y-0 border-none p-0 shadow-xl sm:top-[20%] sm:h-[unset] sm:w-[600px] sm:rounded-2xl"
|
:title="$t('widgets.lockScreen.title')"
|
||||||
@close="handleClose"
|
>
|
||||||
>
|
<div
|
||||||
<DialogDescription />
|
class="mb-10 flex w-full flex-col items-center px-10"
|
||||||
<DialogHeader>
|
@keypress.enter.prevent="handleSubmit"
|
||||||
<DialogTitle
|
>
|
||||||
class="border-border flex h-8 items-center px-5 font-normal"
|
<div class="w-full">
|
||||||
>
|
<div class="ml-2 flex w-full flex-col items-center">
|
||||||
{{ $t('widgets.lockScreen.title') }}
|
<VbenAvatar
|
||||||
</DialogTitle>
|
:src="avatar"
|
||||||
</DialogHeader>
|
class="size-20"
|
||||||
<div
|
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
|
||||||
class="mb-10 flex w-full flex-col items-center"
|
/>
|
||||||
@keypress.enter.prevent="handleSubmit"
|
<div class="text-foreground my-6 flex items-center font-medium">
|
||||||
>
|
{{ text }}
|
||||||
<div class="w-2/3">
|
|
||||||
<div class="ml-2 flex w-full flex-col items-center">
|
|
||||||
<VbenAvatar
|
|
||||||
:src="avatar"
|
|
||||||
class="size-24"
|
|
||||||
dot-class="bottom-0 right-1 border-2 size-4 bg-green-500"
|
|
||||||
/>
|
|
||||||
<div class="text-foreground my-6 flex items-center font-medium">
|
|
||||||
{{ text }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<VbenInputPassword
|
|
||||||
v-model="formState.lockScreenPassword"
|
|
||||||
:error-tip="$t('widgets.lockScreen.placeholder')"
|
|
||||||
:label="$t('widgets.lockScreen.password')"
|
|
||||||
:placeholder="$t('widgets.lockScreen.placeholder')"
|
|
||||||
:status="passwordStatus"
|
|
||||||
name="password"
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
<VbenButton class="w-full" @click="handleSubmit">
|
|
||||||
{{ $t('widgets.lockScreen.screenButton') }}
|
|
||||||
</VbenButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
<VbenInputPassword
|
||||||
</Dialog>
|
v-model="formState.lockScreenPassword"
|
||||||
</div>
|
:error-tip="$t('widgets.lockScreen.placeholder')"
|
||||||
|
:label="$t('widgets.lockScreen.password')"
|
||||||
|
:placeholder="$t('widgets.lockScreen.placeholder')"
|
||||||
|
:status="passwordStatus"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<VbenButton class="w-full" @click="handleSubmit">
|
||||||
|
{{ $t('widgets.lockScreen.screenButton') }}
|
||||||
|
</VbenButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -15,7 +15,7 @@ const shortcutKeysGlobalSearch = defineModel<boolean>(
|
|||||||
'shortcutKeysGlobalSearch',
|
'shortcutKeysGlobalSearch',
|
||||||
);
|
);
|
||||||
const shortcutKeysLogout = defineModel<boolean>('shortcutKeysLogout');
|
const shortcutKeysLogout = defineModel<boolean>('shortcutKeysLogout');
|
||||||
const shortcutKeysPreferences = defineModel<boolean>('shortcutKeysPreferences');
|
// const shortcutKeysPreferences = defineModel<boolean>('shortcutKeysPreferences');
|
||||||
const shortcutKeysLockScreen = defineModel<boolean>('shortcutKeysLockScreen');
|
const shortcutKeysLockScreen = defineModel<boolean>('shortcutKeysLockScreen');
|
||||||
|
|
||||||
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
||||||
@@ -39,10 +39,10 @@ const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
|||||||
{{ $t('preferences.shortcutKeys.logout') }}
|
{{ $t('preferences.shortcutKeys.logout') }}
|
||||||
<template #shortcut> {{ altView }} Q </template>
|
<template #shortcut> {{ altView }} Q </template>
|
||||||
</SwitchItem>
|
</SwitchItem>
|
||||||
<SwitchItem v-model="shortcutKeysPreferences" :disabled="!shortcutKeysEnable">
|
<!-- <SwitchItem v-model="shortcutKeysPreferences" :disabled="!shortcutKeysEnable">
|
||||||
{{ $t('preferences.shortcutKeys.preferences') }}
|
{{ $t('preferences.shortcutKeys.preferences') }}
|
||||||
<template #shortcut> {{ altView }} , </template>
|
<template #shortcut> {{ altView }} , </template>
|
||||||
</SwitchItem>
|
</SwitchItem> -->
|
||||||
<SwitchItem v-model="shortcutKeysLockScreen" :disabled="!shortcutKeysEnable">
|
<SwitchItem v-model="shortcutKeysLockScreen" :disabled="!shortcutKeysEnable">
|
||||||
{{ $t('widgets.lockScreen.title') }}
|
{{ $t('widgets.lockScreen.title') }}
|
||||||
<template #shortcut> {{ altView }} L </template>
|
<template #shortcut> {{ altView }} L </template>
|
||||||
|
@@ -14,7 +14,7 @@ import type { SegmentedItem } from '@vben-core/shadcn-ui';
|
|||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { Copy, RotateCw, Settings } from '@vben/icons';
|
import { Copy, RotateCw } from '@vben/icons';
|
||||||
import { $t, loadLocaleMessages } from '@vben/locales';
|
import { $t, loadLocaleMessages } from '@vben/locales';
|
||||||
import {
|
import {
|
||||||
clearPreferencesCache,
|
clearPreferencesCache,
|
||||||
@@ -22,12 +22,12 @@ import {
|
|||||||
resetPreferences,
|
resetPreferences,
|
||||||
usePreferences,
|
usePreferences,
|
||||||
} from '@vben/preferences';
|
} from '@vben/preferences';
|
||||||
|
import { useVbenDrawer } from '@vben-core/popup-ui';
|
||||||
import {
|
import {
|
||||||
useToast,
|
useToast,
|
||||||
VbenButton,
|
VbenButton,
|
||||||
VbenIconButton,
|
VbenIconButton,
|
||||||
VbenSegmented,
|
VbenSegmented,
|
||||||
VbenSheet,
|
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { useClipboard } from '@vueuse/core';
|
import { useClipboard } from '@vueuse/core';
|
||||||
@@ -52,7 +52,6 @@ import {
|
|||||||
Theme,
|
Theme,
|
||||||
Widget,
|
Widget,
|
||||||
} from './blocks';
|
} from './blocks';
|
||||||
import { useOpenPreferences } from './use-open-preferences';
|
|
||||||
|
|
||||||
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -134,9 +133,7 @@ const shortcutKeysGlobalSearch = defineModel<boolean>(
|
|||||||
const shortcutKeysGlobalLogout = defineModel<boolean>(
|
const shortcutKeysGlobalLogout = defineModel<boolean>(
|
||||||
'shortcutKeysGlobalLogout',
|
'shortcutKeysGlobalLogout',
|
||||||
);
|
);
|
||||||
const shortcutKeysGlobalPreferences = defineModel<boolean>(
|
|
||||||
'shortcutKeysGlobalPreferences',
|
|
||||||
);
|
|
||||||
const shortcutKeysGlobalLockScreen = defineModel<boolean>(
|
const shortcutKeysGlobalLockScreen = defineModel<boolean>(
|
||||||
'shortcutKeysGlobalLockScreen',
|
'shortcutKeysGlobalLockScreen',
|
||||||
);
|
);
|
||||||
@@ -161,6 +158,8 @@ const {
|
|||||||
} = usePreferences();
|
} = usePreferences();
|
||||||
const { copy } = useClipboard();
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
const [Drawer] = useVbenDrawer();
|
||||||
|
|
||||||
const activeTab = ref('appearance');
|
const activeTab = ref('appearance');
|
||||||
|
|
||||||
const tabs = computed((): SegmentedItem[] => {
|
const tabs = computed((): SegmentedItem[] => {
|
||||||
@@ -193,8 +192,6 @@ const showBreadcrumbConfig = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { openPreferences } = useOpenPreferences();
|
|
||||||
|
|
||||||
async function handleCopy() {
|
async function handleCopy() {
|
||||||
await copy(JSON.stringify(diffPreference.value, null, 2));
|
await copy(JSON.stringify(diffPreference.value, null, 2));
|
||||||
|
|
||||||
@@ -225,21 +222,11 @@ async function handleReset() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<VbenSheet
|
<Drawer
|
||||||
v-model:open="openPreferences"
|
|
||||||
:description="$t('preferences.subtitle')"
|
:description="$t('preferences.subtitle')"
|
||||||
:title="$t('preferences.title')"
|
:title="$t('preferences.title')"
|
||||||
|
class="sm:max-w-sm"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
|
||||||
<slot name="trigger">
|
|
||||||
<VbenButton
|
|
||||||
:title="$t('preferences.title')"
|
|
||||||
class="bg-primary flex-col-center size-10 cursor-pointer rounded-l-lg rounded-r-none border-none"
|
|
||||||
>
|
|
||||||
<Settings class="size-5" />
|
|
||||||
</VbenButton>
|
|
||||||
</slot>
|
|
||||||
</template>
|
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<VbenIconButton
|
<VbenIconButton
|
||||||
@@ -256,7 +243,7 @@ async function handleReset() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="p-4 pt-4">
|
<div class="p-1">
|
||||||
<VbenSegmented v-model="activeTab" :tabs="tabs">
|
<VbenSegmented v-model="activeTab" :tabs="tabs">
|
||||||
<template #general>
|
<template #general>
|
||||||
<Block :title="$t('preferences.general')">
|
<Block :title="$t('preferences.general')">
|
||||||
@@ -402,9 +389,6 @@ async function handleReset() {
|
|||||||
v-model:shortcut-keys-global-search="shortcutKeysGlobalSearch"
|
v-model:shortcut-keys-global-search="shortcutKeysGlobalSearch"
|
||||||
v-model:shortcut-keys-lock-screen="shortcutKeysGlobalLockScreen"
|
v-model:shortcut-keys-lock-screen="shortcutKeysGlobalLockScreen"
|
||||||
v-model:shortcut-keys-logout="shortcutKeysGlobalLogout"
|
v-model:shortcut-keys-logout="shortcutKeysGlobalLogout"
|
||||||
v-model:shortcut-keys-preferences="
|
|
||||||
shortcutKeysGlobalPreferences
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
</template>
|
</template>
|
||||||
@@ -433,6 +417,6 @@ async function handleReset() {
|
|||||||
{{ $t('preferences.clearAndLogout') }}
|
{{ $t('preferences.clearAndLogout') }}
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
</template>
|
</template>
|
||||||
</VbenSheet>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
@@ -1,11 +1,18 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { loadLocaleMessages } from '@vben/locales';
|
import { Settings } from '@vben/icons';
|
||||||
|
import { $t, loadLocaleMessages } from '@vben/locales';
|
||||||
import { preferences, updatePreferences } from '@vben/preferences';
|
import { preferences, updatePreferences } from '@vben/preferences';
|
||||||
import { capitalizeFirstLetter } from '@vben/utils';
|
import { capitalizeFirstLetter } from '@vben/utils';
|
||||||
|
import { useVbenDrawer } from '@vben-core/popup-ui';
|
||||||
|
import { VbenButton } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import PreferencesSheet from './preferences-sheet.vue';
|
import PreferencesDrawer from './preferences-drawer.vue';
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
|
connectedComponent: PreferencesDrawer,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* preferences 转成 vue props
|
* preferences 转成 vue props
|
||||||
@@ -47,9 +54,18 @@ const listen = computed(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<PreferencesSheet v-bind="attrs" v-on="listen">
|
<div>
|
||||||
<template #trigger>
|
<Drawer v-bind="attrs" v-on="listen" />
|
||||||
<slot></slot>
|
|
||||||
</template>
|
<div @click="() => drawerApi.open()">
|
||||||
</PreferencesSheet>
|
<slot>
|
||||||
|
<VbenButton
|
||||||
|
:title="$t('preferences.title')"
|
||||||
|
class="bg-primary flex-col-center size-10 cursor-pointer rounded-l-lg rounded-r-none border-none"
|
||||||
|
>
|
||||||
|
<Settings class="size-5" />
|
||||||
|
</VbenButton>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -4,11 +4,12 @@ import type { AnyFunction } from '@vben/types';
|
|||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { LockKeyhole, LogOut, SwatchBook } from '@vben/icons';
|
import { LockKeyhole, LogOut } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { preferences, usePreferences } from '@vben/preferences';
|
import { preferences, usePreferences } from '@vben/preferences';
|
||||||
import { useLockStore } from '@vben/stores';
|
import { useLockStore } from '@vben/stores';
|
||||||
import { isWindowsOs } from '@vben/utils';
|
import { isWindowsOs } from '@vben/utils';
|
||||||
|
import { useVbenModal } from '@vben-core/popup-ui';
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
VbenAlertDialog,
|
|
||||||
VbenAvatar,
|
VbenAvatar,
|
||||||
VbenIcon,
|
VbenIcon,
|
||||||
} from '@vben-core/shadcn-ui';
|
} from '@vben-core/shadcn-ui';
|
||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||||
|
|
||||||
import { LockScreenModal } from '../lock-screen';
|
import { LockScreenModal } from '../lock-screen';
|
||||||
import { useOpenPreferences } from '../preferences';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
@@ -72,16 +71,18 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
|
|
||||||
const emit = defineEmits<{ logout: [] }>();
|
const emit = defineEmits<{ logout: [] }>();
|
||||||
const openPopover = ref(false);
|
const openPopover = ref(false);
|
||||||
const openDialog = ref(false);
|
|
||||||
const openLock = ref(false);
|
|
||||||
|
|
||||||
const {
|
const { globalLockScreenShortcutKey, globalLogoutShortcutKey } =
|
||||||
globalLockScreenShortcutKey,
|
usePreferences();
|
||||||
globalLogoutShortcutKey,
|
|
||||||
globalPreferencesShortcutKey,
|
|
||||||
} = usePreferences();
|
|
||||||
const lockStore = useLockStore();
|
const lockStore = useLockStore();
|
||||||
const { handleOpenPreference } = useOpenPreferences();
|
const [LockModal, lockModalApi] = useVbenModal({
|
||||||
|
connectedComponent: LockScreenModal,
|
||||||
|
});
|
||||||
|
const [LogoutModal, logoutModalApi] = useVbenModal({
|
||||||
|
onConfirm() {
|
||||||
|
handleSubmitLogout();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
|
||||||
|
|
||||||
@@ -97,12 +98,8 @@ const enableShortcutKey = computed(() => {
|
|||||||
return props.enableShortcutKey && preferences.shortcutKeys.enable;
|
return props.enableShortcutKey && preferences.shortcutKeys.enable;
|
||||||
});
|
});
|
||||||
|
|
||||||
const enablePreferencesShortcutKey = computed(() => {
|
|
||||||
return props.enableShortcutKey && globalPreferencesShortcutKey.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleOpenLock() {
|
function handleOpenLock() {
|
||||||
openLock.value = true;
|
lockModalApi.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmitLock({
|
function handleSubmitLock({
|
||||||
@@ -110,18 +107,19 @@ function handleSubmitLock({
|
|||||||
}: {
|
}: {
|
||||||
lockScreenPassword: string;
|
lockScreenPassword: string;
|
||||||
}) {
|
}) {
|
||||||
openLock.value = false;
|
lockModalApi.close();
|
||||||
lockStore.lockScreen(lockScreenPassword);
|
lockStore.lockScreen(lockScreenPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
// emit
|
// emit
|
||||||
openDialog.value = true;
|
logoutModalApi.open();
|
||||||
openPopover.value = false;
|
openPopover.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmitLogout() {
|
function handleSubmitLogout() {
|
||||||
emit('logout');
|
emit('logout');
|
||||||
openDialog.value = false;
|
logoutModalApi.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableShortcutKey.value) {
|
if (enableShortcutKey.value) {
|
||||||
@@ -132,12 +130,6 @@ if (enableShortcutKey.value) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
whenever(keys['Alt+Comma']!, () => {
|
|
||||||
if (enablePreferencesShortcutKey.value) {
|
|
||||||
handleOpenPreference();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
whenever(keys['Alt+KeyL']!, () => {
|
whenever(keys['Alt+KeyL']!, () => {
|
||||||
if (enableLockScreenShortcutKey.value) {
|
if (enableLockScreenShortcutKey.value) {
|
||||||
handleOpenLock();
|
handleOpenLock();
|
||||||
@@ -147,21 +139,25 @@ if (enableShortcutKey.value) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LockScreenModal
|
<LockModal
|
||||||
v-if="preferences.widget.lockScreen"
|
v-if="preferences.widget.lockScreen"
|
||||||
v-model:open="openLock"
|
|
||||||
:avatar="avatar"
|
:avatar="avatar"
|
||||||
:text="text"
|
:text="text"
|
||||||
@submit="handleSubmitLock"
|
@submit="handleSubmitLock"
|
||||||
/>
|
/>
|
||||||
<VbenAlertDialog
|
|
||||||
v-model:open="openDialog"
|
<LogoutModal
|
||||||
:cancel-text="$t('common.cancel')"
|
:cancel-text="$t('common.cancel')"
|
||||||
:content="$t('widgets.logoutTip')"
|
:confirm-text="$t('common.confirm')"
|
||||||
:submit-text="$t('common.confirm')"
|
:fullscreen-button="false"
|
||||||
:title="$t('common.prompt')"
|
:title="$t('common.prompt')"
|
||||||
@submit="handleSubmitLogout"
|
centered
|
||||||
/>
|
content-class="px-8 min-h-10"
|
||||||
|
footer-class="border-none mb-4 mr-4"
|
||||||
|
header-class="border-none"
|
||||||
|
>
|
||||||
|
{{ $t('widgets.logoutTip') }}
|
||||||
|
</LogoutModal>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
@@ -205,17 +201,6 @@ if (enableShortcutKey.value) {
|
|||||||
{{ menu.text }}
|
{{ menu.text }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
|
||||||
v-if="preferences.app.enablePreferences"
|
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
|
||||||
@click="handleOpenPreference"
|
|
||||||
>
|
|
||||||
<SwatchBook class="mr-2 size-4" />
|
|
||||||
{{ $t('preferences.title') }}
|
|
||||||
<DropdownMenuShortcut v-if="enablePreferencesShortcutKey">
|
|
||||||
{{ altView }} ,
|
|
||||||
</DropdownMenuShortcut>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
v-if="preferences.widget.lockScreen"
|
v-if="preferences.widget.lockScreen"
|
||||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { findMenuByPath, findRootMenuByPath } from './find-menu-by-path';
|
import { findMenuByPath, findRootMenuByPath } from '../find-menu-by-path';
|
||||||
|
|
||||||
// 示例菜单数据
|
// 示例菜单数据
|
||||||
const menus: any[] = [
|
const menus: any[] = [
|
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { generateMenus } from './generate-menus'; // 替换为您的实际路径
|
import { generateMenus } from '../generate-menus'; // 替换为您的实际路径
|
||||||
import {
|
import {
|
||||||
createRouter,
|
createRouter,
|
||||||
createWebHistory,
|
createWebHistory,
|
@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
generateRoutesByFrontend,
|
generateRoutesByFrontend,
|
||||||
hasAuthority,
|
hasAuthority,
|
||||||
} from './generate-routes-frontend';
|
} from '../generate-routes-frontend';
|
||||||
|
|
||||||
// Mock 路由数据
|
// Mock 路由数据
|
||||||
const mockRoutes = [
|
const mockRoutes = [
|
@@ -1,10 +1,10 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
import type { RouteModuleType } from './merge-route-modules';
|
import type { RouteModuleType } from '../merge-route-modules';
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { mergeRouteModules } from './merge-route-modules';
|
import { mergeRouteModules } from '../merge-route-modules';
|
||||||
|
|
||||||
describe('mergeRouteModules', () => {
|
describe('mergeRouteModules', () => {
|
||||||
it('should merge route modules correctly', () => {
|
it('should merge route modules correctly', () => {
|
@@ -63,6 +63,12 @@
|
|||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"title": "Examples",
|
"title": "Examples",
|
||||||
|
"modal": {
|
||||||
|
"title": "Modal"
|
||||||
|
},
|
||||||
|
"drawer": {
|
||||||
|
"title": "Drawer"
|
||||||
|
},
|
||||||
"ellipsis": {
|
"ellipsis": {
|
||||||
"title": "EllipsisText"
|
"title": "EllipsisText"
|
||||||
}
|
}
|
||||||
|
@@ -63,6 +63,12 @@
|
|||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"title": "示例",
|
"title": "示例",
|
||||||
|
"modal": {
|
||||||
|
"title": "弹窗"
|
||||||
|
},
|
||||||
|
"drawer": {
|
||||||
|
"title": "抽屉"
|
||||||
|
},
|
||||||
"ellipsis": {
|
"ellipsis": {
|
||||||
"title": "文本省略"
|
"title": "文本省略"
|
||||||
}
|
}
|
||||||
|
@@ -16,13 +16,29 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/examples',
|
path: '/examples',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
name: 'EllipsisDemo',
|
name: 'EllipsisExample',
|
||||||
path: 'ellipsis',
|
path: '/examples/ellipsis',
|
||||||
component: () => import('#/views/examples/ellipsis/index.vue'),
|
component: () => import('#/views/examples/ellipsis/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('page.examples.ellipsis.title'),
|
title: $t('page.examples.ellipsis.title'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ModalExample',
|
||||||
|
path: '/examples/modal',
|
||||||
|
component: () => import('#/views/examples/modal/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.examples.modal.title'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DrawerExample',
|
||||||
|
path: '/examples/drawer',
|
||||||
|
component: () => import('#/views/examples/drawer/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.examples.drawer.title'),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
40
playground/src/views/examples/drawer/auto-height-demo.vue
Normal file
40
playground/src/views/examples/drawer/auto-height-demo.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
|
onCancel() {
|
||||||
|
drawerApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// drawerApi.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = ref<number[]>([]);
|
||||||
|
|
||||||
|
list.value = Array.from({ length: 10 }, (_v, k) => k + 1);
|
||||||
|
|
||||||
|
function handleUpdate() {
|
||||||
|
list.value = Array.from({ length: 6 }, (_v, k) => k + 1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Drawer title="自动计算高度">
|
||||||
|
<div
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item"
|
||||||
|
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #prepend-footer>
|
||||||
|
<Button type="link" @click="handleUpdate">点击更新数据</Button>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
32
playground/src/views/examples/drawer/base-demo.vue
Normal file
32
playground/src/views/examples/drawer/base-demo.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
|
onCancel() {
|
||||||
|
drawerApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// drawerApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen) {
|
||||||
|
if (isOpen) {
|
||||||
|
drawerApi.setState({ loading: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
drawerApi.setState({ loading: false });
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Drawer title="基础抽屉示例" title-tooltip="标题提示内容">
|
||||||
|
<template #extra> extra </template>
|
||||||
|
base demo
|
||||||
|
|
||||||
|
<!-- <template #prepend-footer> slot </template> -->
|
||||||
|
<!-- <template #append-footer> prepend slot </template> -->
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
31
playground/src/views/examples/drawer/dynamic-demo.vue
Normal file
31
playground/src/views/examples/drawer/dynamic-demo.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
|
onCancel() {
|
||||||
|
drawerApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// drawerApi.close();
|
||||||
|
},
|
||||||
|
title: '动态修改配置示例',
|
||||||
|
});
|
||||||
|
|
||||||
|
// const state = drawerApi.useStore();
|
||||||
|
|
||||||
|
function handleUpdateTitle() {
|
||||||
|
drawerApi.setState({ title: '内部动态标题' });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Drawer>
|
||||||
|
<div class="flex-col-center">
|
||||||
|
<Button class="mb-3" type="primary" @click="handleUpdateTitle()">
|
||||||
|
内部动态修改标题
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
90
playground/src/views/examples/drawer/index.vue
Normal file
90
playground/src/views/examples/drawer/index.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import AutoHeightDemo from './auto-height-demo.vue';
|
||||||
|
import BaseDemo from './base-demo.vue';
|
||||||
|
import DynamicDemo from './dynamic-demo.vue';
|
||||||
|
import SharedDataDemo from './shared-data-demo.vue';
|
||||||
|
|
||||||
|
const [BaseDrawer, baseDrawerApi] = useVbenDrawer({
|
||||||
|
// 链接抽离的组件
|
||||||
|
connectedComponent: BaseDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AutoHeightDrawer, autoHeightDrawerApi] = useVbenDrawer({
|
||||||
|
// 链接抽离的组件
|
||||||
|
connectedComponent: AutoHeightDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DynamicDrawer, dynamicDrawerApi] = useVbenDrawer({
|
||||||
|
connectedComponent: DynamicDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [SharedDataDrawer, sharedDrawerApi] = useVbenDrawer({
|
||||||
|
connectedComponent: SharedDataDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openBaseDrawer() {
|
||||||
|
baseDrawerApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAutoHeightDrawer() {
|
||||||
|
autoHeightDrawerApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDynamicDrawer() {
|
||||||
|
dynamicDrawerApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateTitle() {
|
||||||
|
dynamicDrawerApi.setState({ title: '外部动态标题' });
|
||||||
|
dynamicDrawerApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSharedDrawer() {
|
||||||
|
sharedDrawerApi.setData({
|
||||||
|
content: '外部传递的数据 content',
|
||||||
|
payload: '外部传递的数据 payload',
|
||||||
|
});
|
||||||
|
sharedDrawerApi.open();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
description="抽屉组件通常用于在当前页面上显示一个覆盖层,用以展示重要信息或提供用户交互界面。"
|
||||||
|
title="抽屉组件示例"
|
||||||
|
>
|
||||||
|
<BaseDrawer />
|
||||||
|
<AutoHeightDrawer />
|
||||||
|
<DynamicDrawer />
|
||||||
|
<SharedDataDrawer />
|
||||||
|
|
||||||
|
<Card class="mb-4" title="基本使用">
|
||||||
|
<p class="mb-3">一个基础的抽屉示例</p>
|
||||||
|
<Button type="primary" @click="openBaseDrawer">打开抽屉</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-4" title="内容高度自适应滚动">
|
||||||
|
<p class="mb-3">可根据内容自动计算滚动高度</p>
|
||||||
|
<Button type="primary" @click="openAutoHeightDrawer">打开抽屉</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-4" title="动态配置示例">
|
||||||
|
<p class="mb-3">通过 setState 动态调整抽屉数据</p>
|
||||||
|
<Button type="primary" @click="openDynamicDrawer">打开抽屉</Button>
|
||||||
|
<Button class="ml-2" type="primary" @click="handleUpdateTitle">
|
||||||
|
从外部修改标题并打开
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-4" title="内外数据共享示例">
|
||||||
|
<p class="mb-3">通过共享 sharedData 来进行数据交互</p>
|
||||||
|
<Button type="primary" @click="openSharedDrawer">
|
||||||
|
打开抽屉并传递数据
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
29
playground/src/views/examples/drawer/shared-data-demo.vue
Normal file
29
playground/src/views/examples/drawer/shared-data-demo.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const data = ref();
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useVbenDrawer({
|
||||||
|
onCancel() {
|
||||||
|
drawerApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// drawerApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
data.value = drawerApi.getData<Record<string, any>>();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Drawer title="数据共享示例">
|
||||||
|
<div class="flex-col-center">外部传递数据: {{ data }}</div>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
@@ -13,7 +13,7 @@ const text = ref(longText);
|
|||||||
<template>
|
<template>
|
||||||
<Page
|
<Page
|
||||||
description="用于多行文本省略,支持点击展开和自定义内容。"
|
description="用于多行文本省略,支持点击展开和自定义内容。"
|
||||||
title="文本省略示例"
|
title="文本省略组件示例"
|
||||||
>
|
>
|
||||||
<Card class="mb-4" title="基本使用">
|
<Card class="mb-4" title="基本使用">
|
||||||
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
<EllipsisText :max-width="500">{{ text }}</EllipsisText>
|
||||||
|
40
playground/src/views/examples/modal/auto-height-demo.vue
Normal file
40
playground/src/views/examples/modal/auto-height-demo.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// modalApi.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = ref<number[]>([]);
|
||||||
|
|
||||||
|
list.value = Array.from({ length: 10 }, (_v, k) => k + 1);
|
||||||
|
|
||||||
|
function handleUpdate() {
|
||||||
|
list.value = Array.from({ length: 6 }, (_v, k) => k + 1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal title="自动计算高度">
|
||||||
|
<div
|
||||||
|
v-for="item in list"
|
||||||
|
:key="item"
|
||||||
|
class="even:bg-heavy bg-muted flex-center h-[220px] w-full"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #prepend-footer>
|
||||||
|
<Button type="link" @click="handleUpdate">点击更新数据</Button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
28
playground/src/views/examples/modal/base-demo.vue
Normal file
28
playground/src/views/examples/modal/base-demo.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen) {
|
||||||
|
if (isOpen) {
|
||||||
|
modalApi.setState({ loading: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
modalApi.setState({ loading: false });
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal class="w-[600px]" title="基础弹窗示例" title-tooltip="标题提示内容">
|
||||||
|
base demo
|
||||||
|
</Modal>
|
||||||
|
</template>
|
19
playground/src/views/examples/modal/drag-demo.vue
Normal file
19
playground/src/views/examples/modal/drag-demo.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// modalApi.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal title="可拖拽示例"> 鼠标移动到 header 上,可拖拽弹窗 </Modal>
|
||||||
|
</template>
|
41
playground/src/views/examples/modal/dynamic-demo.vue
Normal file
41
playground/src/views/examples/modal/dynamic-demo.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// modalApi.close();
|
||||||
|
},
|
||||||
|
title: '动态修改配置示例',
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = modalApi.useStore();
|
||||||
|
|
||||||
|
function handleUpdateTitle() {
|
||||||
|
modalApi.setState({ title: '内部动态标题' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleFullscreen() {
|
||||||
|
modalApi.setState((prev) => {
|
||||||
|
return { ...prev, fullscreen: !prev.fullscreen };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal>
|
||||||
|
<div class="flex-col-center">
|
||||||
|
<Button class="mb-3" type="primary" @click="handleUpdateTitle()">
|
||||||
|
内部动态修改标题
|
||||||
|
</Button>
|
||||||
|
<Button class="mb-3" type="primary" @click="handleToggleFullscreen()">
|
||||||
|
{{ state.fullscreen ? '退出全屏' : '打开全屏' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
104
playground/src/views/examples/modal/index.vue
Normal file
104
playground/src/views/examples/modal/index.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import AutoHeightDemo from './auto-height-demo.vue';
|
||||||
|
import BaseDemo from './base-demo.vue';
|
||||||
|
import DragDemo from './drag-demo.vue';
|
||||||
|
import DynamicDemo from './dynamic-demo.vue';
|
||||||
|
import SharedDataDemo from './shared-data-demo.vue';
|
||||||
|
|
||||||
|
const [BaseModal, baseModalApi] = useVbenModal({
|
||||||
|
// 链接抽离的组件
|
||||||
|
connectedComponent: BaseDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AutoHeightModal, autoHeightModalApi] = useVbenModal({
|
||||||
|
connectedComponent: AutoHeightDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DragModal, dragModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DragDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DynamicModal, dynamicModalApi] = useVbenModal({
|
||||||
|
connectedComponent: DynamicDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [SharedDataModal, sharedModalApi] = useVbenModal({
|
||||||
|
connectedComponent: SharedDataDemo,
|
||||||
|
});
|
||||||
|
|
||||||
|
function openBaseModal() {
|
||||||
|
baseModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAutoHeightModal() {
|
||||||
|
autoHeightModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDargModal() {
|
||||||
|
dragModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDynamicModal() {
|
||||||
|
dynamicModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSharedModal() {
|
||||||
|
sharedModalApi.setData({
|
||||||
|
content: '外部传递的数据 content',
|
||||||
|
payload: '外部传递的数据 payload',
|
||||||
|
});
|
||||||
|
sharedModalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateTitle() {
|
||||||
|
dynamicModalApi.setState({ title: '外部动态标题' });
|
||||||
|
dynamicModalApi.open();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page
|
||||||
|
description="弹窗组件常用于在不离开当前页面的情况下,显示额外的信息、表单或操作提示。"
|
||||||
|
title="弹窗组件示例"
|
||||||
|
>
|
||||||
|
<BaseModal />
|
||||||
|
<AutoHeightModal />
|
||||||
|
<DragModal />
|
||||||
|
<DynamicModal />
|
||||||
|
<SharedDataModal />
|
||||||
|
|
||||||
|
<Card class="mb-4" title="基本使用">
|
||||||
|
<p class="mb-3">一个基础的弹窗示例</p>
|
||||||
|
<Button type="primary" @click="openBaseModal">打开弹窗</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-4" title="内容高度自适应">
|
||||||
|
<p class="mb-3">可根据内容并自动调整高度</p>
|
||||||
|
<Button type="primary" @click="openAutoHeightModal">打开弹窗</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-4" title="可拖拽示例">
|
||||||
|
<p class="mb-3">配置 draggable 可开启拖拽功能</p>
|
||||||
|
<Button type="primary" @click="openDargModal">打开弹窗</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-4" title="动态配置示例">
|
||||||
|
<p class="mb-3">通过 setState 动态调整弹窗数据</p>
|
||||||
|
<Button type="primary" @click="openDynamicModal">打开弹窗</Button>
|
||||||
|
<Button class="ml-2" type="primary" @click="handleUpdateTitle">
|
||||||
|
从外部修改标题并打开
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card class="mb-4" title="内外数据共享示例">
|
||||||
|
<p class="mb-3">通过共享 sharedData 来进行数据交互</p>
|
||||||
|
<Button type="primary" @click="openSharedModal">
|
||||||
|
打开弹窗并传递数据
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Page>
|
||||||
|
</template>
|
29
playground/src/views/examples/modal/shared-data-demo.vue
Normal file
29
playground/src/views/examples/modal/shared-data-demo.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const data = ref();
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm() {
|
||||||
|
message.info('onConfirm');
|
||||||
|
// modalApi.close();
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
data.value = modalApi.getData<Record<string, any>>();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Modal title="数据共享示例">
|
||||||
|
<div class="flex-col-center">外部传递数据: {{ data }}</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -804,6 +804,27 @@ importers:
|
|||||||
specifier: 3.4.38
|
specifier: 3.4.38
|
||||||
version: 3.4.38(typescript@5.5.4)
|
version: 3.4.38(typescript@5.5.4)
|
||||||
|
|
||||||
|
packages/@core/ui-kit/popup-ui:
|
||||||
|
dependencies:
|
||||||
|
'@vben-core/composables':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../composables
|
||||||
|
'@vben-core/icons':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../base/icons
|
||||||
|
'@vben-core/shadcn-ui':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../shadcn-ui
|
||||||
|
'@vben-core/shared':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../base/shared
|
||||||
|
'@vueuse/core':
|
||||||
|
specifier: ^11.0.1
|
||||||
|
version: 11.0.1(vue@3.4.38(typescript@5.5.4))
|
||||||
|
vue:
|
||||||
|
specifier: 3.4.38
|
||||||
|
version: 3.4.38(typescript@5.5.4)
|
||||||
|
|
||||||
packages/@core/ui-kit/shadcn-ui:
|
packages/@core/ui-kit/shadcn-ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-icons/vue':
|
'@radix-icons/vue':
|
||||||
@@ -896,6 +917,9 @@ importers:
|
|||||||
|
|
||||||
packages/effects/common-ui:
|
packages/effects/common-ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@vben-core/popup-ui':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../@core/ui-kit/popup-ui
|
||||||
'@vben-core/shadcn-ui':
|
'@vben-core/shadcn-ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../@core/ui-kit/shadcn-ui
|
version: link:../../@core/ui-kit/shadcn-ui
|
||||||
@@ -966,6 +990,9 @@ importers:
|
|||||||
'@vben-core/menu-ui':
|
'@vben-core/menu-ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../@core/ui-kit/menu-ui
|
version: link:../../@core/ui-kit/menu-ui
|
||||||
|
'@vben-core/popup-ui':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../@core/ui-kit/popup-ui
|
||||||
'@vben-core/shadcn-ui':
|
'@vben-core/shadcn-ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../@core/ui-kit/shadcn-ui
|
version: link:../../@core/ui-kit/shadcn-ui
|
||||||
@@ -3411,7 +3438,6 @@ packages:
|
|||||||
|
|
||||||
'@ls-lint/ls-lint@2.2.3':
|
'@ls-lint/ls-lint@2.2.3':
|
||||||
resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==}
|
resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==}
|
||||||
cpu: [x64, arm64, s390x]
|
|
||||||
os: [darwin, linux, win32]
|
os: [darwin, linux, win32]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ const IGNORE_DIR = [
|
|||||||
'internal',
|
'internal',
|
||||||
'packages/effects/request/src/',
|
'packages/effects/request/src/',
|
||||||
'packages/@core/ui-kit/menu-ui/src/',
|
'packages/@core/ui-kit/menu-ui/src/',
|
||||||
|
'packages/@core/ui-kit/popup-ui/src/',
|
||||||
].join(',');
|
].join(',');
|
||||||
|
|
||||||
const IGNORE = [`**/{${IGNORE_DIR}}/**`];
|
const IGNORE = [`**/{${IGNORE_DIR}}/**`];
|
||||||
|
@@ -84,6 +84,10 @@
|
|||||||
"name": "@vben-core/menu-ui",
|
"name": "@vben-core/menu-ui",
|
||||||
"path": "packages/@core/ui-kit/menu-ui",
|
"path": "packages/@core/ui-kit/menu-ui",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "@vben-core/popup-ui",
|
||||||
|
"path": "packages/@core/ui-kit/popup-ui",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "@vben-core/shadcn-ui",
|
"name": "@vben-core/shadcn-ui",
|
||||||
"path": "packages/@core/ui-kit/shadcn-ui",
|
"path": "packages/@core/ui-kit/shadcn-ui",
|
||||||
|
Reference in New Issue
Block a user