chore: rename @vben-core -> @core
This commit is contained in:
6
packages/@core/shared/README.md
Normal file
6
packages/@core/shared/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# shared
|
||||
|
||||
全局共享包,请勿引入 workspace 依赖
|
||||
|
||||
- typings 共享类型
|
||||
- toolkit 共享工具类
|
||||
7
packages/@core/shared/chche/build.config.ts
Normal file
7
packages/@core/shared/chche/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
||||
43
packages/@core/shared/chche/package.json
Normal file
43
packages/@core/shared/chche/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@vben-core/cache",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/@vben-core/shared/cache"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"stub": "pnpm unbuild --stub"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
1
packages/@core/shared/chche/src/index.ts
Normal file
1
packages/@core/shared/chche/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './storage-manager';
|
||||
130
packages/@core/shared/chche/src/storage-manager.test.ts
Normal file
130
packages/@core/shared/chche/src/storage-manager.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { StorageManager } from './storage-manager';
|
||||
|
||||
describe('storageManager', () => {
|
||||
let storageManager: StorageManager<{ age: number; name: string }>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
localStorage.clear();
|
||||
storageManager = new StorageManager<{ age: number; name: string }>({
|
||||
prefix: 'test_',
|
||||
});
|
||||
});
|
||||
|
||||
it('should set and get an item', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should return default value if item does not exist', () => {
|
||||
const user = storageManager.getItem('nonexistent', {
|
||||
age: 0,
|
||||
name: 'Default User',
|
||||
});
|
||||
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||
});
|
||||
|
||||
it('should remove an item', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
storageManager.removeItem('user');
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear all items with the prefix', () => {
|
||||
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||
storageManager.clear();
|
||||
expect(storageManager.getItem('user1')).toBeNull();
|
||||
expect(storageManager.getItem('user2')).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear expired items', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
vi.advanceTimersByTime(1001); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should not clear non-expired items', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||
vi.advanceTimersByTime(5000); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should handle JSON parse errors gracefully', () => {
|
||||
localStorage.setItem('test_user', '{ invalid JSON }');
|
||||
const user = storageManager.getItem('user', {
|
||||
age: 0,
|
||||
name: 'Default User',
|
||||
});
|
||||
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||
});
|
||||
it('should return null for non-existent items without default value', () => {
|
||||
const user = storageManager.getItem('nonexistent');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should overwrite existing items', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
storageManager.setItem('user', { age: 25, name: 'Jane Doe' });
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 25, name: 'Jane Doe' });
|
||||
});
|
||||
|
||||
it('should handle items without expiry correctly', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
vi.advanceTimersByTime(5000);
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should remove expired items when accessed', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
vi.advanceTimersByTime(1001); // 快进时间
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should not remove non-expired items when accessed', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||
vi.advanceTimersByTime(5000); // 快进时间
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should handle multiple items with different expiry times', () => {
|
||||
storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
|
||||
vi.advanceTimersByTime(1500); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user1 = storageManager.getItem('user1');
|
||||
const user2 = storageManager.getItem('user2');
|
||||
expect(user1).toBeNull();
|
||||
expect(user2).toEqual({ age: 25, name: 'Jane Doe' });
|
||||
});
|
||||
|
||||
it('should handle items with no expiry', () => {
|
||||
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||
vi.advanceTimersByTime(10_000); // 快进时间
|
||||
storageManager.clearExpiredItems();
|
||||
const user = storageManager.getItem('user');
|
||||
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||
});
|
||||
|
||||
it('should clear all items correctly', () => {
|
||||
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||
storageManager.clear();
|
||||
const user1 = storageManager.getItem('user1');
|
||||
const user2 = storageManager.getItem('user2');
|
||||
expect(user1).toBeNull();
|
||||
expect(user2).toBeNull();
|
||||
});
|
||||
});
|
||||
118
packages/@core/shared/chche/src/storage-manager.ts
Normal file
118
packages/@core/shared/chche/src/storage-manager.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
type StorageType = 'localStorage' | 'sessionStorage';
|
||||
|
||||
interface StorageManagerOptions {
|
||||
prefix?: string;
|
||||
storageType?: StorageType;
|
||||
}
|
||||
|
||||
interface StorageItem<T> {
|
||||
expiry?: number;
|
||||
value: T;
|
||||
}
|
||||
|
||||
class StorageManager {
|
||||
private prefix: string;
|
||||
private storage: Storage;
|
||||
|
||||
constructor({
|
||||
prefix = '',
|
||||
storageType = 'localStorage',
|
||||
}: StorageManagerOptions = {}) {
|
||||
this.prefix = prefix;
|
||||
this.storage =
|
||||
storageType === 'localStorage'
|
||||
? window.localStorage
|
||||
: window.sessionStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的存储键
|
||||
* @param key 原始键
|
||||
* @returns 带前缀的完整键
|
||||
*/
|
||||
private getFullKey(key: string): string {
|
||||
return `${this.prefix}-${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有带前缀的存储项
|
||||
*/
|
||||
clear(): void {
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i);
|
||||
if (key && key.startsWith(this.prefix)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
keysToRemove.forEach((key) => this.storage.removeItem(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有过期的存储项
|
||||
*/
|
||||
clearExpiredItems(): void {
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i);
|
||||
if (key && key.startsWith(this.prefix)) {
|
||||
const shortKey = key.replace(this.prefix, '');
|
||||
this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储项
|
||||
* @param key 键
|
||||
* @param defaultValue 当项不存在或已过期时返回的默认值
|
||||
* @returns 值,如果项已过期或解析错误则返回默认值
|
||||
*/
|
||||
getItem<T>(key: string, defaultValue: T | null = null): T | null {
|
||||
const fullKey = this.getFullKey(key);
|
||||
const itemStr = this.storage.getItem(fullKey);
|
||||
if (!itemStr) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
const item: StorageItem<T> = JSON.parse(itemStr);
|
||||
if (item.expiry && Date.now() > item.expiry) {
|
||||
this.storage.removeItem(fullKey);
|
||||
return defaultValue;
|
||||
}
|
||||
return item.value;
|
||||
} catch (error) {
|
||||
console.error(`Error parsing item with key "${fullKey}":`, error);
|
||||
this.storage.removeItem(fullKey); // 如果解析失败,删除该项
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除存储项
|
||||
* @param key 键
|
||||
*/
|
||||
removeItem(key: string): void {
|
||||
const fullKey = this.getFullKey(key);
|
||||
this.storage.removeItem(fullKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置存储项
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 存活时间(毫秒)
|
||||
*/
|
||||
setItem<T>(key: string, value: T, ttl?: number): void {
|
||||
const fullKey = this.getFullKey(key);
|
||||
const expiry = ttl ? Date.now() + ttl : undefined;
|
||||
const item: StorageItem<T> = { expiry, value };
|
||||
try {
|
||||
this.storage.setItem(fullKey, JSON.stringify(item));
|
||||
} catch (error) {
|
||||
console.error(`Error setting item with key "${fullKey}":`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { StorageManager };
|
||||
17
packages/@core/shared/chche/src/types.ts
Normal file
17
packages/@core/shared/chche/src/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
type StorageType = 'localStorage' | 'sessionStorage';
|
||||
|
||||
interface StorageValue<T> {
|
||||
data: T;
|
||||
expiry: null | number;
|
||||
}
|
||||
|
||||
interface IStorageCache {
|
||||
clear(): void;
|
||||
getItem<T>(key: string): T | null;
|
||||
key(index: number): null | string;
|
||||
length(): number;
|
||||
removeItem(key: string): void;
|
||||
setItem<T>(key: string, value: T, expiryInMinutes?: number): void;
|
||||
}
|
||||
|
||||
export type { IStorageCache, StorageType, StorageValue };
|
||||
6
packages/@core/shared/chche/tsconfig.json
Normal file
6
packages/@core/shared/chche/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
3
packages/@core/shared/design-tokens/README.md
Normal file
3
packages/@core/shared/design-tokens/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @vben-core/design-tokens
|
||||
|
||||
用于维护全局所有的 css 变量,它由 vite 插件在全局注入,不需要手动引入
|
||||
43
packages/@core/shared/design-tokens/package.json
Normal file
43
packages/@core/shared/design-tokens/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@vben-core/design-tokens",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/@vben-core/shared/design-tokens"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"scripts": {
|
||||
"build": "pnpm vite build",
|
||||
"dts": "vue-tsc --declaration --emitDeclarationOnly --declarationDir dist",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"main": "./dist/index.css",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.css"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
packages/@core/shared/design-tokens/src/dark/index.scss
Normal file
81
packages/@core/shared/design-tokens/src/dark/index.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
:root.dark {
|
||||
/* 基础背景颜色颜色 */
|
||||
|
||||
/* --color-background: 240 6% 18%; */
|
||||
// --color-body: 220deg 13.04% 8%;
|
||||
// --color-body: hsl(240deg 11% 4%);
|
||||
--color-background: 220deg 13.04% 8%;
|
||||
|
||||
/* --color-background: 219 42% 11%; */
|
||||
|
||||
/* 基础文本颜色 */
|
||||
--color-foreground: 220 13% 91%;
|
||||
|
||||
/* 主题颜色 */
|
||||
--color-primary: 211 91% 39%;
|
||||
|
||||
/* 前景色,如按钮文本颜色 */
|
||||
--color-primary-foreground: 0 0 98%;
|
||||
|
||||
/* 颜色次要 */
|
||||
--color-secondary: 240 5% 17%;
|
||||
|
||||
/* 前景色,如按钮文本颜色 */
|
||||
--color-secondary-foreground: 0 0 98%;
|
||||
|
||||
/* 次要文本颜色 */
|
||||
--color-secondary-desc: 210 12.16% 70.98%;
|
||||
|
||||
/* 普通颜色 */
|
||||
|
||||
/* --color-accent: 240 3.7% 15.9%; */
|
||||
|
||||
/* --color-accent: 220deg 7.32% 16.08%; */
|
||||
--color-accent: 0deg 0% 100% / 8%;
|
||||
--color-accent-hover: 0deg 0% 100% / 12%;
|
||||
|
||||
/* 普通颜色前景色,如按钮文本颜色 */
|
||||
--color-accent-foreground: 0 0 98%;
|
||||
|
||||
/* 破坏性颜色 */
|
||||
--color-destructive: 0 63% 31%;
|
||||
|
||||
/* 破坏性颜色 */
|
||||
--color-destructive-foreground: 0 86% 97%;
|
||||
--color-muted: 220deg 6.82% 17.25%;
|
||||
--color-muted-foreground: 215 20.2% 65.1%;
|
||||
--color-heavy: 0deg 0% 100% / 12%;
|
||||
--color-heavy-foreground: var(--color-accent-foreground);
|
||||
|
||||
/* 基础边框色 */
|
||||
--color-border: 0deg 0% 100% / 10%;
|
||||
|
||||
/* --color-popover: 240 4% 29%; */
|
||||
--color-popover: 222.86deg 8.43% 16.27%;
|
||||
--color-popover-foreground: 210 40% 98%;
|
||||
--color-card: 222.2 84% 4.9%;
|
||||
--color-card-foreground: 210 40% 98%;
|
||||
|
||||
/* 基础文本边框色 */
|
||||
--color-input: 0deg 0% 100% / 10%;
|
||||
|
||||
/* input placeholder 颜色 */
|
||||
--color-input-placeholder: 218deg 11% 65%;
|
||||
|
||||
/* 基础文本背景色 */
|
||||
|
||||
/* --color-input-background: 216deg 5.38% 18.24%; */
|
||||
--color-input-background: 0deg 0% 100% / 5%;
|
||||
|
||||
/* 遮罩颜色 */
|
||||
--color-overlay: 0deg 0% 0% / 40%;
|
||||
--color-ring: 222.2 84% 4.9%;
|
||||
|
||||
/* 基本文字大小 */
|
||||
--font-size-base: 16px;
|
||||
|
||||
/* 基本圆角大小 */
|
||||
--radius-base: 0.5rem;
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
96
packages/@core/shared/design-tokens/src/default/index.scss
Normal file
96
packages/@core/shared/design-tokens/src/default/index.scss
Normal file
@@ -0,0 +1,96 @@
|
||||
/* https://gavin-yyc.github.io/colorconvert/ */
|
||||
:root {
|
||||
/* 基础背景颜色颜色 */
|
||||
|
||||
/* --color-background: 210deg 25% 96.86%; */
|
||||
// --color-main: 210deg 25% 96.86%;
|
||||
--color-background: 0 0 100%;
|
||||
// --color-darken-background: 220deg 13.04% 8%;
|
||||
|
||||
/* --color-background: 220 14% 95%; */
|
||||
|
||||
/* 基础文本颜色 */
|
||||
--color-foreground: 210 6% 21%;
|
||||
|
||||
/* 主题颜色 */
|
||||
--color-primary: 211 91% 39%;
|
||||
|
||||
/* 前景色,如按钮文本颜色 */
|
||||
--color-primary-foreground: 0 0 98%;
|
||||
|
||||
/* 颜色次要 */
|
||||
--color-secondary: 240 5% 96%;
|
||||
|
||||
/* 前景色,如按钮文本颜色 */
|
||||
--color-secondary-foreground: 240 6% 10%;
|
||||
|
||||
/* 次要文本颜色 */
|
||||
--color-secondary-desc: 216.4 16.09% 34.12%;
|
||||
|
||||
/* 普通颜色 */
|
||||
--color-accent: 240 5% 96%;
|
||||
--color-accent-hover: 200deg 10% 90%;
|
||||
|
||||
/* 普通颜色前景色,如按钮文本颜色 */
|
||||
--color-accent-foreground: 240 6% 10%;
|
||||
|
||||
/* 破坏性颜色 */
|
||||
--color-destructive: 0 77.78% 68.24%;
|
||||
|
||||
/* 破坏性颜色 */
|
||||
--color-destructive-foreground: 0 0 98%;
|
||||
--color-muted: 210 40% 96.1%;
|
||||
--color-muted-foreground: 215.4 16.3% 46.9%;
|
||||
--color-heavy: 192deg 9.43% 89.61%;
|
||||
--color-heavy-foreground: var(--color-accent-foreground);
|
||||
--color-popover: 0 0% 100%;
|
||||
--color-popover-foreground: 222.2 84% 4.9%;
|
||||
--color-card: 0 0% 100%;
|
||||
--color-card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
/* 基础边框色 */
|
||||
--color-border: 240 6% 90%;
|
||||
|
||||
/* 基础文本边框色 */
|
||||
--color-input: 240deg 5.88% 90%;
|
||||
|
||||
/* input placeholder 颜色 */
|
||||
--color-input-placeholder: 217 10.6% 65%;
|
||||
|
||||
/* 基础文本背景色 */
|
||||
--color-input-background: 0 0 100%;
|
||||
--color-ring: 222.2 84% 4.9%;
|
||||
|
||||
/* 遮罩颜色 */
|
||||
--color-overlay: 0deg 0% 0% / 40%;
|
||||
|
||||
/* dark */
|
||||
--color-dark-foreground: 220 13% 91%;
|
||||
--color-dark-border: 0deg 0% 100% / 10%;
|
||||
--color-dark-accent: 0deg 0% 100% / 8%;
|
||||
--color-dark-accent-hover: 0deg 0% 100% / 12%;
|
||||
|
||||
/* 基本文字大小 */
|
||||
--font-size-base: 16px;
|
||||
|
||||
/* 基本圆角大小 */
|
||||
--radius-base: 0.5rem;
|
||||
|
||||
/* ======================================== */
|
||||
|
||||
/* =============component & UI============= */
|
||||
|
||||
/* ======================================== */
|
||||
|
||||
/* menu */
|
||||
--color-menu-dark: 225deg 12% 13%;
|
||||
--color-menu-dark-darken: 223deg 11% 10%;
|
||||
// --color-menu-darken: var(--color-background);
|
||||
// --color-menu-opened-dark: 225deg 12.12% 11%;
|
||||
--color-menu: 0deg 0% 100%;
|
||||
--color-menu-darken: 0deg 0% 95%;
|
||||
|
||||
accent-color: var(--color-primary);
|
||||
color-scheme: light;
|
||||
// --color-menu-opened: 0deg 0% 100%;
|
||||
}
|
||||
4
packages/@core/shared/design-tokens/src/index.ts
Normal file
4
packages/@core/shared/design-tokens/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import './default/index.scss';
|
||||
import './dark/index.scss';
|
||||
|
||||
export {};
|
||||
6
packages/@core/shared/design-tokens/tsconfig.json
Normal file
6
packages/@core/shared/design-tokens/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
3
packages/@core/shared/design-tokens/vite.config.mts
Normal file
3
packages/@core/shared/design-tokens/vite.config.mts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig();
|
||||
22
packages/@core/shared/design/build.config.ts
Normal file
22
packages/@core/shared/design/build.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: [
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
loaders: ['sass'],
|
||||
outDir: './dist',
|
||||
pattern: ['index.scss'],
|
||||
},
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
loaders: ['postcss'],
|
||||
outDir: './dist',
|
||||
pattern: ['tailwind.css'],
|
||||
},
|
||||
],
|
||||
});
|
||||
41
packages/@core/shared/design/package.json
Normal file
41
packages/@core/shared/design/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@vben-core/design",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/@vben-core/shared/design"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"stub": "pnpm unbuild --stub"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"development": "./src/scss/index.scss",
|
||||
"default": "./dist/index.css"
|
||||
},
|
||||
"./tailwind": {
|
||||
"development": "./src/tailwind.css",
|
||||
"default": "./dist/tailwind.css"
|
||||
},
|
||||
"./global": {
|
||||
"default": "./src/scss/global.scss"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"modern-normalize": "^2.0.0"
|
||||
}
|
||||
}
|
||||
327
packages/@core/shared/design/src/index.css
Normal file
327
packages/@core/shared/design/src/index.css
Normal file
@@ -0,0 +1,327 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
/** css 样式重置 */
|
||||
@import 'modern-normalize/modern-normalize.css';
|
||||
|
||||
#app,
|
||||
.ant-app,
|
||||
body,
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before {
|
||||
@apply border-border;
|
||||
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
body.invert-mode {
|
||||
@apply invert;
|
||||
}
|
||||
|
||||
body.grayscale-mode {
|
||||
@apply grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply text-foreground bg-background;
|
||||
|
||||
font-variation-settings: normal;
|
||||
text-size-adjust: 100%;
|
||||
font-synthesis-weight: none;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a,
|
||||
a:active,
|
||||
a:hover,
|
||||
a:link,
|
||||
a:visited {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
::view-transition-new(root),
|
||||
::view-transition-old(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 2147483646;
|
||||
}
|
||||
|
||||
html.dark::view-transition-old(root) {
|
||||
z-index: 2147483646;
|
||||
}
|
||||
|
||||
html.dark::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
border: none;
|
||||
box-shadow: 0 0 0 1000px transparent inset;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.slide-up-move {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.slide-down-move {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
}
|
||||
|
||||
.slide-left-enter-active,
|
||||
.slide-left-leave-active {
|
||||
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.slide-left-move {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.slide-left-enter-from,
|
||||
.slide-left-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
|
||||
.slide-right-enter-active,
|
||||
.slide-right-leave-active {
|
||||
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
.slide-right-move {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.slide-right-enter-from,
|
||||
.slide-right-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
|
||||
.fade-transition-enter-active,
|
||||
.fade-transition-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-transition-enter-from,
|
||||
.fade-transition-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.fade-down-enter-active,
|
||||
.fade-down-leave-active {
|
||||
transition:
|
||||
opacity 0.25s,
|
||||
transform 0.3s;
|
||||
}
|
||||
|
||||
.fade-down-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
|
||||
.fade-down-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
.fade-scale-leave-active,
|
||||
.fade-scale-enter-active {
|
||||
transition: all 0.28s;
|
||||
}
|
||||
|
||||
.fade-scale-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.fade-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.fade-up-enter-active,
|
||||
.fade-up-leave-active {
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.25s;
|
||||
}
|
||||
|
||||
.fade-up-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
.fade-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
|
||||
@keyframes fade-slide {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-down {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-slow {
|
||||
animation: fade 3s infinite;
|
||||
}
|
||||
|
||||
.fade-slide-slow {
|
||||
animation: fade-slide 3s infinite;
|
||||
}
|
||||
|
||||
.fade-up-slow {
|
||||
animation: fade-up 3s infinite;
|
||||
}
|
||||
|
||||
.fade-down-slow {
|
||||
animation: fade-down 3s infinite;
|
||||
}
|
||||
|
||||
.collapse-transition {
|
||||
transition:
|
||||
0.2s height ease-in-out,
|
||||
0.2s padding-top ease-in-out,
|
||||
0.2s padding-bottom ease-in-out;
|
||||
}
|
||||
|
||||
.collapse-transition-leave-active,
|
||||
.collapse-transition-enter-active {
|
||||
transition:
|
||||
0.2s max-height ease-in-out,
|
||||
0.2s padding-top ease-in-out,
|
||||
0.2s margin-top ease-in-out;
|
||||
}
|
||||
1
packages/@core/shared/design/src/index.scss
Normal file
1
packages/@core/shared/design/src/index.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import './scss/index';
|
||||
86
packages/@core/shared/design/src/scss/common/base.scss
Normal file
86
packages/@core/shared/design/src/scss/common/base.scss
Normal file
@@ -0,0 +1,86 @@
|
||||
#app,
|
||||
.ant-app,
|
||||
body,
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
*,
|
||||
::after,
|
||||
::before {
|
||||
@apply border-border;
|
||||
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
body.invert-mode {
|
||||
@apply invert;
|
||||
}
|
||||
|
||||
body.grayscale-mode {
|
||||
@apply grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
@apply text-foreground bg-background;
|
||||
|
||||
// font-size: 62.5%;
|
||||
font-variation-settings: normal;
|
||||
text-size-adjust: 100%;
|
||||
font-synthesis-weight: none;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizelegibility;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a,
|
||||
a:active,
|
||||
a:hover,
|
||||
a:link,
|
||||
a:visited {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
::view-transition-new(root),
|
||||
::view-transition-old(root) {
|
||||
mix-blend-mode: normal;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 2147483646;
|
||||
}
|
||||
|
||||
html.dark::view-transition-old(root) {
|
||||
z-index: 2147483646;
|
||||
}
|
||||
|
||||
html.dark::view-transition-new(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
// color: hsl(var(--color-input-placeholder)) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
border: none;
|
||||
box-shadow: 0 0 0 1000px transparent inset;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
$namespace: 'vben' !default;
|
||||
$common-separator: '-' !default;
|
||||
$element-separator: '__' !default;
|
||||
$modifier-separator: '--' !default;
|
||||
$state-prefix: 'is' !default;
|
||||
34
packages/@core/shared/design/src/scss/global.scss
Normal file
34
packages/@core/shared/design/src/scss/global.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
@forward './common/constants.scss';
|
||||
|
||||
@mixin b($block) {
|
||||
$B: $namespace + '-' + $block !global;
|
||||
|
||||
.#{$B} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin e($name) {
|
||||
@at-root {
|
||||
&#{$element-separator}#{$name} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin m($name) {
|
||||
@at-root {
|
||||
&#{$modifier-separator}#{$name} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// block__element.is-active {}
|
||||
@mixin is($state, $prefix: $state-prefix) {
|
||||
@at-root {
|
||||
&.#{$prefix}-#{$state} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
packages/@core/shared/design/src/scss/index.scss
Normal file
4
packages/@core/shared/design/src/scss/index.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
/** css 样式重置 */
|
||||
@import 'modern-normalize/modern-normalize.css';
|
||||
@import './common/base';
|
||||
@import './transition';
|
||||
@@ -0,0 +1,81 @@
|
||||
@keyframes fade-slide {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-up {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-down {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-slow {
|
||||
animation: fade 3s infinite;
|
||||
}
|
||||
|
||||
// .fade-slide-fast {
|
||||
// animation: fade-slide 0.3s infinite;
|
||||
// }
|
||||
|
||||
.fade-slide-slow {
|
||||
animation: fade-slide 3s infinite;
|
||||
}
|
||||
|
||||
.fade-up-slow {
|
||||
animation: fade-up 3s infinite;
|
||||
}
|
||||
|
||||
.fade-down-slow {
|
||||
animation: fade-down 3s infinite;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.collapse-transition {
|
||||
transition:
|
||||
0.2s height ease-in-out,
|
||||
0.2s padding-top ease-in-out,
|
||||
0.2s padding-bottom ease-in-out;
|
||||
}
|
||||
|
||||
.collapse-transition-leave-active,
|
||||
.collapse-transition-enter-active {
|
||||
transition:
|
||||
0.2s max-height ease-in-out,
|
||||
0.2s padding-top ease-in-out,
|
||||
0.2s margin-top ease-in-out;
|
||||
}
|
||||
97
packages/@core/shared/design/src/scss/transition/fade.scss
Normal file
97
packages/@core/shared/design/src/scss/transition/fade.scss
Normal file
@@ -0,0 +1,97 @@
|
||||
.fade-transition {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////
|
||||
// Fade down
|
||||
// ///////////////////////////////////////////////
|
||||
|
||||
// Speed: 1x
|
||||
.fade-down-enter-active,
|
||||
.fade-down-leave-active {
|
||||
transition:
|
||||
opacity 0.25s,
|
||||
transform 0.3s;
|
||||
}
|
||||
|
||||
.fade-down-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
|
||||
.fade-down-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
// fade-scale
|
||||
.fade-scale-leave-active,
|
||||
.fade-scale-enter-active {
|
||||
transition: all 0.28s;
|
||||
}
|
||||
|
||||
.fade-scale-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.fade-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
// ///////////////////////////////////////////////
|
||||
// Fade Top
|
||||
// ///////////////////////////////////////////////
|
||||
|
||||
// Speed: 1x
|
||||
.fade-up-enter-active,
|
||||
.fade-up-leave-active {
|
||||
transition:
|
||||
opacity 0.2s,
|
||||
transform 0.25s;
|
||||
}
|
||||
|
||||
.fade-up-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10%);
|
||||
}
|
||||
|
||||
.fade-up-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10%);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@import './slide';
|
||||
@import './fade';
|
||||
@import './animation';
|
||||
@import './collapse';
|
||||
10
packages/@core/shared/design/src/scss/transition/mixin.scss
Normal file
10
packages/@core/shared/design/src/scss/transition/mixin.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
@mixin transition() {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||
}
|
||||
|
||||
&-move {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
41
packages/@core/shared/design/src/scss/transition/slide.scss
Normal file
41
packages/@core/shared/design/src/scss/transition/slide.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@use 'mixin.scss' as *;
|
||||
|
||||
.slide-up {
|
||||
@include transition;
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-down {
|
||||
@include transition;
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-left {
|
||||
@include transition;
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-right {
|
||||
@include transition;
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
}
|
||||
39
packages/@core/shared/design/src/tailwind.css
Normal file
39
packages/@core/shared/design/src/tailwind.css
Normal file
@@ -0,0 +1,39 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.flex-center {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
|
||||
.flex-col-center {
|
||||
@apply flex flex-col items-center justify-center;
|
||||
}
|
||||
|
||||
.outline-box {
|
||||
@apply outline-border relative cursor-pointer rounded-md p-1 outline outline-1;
|
||||
}
|
||||
|
||||
.outline-box::after {
|
||||
@apply absolute left-1/2 top-1/2 z-20 h-0 w-[1px] rounded-sm opacity-0 outline outline-2 outline-transparent transition-all duration-300 content-[""];
|
||||
}
|
||||
|
||||
.outline-box.outline-box-active {
|
||||
@apply outline-primary outline outline-2;
|
||||
}
|
||||
|
||||
.outline-box.outline-box-active::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.outline-box:not(.outline-box-active):hover::after {
|
||||
@apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
|
||||
}
|
||||
}
|
||||
6
packages/@core/shared/design/tsconfig.json
Normal file
6
packages/@core/shared/design/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
30
packages/@core/shared/iconify/package.json
Normal file
30
packages/@core/shared/iconify/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@vben-core/iconify",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/@vben-core/shared/iconify"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"vue": "3.4.27"
|
||||
}
|
||||
}
|
||||
13
packages/@core/shared/iconify/src/factory.ts
Normal file
13
packages/@core/shared/iconify/src/factory.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
function createIcon(name: string) {
|
||||
return defineComponent({
|
||||
setup(props, { attrs }) {
|
||||
return () => h(Icon, { icon: name, ...props, ...attrs });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { createIcon };
|
||||
5
packages/@core/shared/iconify/src/index.ts
Normal file
5
packages/@core/shared/iconify/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './factory';
|
||||
export * from './material';
|
||||
export * from './mdi';
|
||||
|
||||
export * from '@iconify/vue';
|
||||
77
packages/@core/shared/iconify/src/material.ts
Normal file
77
packages/@core/shared/iconify/src/material.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createIcon } from './factory';
|
||||
|
||||
export const IconDefault = createIcon('ic:round-auto-awesome');
|
||||
|
||||
export const IcRoundKeyboardArrowDown = createIcon(
|
||||
'ic:round-keyboard-arrow-down',
|
||||
);
|
||||
|
||||
export const IcRoundChevronRight = createIcon('ic:round-chevron-right');
|
||||
|
||||
export const IcRoundKeyboard = createIcon('ic:round-keyboard');
|
||||
// export const IcRoundMenuOpen = createIcon('ic:round-menu-open');
|
||||
|
||||
export const IcRoundMenu = createIcon('ic:round-menu');
|
||||
|
||||
export const IcRoundMoreHoriz = createIcon('ic:round-more-horiz');
|
||||
|
||||
export const IcRoundFitScreen = createIcon('ic:round-fit-screen');
|
||||
|
||||
export const IcTwotoneFitScreen = createIcon('ic:twotone-fit-screen');
|
||||
|
||||
export const IcRoundColorLens = createIcon('ic:round-color-lens');
|
||||
|
||||
export const IcRoundMoreVert = createIcon('ic:round-more-vert');
|
||||
|
||||
export const IcRoundFullscreen = createIcon('ic:round-fullscreen');
|
||||
|
||||
export const IcRoundFullscreenExit = createIcon('ic:round-fullscreen-exit');
|
||||
|
||||
export const IcRoundAutoAwesome = createIcon('ic:round-auto-awesome');
|
||||
|
||||
export const IcRoundClose = createIcon('ic:round-close');
|
||||
|
||||
export const IcRoundRestartAlt = createIcon('ic:round-restart-alt');
|
||||
|
||||
export const IcRoundLogout = createIcon('ic:round-logout');
|
||||
|
||||
export const IcOutlineVisibility = createIcon('ic:outline-visibility');
|
||||
|
||||
export const IcOutlineVisibilityOff = createIcon('ic:outline-visibility-off');
|
||||
|
||||
export const IcRoundSearch = createIcon('ic:round-search');
|
||||
|
||||
export const IcRoundFolderCopy = createIcon('ic:round-folder-copy');
|
||||
|
||||
export const IcRoundSubdirectoryArrowLeft = createIcon(
|
||||
'ic:round-subdirectory-arrow-left',
|
||||
);
|
||||
export const IcRoundArrowUpward = createIcon('ic:round-arrow-upward');
|
||||
|
||||
export const IcRoundArrowDownward = createIcon('ic:round-arrow-downward');
|
||||
|
||||
export const IcBaselineLanguage = createIcon('ic:baseline-language');
|
||||
|
||||
export const IcRoundSearchOff = createIcon('ic:round-search-off');
|
||||
|
||||
export const IcRoundNotificationsNone = createIcon(
|
||||
'ic:round-notifications-none',
|
||||
);
|
||||
|
||||
export const IcRoundMarkEmailRead = createIcon('ic:round-mark-email-read');
|
||||
|
||||
export const IcRoundWbSunny = createIcon('ic:round-wb-sunny');
|
||||
|
||||
export const IcRoundMotionPhotosAuto = createIcon(
|
||||
'ic:round-motion-photos-auto',
|
||||
);
|
||||
|
||||
export const IcRoundSettingsSuggest = createIcon('ic:round-settings-suggest');
|
||||
|
||||
export const IcRoundArrowBackIosNew = createIcon('ic:round-arrow-back-ios-new');
|
||||
|
||||
export const IcRoundMultipleStop = createIcon('ic:round-multiple-stop');
|
||||
|
||||
export const IcRoundRefresh = createIcon('ic:round-refresh');
|
||||
|
||||
export const IcRoundCreditScore = createIcon('ic:round-credit-score');
|
||||
49
packages/@core/shared/iconify/src/mdi.ts
Normal file
49
packages/@core/shared/iconify/src/mdi.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createIcon } from './factory';
|
||||
|
||||
export const MdiKeyboardEsc = createIcon('mdi:keyboard-esc');
|
||||
|
||||
export const MdiLoading = createIcon('mdi:loading');
|
||||
|
||||
export const MdiWechat = createIcon('mdi:wechat');
|
||||
|
||||
export const MdiGithub = createIcon('mdi:github');
|
||||
|
||||
export const MdiGoogle = createIcon('mdi:google');
|
||||
|
||||
export const MdiQqchat = createIcon('mdi:qqchat');
|
||||
|
||||
export const MdiPin = createIcon('mdi:pin');
|
||||
|
||||
export const MdiPinOff = createIcon('mdi:pin-off');
|
||||
|
||||
export const MdiFormatHorizontalAlignLeft = createIcon(
|
||||
'mdi:format-horizontal-align-left',
|
||||
);
|
||||
|
||||
export const MdiFormatHorizontalAlignRight = createIcon(
|
||||
'mdi:format-horizontal-align-right',
|
||||
);
|
||||
|
||||
export const MdiArrowExpandHorizontal = createIcon(
|
||||
'mdi:arrow-expand-horizontal',
|
||||
);
|
||||
|
||||
export const MdiMenuClose = createIcon('mdi:menu-close');
|
||||
|
||||
export const MdiMenuOpen = createIcon('mdi:menu-open');
|
||||
|
||||
export const MdiDockLeft = createIcon('mdi:dock-left');
|
||||
|
||||
export const MdiDockRight = createIcon('mdi:dock-right');
|
||||
|
||||
export const MdiDockBottom = createIcon('mdi:dock-bottom');
|
||||
|
||||
export const MdiDriveDocument = createIcon('mdi:drive-document');
|
||||
|
||||
export const MdiMoonAndStars = createIcon('mdi:moon-and-stars');
|
||||
|
||||
export const MdiEditBoxOutline = createIcon('mdi:edit-box-outline');
|
||||
|
||||
export const MdiQuestionMarkCircleOutline = createIcon(
|
||||
'mdi:question-mark-circle-outline',
|
||||
);
|
||||
6
packages/@core/shared/iconify/tsconfig.json
Normal file
6
packages/@core/shared/iconify/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
7
packages/@core/shared/toolkit/build.config.ts
Normal file
7
packages/@core/shared/toolkit/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
||||
51
packages/@core/shared/toolkit/package.json
Normal file
51
packages/@core/shared/toolkit/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@vben-core/toolkit",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/@vben-core/shared/toolkit"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"stub": "pnpm unbuild --stub"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "4.1.0",
|
||||
"@vue/shared": "^3.4.27",
|
||||
"dayjs": "^1.11.11",
|
||||
"defu": "^6.1.4",
|
||||
"nprogress": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nprogress": "^0.2.3"
|
||||
}
|
||||
}
|
||||
41
packages/@core/shared/toolkit/src/color.test.ts
Normal file
41
packages/@core/shared/toolkit/src/color.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { convertToHsl, convertToHslCssVar, isValidColor } from './color';
|
||||
|
||||
describe('color conversion functions', () => {
|
||||
it('should correctly convert color to HSL format', () => {
|
||||
const color = '#ff0000';
|
||||
const expectedHsl = 'hsl(0 100% 50%)';
|
||||
expect(convertToHsl(color)).toEqual(expectedHsl);
|
||||
});
|
||||
|
||||
it('should correctly convert color with alpha to HSL format', () => {
|
||||
const color = 'rgba(255, 0, 0, 0.5)';
|
||||
const expectedHsl = 'hsl(0 100% 50%) 0.5';
|
||||
expect(convertToHsl(color)).toEqual(expectedHsl);
|
||||
});
|
||||
|
||||
it('should correctly convert color to HSL CSS variable format', () => {
|
||||
const color = '#ff0000';
|
||||
const expectedHsl = '0 100% 50%';
|
||||
expect(convertToHslCssVar(color)).toEqual(expectedHsl);
|
||||
});
|
||||
|
||||
it('should correctly convert color with alpha to HSL CSS variable format', () => {
|
||||
const color = 'rgba(255, 0, 0, 0.5)';
|
||||
const expectedHsl = '0 100% 50% / 0.5';
|
||||
expect(convertToHslCssVar(color)).toEqual(expectedHsl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidColor', () => {
|
||||
it('isValidColor function', () => {
|
||||
// 测试有效颜色
|
||||
expect(isValidColor('blue')).toBe(true);
|
||||
expect(isValidColor('#000000')).toBe(true);
|
||||
|
||||
// 测试无效颜色
|
||||
expect(isValidColor('invalid color')).toBe(false);
|
||||
expect(isValidColor()).toBe(false);
|
||||
});
|
||||
});
|
||||
44
packages/@core/shared/toolkit/src/color.ts
Normal file
44
packages/@core/shared/toolkit/src/color.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
/**
|
||||
* 将颜色转换为HSL格式。
|
||||
*
|
||||
* HSL是一种颜色模型,包括色相(Hue)、饱和度(Saturation)和亮度(Lightness)三个部分。
|
||||
* 这个函数使用TinyColor库将输入的颜色转换为HSL格式,并返回一个字符串。
|
||||
*
|
||||
* @param {string} color 输入的颜色,可以是任何TinyColor支持的颜色格式。
|
||||
* @returns {string} HSL格式的颜色字符串。
|
||||
*/
|
||||
function convertToHsl(color: string): string {
|
||||
const { a, h, l, s } = new TinyColor(color).toHsl();
|
||||
const hsl = `hsl(${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%)`;
|
||||
return a < 1 ? `${hsl} ${a}` : hsl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将颜色转换为HSL CSS变量。
|
||||
*
|
||||
* 这个函数与convertToHsl函数类似,但是返回的字符串格式稍有不同,
|
||||
* 以便可以作为CSS变量使用。
|
||||
*
|
||||
* @param {string} color 输入的颜色,可以是任何TinyColor支持的颜色格式。
|
||||
* @returns {string} 可以作为CSS变量使用的HSL格式的颜色字符串。
|
||||
*/
|
||||
function convertToHslCssVar(color: string): string {
|
||||
const { a, h, l, s } = new TinyColor(color).toHsl();
|
||||
const hsl = `${Math.round(h)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
||||
return a < 1 ? `${hsl} / ${a}` : hsl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查颜色是否有效
|
||||
* @param {string} color - 待检查的颜色
|
||||
* 如果颜色有效返回true,否则返回false
|
||||
*/
|
||||
function isValidColor(color?: string) {
|
||||
if (!color) {
|
||||
return false;
|
||||
}
|
||||
return new TinyColor(color).isValid;
|
||||
}
|
||||
|
||||
export { TinyColor, convertToHsl, convertToHslCssVar, isValidColor };
|
||||
35
packages/@core/shared/toolkit/src/date.test.ts
Normal file
35
packages/@core/shared/toolkit/src/date.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatDate, formatDateTime } from './date';
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('should return "2023-01-01" when passed new Date("2023-01-01T00:00:00.000Z")', () => {
|
||||
const date = new Date('2023-01-01T00:00:00.000Z');
|
||||
const actual = formatDate(date);
|
||||
expect(actual).toBe('2023-01-01');
|
||||
});
|
||||
|
||||
it('should return "2023-01-01" when passed new Date("2023-01-01T00:00:00")', () => {
|
||||
const date = new Date('2023-01-01T00:00:00.000Z');
|
||||
const actual = formatDate(date, 'YYYY-MM-DD');
|
||||
expect(actual).toBe('2023-01-01');
|
||||
});
|
||||
|
||||
it('should throw an error when passed an invalid date', () => {
|
||||
const date = '2018-10-10-10-10-10';
|
||||
expect(formatDate(date)).toBe('Invalid Date');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDateTime', () => {
|
||||
it('should return "2023-01-01" when passed new Date("2023-01-01T00:00:00.000Z")', () => {
|
||||
const date = new Date('2023-01-01T00:00:00.000Z');
|
||||
const actual = formatDateTime(date);
|
||||
expect(actual).toBe('2023-01-01 08:00:00');
|
||||
});
|
||||
|
||||
it('should throw an error when passed an invalid date', () => {
|
||||
const date = '2018-10-10-10-10-10';
|
||||
expect(formatDateTime(date)).toBe('Invalid Date');
|
||||
});
|
||||
});
|
||||
25
packages/@core/shared/toolkit/src/date.ts
Normal file
25
packages/@core/shared/toolkit/src/date.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import dateFunc, { type ConfigType } from 'dayjs';
|
||||
|
||||
const DATE_TIME_TEMPLATE = 'YYYY-MM-DD HH:mm:ss';
|
||||
const DATE_TEMPLATE = 'YYYY-MM-DD';
|
||||
|
||||
/**
|
||||
* @zh_CN 格式化日期时间
|
||||
* @param date 待格式化的日期时间
|
||||
* @param format 格式化的方式
|
||||
* @returns 格式化后的日期字符串,默认:YYYY-MM-DD HH:mm:ss
|
||||
*/
|
||||
function formatDate(date?: ConfigType, format = DATE_TEMPLATE): string {
|
||||
return dateFunc(date).format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 格式化日期时间
|
||||
* @param date 待格式化的日期时间
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
function formatDateTime(date?: ConfigType): string {
|
||||
return formatDate(date, DATE_TIME_TEMPLATE);
|
||||
}
|
||||
|
||||
export { formatDate, formatDateTime };
|
||||
60
packages/@core/shared/toolkit/src/diff.test.ts
Normal file
60
packages/@core/shared/toolkit/src/diff.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { diff } from './diff';
|
||||
|
||||
describe('diff function', () => {
|
||||
it('should correctly find differences in flat objects', () => {
|
||||
const oldObj = { a: 1, b: 2, c: 3 };
|
||||
const newObj = { a: 1, b: 3, c: 3 };
|
||||
expect(diff(oldObj, newObj)).toEqual({ b: 3 });
|
||||
});
|
||||
|
||||
it('should correctly handle nested objects', () => {
|
||||
const oldObj = { a: { b: 1, c: 2 }, d: 3 };
|
||||
const newObj = { a: { b: 1, c: 3 }, d: 3 };
|
||||
expect(diff(oldObj, newObj)).toEqual({ a: { b: 1, c: 3 } });
|
||||
});
|
||||
|
||||
it('should correctly handle arrays`', () => {
|
||||
const oldObj = { a: [1, 2, 3] };
|
||||
const newObj = { a: [1, 2, 4] };
|
||||
expect(diff(oldObj, newObj)).toEqual({ a: [1, 2, 4] });
|
||||
});
|
||||
|
||||
it('should correctly handle nested arrays', () => {
|
||||
const oldObj = {
|
||||
a: [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
],
|
||||
};
|
||||
const newObj = {
|
||||
a: [
|
||||
[1, 2],
|
||||
[3, 5],
|
||||
],
|
||||
};
|
||||
expect(diff(oldObj, newObj)).toEqual({
|
||||
a: [
|
||||
[1, 2],
|
||||
[3, 5],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if objects are identical', () => {
|
||||
const oldObj = { a: 1, b: 2, c: 3 };
|
||||
const newObj = { a: 1, b: 2, c: 3 };
|
||||
expect(diff(oldObj, newObj)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return differences between two objects excluding ignored fields', () => {
|
||||
const oldObj = { a: 1, b: 2, c: 3, d: 6 };
|
||||
const newObj = { a: 2, b: 2, c: 4, d: 5 };
|
||||
const ignoreFields: (keyof typeof newObj)[] = ['a', 'd'];
|
||||
|
||||
const result = diff(oldObj, newObj, ignoreFields);
|
||||
|
||||
expect(result).toEqual({ c: 4 });
|
||||
});
|
||||
});
|
||||
58
packages/@core/shared/toolkit/src/diff.ts
Normal file
58
packages/@core/shared/toolkit/src/diff.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
type Diff<T = any> = T;
|
||||
|
||||
// 比较两个数组是否相等
|
||||
|
||||
function arraysEqual<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false;
|
||||
const counter = new Map<T, number>();
|
||||
for (const value of a) {
|
||||
counter.set(value, (counter.get(value) || 0) + 1);
|
||||
}
|
||||
for (const value of b) {
|
||||
const count = counter.get(value);
|
||||
if (count === undefined || count === 0) {
|
||||
return false;
|
||||
}
|
||||
counter.set(value, count - 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 深度对比两个值
|
||||
function deepEqual<T>(oldVal: T, newVal: T): boolean {
|
||||
if (
|
||||
typeof oldVal === 'object' &&
|
||||
oldVal !== null &&
|
||||
typeof newVal === 'object' &&
|
||||
newVal !== null
|
||||
) {
|
||||
return Array.isArray(oldVal) && Array.isArray(newVal)
|
||||
? arraysEqual(oldVal, newVal)
|
||||
: diff(oldVal as any, newVal as any) === null;
|
||||
} else {
|
||||
return oldVal === newVal;
|
||||
}
|
||||
}
|
||||
|
||||
// 主要的 diff 函数
|
||||
function diff<T extends object>(
|
||||
oldObj: T,
|
||||
newObj: T,
|
||||
ignoreFields: (keyof T)[] = [],
|
||||
): { [K in keyof T]?: Diff<T[K]> } | null {
|
||||
const difference: { [K in keyof T]?: Diff<T[K]> } = {};
|
||||
|
||||
for (const key in oldObj) {
|
||||
if (ignoreFields.includes(key)) continue;
|
||||
const oldValue = oldObj[key];
|
||||
const newValue = newObj[key];
|
||||
|
||||
if (!deepEqual(oldValue, newValue)) {
|
||||
difference[key] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(difference).length === 0 ? null : difference;
|
||||
}
|
||||
|
||||
export { arraysEqual, diff };
|
||||
22
packages/@core/shared/toolkit/src/hash.test.ts
Normal file
22
packages/@core/shared/toolkit/src/hash.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { generateUUID } from './hash';
|
||||
|
||||
describe('generateUUID', () => {
|
||||
it('should return a string', () => {
|
||||
const uuid = generateUUID();
|
||||
expect(typeof uuid).toBe('string');
|
||||
});
|
||||
|
||||
it('should be length 32', () => {
|
||||
const uuid = generateUUID();
|
||||
expect(uuid.length).toBe(36);
|
||||
});
|
||||
|
||||
it('should have the correct format', () => {
|
||||
const uuid = generateUUID();
|
||||
const uuidRegex =
|
||||
/^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i;
|
||||
expect(uuidRegex.test(uuid)).toBe(true);
|
||||
});
|
||||
});
|
||||
31
packages/@core/shared/toolkit/src/hash.ts
Normal file
31
packages/@core/shared/toolkit/src/hash.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 生成一个UUID(通用唯一标识符)。
|
||||
*
|
||||
* UUID是一种用于软件构建的标识符,其目的是能够生成一个唯一的ID,以便在全局范围内标识信息。
|
||||
* 此函数用于生成一个符合version 4的UUID,这种UUID是随机生成的。
|
||||
*
|
||||
* 生成的UUID的格式为:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
* 其中,x是任意16进制数字,y是一个16进制数字,取值范围为[8, b]。
|
||||
*
|
||||
* @returns {string} 生成的UUID。
|
||||
*/
|
||||
function generateUUID(): string {
|
||||
let d = Date.now();
|
||||
if (
|
||||
typeof performance !== 'undefined' &&
|
||||
typeof performance.now === 'function'
|
||||
) {
|
||||
d += performance.now(); // use high-precision timer if available
|
||||
}
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replaceAll(
|
||||
/[xy]/g,
|
||||
(c) => {
|
||||
const r = Math.trunc((d + Math.random() * 16) % 16);
|
||||
d = Math.floor(d / 16);
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
||||
},
|
||||
);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
export { generateUUID };
|
||||
11
packages/@core/shared/toolkit/src/index.ts
Normal file
11
packages/@core/shared/toolkit/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from './color';
|
||||
export * from './date';
|
||||
export * from './diff';
|
||||
export * from './hash';
|
||||
export * from './inference';
|
||||
export * from './letter';
|
||||
export * from './merge';
|
||||
export * from './namespace';
|
||||
export * from './nprogress';
|
||||
export * from './tree';
|
||||
export * from './window';
|
||||
114
packages/@core/shared/toolkit/src/inference.test.ts
Normal file
114
packages/@core/shared/toolkit/src/inference.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
isEmpty,
|
||||
isHttpUrl,
|
||||
isObject,
|
||||
isUndefined,
|
||||
isWindow,
|
||||
} from './inference';
|
||||
|
||||
describe('isHttpUrl', () => {
|
||||
it("should return true when given 'http://example.com'", () => {
|
||||
expect(isHttpUrl('http://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when given 'https://example.com'", () => {
|
||||
expect(isHttpUrl('https://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when given 'ftp://example.com'", () => {
|
||||
expect(isHttpUrl('ftp://example.com')).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when given 'example.com'", () => {
|
||||
expect(isHttpUrl('example.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUndefined', () => {
|
||||
it('isUndefined should return true for undefined values', () => {
|
||||
expect(isUndefined()).toBe(true);
|
||||
});
|
||||
|
||||
it('isUndefined should return false for null values', () => {
|
||||
expect(isUndefined(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('isUndefined should return false for defined values', () => {
|
||||
expect(isUndefined(0)).toBe(false);
|
||||
expect(isUndefined('')).toBe(false);
|
||||
expect(isUndefined(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('isUndefined should return false for objects and arrays', () => {
|
||||
expect(isUndefined({})).toBe(false);
|
||||
expect(isUndefined([])).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmpty', () => {
|
||||
it('should return true for empty string', () => {
|
||||
expect(isEmpty('')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for empty array', () => {
|
||||
expect(isEmpty([])).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for empty object', () => {
|
||||
expect(isEmpty({})).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-empty string', () => {
|
||||
expect(isEmpty('hello')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-empty array', () => {
|
||||
expect(isEmpty([1, 2, 3])).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-empty object', () => {
|
||||
expect(isEmpty({ a: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for null or undefined', () => {
|
||||
expect(isEmpty(null)).toBe(true);
|
||||
expect(isEmpty()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for number or boolean', () => {
|
||||
expect(isEmpty(0)).toBe(false);
|
||||
expect(isEmpty(true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWindow', () => {
|
||||
it('should return true for the window object', () => {
|
||||
expect(isWindow(window)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other objects', () => {
|
||||
expect(isWindow({})).toBe(false);
|
||||
expect(isWindow([])).toBe(false);
|
||||
expect(isWindow(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObject', () => {
|
||||
it('should return true for objects', () => {
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject({ a: 1 })).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(isObject([1, 2, 3])).toBe(true);
|
||||
expect(isObject(new Date())).toBe(true);
|
||||
expect(isObject(/regex/)).toBe(true);
|
||||
});
|
||||
});
|
||||
110
packages/@core/shared/toolkit/src/inference.ts
Normal file
110
packages/@core/shared/toolkit/src/inference.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { isFunction, isObject, isString } from '@vue/shared';
|
||||
|
||||
/**
|
||||
* 检查传入的值是否为undefined。
|
||||
*
|
||||
* @param {unknown} value 要检查的值。
|
||||
* @returns {boolean} 如果值是undefined,返回true,否则返回false。
|
||||
*/
|
||||
function isUndefined(value?: unknown): value is undefined {
|
||||
return value === undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查传入的值是否为空。
|
||||
*
|
||||
* 以下情况将被认为是空:
|
||||
* - 值为null。
|
||||
* - 值为undefined。
|
||||
* - 值为一个空字符串。
|
||||
* - 值为一个长度为0的数组。
|
||||
* - 值为一个没有元素的Map或Set。
|
||||
* - 值为一个没有属性的对象。
|
||||
*
|
||||
* @param {T} value 要检查的值。
|
||||
* @returns {boolean} 如果值为空,返回true,否则返回false。
|
||||
*/
|
||||
function isEmpty<T = unknown>(value: T): value is T {
|
||||
if (value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(value) || isString(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
|
||||
if (value instanceof Map || value instanceof Set) {
|
||||
return value.size === 0;
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
return Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查传入的字符串是否为有效的HTTP或HTTPS URL。
|
||||
*
|
||||
* @param {string} url 要检查的字符串。
|
||||
* @return {boolean} 如果字符串是有效的HTTP或HTTPS URL,返回true,否则返回false。
|
||||
*/
|
||||
function isHttpUrl(url?: string): boolean {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
// 使用正则表达式测试URL是否以http:// 或 https:// 开头
|
||||
const httpRegex = /^https?:\/\/.*$/;
|
||||
return httpRegex.test(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查传入的值是否为window对象。
|
||||
*
|
||||
* @param {any} value 要检查的值。
|
||||
* @returns {boolean} 如果值是window对象,返回true,否则返回false。
|
||||
*/
|
||||
function isWindow(value: any): value is Window {
|
||||
return (
|
||||
typeof window !== 'undefined' && value !== null && value === value.window
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前运行环境是否为Mac OS。
|
||||
*
|
||||
* 这个函数通过检查navigator.userAgent字符串来判断当前运行环境。
|
||||
* 如果userAgent字符串中包含"macintosh"或"mac os x"(不区分大小写),则认为当前环境是Mac OS。
|
||||
*
|
||||
* @returns {boolean} 如果当前环境是Mac OS,返回true,否则返回false。
|
||||
*/
|
||||
function isMacOs(): boolean {
|
||||
const macRegex = /macintosh|mac os x/i;
|
||||
return macRegex.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前运行环境是否为Windows OS。
|
||||
*
|
||||
* 这个函数通过检查navigator.userAgent字符串来判断当前运行环境。
|
||||
* 如果userAgent字符串中包含"windows"或"win32"(不区分大小写),则认为当前环境是Windows OS。
|
||||
*
|
||||
* @returns {boolean} 如果当前环境是Windows OS,返回true,否则返回false。
|
||||
*/
|
||||
function isWindowsOs(): boolean {
|
||||
const windowsRegex = /windows|win32/i;
|
||||
return windowsRegex.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
export {
|
||||
isEmpty,
|
||||
isFunction,
|
||||
isHttpUrl,
|
||||
isMacOs,
|
||||
isObject,
|
||||
isString,
|
||||
isUndefined,
|
||||
isWindow,
|
||||
isWindowsOs,
|
||||
};
|
||||
78
packages/@core/shared/toolkit/src/letter.test.ts
Normal file
78
packages/@core/shared/toolkit/src/letter.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
toCamelCase,
|
||||
toLowerCaseFirstLetter,
|
||||
} from './letter';
|
||||
|
||||
// 编写测试用例
|
||||
describe('capitalizeFirstLetter', () => {
|
||||
it('should capitalize the first letter of a string', () => {
|
||||
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
||||
expect(capitalizeFirstLetter('world')).toBe('World');
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(capitalizeFirstLetter('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle single character strings', () => {
|
||||
expect(capitalizeFirstLetter('a')).toBe('A');
|
||||
expect(capitalizeFirstLetter('b')).toBe('B');
|
||||
});
|
||||
|
||||
it('should not change the case of other characters', () => {
|
||||
expect(capitalizeFirstLetter('hElLo')).toBe('HElLo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toLowerCaseFirstLetter', () => {
|
||||
it('should convert the first letter to lowercase', () => {
|
||||
expect(toLowerCaseFirstLetter('CommonAppName')).toBe('commonAppName');
|
||||
expect(toLowerCaseFirstLetter('AnotherKeyExample')).toBe(
|
||||
'anotherKeyExample',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the same string if the first letter is already lowercase', () => {
|
||||
expect(toLowerCaseFirstLetter('alreadyLowerCase')).toBe('alreadyLowerCase');
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(toLowerCaseFirstLetter('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle single character strings', () => {
|
||||
expect(toLowerCaseFirstLetter('A')).toBe('a');
|
||||
expect(toLowerCaseFirstLetter('a')).toBe('a');
|
||||
});
|
||||
|
||||
it('should handle strings with only one uppercase letter', () => {
|
||||
expect(toLowerCaseFirstLetter('A')).toBe('a');
|
||||
});
|
||||
|
||||
it('should handle strings with special characters', () => {
|
||||
expect(toLowerCaseFirstLetter('!Special')).toBe('!Special');
|
||||
expect(toLowerCaseFirstLetter('123Number')).toBe('123Number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toCamelCase', () => {
|
||||
it('should return the key if parentKey is empty', () => {
|
||||
expect(toCamelCase('child', '')).toBe('child');
|
||||
});
|
||||
|
||||
it('should combine parentKey and key in camel case', () => {
|
||||
expect(toCamelCase('child', 'parent')).toBe('parentChild');
|
||||
});
|
||||
|
||||
it('should handle empty key and parentKey', () => {
|
||||
expect(toCamelCase('', '')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle key with capital letters', () => {
|
||||
expect(toCamelCase('Child', 'parent')).toBe('parentChild');
|
||||
expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');
|
||||
});
|
||||
});
|
||||
32
packages/@core/shared/toolkit/src/letter.ts
Normal file
32
packages/@core/shared/toolkit/src/letter.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 将字符串的首字母大写
|
||||
* @param string
|
||||
*/
|
||||
function capitalizeFirstLetter(string: string): string {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串的首字母转换为小写。
|
||||
*
|
||||
* @param str 要转换的字符串
|
||||
* @returns 首字母小写的字符串
|
||||
*/
|
||||
function toLowerCaseFirstLetter(str: string): string {
|
||||
if (!str) return str; // 如果字符串为空,直接返回
|
||||
return str.charAt(0).toLowerCase() + str.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成驼峰命名法的键名
|
||||
* @param key
|
||||
* @param parentKey
|
||||
*/
|
||||
function toCamelCase(key: string, parentKey: string): string {
|
||||
if (!parentKey) {
|
||||
return key;
|
||||
}
|
||||
return parentKey + key.charAt(0).toUpperCase() + key.slice(1);
|
||||
}
|
||||
|
||||
export { capitalizeFirstLetter, toCamelCase, toLowerCaseFirstLetter };
|
||||
1
packages/@core/shared/toolkit/src/merge.ts
Normal file
1
packages/@core/shared/toolkit/src/merge.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { defu as merge } from 'defu';
|
||||
105
packages/@core/shared/toolkit/src/namespace.ts
Normal file
105
packages/@core/shared/toolkit/src/namespace.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts
|
||||
*/
|
||||
|
||||
export const defaultNamespace = 'vben';
|
||||
const statePrefix = 'is-';
|
||||
|
||||
const _bem = (
|
||||
namespace: string,
|
||||
block: string,
|
||||
blockSuffix: string,
|
||||
element: string,
|
||||
modifier: string,
|
||||
) => {
|
||||
let cls = `${namespace}-${block}`;
|
||||
if (blockSuffix) {
|
||||
cls += `-${blockSuffix}`;
|
||||
}
|
||||
if (element) {
|
||||
cls += `__${element}`;
|
||||
}
|
||||
if (modifier) {
|
||||
cls += `--${modifier}`;
|
||||
}
|
||||
return cls;
|
||||
};
|
||||
|
||||
const is: {
|
||||
(name: string): string;
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
(name: string, state: boolean | undefined): string;
|
||||
} = (name: string, ...args: [] | [boolean | undefined]) => {
|
||||
const state = args.length > 0 ? args[0] : true;
|
||||
return name && state ? `${statePrefix}${name}` : '';
|
||||
};
|
||||
|
||||
const useNamespace = (block: string) => {
|
||||
const namespace = defaultNamespace;
|
||||
const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '');
|
||||
const e = (element?: string) =>
|
||||
element ? _bem(namespace, block, '', element, '') : '';
|
||||
const m = (modifier?: string) =>
|
||||
modifier ? _bem(namespace, block, '', '', modifier) : '';
|
||||
const be = (blockSuffix?: string, element?: string) =>
|
||||
blockSuffix && element
|
||||
? _bem(namespace, block, blockSuffix, element, '')
|
||||
: '';
|
||||
const em = (element?: string, modifier?: string) =>
|
||||
element && modifier ? _bem(namespace, block, '', element, modifier) : '';
|
||||
const bm = (blockSuffix?: string, modifier?: string) =>
|
||||
blockSuffix && modifier
|
||||
? _bem(namespace, block, blockSuffix, '', modifier)
|
||||
: '';
|
||||
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
|
||||
blockSuffix && element && modifier
|
||||
? _bem(namespace, block, blockSuffix, element, modifier)
|
||||
: '';
|
||||
|
||||
// for css var
|
||||
// --el-xxx: value;
|
||||
const cssVar = (object: Record<string, string>) => {
|
||||
const styles: Record<string, string> = {};
|
||||
for (const key in object) {
|
||||
if (object[key]) {
|
||||
styles[`--${namespace}-${key}`] = object[key];
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
};
|
||||
// with block
|
||||
const cssVarBlock = (object: Record<string, string>) => {
|
||||
const styles: Record<string, string> = {};
|
||||
for (const key in object) {
|
||||
if (object[key]) {
|
||||
styles[`--${namespace}-${block}-${key}`] = object[key];
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
};
|
||||
|
||||
const cssVarName = (name: string) => `--${namespace}-${name}`;
|
||||
const cssVarBlockName = (name: string) => `--${namespace}-${block}-${name}`;
|
||||
|
||||
return {
|
||||
b,
|
||||
be,
|
||||
bem,
|
||||
bm,
|
||||
// css
|
||||
cssVar,
|
||||
cssVarBlock,
|
||||
cssVarBlockName,
|
||||
cssVarName,
|
||||
e,
|
||||
em,
|
||||
is,
|
||||
m,
|
||||
namespace,
|
||||
};
|
||||
};
|
||||
|
||||
type UseNamespaceReturn = ReturnType<typeof useNamespace>;
|
||||
|
||||
export type { UseNamespaceReturn };
|
||||
export { useNamespace };
|
||||
43
packages/@core/shared/toolkit/src/nprogress.ts
Normal file
43
packages/@core/shared/toolkit/src/nprogress.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type NProgress from 'nprogress';
|
||||
|
||||
// 创建一个NProgress实例的变量,初始值为null
|
||||
let nProgressInstance: null | typeof NProgress = null;
|
||||
|
||||
/**
|
||||
* 动态加载NProgress库,并进行配置。
|
||||
* 此函数首先检查是否已经加载过NProgress库,如果已经加载过,则直接返回NProgress实例。
|
||||
* 否则,动态导入NProgress库,进行配置,然后返回NProgress实例。
|
||||
*
|
||||
* @returns NProgress实例的Promise对象。
|
||||
*/
|
||||
async function loadNprogress() {
|
||||
if (nProgressInstance) {
|
||||
return nProgressInstance;
|
||||
}
|
||||
nProgressInstance = await import('nprogress');
|
||||
nProgressInstance.configure({
|
||||
showSpinner: true,
|
||||
speed: 300,
|
||||
});
|
||||
return nProgressInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始显示进度条。
|
||||
* 此函数首先加载NProgress库,然后调用NProgress的start方法开始显示进度条。
|
||||
*/
|
||||
async function startProgress() {
|
||||
const nprogress = await loadNprogress();
|
||||
nprogress?.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止显示进度条,并隐藏进度条。
|
||||
* 此函数首先加载NProgress库,然后调用NProgress的done方法停止并隐藏进度条。
|
||||
*/
|
||||
async function stopProgress() {
|
||||
const nprogress = await loadNprogress();
|
||||
nprogress?.done();
|
||||
}
|
||||
|
||||
export { startProgress, stopProgress };
|
||||
196
packages/@core/shared/toolkit/src/tree.test.ts
Normal file
196
packages/@core/shared/toolkit/src/tree.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { filterTree, mapTree, traverseTreeValues } from './tree';
|
||||
|
||||
describe('traverseTreeValues', () => {
|
||||
interface Node {
|
||||
children?: Node[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
type NodeValue = string;
|
||||
|
||||
const sampleTree: Node[] = [
|
||||
{
|
||||
name: 'A',
|
||||
children: [
|
||||
{ name: 'B' },
|
||||
{
|
||||
name: 'C',
|
||||
children: [{ name: 'D' }, { name: 'E' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'F',
|
||||
children: [
|
||||
{ name: 'G' },
|
||||
{
|
||||
name: 'H',
|
||||
children: [{ name: 'I' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
it('traverses tree and returns all node values', () => {
|
||||
const values = traverseTreeValues<Node, NodeValue>(
|
||||
sampleTree,
|
||||
(node) => node.name,
|
||||
{
|
||||
childProps: 'children',
|
||||
},
|
||||
);
|
||||
expect(values).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']);
|
||||
});
|
||||
|
||||
it('handles empty tree', () => {
|
||||
const values = traverseTreeValues<Node, NodeValue>([], (node) => node.name);
|
||||
expect(values).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles tree with only root node', () => {
|
||||
const rootNode = { name: 'A' };
|
||||
const values = traverseTreeValues<Node, NodeValue>(
|
||||
[rootNode],
|
||||
(node) => node.name,
|
||||
);
|
||||
expect(values).toEqual(['A']);
|
||||
});
|
||||
|
||||
it('handles tree with only leaf nodes', () => {
|
||||
const leafNodes = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
|
||||
const values = traverseTreeValues<Node, NodeValue>(
|
||||
leafNodes,
|
||||
(node) => node.name,
|
||||
);
|
||||
expect(values).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterTree', () => {
|
||||
const tree = [
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{ id: 2 },
|
||||
{ id: 3, children: [{ id: 4 }, { id: 5 }, { id: 6 }] },
|
||||
{ id: 7 },
|
||||
],
|
||||
},
|
||||
{ id: 8, children: [{ id: 9 }, { id: 10 }] },
|
||||
{ id: 11 },
|
||||
];
|
||||
|
||||
it('should return all nodes when condition is always true', () => {
|
||||
const result = filterTree(tree, () => true, { childProps: 'children' });
|
||||
expect(result).toEqual(tree);
|
||||
});
|
||||
|
||||
it('should return only root nodes when condition is always false', () => {
|
||||
const result = filterTree(tree, () => false);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return nodes with even id values', () => {
|
||||
const result = filterTree(tree, (node) => node.id % 2 === 0);
|
||||
expect(result).toEqual([{ id: 8, children: [{ id: 10 }] }]);
|
||||
});
|
||||
|
||||
it('should return nodes with odd id values and their ancestors', () => {
|
||||
const result = filterTree(tree, (node) => node.id % 2 === 1);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
children: [{ id: 3, children: [{ id: 5 }] }, { id: 7 }],
|
||||
},
|
||||
{ id: 11 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return nodes with "leaf" in their name', () => {
|
||||
const tree = [
|
||||
{
|
||||
name: 'root',
|
||||
children: [
|
||||
{ name: 'leaf 1' },
|
||||
{
|
||||
name: 'branch',
|
||||
children: [{ name: 'leaf 2' }, { name: 'leaf 3' }],
|
||||
},
|
||||
{ name: 'leaf 4' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = filterTree(
|
||||
tree,
|
||||
(node) => node.name.includes('leaf') || node.name === 'root',
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'root',
|
||||
children: [{ name: 'leaf 1' }, { name: 'leaf 4' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapTree', () => {
|
||||
it('map infinite depth tree using mapTree', () => {
|
||||
const tree = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'node1',
|
||||
children: [
|
||||
{ id: 2, name: 'node2' },
|
||||
{ id: 3, name: 'node3' },
|
||||
{
|
||||
id: 4,
|
||||
name: 'node4',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'node5',
|
||||
children: [
|
||||
{ id: 6, name: 'node6' },
|
||||
{ id: 7, name: 'node7' },
|
||||
],
|
||||
},
|
||||
{ id: 8, name: 'node8' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const newTree = mapTree(tree, (node) => ({
|
||||
...node,
|
||||
name: `${node.name}-new`,
|
||||
}));
|
||||
|
||||
expect(newTree).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'node1-new',
|
||||
children: [
|
||||
{ id: 2, name: 'node2-new' },
|
||||
{ id: 3, name: 'node3-new' },
|
||||
{
|
||||
id: 4,
|
||||
name: 'node4-new',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'node5-new',
|
||||
children: [
|
||||
{ id: 6, name: 'node6-new' },
|
||||
{ id: 7, name: 'node7-new' },
|
||||
],
|
||||
},
|
||||
{ id: 8, name: 'node8-new' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
97
packages/@core/shared/toolkit/src/tree.ts
Normal file
97
packages/@core/shared/toolkit/src/tree.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
interface TreeConfigOptions {
|
||||
// 子属性的名称,默认为'children'
|
||||
childProps: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh_CN 遍历树形结构,并返回所有节点中指定的值。
|
||||
* @param tree 树形结构数组
|
||||
* @param getValue 获取节点值的函数
|
||||
* @param options 作为子节点数组的可选属性名称。
|
||||
* @returns 所有节点中指定的值的数组
|
||||
*/
|
||||
function traverseTreeValues<T, V>(
|
||||
tree: T[],
|
||||
getValue: (node: T) => V,
|
||||
options?: TreeConfigOptions,
|
||||
): V[] {
|
||||
const result: V[] = [];
|
||||
const { childProps } = options || {
|
||||
childProps: 'children',
|
||||
};
|
||||
|
||||
const dfs = (treeNode: T) => {
|
||||
const value = getValue(treeNode);
|
||||
result.push(value);
|
||||
const children = (treeNode as Record<string, any>)?.[childProps];
|
||||
if (!children) {
|
||||
return;
|
||||
}
|
||||
if (children.length > 0) {
|
||||
for (const child of children) {
|
||||
dfs(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const treeNode of tree) {
|
||||
dfs(treeNode);
|
||||
}
|
||||
return result.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件过滤给定树结构的节点,并以原有顺序返回所有匹配节点的数组。
|
||||
* @param tree 要过滤的树结构的根节点数组。
|
||||
* @param filter 用于匹配每个节点的条件。
|
||||
* @param options 作为子节点数组的可选属性名称。
|
||||
* @returns 包含所有匹配节点的数组。
|
||||
*/
|
||||
function filterTree<T extends Record<string, any>>(
|
||||
tree: T[],
|
||||
filter: (node: T) => boolean,
|
||||
options?: TreeConfigOptions,
|
||||
): T[] {
|
||||
const { childProps } = options || {
|
||||
childProps: 'children',
|
||||
};
|
||||
|
||||
const _filterTree = (nodes: T[]): T[] => {
|
||||
return nodes.filter((node: Record<string, any>) => {
|
||||
if (filter(node as T)) {
|
||||
if (node[childProps]) {
|
||||
node[childProps] = _filterTree(node[childProps]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
return _filterTree(tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件重新映射给定树结构的节
|
||||
* @param tree 要过滤的树结构的根节点数组。
|
||||
* @param mapper 用于map每个节点的条件。
|
||||
* @param options 作为子节点数组的可选属性名称。
|
||||
*/
|
||||
function mapTree<T, V extends Record<string, any>>(
|
||||
tree: T[],
|
||||
mapper: (node: T) => V,
|
||||
options?: TreeConfigOptions,
|
||||
): V[] {
|
||||
const { childProps } = options || {
|
||||
childProps: 'children',
|
||||
};
|
||||
return tree.map((node) => {
|
||||
const mapperNode: Record<string, any> = mapper(node);
|
||||
if (mapperNode[childProps]) {
|
||||
mapperNode[childProps] = mapTree(mapperNode[childProps], mapper, options);
|
||||
}
|
||||
return mapperNode as V;
|
||||
});
|
||||
}
|
||||
|
||||
export { filterTree, mapTree, traverseTreeValues };
|
||||
33
packages/@core/shared/toolkit/src/window.test.ts
Normal file
33
packages/@core/shared/toolkit/src/window.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { openWindow } from './window'; // 假设你的函数在 'openWindow' 文件中
|
||||
|
||||
describe('openWindow', () => {
|
||||
// 保存原始的 window.open 函数
|
||||
let originalOpen: typeof window.open;
|
||||
|
||||
beforeEach(() => {
|
||||
originalOpen = window.open;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.open = originalOpen;
|
||||
});
|
||||
|
||||
it('should call window.open with correct arguments', () => {
|
||||
const url = 'https://example.com';
|
||||
const options = { noopener: true, noreferrer: true, target: '_blank' };
|
||||
|
||||
window.open = vi.fn();
|
||||
|
||||
// 调用函数
|
||||
openWindow(url, options);
|
||||
|
||||
// 验证 window.open 是否被正确地调用
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
url,
|
||||
options.target,
|
||||
'noopener=yes,noreferrer=yes',
|
||||
);
|
||||
});
|
||||
});
|
||||
26
packages/@core/shared/toolkit/src/window.ts
Normal file
26
packages/@core/shared/toolkit/src/window.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
interface OpenWindowOptions {
|
||||
noopener?: boolean;
|
||||
noreferrer?: boolean;
|
||||
target?: '_blank' | '_parent' | '_self' | '_top' | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新窗口打开URL。
|
||||
*
|
||||
* @param url - 需要打开的网址。
|
||||
* @param options - 打开窗口的选项。
|
||||
*/
|
||||
function openWindow(url: string, options: OpenWindowOptions = {}): void {
|
||||
// 解构并设置默认值
|
||||
const { noopener = true, noreferrer = true, target = '_blank' } = options;
|
||||
|
||||
// 基于选项创建特性字符串
|
||||
const features = [noopener && 'noopener=yes', noreferrer && 'noreferrer=yes']
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
|
||||
// 打开窗口
|
||||
window.open(url, target, features);
|
||||
}
|
||||
|
||||
export { openWindow };
|
||||
6
packages/@core/shared/toolkit/tsconfig.json
Normal file
6
packages/@core/shared/toolkit/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
7
packages/@core/shared/typings/build.config.ts
Normal file
7
packages/@core/shared/typings/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
||||
48
packages/@core/shared/typings/package.json
Normal file
48
packages/@core/shared/typings/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@vben-core/typings",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/vbenjs/vue-vben-admin",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
|
||||
"directory": "packages/@vben-core/shared/typings"
|
||||
},
|
||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||
"scripts": {
|
||||
"build": "pnpm unbuild",
|
||||
"stub": "pnpm build --stub"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"imports": {
|
||||
"#*": "./src/*"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./vue-router": {
|
||||
"types": "./vue-router.d.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "3.4.27",
|
||||
"vue-router": "^4.3.2"
|
||||
}
|
||||
}
|
||||
22
packages/@core/shared/typings/src/app.d.ts
vendored
Normal file
22
packages/@core/shared/typings/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
type SupportedLanguagesType = 'en-US' | 'zh-CN';
|
||||
|
||||
type LayoutType =
|
||||
| 'full-content'
|
||||
| 'header-nav'
|
||||
| 'mixed-nav'
|
||||
| 'side-mixed-nav'
|
||||
| 'side-nav';
|
||||
|
||||
type ThemeModeType = 'auto' | 'dark' | 'light';
|
||||
|
||||
type ContentCompactType = 'compact' | 'wide';
|
||||
|
||||
type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
|
||||
|
||||
export type {
|
||||
ContentCompactType,
|
||||
LayoutHeaderModeType,
|
||||
LayoutType,
|
||||
SupportedLanguagesType,
|
||||
ThemeModeType,
|
||||
};
|
||||
40
packages/@core/shared/typings/src/flatten.d.ts
vendored
Normal file
40
packages/@core/shared/typings/src/flatten.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// `Prev` 类型用于表示递归深度的递减。它是一个元组,其索引代表了递归的层数,通过索引访问可以得到减少后的层数。
|
||||
// 例如,Prev[3] 等于 2,表示递归深度从 3 减少到 2。
|
||||
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
|
||||
|
||||
// `FlattenDepth` 类型用于将一个嵌套的对象类型“展平”,同时考虑到了递归的深度。
|
||||
// 它接受三个泛型参数:T(要处理的类型),Prefix(属性名前缀,默认为空字符串),Depth(递归深度,默认为3)。
|
||||
// 如果当前深度(Depth)为 0,则停止递归并返回 `never`。否则,如果属性值是对象类型,则递归调用 `FlattenDepth` 并递减深度。
|
||||
// 对于非对象类型的属性,将其直接映射到结果类型中,并根据前缀构造属性名。
|
||||
|
||||
type FlattenDepth<T, Prefix extends string = '', Depth extends number = 4> = {
|
||||
[K in keyof T]: T[K] extends object
|
||||
? Depth extends 0
|
||||
? never
|
||||
: FlattenDepth<
|
||||
T[K],
|
||||
`${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`,
|
||||
Prev[Depth]
|
||||
>
|
||||
: {
|
||||
[P in `${Prefix}${K extends string ? (Prefix extends '' ? K : Capitalize<K>) : ''}`]: T[K];
|
||||
};
|
||||
}[keyof T] extends infer O
|
||||
? { [P in keyof O]: O[P] }
|
||||
: never;
|
||||
|
||||
// `UnionToIntersection` 类型用于将一个联合类型转换为交叉类型。
|
||||
// 这个类型通过条件类型和类型推断的方式来实现。它先尝试将输入类型(U)映射为一个函数类型,
|
||||
// 然后通过推断这个函数类型的返回类型(infer I),最终得到一个交叉类型。
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
? I
|
||||
: never;
|
||||
|
||||
type Flatten<T> = UnionToIntersection<FlattenDepth<T>>;
|
||||
|
||||
type FlattenObject<T> = FlattenDepth<T>;
|
||||
type FlattenObjectKeys<T> = keyof FlattenObject<T>;
|
||||
|
||||
export type { Flatten, FlattenObject, FlattenObjectKeys, UnionToIntersection };
|
||||
126
packages/@core/shared/typings/src/helper.d.ts
vendored
Normal file
126
packages/@core/shared/typings/src/helper.d.ts
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
import { type ComputedRef, type MaybeRef } from 'vue';
|
||||
|
||||
/**
|
||||
* 深层递归所有属性为可选
|
||||
*/
|
||||
type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
/**
|
||||
* 深层递归所有属性为只读
|
||||
*/
|
||||
type DeepReadonly<T> = {
|
||||
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
|
||||
};
|
||||
|
||||
/**
|
||||
* 任意类型的异步函数
|
||||
*/
|
||||
|
||||
type AnyPromiseFunction<T extends any[] = any[], R = void> = (
|
||||
...arg: T
|
||||
) => PromiseLike<R>;
|
||||
|
||||
/**
|
||||
* 任意类型的普通函数
|
||||
*/
|
||||
type AnyNormalFunction<T extends any[] = any[], R = void> = (...arg: T) => R;
|
||||
|
||||
/**
|
||||
* 任意类型的函数
|
||||
*/
|
||||
type AnyFunction<T extends any[] = any[], R = void> =
|
||||
| AnyNormalFunction<T, R>
|
||||
| AnyPromiseFunction<T, R>;
|
||||
|
||||
/**
|
||||
* T | null 包装
|
||||
*/
|
||||
type Nullable<T> = T | null;
|
||||
|
||||
/**
|
||||
* T | Not null 包装
|
||||
*/
|
||||
type NonNullable<T> = T extends null | undefined ? never : T;
|
||||
|
||||
/**
|
||||
* 字符串类型对象
|
||||
*/
|
||||
type Recordable<T> = Record<string, T>;
|
||||
|
||||
/**
|
||||
* 字符串类型对象(只读)
|
||||
*/
|
||||
interface ReadonlyRecordable<T = any> {
|
||||
readonly [key: string]: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* setTimeout 返回值类型
|
||||
*/
|
||||
type TimeoutHandle = ReturnType<typeof setTimeout>;
|
||||
|
||||
/**
|
||||
* setInterval 返回值类型
|
||||
*/
|
||||
type IntervalHandle = ReturnType<typeof setInterval>;
|
||||
|
||||
/**
|
||||
* 也许它是一个计算的 ref,或者一个 getter 函数
|
||||
*
|
||||
*/
|
||||
type MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>;
|
||||
|
||||
/**
|
||||
* 也许它是一个 ref,或者一个普通值,或者一个 getter 函数
|
||||
*
|
||||
*/
|
||||
type MaybeComputedRef<T> = MaybeReadonlyRef<T> | MaybeRef<T>;
|
||||
|
||||
type Merge<O extends object, T extends object> = {
|
||||
[K in keyof O | keyof T]: K extends keyof T
|
||||
? T[K]
|
||||
: K extends keyof O
|
||||
? O[K]
|
||||
: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* T = [
|
||||
* { name: string; age: number; },
|
||||
* { sex: 'male' | 'female'; age: string }
|
||||
* ]
|
||||
* =>
|
||||
* MergeAll<T> = {
|
||||
* name: string;
|
||||
* sex: 'male' | 'female';
|
||||
* age: string
|
||||
* }
|
||||
*/
|
||||
type MergeAll<
|
||||
T extends object[],
|
||||
R extends object = Record<string, any>,
|
||||
> = T extends [infer F extends object, ...infer Rest extends object[]]
|
||||
? MergeAll<Rest, Merge<R, F>>
|
||||
: R;
|
||||
|
||||
export {
|
||||
type AnyFunction,
|
||||
type AnyNormalFunction,
|
||||
type AnyPromiseFunction,
|
||||
type DeepPartial,
|
||||
type DeepReadonly,
|
||||
type IntervalHandle,
|
||||
type MaybeComputedRef,
|
||||
type MaybeReadonlyRef,
|
||||
type Merge,
|
||||
type MergeAll,
|
||||
type NonNullable,
|
||||
type Nullable,
|
||||
type ReadonlyRecordable,
|
||||
type Recordable,
|
||||
type TimeoutHandle,
|
||||
};
|
||||
6
packages/@core/shared/typings/src/index.ts
Normal file
6
packages/@core/shared/typings/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type * from './app';
|
||||
export type * from './flatten';
|
||||
export type * from './helper';
|
||||
export type * from './menu-record';
|
||||
export type * from './tabs';
|
||||
export type * from './vue-router';
|
||||
71
packages/@core/shared/typings/src/menu-record.ts
Normal file
71
packages/@core/shared/typings/src/menu-record.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
/**
|
||||
* 扩展路由原始对象
|
||||
*/
|
||||
type ExRouteRecordRaw = {
|
||||
parent?: string;
|
||||
parents?: string[];
|
||||
path?: any;
|
||||
} & RouteRecordRaw;
|
||||
|
||||
interface MenuRecordBadgeRaw {
|
||||
/**
|
||||
* 徽标
|
||||
*/
|
||||
badge?: string;
|
||||
/**
|
||||
* 徽标类型
|
||||
*/
|
||||
badgeType?: 'dot' | 'normal';
|
||||
/**
|
||||
* 徽标颜色
|
||||
*/
|
||||
badgeVariants?: 'destructive' | 'primary' | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单原始对象
|
||||
*/
|
||||
interface MenuRecordRaw extends MenuRecordBadgeRaw {
|
||||
/**
|
||||
* 子菜单
|
||||
*/
|
||||
children?: MenuRecordRaw[];
|
||||
/**
|
||||
* 是否禁用菜单
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 图标名
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* 菜单名
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 排序号
|
||||
*/
|
||||
order?: number;
|
||||
/**
|
||||
* 父级路径
|
||||
*/
|
||||
parent?: string;
|
||||
/**
|
||||
* 所有父级路径
|
||||
*/
|
||||
parents?: string[];
|
||||
/**
|
||||
* 菜单路径,唯一,可当作key
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* 是否显示菜单
|
||||
* @default true
|
||||
*/
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
export type { ExRouteRecordRaw, MenuRecordBadgeRaw, MenuRecordRaw };
|
||||
3
packages/@core/shared/typings/src/tabs.ts
Normal file
3
packages/@core/shared/typings/src/tabs.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
export type TabItem = RouteLocationNormalized;
|
||||
94
packages/@core/shared/typings/src/vue-router.d.ts
vendored
Normal file
94
packages/@core/shared/typings/src/vue-router.d.ts
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
interface RouteMeta {
|
||||
/**
|
||||
* 是否固定标签页
|
||||
* @default false
|
||||
*/
|
||||
affixTab?: boolean;
|
||||
/**
|
||||
* 需要特定的角色标识才可以访问
|
||||
* @default []
|
||||
*/
|
||||
authority?: string[];
|
||||
/**
|
||||
* 徽标
|
||||
*/
|
||||
badge?: string;
|
||||
/**
|
||||
* 徽标类型
|
||||
*/
|
||||
badgeType?: 'dot' | 'normal';
|
||||
/**
|
||||
* 徽标颜色
|
||||
*/
|
||||
badgeVariants?:
|
||||
| 'default'
|
||||
| 'destructive'
|
||||
| 'primary'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| string;
|
||||
/**
|
||||
* 当前路由的子级在菜单中不展现
|
||||
* @default false
|
||||
*/
|
||||
hideChildrenInMenu?: boolean;
|
||||
/**
|
||||
* 当前路由在面包屑中不展现
|
||||
* @default false
|
||||
*/
|
||||
hideInBreadcrumb?: boolean;
|
||||
/**
|
||||
* 当前路由在菜单中不展现
|
||||
* @default false
|
||||
*/
|
||||
hideInMenu?: boolean;
|
||||
/**
|
||||
* 当前路由在标签页不展现
|
||||
* @default false
|
||||
*/
|
||||
hideInTab?: boolean;
|
||||
/**
|
||||
* 路由跳转地址
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* 图标(菜单/tab)
|
||||
*/
|
||||
icon?: string;
|
||||
/**
|
||||
* iframe 地址
|
||||
*/
|
||||
iframeSrc?: string;
|
||||
/**
|
||||
* 忽略权限,直接可以访问
|
||||
* @default false
|
||||
*/
|
||||
ignoreAccess?: boolean;
|
||||
/**
|
||||
* 开启KeepAlive缓存
|
||||
*/
|
||||
keepAlive?: boolean;
|
||||
/**
|
||||
* 外链-跳转路径
|
||||
*/
|
||||
link?: string;
|
||||
/**
|
||||
* 路由是否已经加载过
|
||||
*/
|
||||
loaded?: boolean;
|
||||
/**
|
||||
* 菜单可以看到,但是访问会被重定向到403
|
||||
*/
|
||||
menuVisibleWithForbidden?: boolean;
|
||||
/**
|
||||
* 用于路由->菜单排序
|
||||
*/
|
||||
order?: number;
|
||||
|
||||
/**
|
||||
* 标题名称
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type { RouteMeta };
|
||||
6
packages/@core/shared/typings/tsconfig.json
Normal file
6
packages/@core/shared/typings/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
7
packages/@core/shared/typings/vue-router.d.ts
vendored
Normal file
7
packages/@core/shared/typings/vue-router.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { RouteMeta as IRouteMeta } from '@vben-core/typings';
|
||||
|
||||
import 'vue-router';
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta extends IRouteMeta {}
|
||||
}
|
||||
Reference in New Issue
Block a user