diff --git a/.vscode/settings.json b/.vscode/settings.json
index cafc2857..66f3fdaa 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -168,9 +168,10 @@
"i18n-ally.localesPaths": [
"packages/locales/src/langs",
- "playground/src/langs",
+ "playground/src/locales/langs",
"apps/*/src/locales/langs"
],
+ "i18n-ally.pathMatcher": "{locale}.json",
"i18n-ally.enabledParsers": ["json", "ts", "js", "yaml"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
diff --git a/apps/backend-mock/utils/jwt-utils.ts b/apps/backend-mock/utils/jwt-utils.ts
index 0ee13da6..8cfc6843 100644
--- a/apps/backend-mock/utils/jwt-utils.ts
+++ b/apps/backend-mock/utils/jwt-utils.ts
@@ -14,7 +14,7 @@ export interface UserPayload extends 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) {
diff --git a/cspell.json b/cspell.json
index 92cf497a..60020248 100644
--- a/cspell.json
+++ b/cspell.json
@@ -37,6 +37,7 @@
"astro",
"ui-kit",
"styl",
+ "vnode",
"nocheck",
"prefixs",
"vitepress",
@@ -53,6 +54,9 @@
"**/*-dist/**",
"**/icons/**",
"pnpm-lock.yaml",
- "**/*.log"
+ "**/*.log",
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/__tests__/**"
]
}
diff --git a/internal/node-utils/src/hash.test.ts b/internal/node-utils/src/__tests__/hash.test.ts
similarity index 97%
rename from internal/node-utils/src/hash.test.ts
rename to internal/node-utils/src/__tests__/hash.test.ts
index 8b302f8f..38513060 100644
--- a/internal/node-utils/src/hash.test.ts
+++ b/internal/node-utils/src/__tests__/hash.test.ts
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
import { describe, expect, it } from 'vitest';
-import { generatorContentHash } from './hash';
+import { generatorContentHash } from '../hash';
describe('generatorContentHash', () => {
it('should generate an MD5 hash for the content', () => {
diff --git a/internal/node-utils/src/path.test.ts b/internal/node-utils/src/__tests__/path.test.ts
similarity index 98%
rename from internal/node-utils/src/path.test.ts
rename to internal/node-utils/src/__tests__/path.test.ts
index 4b550b4f..3bab5a16 100644
--- a/internal/node-utils/src/path.test.ts
+++ b/internal/node-utils/src/__tests__/path.test.ts
@@ -2,7 +2,7 @@
import { describe, expect, it } from 'vitest';
-import { toPosixPath } from './path';
+import { toPosixPath } from '../path';
describe('toPosixPath', () => {
// 测试 Windows 风格路径到 POSIX 风格路径的转换
diff --git a/internal/vite-config/src/plugins/inject-app-loading/default-loading-antd.html b/internal/vite-config/src/plugins/inject-app-loading/default-loading-antd.html
index 459f863f..20a21fb7 100644
--- a/internal/vite-config/src/plugins/inject-app-loading/default-loading-antd.html
+++ b/internal/vite-config/src/plugins/inject-app-loading/default-loading-antd.html
@@ -34,13 +34,6 @@
transition: all 0.6s ease-out;
}
- .loading .dots {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 98px;
- }
-
.loading .title {
margin-top: 36px;
font-size: 30px;
@@ -109,6 +102,6 @@
}
-
+
<%= VITE_APP_TITLE %>
diff --git a/packages/@core/base/icons/src/lucide.ts b/packages/@core/base/icons/src/lucide.ts
index 02d96f17..22de4a15 100644
--- a/packages/@core/base/icons/src/lucide.ts
+++ b/packages/@core/base/icons/src/lucide.ts
@@ -20,12 +20,14 @@ export {
CornerDownLeft,
Disc as IconDefault,
Ellipsis,
+ Expand,
ExternalLink,
Eye,
EyeOff,
FoldHorizontal,
Fullscreen,
Github,
+ Info,
InspectionPanel,
Languages,
LoaderCircle,
@@ -46,6 +48,7 @@ export {
Search,
SearchX,
Settings,
+ Shrink,
Sun,
SunMoon,
SwatchBook,
diff --git a/packages/@core/base/shared/src/cache/storage-manager.test.ts b/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts
similarity index 98%
rename from packages/@core/base/shared/src/cache/storage-manager.test.ts
rename to packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts
index 915af72f..a5abe5a7 100644
--- a/packages/@core/base/shared/src/cache/storage-manager.test.ts
+++ b/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { StorageManager } from './storage-manager';
+import { StorageManager } from '../storage-manager';
describe('storageManager', () => {
let storageManager: StorageManager;
diff --git a/packages/@core/base/shared/src/color/convert.test.ts b/packages/@core/base/shared/src/color/__tests__/convert.test.ts
similarity index 98%
rename from packages/@core/base/shared/src/color/convert.test.ts
rename to packages/@core/base/shared/src/color/__tests__/convert.test.ts
index ee174439..fc4256c5 100644
--- a/packages/@core/base/shared/src/color/convert.test.ts
+++ b/packages/@core/base/shared/src/color/__tests__/convert.test.ts
@@ -5,7 +5,7 @@ import {
convertToHslCssVar,
convertToRgb,
isValidColor,
-} from './convert';
+} from '../convert';
describe('color conversion functions', () => {
it('should correctly convert color to HSL format', () => {
diff --git a/packages/@core/base/shared/src/utils/diff.test.ts b/packages/@core/base/shared/src/utils/__tests__/diff.test.ts
similarity index 98%
rename from packages/@core/base/shared/src/utils/diff.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/diff.test.ts
index c858a3bf..cb3227b5 100644
--- a/packages/@core/base/shared/src/utils/diff.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/diff.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
-import { diff } from './diff';
+import { diff } from '../diff';
describe('diff function', () => {
it('should return an empty object when comparing identical objects', () => {
diff --git a/packages/@core/base/shared/src/utils/dom.test.ts b/packages/@core/base/shared/src/utils/__tests__/dom.test.ts
similarity index 97%
rename from packages/@core/base/shared/src/utils/dom.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/dom.test.ts
index 66e6466b..c3c558fc 100644
--- a/packages/@core/base/shared/src/utils/dom.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/dom.test.ts
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { getElementVisibleRect } from './dom'; // 假设函数位于 utils.ts 中
+import { getElementVisibleRect } from '../dom'; // 假设函数位于 utils.ts 中
describe('getElementVisibleRect', () => {
// 设置浏览器视口尺寸的 mock
diff --git a/packages/@core/base/shared/src/utils/inference.test.ts b/packages/@core/base/shared/src/utils/__tests__/inference.test.ts
similarity index 63%
rename from packages/@core/base/shared/src/utils/inference.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/inference.test.ts
index 3d613d9a..bfbcf0a1 100644
--- a/packages/@core/base/shared/src/utils/inference.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/inference.test.ts
@@ -1,12 +1,13 @@
import { describe, expect, it } from 'vitest';
import {
+ getFirstNonNullOrUndefined,
isEmpty,
isHttpUrl,
isObject,
isUndefined,
isWindow,
-} from './inference';
+} from '../inference';
describe('isHttpUrl', () => {
it("should return true when given 'http://example.com'", () => {
@@ -103,7 +104,6 @@ describe('isObject', () => {
it('should return false for non-objects', () => {
expect(isObject(null)).toBe(false);
- expect(isObject()).toBe(false);
expect(isObject(42)).toBe(false);
expect(isObject('string')).toBe(false);
expect(isObject(true)).toBe(false);
@@ -112,3 +112,56 @@ describe('isObject', () => {
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(undefined, null, 0, 42)).toBe(
+ 0,
+ );
+ expect(getFirstNonNullOrUndefined(null, undefined, 42, 123)).toBe(
+ 42,
+ );
+ });
+
+ it('should return the first non-null and non-undefined value for a string array', () => {
+ expect(
+ getFirstNonNullOrUndefined(undefined, null, '', 'hello'),
+ ).toBe('');
+ expect(
+ getFirstNonNullOrUndefined(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(
+ undefined,
+ null,
+ 'test',
+ 123,
+ { key: 'value' },
+ ),
+ ).toBe('test');
+ expect(
+ getFirstNonNullOrUndefined(
+ null,
+ undefined,
+ [1, 2, 3],
+ 'string',
+ ),
+ ).toEqual([1, 2, 3]);
+ });
+ });
+});
diff --git a/packages/@core/base/shared/src/utils/letter.test.ts b/packages/@core/base/shared/src/utils/__tests__/letter.test.ts
similarity index 65%
rename from packages/@core/base/shared/src/utils/letter.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/letter.test.ts
index 6f650bdb..2e4037a2 100644
--- a/packages/@core/base/shared/src/utils/letter.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/letter.test.ts
@@ -2,9 +2,10 @@ import { describe, expect, it } from 'vitest';
import {
capitalizeFirstLetter,
+ kebabToCamelCase,
toCamelCase,
toLowerCaseFirstLetter,
-} from './letter';
+} from '../letter';
// 编写测试用例
describe('capitalizeFirstLetter', () => {
@@ -76,3 +77,41 @@ describe('toCamelCase', () => {
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');
+ });
+});
diff --git a/packages/@core/base/shared/src/utils/tree.test.ts b/packages/@core/base/shared/src/utils/__tests__/tree.test.ts
similarity index 98%
rename from packages/@core/base/shared/src/utils/tree.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/tree.test.ts
index f1000d6e..afe43cc5 100644
--- a/packages/@core/base/shared/src/utils/tree.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/tree.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
-import { filterTree, mapTree, traverseTreeValues } from './tree';
+import { filterTree, mapTree, traverseTreeValues } from '../tree';
describe('traverseTreeValues', () => {
interface Node {
diff --git a/packages/@core/base/shared/src/utils/unique.test.ts b/packages/@core/base/shared/src/utils/__tests__/unique.test.ts
similarity index 97%
rename from packages/@core/base/shared/src/utils/unique.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/unique.test.ts
index 50f48bbf..c5ebcacc 100644
--- a/packages/@core/base/shared/src/utils/unique.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/unique.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
-import { uniqueByField } from './unique';
+import { uniqueByField } from '../unique';
describe('uniqueByField', () => {
it('should return an array with unique items based on id field', () => {
diff --git a/packages/@core/base/shared/src/utils/update-css-variables.test.ts b/packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts
similarity index 94%
rename from packages/@core/base/shared/src/utils/update-css-variables.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts
index 8b5be0d0..4a9cdadc 100644
--- a/packages/@core/base/shared/src/utils/update-css-variables.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts
@@ -1,6 +1,6 @@
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', () => {
// 模拟初始的内联样式表内容
diff --git a/packages/@core/base/shared/src/utils/window.test.ts b/packages/@core/base/shared/src/utils/__tests__/window.test.ts
similarity index 89%
rename from packages/@core/base/shared/src/utils/window.test.ts
rename to packages/@core/base/shared/src/utils/__tests__/window.test.ts
index 8152dcb2..69ad4f7d 100644
--- a/packages/@core/base/shared/src/utils/window.test.ts
+++ b/packages/@core/base/shared/src/utils/__tests__/window.test.ts
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { openWindow } from './window'; // 假设你的函数在 'openWindow' 文件中
+import { openWindow } from '../window'; // 假设你的函数在 'openWindow' 文件中
describe('openWindow', () => {
// 保存原始的 window.open 函数
diff --git a/packages/@core/base/shared/src/utils/inference.ts b/packages/@core/base/shared/src/utils/inference.ts
index 4e33ba2c..89cd355e 100644
--- a/packages/@core/base/shared/src/utils/inference.ts
+++ b/packages/@core/base/shared/src/utils/inference.ts
@@ -24,7 +24,7 @@ function isUndefined(value?: unknown): value is undefined {
* @param {T} value 要检查的值。
* @returns {boolean} 如果值为空,返回true,否则返回false。
*/
-function isEmpty(value: T): value is T {
+function isEmpty(value?: T): value is T {
if (value === null || value === undefined) {
return true;
}
@@ -105,7 +105,42 @@ function isNumber(value: any): value is number {
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(
+ ...values: (null | T | undefined)[]
+): T | undefined {
+ for (const value of values) {
+ if (value !== undefined && value !== null) {
+ return value;
+ }
+ }
+ return undefined;
+}
+
export {
+ getFirstNonNullOrUndefined,
isEmpty,
isFunction,
isHttpUrl,
diff --git a/packages/@core/base/shared/src/utils/letter.ts b/packages/@core/base/shared/src/utils/letter.ts
index 713c459c..65a1c229 100644
--- a/packages/@core/base/shared/src/utils/letter.ts
+++ b/packages/@core/base/shared/src/utils/letter.ts
@@ -29,4 +29,19 @@ function toCamelCase(key: string, parentKey: string): string {
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,
+};
diff --git a/packages/@core/composables/src/use-sortable.test.ts b/packages/@core/composables/src/__tests__/use-sortable.test.ts
similarity index 92%
rename from packages/@core/composables/src/use-sortable.test.ts
rename to packages/@core/composables/src/__tests__/use-sortable.test.ts
index 70aa2234..e7ba1f13 100644
--- a/packages/@core/composables/src/use-sortable.test.ts
+++ b/packages/@core/composables/src/__tests__/use-sortable.test.ts
@@ -2,7 +2,7 @@ import type { SortableOptions } from 'sortablejs';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { useSortable } from './use-sortable';
+import { useSortable } from '../use-sortable';
describe('useSortable', () => {
beforeEach(() => {
@@ -30,7 +30,6 @@ describe('useSortable', () => {
// Import sortablejs to access the mocked create function
const Sortable = await import(
- // @ts-expect-error - This is a dynamic import
'sortablejs/modular/sortable.complete.esm.js'
);
diff --git a/packages/@core/composables/src/index.ts b/packages/@core/composables/src/index.ts
index d0015f15..a41b2199 100644
--- a/packages/@core/composables/src/index.ts
+++ b/packages/@core/composables/src/index.ts
@@ -1,5 +1,6 @@
export * from './use-content-style';
export * from './use-namespace';
+export * from './use-priority-value';
export * from './use-sortable';
export {
useEmitAsProps,
diff --git a/packages/@core/composables/src/use-priority-value.ts b/packages/@core/composables/src/use-priority-value.ts
new file mode 100644
index 00000000..1704e5e0
--- /dev/null
+++ b/packages/@core/composables/src/use-priority-value.ts
@@ -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,
+ S extends Record,
+ K extends keyof T = keyof T,
+>(key: K, props: T, state: Readonly[>> | 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;
+}
diff --git a/packages/@core/preferences/src/use-preferences.ts b/packages/@core/preferences/src/use-preferences.ts
index b13b327b..03e959fb 100644
--- a/packages/@core/preferences/src/use-preferences.ts
+++ b/packages/@core/preferences/src/use-preferences.ts
@@ -149,14 +149,6 @@ function usePreferences() {
return enable && globalLockScreen;
});
- /**
- * @zh_CN 是否启用全局偏好设置快捷键
- */
- const globalPreferencesShortcutKey = computed(() => {
- const { enable, globalPreferences } = shortcutKeysPreferences.value;
- return enable && globalPreferences;
- });
-
return {
authPanelCenter,
authPanelLeft,
@@ -165,7 +157,6 @@ function usePreferences() {
diffPreference,
globalLockScreenShortcutKey,
globalLogoutShortcutKey,
- globalPreferencesShortcutKey,
globalSearchShortcutKey,
isDark,
isFullContent,
diff --git a/packages/@core/ui-kit/popup-ui/build.config.ts b/packages/@core/ui-kit/popup-ui/build.config.ts
new file mode 100644
index 00000000..18eaa604
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/build.config.ts
@@ -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'],
+ },
+ ],
+});
diff --git a/packages/@core/ui-kit/popup-ui/package.json b/packages/@core/ui-kit/popup-ui/package.json
new file mode 100644
index 00000000..5f536288
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/package.json
@@ -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"
+ }
+}
diff --git a/packages/@core/ui-kit/popup-ui/postcss.config.mjs b/packages/@core/ui-kit/popup-ui/postcss.config.mjs
new file mode 100644
index 00000000..3d807045
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/postcss.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config/postcss';
diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts b/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts
new file mode 100644
index 00000000..2aebcc09
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts
@@ -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');
+ });
+});
diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts b/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
new file mode 100644
index 00000000..11fa8369
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts
@@ -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;
+
+ 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(
+ {
+ ...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>() {
+ return (this.sharedData?.payload ?? {}) as T;
+ }
+
+ /**
+ * 取消操作
+ */
+ onCancel() {
+ this.api.onCancel?.();
+ }
+
+ /**
+ * 确认操作
+ */
+ onConfirm() {
+ this.api.onConfirm?.();
+ }
+
+ open() {
+ this.store.setState((prev) => ({ ...prev, isOpen: true }));
+ }
+
+ setData(payload: T) {
+ this.sharedData.payload = payload;
+ }
+
+ setState(
+ stateOrFn:
+ | ((prev: DrawerState) => Partial)
+ | Partial,
+ ) {
+ if (isFunction(stateOrFn)) {
+ this.store.setState(stateOrFn);
+ } else {
+ this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
+ }
+ }
+}
diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
new file mode 100644
index 00000000..1d62c511
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts
@@ -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;
+}
+
+export type ExtendedDrawerApi = {
+ useStore: >(
+ selector?: (state: NoInfer) => T,
+ ) => Readonly][>;
+} & DrawerApi;
+
+export interface DrawerApiOptions extends DrawerState {
+ /**
+ * 独立的弹窗组件
+ */
+ connectedComponent?: Component;
+ /**
+ * 关闭前的回调,返回 false 可以阻止关闭
+ * @returns
+ */
+ onBeforeClose?: () => void;
+ /**
+ * 点击取消按钮的回调
+ */
+ onCancel?: () => void;
+ /**
+ * 点击确定按钮的回调
+ */
+ onConfirm?: () => void;
+ /**
+ * 弹窗状态变化回调
+ * @param isOpen
+ * @returns
+ */
+ onOpenChange?: (isOpen: boolean) => void;
+}
diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
new file mode 100644
index 00000000..81f42630
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue
@@ -0,0 +1,141 @@
+
+
+ drawerApi?.close()"
+ >
+
+
+ ]
+
+
+ {{ title }}
+
+
+
+
+
+ {{ titleTooltip }}
+
+
+
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ drawerApi?.onCancel()"
+ >
+
+ {{ cancelText }}
+
+
+ drawerApi?.onConfirm()"
+ >
+
+ {{ confirmText }}
+
+
+
+
+
+
+
+
diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/index.ts b/packages/@core/ui-kit/popup-ui/src/drawer/index.ts
new file mode 100644
index 00000000..f2dd8396
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/drawer/index.ts
@@ -0,0 +1,3 @@
+export type * from './drawer';
+export { default as VbenDrawer } from './drawer.vue';
+export { useVbenDrawer } from './use-drawer';
diff --git a/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts b/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts
new file mode 100644
index 00000000..c8040e3d
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts
@@ -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(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) {
+ 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.`,
+ );
+ }
+ }
+}
diff --git a/packages/@core/ui-kit/popup-ui/src/index.ts b/packages/@core/ui-kit/popup-ui/src/index.ts
new file mode 100644
index 00000000..56e7ade5
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/index.ts
@@ -0,0 +1,2 @@
+export * from './drawer';
+export * from './modal';
diff --git a/packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts b/packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts
new file mode 100644
index 00000000..fa10d882
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts
@@ -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');
+ });
+});
diff --git a/packages/@core/ui-kit/popup-ui/src/modal/index.ts b/packages/@core/ui-kit/popup-ui/src/modal/index.ts
new file mode 100644
index 00000000..c8d3498c
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/modal/index.ts
@@ -0,0 +1,3 @@
+export type * from './modal';
+export { default as VbenModal } from './modal.vue';
+export { useVbenModal } from './use-modal';
diff --git a/packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts b/packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
new file mode 100644
index 00000000..179e2562
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts
@@ -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;
+
+ 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(
+ {
+ ...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>() {
+ 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(payload: T) {
+ this.sharedData.payload = payload;
+ }
+
+ setState(
+ stateOrFn:
+ | ((prev: ModalState) => Partial)
+ | Partial,
+ ) {
+ if (isFunction(stateOrFn)) {
+ this.store.setState(stateOrFn);
+ } else {
+ this.store.setState((prev) => ({ ...prev, ...stateOrFn }));
+ }
+ }
+}
diff --git a/packages/@core/ui-kit/popup-ui/src/modal/modal.ts b/packages/@core/ui-kit/popup-ui/src/modal/modal.ts
new file mode 100644
index 00000000..88b8aab3
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/modal/modal.ts
@@ -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;
+}
+
+export type ExtendedModalApi = {
+ useStore: >(
+ selector?: (state: NoInfer) => T,
+ ) => Readonly[>;
+} & ModalApi;
+
+export interface ModalApiOptions extends ModalState {
+ /**
+ * 独立的弹窗组件
+ */
+ connectedComponent?: Component;
+ /**
+ * 关闭前的回调,返回 false 可以阻止关闭
+ * @returns
+ */
+ onBeforeClose?: () => void;
+ /**
+ * 点击取消按钮的回调
+ */
+ onCancel?: () => void;
+ /**
+ * 点击确定按钮的回调
+ */
+ onConfirm?: () => void;
+ /**
+ * 弹窗状态变化回调
+ * @param isOpen
+ * @returns
+ */
+ onOpenChange?: (isOpen: boolean) => void;
+}
diff --git a/packages/@core/ui-kit/popup-ui/src/modal/modal.vue b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue
new file mode 100644
index 00000000..57d27ef9
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/modal/modal.vue
@@ -0,0 +1,231 @@
+
+
+
+
diff --git a/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts b/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts
new file mode 100644
index 00000000..60ea1324
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts
@@ -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,
+ dragRef: Ref,
+ draggable: ComputedRef,
+) {
+ 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 };
+// }
diff --git a/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts b/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts
new file mode 100644
index 00000000..cbce9c52
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts
@@ -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(
+ 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(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) {
+ 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.`,
+ );
+ }
+ }
+}
diff --git a/packages/@core/ui-kit/popup-ui/tailwind.config.mjs b/packages/@core/ui-kit/popup-ui/tailwind.config.mjs
new file mode 100644
index 00000000..f17f556f
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/tailwind.config.mjs
@@ -0,0 +1 @@
+export { default } from '@vben/tailwind-config';
diff --git a/packages/@core/ui-kit/popup-ui/tsconfig.json b/packages/@core/ui-kit/popup-ui/tsconfig.json
new file mode 100644
index 00000000..ce1a891f
--- /dev/null
+++ b/packages/@core/ui-kit/popup-ui/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "@vben/tsconfig/web.json",
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/alert-dialog/alert-dialog.vue b/packages/@core/ui-kit/shadcn-ui/src/components/alert-dialog/alert-dialog.vue
deleted file mode 100644
index 3cc31f66..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/alert-dialog/alert-dialog.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
-
- {{ title }}
-
- {{ content }}
-
-
-
-
- {{ cancelText }}
-
-
- {{ submitText }}
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/alert-dialog/index.ts b/packages/@core/ui-kit/shadcn-ui/src/components/alert-dialog/index.ts
deleted file mode 100644
index 8837d665..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/alert-dialog/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as VbenAlertDialog } from './alert-dialog.vue';
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/index.ts b/packages/@core/ui-kit/shadcn-ui/src/components/index.ts
index 73fe0c90..d3ccfa4c 100644
--- a/packages/@core/ui-kit/shadcn-ui/src/components/index.ts
+++ b/packages/@core/ui-kit/shadcn-ui/src/components/index.ts
@@ -1,4 +1,3 @@
-export * from './alert-dialog';
export * from './avatar';
export * from './back-top';
export * from './breadcrumb';
@@ -20,11 +19,9 @@ export * from './popover';
export * from './render-content';
export * from './scrollbar';
export * from './segmented';
-export * from './sheet';
export * from './spinner';
export * from './swap';
export * from './tooltip';
-export * from './ui/alert-dialog';
export * from './ui/avatar';
export * from './ui/badge';
export * from './ui/breadcrumb';
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/sheet/index.ts b/packages/@core/ui-kit/shadcn-ui/src/components/sheet/index.ts
deleted file mode 100644
index 8eb932ac..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/sheet/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as VbenSheet } from './sheet.vue';
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/sheet/sheet.vue b/packages/@core/ui-kit/shadcn-ui/src/components/sheet/sheet.vue
deleted file mode 100644
index 43bd2fe7..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/sheet/sheet.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-
-
-
-
-
-
-
-
-
- ]
-
- {{ title }}
-
- {{ description }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ cancelText }}
-
-
- {{ submitText }}
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/index.ts b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/index.ts
index a26467a5..4bba7405 100644
--- a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/index.ts
+++ b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/index.ts
@@ -1 +1,2 @@
+export { default as VbenLoading } from './loading.vue';
export { default as VbenSpinner } from './spinner.vue';
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue
new file mode 100644
index 00000000..cdf9c1c2
--- /dev/null
+++ b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue
index 318945c2..0ac6d52e 100644
--- a/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue
+++ b/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue
@@ -1,7 +1,10 @@
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogAction.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogAction.vue
deleted file mode 100644
index 52f05d6e..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogAction.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogCancel.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogCancel.vue
deleted file mode 100644
index 088113f8..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogCancel.vue
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogContent.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogContent.vue
deleted file mode 100644
index e986c0fa..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogContent.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogDescription.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogDescription.vue
deleted file mode 100644
index c057ebfe..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogDescription.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogFooter.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogFooter.vue
deleted file mode 100644
index 3d9ae39d..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogFooter.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogHeader.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogHeader.vue
deleted file mode 100644
index db8dc643..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogHeader.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogTitle.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogTitle.vue
deleted file mode 100644
index 711247e9..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogTitle.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogTrigger.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogTrigger.vue
deleted file mode 100644
index 32726258..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/AlertDialogTrigger.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/index.ts b/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/index.ts
deleted file mode 100644
index 88770f1e..00000000
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/alert-dialog/index.ts
+++ /dev/null
@@ -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';
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue b/packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue
index 823afeb5..f40543b6 100644
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue
+++ b/packages/@core/ui-kit/shadcn-ui/src/components/ui/dialog/DialogContent.vue
@@ -1,5 +1,5 @@
@@ -41,10 +48,11 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
@click="() => emits('close')"
/>
emits('close')"
>
diff --git a/packages/@core/ui-kit/shadcn-ui/src/components/ui/sheet/sheet.ts b/packages/@core/ui-kit/shadcn-ui/src/components/ui/sheet/sheet.ts
index 69590d5d..f41568b7 100644
--- a/packages/@core/ui-kit/shadcn-ui/src/components/ui/sheet/sheet.ts
+++ b/packages/@core/ui-kit/shadcn-ui/src/components/ui/sheet/sheet.ts
@@ -1,7 +1,7 @@
import { cva, type VariantProps } from 'class-variance-authority';
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: {
side: 'right',
@@ -10,9 +10,9 @@ export const sheetVariants = cva(
side: {
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:
- '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',
},
},
diff --git a/packages/effects/common-ui/package.json b/packages/effects/common-ui/package.json
index 2c32f353..6330b328 100644
--- a/packages/effects/common-ui/package.json
+++ b/packages/effects/common-ui/package.json
@@ -20,6 +20,7 @@
}
},
"dependencies": {
+ "@vben-core/popup-ui": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
diff --git a/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue b/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue
index 4455a00a..7e6572eb 100644
--- a/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue
+++ b/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue
@@ -95,41 +95,43 @@ function handleExpand() {
}
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+