提交新版本

This commit is contained in:
luob
2025-12-23 17:14:38 +08:00
3632 changed files with 498895 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
{
"name": "@vben-core/design",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/@vben-core/base/design"
},
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm vite build",
"prepublishOnly": "npm run build"
},
"files": [
"dist",
"src"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"exports": {
"./bem": {
"development": "./src/scss-bem/bem.scss",
"default": "./dist/bem.scss"
},
".": {
"types": "./src/index.ts",
"development": "./src/index.ts",
"default": "./dist/design.css"
}
},
"publishConfig": {
"exports": {
".": {
"default": "./dist/index.mjs"
}
}
}
}

View File

@@ -0,0 +1,101 @@
.side-content {
animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.side-content[data-side='top'] {
animation-name: slide-up;
}
.side-content[data-side='bottom'] {
animation-name: slide-down;
}
.side-content[data-side='left'] {
animation-name: slide-left;
}
.side-content[data-side='right'] {
animation-name: slide-right;
}
.breadcrumb-transition-enter-active {
transition:
transform 0.4s cubic-bezier(0.76, 0, 0.24, 1),
opacity 0.4s cubic-bezier(0.76, 0, 0.24, 1);
}
.breadcrumb-transition-leave-active {
display: none;
}
.breadcrumb-transition-enter-from {
opacity: 0;
transform: translateX(30px) skewX(-30deg);
}
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-left {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slide-right {
from {
opacity: 0;
transform: translateX(50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.z-popup {
z-index: var(--popup-z-index);
}
@keyframes shrink {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}

View File

@@ -0,0 +1,2 @@
import './default.css';
import './dark.css';

View File

@@ -0,0 +1,6 @@
import './design-tokens';
import './css/global.css';
import './css/transition.css';
import './css/nprogress.css';
import './css/ui.css';

View File

@@ -0,0 +1,41 @@
{
"name": "@vben-core/icons",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/@vben-core/base/icons"
},
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild"
},
"files": [
"dist"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"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": {
"@iconify/vue": "catalog:",
"lucide-vue-next": "catalog:",
"vue": "catalog:"
}
}

View File

@@ -0,0 +1,76 @@
export {
ArrowDown,
ArrowLeft,
ArrowLeftToLine,
ArrowRightLeft,
ArrowRightToLine,
ArrowUp,
ArrowUpToLine,
Bell,
BookOpenText,
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
Circle,
CircleAlert,
CircleCheckBig,
CircleHelp,
CircleX,
CloudUpload,
Copy,
CornerDownLeft,
Download,
Ellipsis,
Expand,
ExternalLink,
Eye,
EyeOff,
FoldHorizontal,
Fullscreen,
Github,
Grip,
GripVertical,
History,
Menu as IconDefault,
Inbox,
Info,
InspectionPanel,
Languages,
LoaderCircle,
LockKeyhole,
LogOut,
MailCheck,
Maximize,
ArrowRightFromLine as MdiMenuClose,
ArrowLeftFromLine as MdiMenuOpen,
Menu,
Minimize,
Minimize2,
MoonStar,
Palette,
PanelLeft,
PanelRight,
Pin,
PinOff,
Plus,
RefreshCw,
RotateCw,
Search,
SearchX,
Settings,
ShieldQuestion,
Shrink,
Square,
SquareCheckBig,
SquareMinus,
Sun,
SunMoon,
SwatchBook,
Trash2,
Upload,
UserRoundPen,
X,
} from 'lucide-vue-next';

View File

@@ -0,0 +1,104 @@
{
"name": "@vben-core/shared",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/@vben-core/base/shared"
},
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild",
"stub": "pnpm unbuild --stub"
},
"files": [
"dist"
],
"sideEffects": false,
"exports": {
"./constants": {
"types": "./src/constants/index.ts",
"development": "./src/constants/index.ts",
"default": "./dist/constants/index.mjs"
},
"./utils": {
"types": "./src/utils/index.ts",
"development": "./src/utils/index.ts",
"default": "./dist/utils/index.mjs"
},
"./color": {
"types": "./src/color/index.ts",
"development": "./src/color/index.ts",
"default": "./dist/color/index.mjs"
},
"./cache": {
"types": "./src/cache/index.ts",
"development": "./src/cache/index.ts",
"default": "./dist/cache/index.mjs"
},
"./store": {
"types": "./src/store.ts",
"development": "./src/store.ts",
"default": "./dist/store.mjs"
},
"./global-state": {
"types": "./src/global-state.ts",
"development": "./src/global-state.ts",
"default": "./dist/global-state.mjs"
}
},
"publishConfig": {
"exports": {
"./constants": {
"types": "./dist/constants/index.d.ts",
"default": "./dist/constants/index.mjs"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"default": "./dist/utils/index.mjs"
},
"./color": {
"types": "./dist/color/index.d.ts",
"default": "./dist/color/index.mjs"
},
"./cache": {
"types": "./dist/cache/index.d.ts",
"default": "./dist/cache/index.mjs"
},
"./store": {
"types": "./dist/store.d.ts",
"default": "./dist/store.mjs"
},
"./global-state": {
"types": "./dist/global-state.d.ts",
"default": "./dist/global-state.mjs"
}
}
},
"dependencies": {
"@ctrl/tinycolor": "catalog:",
"@tanstack/vue-store": "catalog:",
"@vue/shared": "catalog:",
"clsx": "catalog:",
"crypto-js": "catalog:",
"dayjs": "catalog:",
"defu": "catalog:",
"es-toolkit": "catalog:",
"jsencrypt": "catalog:",
"lodash.clonedeep": "catalog:",
"nprogress": "catalog:",
"tailwind-merge": "catalog:",
"theme-colors": "catalog:"
},
"devDependencies": {
"@types/crypto-js": "catalog:",
"@types/lodash.clonedeep": "catalog:",
"@types/lodash.get": "catalog:",
"@types/lodash.isequal": "catalog:",
"@types/lodash.set": "catalog:",
"@types/nprogress": "catalog:"
}
}

View File

@@ -0,0 +1,31 @@
/**
* @zh_CN GITHUB 仓库地址
*/
// export const VBEN_GITHUB_URL = 'https://github.com/vbenjs/vue-vben-admin';
export const VBEN_GITHUB_URL =
'https://github.com/yudaocode/yudao-ui-admin-vben';
/**
* @zh_CN 文档地址
*/
// export const VBEN_DOC_URL = 'https://doc.vben.pro';
export const VBEN_DOC_URL = 'https://doc.iocoder.cn/';
/**
* @zh_CN Vben Logo
*/
export const VBEN_LOGO_URL =
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp';
/**
* @zh_CN Vben Admin 首页地址
*/
export const VBEN_PREVIEW_URL = 'https://www.vben.pro';
export const VBEN_ELE_PREVIEW_URL = 'https://ele.vben.pro';
export const VBEN_NAIVE_PREVIEW_URL = 'https://naive.vben.pro';
export const VBEN_ANT_PREVIEW_URL = 'https://ant.vben.pro';
export const VBEN_TD_PREVIEW_URL = 'https://tdesign.vben.pro';

View File

@@ -0,0 +1,143 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
formatDate,
formatDateTime,
getCurrentTimezone,
getSystemTimezone,
isDate,
isDayjsObject,
setCurrentTimezone,
} from '../date';
dayjs.extend(utc);
dayjs.extend(timezone);
describe('dateUtils', () => {
const sampleISO = '2024-10-30T12:34:56Z';
const sampleTimestamp = Date.parse(sampleISO);
beforeEach(() => {
// 重置时区
dayjs.tz.setDefault();
setCurrentTimezone(); // 重置为系统默认
});
afterEach(() => {
vi.restoreAllMocks();
});
// ===============================
// formatDate
// ===============================
describe('formatDate', () => {
it('should format a valid ISO date string', () => {
const formatted = formatDate(sampleISO, 'YYYY/MM/DD');
expect(formatted).toMatch(/2024\/10\/30/);
});
it('should format a timestamp correctly', () => {
const formatted = formatDate(sampleTimestamp);
expect(formatted).toMatch(/2024-10-30/);
});
it('should format a Date object', () => {
const formatted = formatDate(new Date(sampleISO));
expect(formatted).toMatch(/2024-10-30/);
});
it('should format a dayjs object', () => {
const formatted = formatDate(dayjs(sampleISO));
expect(formatted).toMatch(/2024-10-30/);
});
it('should return original input if date is invalid', () => {
const invalid = 'not-a-date';
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
const formatted = formatDate(invalid);
expect(formatted).toBe(invalid);
expect(spy).toHaveBeenCalledOnce();
});
it('should apply given format', () => {
const formatted = formatDate(sampleISO, 'YYYY-MM-DD HH:mm');
expect(formatted).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/);
});
});
// ===============================
// formatDateTime
// ===============================
describe('formatDateTime', () => {
it('should format date into full datetime', () => {
const result = formatDateTime(sampleISO);
expect(result).toMatch(/2024-10-30 \d{2}:\d{2}:\d{2}/);
});
});
// ===============================
// isDate
// ===============================
describe('isDate', () => {
it('should return true for Date instances', () => {
expect(isDate(new Date())).toBe(true);
});
it('should return false for non-Date values', () => {
expect(isDate('2024-10-30')).toBe(false);
expect(isDate(null)).toBe(false);
expect(isDate(undefined)).toBe(false);
});
});
// ===============================
// isDayjsObject
// ===============================
describe('isDayjsObject', () => {
it('should return true for dayjs objects', () => {
expect(isDayjsObject(dayjs())).toBe(true);
});
it('should return false for other values', () => {
expect(isDayjsObject(new Date())).toBe(false);
expect(isDayjsObject('string')).toBe(false);
});
});
// ===============================
// getSystemTimezone
// ===============================
describe('getSystemTimezone', () => {
it('should return a valid IANA timezone string', () => {
const tz = getSystemTimezone();
expect(typeof tz).toBe('string');
expect(tz).toMatch(/^[A-Z]+\/[A-Z_]+/i);
});
});
// ===============================
// setCurrentTimezone / getCurrentTimezone
// ===============================
describe('setCurrentTimezone & getCurrentTimezone', () => {
it('should set and retrieve the current timezone', () => {
setCurrentTimezone('Asia/Shanghai');
expect(getCurrentTimezone()).toBe('Asia/Shanghai');
});
it('should reset to system timezone when called with no args', () => {
const guessed = getSystemTimezone();
setCurrentTimezone();
expect(getCurrentTimezone()).toBe(guessed);
});
it('should update dayjs default timezone', () => {
setCurrentTimezone('America/New_York');
const d = dayjs('2024-01-01T00:00:00Z');
// 校验时区转换生效(小时变化)
expect(d.tz().format('HH')).not.toBe('00');
});
});
});

View File

@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { loadScript } from '../resources';
const testJsPath =
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js';
describe('loadScript', () => {
beforeEach(() => {
// 每个测试前清空 head保证环境干净
document.head.innerHTML = '';
});
it('should resolve when the script loads successfully', async () => {
const promise = loadScript(testJsPath);
// 此时脚本元素已被创建并插入
const script = document.querySelector(
`script[src="${testJsPath}"]`,
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 模拟加载成功
script.dispatchEvent(new Event('load'));
// 等待 promise resolve
await expect(promise).resolves.toBeUndefined();
});
it('should not insert duplicate script and resolve immediately if already loaded', async () => {
// 先手动插入一个相同 src 的 script
const existing = document.createElement('script');
existing.src = 'bar.js';
document.head.append(existing);
// 再次调用
const promise = loadScript('bar.js');
// 立即 resolve
await expect(promise).resolves.toBeUndefined();
// head 中只保留一个
const scripts = document.head.querySelectorAll('script[src="bar.js"]');
expect(scripts).toHaveLength(1);
});
it('should reject when the script fails to load', async () => {
const promise = loadScript('error.js');
const script = document.querySelector(
'script[src="error.js"]',
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 模拟加载失败
script.dispatchEvent(new Event('error'));
await expect(promise).rejects.toThrow('Failed to load script: error.js');
});
it('should handle multiple concurrent calls and only insert one script tag', async () => {
const p1 = loadScript(testJsPath);
const p2 = loadScript(testJsPath);
const script = document.querySelector(
`script[src="${testJsPath}"]`,
) as HTMLScriptElement;
expect(script).toBeTruthy();
// 触发一次 load两个 promise 都应该 resolve
script.dispatchEvent(new Event('load'));
await expect(p1).resolves.toBeUndefined();
await expect(p2).resolves.toBeUndefined();
// 只插入一次
const scripts = document.head.querySelectorAll(
`script[src="${testJsPath}"]`,
);
expect(scripts).toHaveLength(1);
});
});

View File

@@ -0,0 +1,99 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
type FormatDate = Date | dayjs.Dayjs | number | string;
type Format =
| 'HH'
| 'HH:mm'
| 'HH:mm:ss'
| 'YYYY'
| 'YYYY-MM'
| 'YYYY-MM-DD'
| 'YYYY-MM-DD HH'
| 'YYYY-MM-DD HH:mm'
| 'YYYY-MM-DD HH:mm:ss'
| (string & {});
export function formatDate(time?: FormatDate, format: Format = 'YYYY-MM-DD') {
// 日期不存在,则返回空
if (!time) {
return '';
}
try {
const date = dayjs.isDayjs(time) ? time : dayjs(time);
if (!date.isValid()) {
throw new Error('Invalid date');
}
return date.tz().format(format);
} catch (error) {
console.error(`Error formatting date: ${error}`);
return String(time ?? '');
}
}
export function formatDateTime(time?: FormatDate) {
return formatDate(time, 'YYYY-MM-DD HH:mm:ss');
}
export function formatDate2(date: Date, format?: string): string {
// 日期不存在,则返回空
if (!date) {
return '';
}
// 日期存在,则进行格式化
return date ? dayjs(date).format(format ?? 'YYYY-MM-DD HH:mm:ss') : '';
}
export function isDate(value: any): value is Date {
return value instanceof Date;
}
export function isDayjsObject(value: any): value is dayjs.Dayjs {
return dayjs.isDayjs(value);
}
/**
* element plus 的时间 Formatter 实现,使用 YYYY-MM-DD HH:mm:ss 格式
*
* @param _row
* @param _column
* @param cellValue 字段值
*/
export function dateFormatter(_row: any, _column: any, cellValue: any): string {
return cellValue ? formatDate(cellValue)?.toString() || '' : '';
}
/**
* 获取当前时区
* @returns 当前时区
*/
export const getSystemTimezone = () => {
return dayjs.tz.guess();
};
/**
* 自定义设置的时区
*/
let currentTimezone = getSystemTimezone();
/**
* 设置默认时区
* @param timezone
*/
export const setCurrentTimezone = (timezone?: string) => {
currentTimezone = timezone || getSystemTimezone();
dayjs.tz.setDefault(currentTimezone);
};
/**
* 获取设置的时区
* @returns 设置的时区
*/
export const getCurrentTimezone = () => {
return currentTimezone;
};

View File

@@ -0,0 +1,267 @@
import CryptoJS from 'crypto-js';
import { JSEncrypt } from 'jsencrypt';
/**
* API 加解密工具类
* 支持 AES 和 RSA 加密算法
*/
/**
* AES 加密工具类
*/
export const AES = {
/**
* AES 加密
* @param data 要加密的数据
* @param key 加密密钥
* @returns 加密后的字符串
*/
encrypt(data: string, key: string): string {
try {
if (!key) {
throw new Error('AES 加密密钥不能为空');
}
if (key.length !== 32 && key.length !== 16) {
throw new Error(
`AES 加密密钥长度必须为 32 位或 16 位,当前长度: ${key.length}`,
);
}
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
} catch (error) {
console.error('AES 加密失败:', error);
throw error;
}
},
/**
* AES 解密
* @param encryptedData 加密的数据
* @param key 解密密钥
* @returns 解密后的字符串
*/
decrypt(encryptedData: string, key: string): string {
try {
if (!key) {
throw new Error('AES 解密密钥不能为空');
}
if (key.length !== 32) {
throw new Error(
`AES 解密密钥长度必须为 32 位,当前长度: ${key.length}`,
);
}
if (!encryptedData) {
throw new Error('AES 解密数据不能为空');
}
const keyUtf8 = CryptoJS.enc.Utf8.parse(key);
const decrypted = CryptoJS.AES.decrypt(encryptedData, keyUtf8, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
const result = decrypted.toString(CryptoJS.enc.Utf8);
if (!result) {
throw new Error('AES 解密结果为空,可能是密钥错误或数据损坏');
}
return result;
} catch (error) {
console.error('AES 解密失败:', error);
throw error;
}
},
};
/**
* RSA 加密工具类
*/
export const RSA = {
/**
* RSA 加密
* @param data 要加密的数据
* @param publicKey 公钥(必需)
* @returns 加密后的字符串
*/
encrypt(data: string, publicKey: string): false | string {
try {
if (!publicKey) {
throw new Error('RSA 公钥不能为空');
}
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
const result = encryptor.encrypt(data);
if (result === false) {
throw new Error('RSA 加密失败,可能是公钥格式错误或数据过长');
}
return result;
} catch (error) {
console.error('RSA 加密失败:', error);
throw error;
}
},
/**
* RSA 解密
* @param encryptedData 加密的数据
* @param privateKey 私钥(必需)
* @returns 解密后的字符串
*/
decrypt(encryptedData: string, privateKey: string): false | string {
try {
if (!privateKey) {
throw new Error('RSA 私钥不能为空');
}
if (!encryptedData) {
throw new Error('RSA 解密数据不能为空');
}
const encryptor = new JSEncrypt();
encryptor.setPrivateKey(privateKey);
const result = encryptor.decrypt(encryptedData);
if (result === false) {
throw new Error('RSA 解密失败,可能是私钥错误或数据损坏');
}
return result;
} catch (error) {
console.error('RSA 解密失败:', error);
throw error;
}
},
};
/**
* API 加解密配置接口
*/
export interface ApiEncryptConfig {
/** 加密算法 */
algorithm: 'AES' | 'RSA';
/** 是否启用加解密 */
enable: boolean;
/** 加密头名称 */
header: string;
/** 请求加密密钥AES密钥或RSA公钥 */
requestKey: string;
/** 响应解密密钥AES密钥或RSA私钥 */
responseKey: string;
}
/**
* API 加解密主类
*/
export class ApiEncrypt {
private config: ApiEncryptConfig;
constructor(config: ApiEncryptConfig) {
this.config = config;
}
/**
* 解密响应数据
* @param encryptedData 加密的响应数据
* @returns 解密后的数据
*/
decryptResponse(encryptedData: string): any {
if (!this.config.enable) {
return encryptedData;
}
try {
let decryptedData: false | string = '';
if (this.config.algorithm.toUpperCase() === 'AES') {
if (!this.config.responseKey) {
throw new Error('AES 响应解密密钥未配置');
}
decryptedData = AES.decrypt(encryptedData, this.config.responseKey);
} else if (this.config.algorithm.toUpperCase() === 'RSA') {
if (!this.config.responseKey) {
throw new Error('RSA 私钥未配置');
}
decryptedData = RSA.decrypt(encryptedData, this.config.responseKey);
if (decryptedData === false) {
throw new Error('RSA 解密失败');
}
} else {
throw new Error(`不支持的解密算法: ${this.config.algorithm}`);
}
if (!decryptedData) {
throw new Error('解密结果为空');
}
// 尝试解析为 JSON如果失败则返回原字符串
try {
return JSON.parse(decryptedData);
} catch {
return decryptedData;
}
} catch (error) {
console.error('响应数据解密失败:', error);
throw error;
}
}
/**
* 加密请求数据
* @param data 要加密的数据
* @returns 加密后的数据
*/
encryptRequest(data: any): string {
if (!this.config.enable) {
return data;
}
try {
const jsonData = typeof data === 'string' ? data : JSON.stringify(data);
if (this.config.algorithm.toUpperCase() === 'AES') {
if (!this.config.requestKey) {
throw new Error('AES 请求加密密钥未配置');
}
return AES.encrypt(jsonData, this.config.requestKey);
} else if (this.config.algorithm.toUpperCase() === 'RSA') {
if (!this.config.requestKey) {
throw new Error('RSA 公钥未配置');
}
const result = RSA.encrypt(jsonData, this.config.requestKey);
if (result === false) {
throw new Error('RSA 加密失败');
}
return result;
} else {
throw new Error(`不支持的加密算法: ${this.config.algorithm}`);
}
} catch (error) {
console.error('请求数据加密失败:', error);
throw error;
}
}
/**
* 获取加密头名称
*/
getEncryptHeader(): string {
return this.config.header;
}
}
/**
* 创建基于环境变量的 API 加解密实例
* @param env 环境变量对象
* @returns ApiEncrypt 实例
*/
export function createApiEncrypt(env: Record<string, any>): ApiEncrypt {
const config: ApiEncryptConfig = {
enable: env.VITE_APP_API_ENCRYPT_ENABLE === 'true',
header: env.VITE_APP_API_ENCRYPT_HEADER || 'X-Api-Encrypt',
algorithm: env.VITE_APP_API_ENCRYPT_ALGORITHM || 'AES',
requestKey: env.VITE_APP_API_ENCRYPT_REQUEST_KEY || '',
responseKey: env.VITE_APP_API_ENCRYPT_RESPONSE_KEY || '',
};
return new ApiEncrypt(config);
}

View File

@@ -0,0 +1,199 @@
import { isEmpty, isString, isUndefined } from './inference';
/**
* 将一个整数转换为分数保留传入的小数
* @param num
* @param digit
*/
export function formatToFractionDigit(
num: number | string | undefined,
digit: number = 2,
): string {
if (isUndefined(num)) return '0.00';
const parsedNumber = isString(num) ? Number.parseFloat(num) : num;
return (parsedNumber / 100).toFixed(digit);
}
/**
* 将一个整数转换为分数保留两位小数
* @param num
*/
export function formatToFraction(num: number | string | undefined): string {
return formatToFractionDigit(num, 2);
}
/**
* 将一个数转换为 1.00 这样
* 数据呈现的时候使用
*
* @param num 整数
*/
export function floatToFixed2(num: number | string | undefined): string {
let str = '0.00';
if (isUndefined(num)) return str;
const f = formatToFraction(num);
const decimalPart = f.toString().split('.')[1];
const len = decimalPart ? decimalPart.length : 0;
switch (len) {
case 0: {
str = `${f.toString()}.00`;
break;
}
case 1: {
str = `${f.toString()}0`;
break;
}
case 2: {
str = f.toString();
break;
}
}
return str;
}
/**
* 将一个分数转换为整数
* @param num
*/
export function convertToInteger(num: number | string | undefined): number {
if (isUndefined(num)) return 0;
const parsedNumber = isString(num) ? Number.parseFloat(num) : num;
return Math.round(parsedNumber * 100);
}
/**
* 元转分
*/
export function yuanToFen(amount: number | string): number {
return convertToInteger(amount);
}
/**
* 分转元
*/
export function fenToYuan(price: number | string): string {
return formatToFraction(price);
}
// 格式化金额【分转元】
export const fenToYuanFormat = (_: any, __: any, cellValue: any, ___: any) => {
return `${floatToFixed2(cellValue)}`;
};
/**
* 计算环比
*
* @param value 当前数值
* @param reference 对比数值
*/
export function calculateRelativeRate(
value?: number,
reference?: number,
): number {
// 防止除0
if (!reference || reference === 0) return 0;
return Number.parseFloat(
((100 * ((value || 0) - reference)) / reference).toFixed(0),
);
}
// ========== ERP 专属方法 ==========
const ERP_COUNT_DIGIT = 3;
const ERP_PRICE_DIGIT = 2;
/**
* 【ERP】格式化 Input 数字
*
* 例如说:库存数量
*
* @param num 数量
* @package
* @return 格式化后的数量
*/
export function erpNumberFormatter(
num: number | string | undefined,
digit: number,
) {
if (num === null || num === undefined) {
return '';
}
if (typeof num === 'string') {
num = Number.parseFloat(num);
}
// 如果非 number则直接返回空串
if (Number.isNaN(num)) {
return '';
}
return num.toFixed(digit);
}
/**
* 【ERP】格式化数量保留三位小数
*
* 例如说:库存数量
*
* @param num 数量
* @return 格式化后的数量
*/
export function erpCountInputFormatter(num: number | string | undefined) {
return erpNumberFormatter(num, ERP_COUNT_DIGIT);
}
/**
* 【ERP】格式化数量保留三位小数
*
* @param cellValue 数量
* @return 格式化后的数量
*/
export function erpCountTableColumnFormatter(cellValue: any) {
return erpNumberFormatter(cellValue, ERP_COUNT_DIGIT);
}
/**
* 【ERP】格式化金额保留二位小数
*
* 例如说:库存数量
*
* @param num 数量
* @return 格式化后的数量
*/
export function erpPriceInputFormatter(num: number | string | undefined) {
return erpNumberFormatter(num, ERP_PRICE_DIGIT);
}
/**
* 【ERP】格式化金额保留二位小数
*
* @param cellValue 数量
* @return 格式化后的数量
*/
export function erpPriceTableColumnFormatter(cellValue: any) {
return erpNumberFormatter(cellValue, ERP_PRICE_DIGIT);
}
/**
* 【ERP】价格计算四舍五入保留两位小数
*
* @param price 价格
* @param count 数量
* @return 总价格。如果有任一为空,则返回 undefined
*/
export function erpPriceMultiply(price: number, count: number) {
if (isEmpty(price) || isEmpty(count)) return undefined;
return Number.parseFloat((price * count).toFixed(ERP_PRICE_DIGIT));
}
/**
* 【ERP】百分比计算四舍五入保留两位小数
*
* 如果 total 为 0则返回 0
*
* @param value 当前值
* @param total 总值
*/
export function erpCalculatePercentage(value: number, total: number) {
if (total === 0) return 0;
return ((value / total) * 100).toFixed(2);
}

View File

@@ -0,0 +1,25 @@
export * from './cn';
export * from './date';
export * from './diff';
export * from './dom';
export * from './download';
export * from './encrypt';
export * from './formatNumber';
export * from './inference';
export * from './letter';
export * from './merge';
export * from './nprogress';
export * from './resources';
export * from './state-handler';
export * from './time';
export * from './to';
export * from './tree';
export * from './unique';
export * from './update-css-variables';
export * from './upload';
export * from './util';
export * from './uuid'; // add by 芋艿:从 vben2.0 复制
export * from './window';
export { get, isEqual, set } from 'es-toolkit/compat';
// export { cloneDeep } from 'es-toolkit/object';
export { default as cloneDeep } from 'lodash.clonedeep';

View File

@@ -0,0 +1,21 @@
/**
* 加载js文件
* @param src js文件地址
*/
function loadScript(src: string) {
return new Promise<void>((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) {
// 如果已经加载过,直接 resolve
return resolve();
}
const script = document.createElement('script');
script.src = src;
script.addEventListener('load', () => resolve());
script.addEventListener('error', () =>
reject(new Error(`Failed to load script: ${src}`)),
);
document.head.append(script);
});
}
export { loadScript };

View File

@@ -0,0 +1,224 @@
/**
* 根据支持的文件类型生成 accept 属性值
*
* @param supportedFileTypes 支持的文件类型数组,如 ['PDF', 'DOC', 'DOCX']
* @returns 用于文件上传组件 accept 属性的字符串
*/
export function generateAcceptedFileTypes(
supportedFileTypes: string[],
): string {
const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase());
const mimeTypes: string[] = [];
// 添加常见的 MIME 类型映射
if (allowedExtensions.includes('txt')) {
mimeTypes.push('text/plain');
}
if (allowedExtensions.includes('pdf')) {
mimeTypes.push('application/pdf');
}
if (allowedExtensions.includes('html') || allowedExtensions.includes('htm')) {
mimeTypes.push('text/html');
}
if (allowedExtensions.includes('csv')) {
mimeTypes.push('text/csv');
}
if (allowedExtensions.includes('xlsx') || allowedExtensions.includes('xls')) {
mimeTypes.push(
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
}
if (allowedExtensions.includes('docx') || allowedExtensions.includes('doc')) {
mimeTypes.push(
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
);
}
if (allowedExtensions.includes('pptx') || allowedExtensions.includes('ppt')) {
mimeTypes.push(
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
);
}
if (allowedExtensions.includes('xml')) {
mimeTypes.push('application/xml', 'text/xml');
}
if (
allowedExtensions.includes('md') ||
allowedExtensions.includes('markdown')
) {
mimeTypes.push('text/markdown');
}
if (allowedExtensions.includes('epub')) {
mimeTypes.push('application/epub+zip');
}
if (allowedExtensions.includes('eml')) {
mimeTypes.push('message/rfc822');
}
if (allowedExtensions.includes('msg')) {
mimeTypes.push('application/vnd.ms-outlook');
}
// 添加文件扩展名
const extensions = allowedExtensions.map((ext) => `.${ext}`);
return [...mimeTypes, ...extensions].join(',');
}
/**
* 从 URL 中提取文件名
*
* @param url 文件 URL
* @returns 文件名,如果无法提取则返回 'unknown'
*/
export function getFileNameFromUrl(url: null | string | undefined): string {
// 处理空值
if (!url) {
return 'unknown';
}
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const fileName = pathname.split('/').pop() || 'unknown';
return decodeURIComponent(fileName);
} catch {
// 如果 URL 解析失败,尝试从字符串中提取
const parts = url.split('/');
return parts[parts.length - 1] || 'unknown';
}
}
/**
* 默认图片类型
*/
export const defaultImageAccepts = [
'bmp',
'gif',
'jpeg',
'jpg',
'png',
'svg',
'webp',
];
/**
* 判断文件是否为图片
*
* @param filename 文件名
* @param accepts 支持的文件类型
* @returns 是否为图片
*/
export function isImage(
filename: null | string | undefined,
accepts: string[] = defaultImageAccepts,
): boolean {
if (!filename || accepts.length === 0) {
return false;
}
const ext = filename.split('.').pop()?.toLowerCase() || '';
return accepts.includes(ext);
}
/**
* 判断文件是否为指定类型
*
* @param file 文件
* @param accepts 支持的文件类型
* @returns 是否为指定类型
*/
export function checkFileType(file: File, accepts: string[]) {
if (!accepts || accepts.length === 0) {
return true;
}
const newTypes = accepts.join('|');
const reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i');
return reg.test(file.name);
}
/**
* 格式化文件大小
*
* @param bytes 文件大小(字节)
* @returns 格式化后的文件大小字符串
*/
export function formatFileSize(bytes: number, digits = 2): string {
if (bytes === 0) {
return '0 B';
}
const k = 1024;
const unitArr = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const index = Math.floor(Math.log(bytes) / Math.log(k));
return `${Number.parseFloat((bytes / k ** index).toFixed(digits))} ${unitArr[index]}`;
}
/**
* 获取文件图标Lucide Icons
*
* @param filename 文件名
* @returns Lucide 图标名称
*/
export function getFileIcon(filename: null | string | undefined): string {
if (!filename) {
return 'lucide:file';
}
const ext = filename.split('.').pop()?.toLowerCase() || '';
if (isImage(ext)) {
return 'lucide:image';
}
if (['pdf'].includes(ext)) {
return 'lucide:file-text';
}
if (['doc', 'docx'].includes(ext)) {
return 'lucide:file-text';
}
if (['xls', 'xlsx'].includes(ext)) {
return 'lucide:file-spreadsheet';
}
if (['ppt', 'pptx'].includes(ext)) {
return 'lucide:presentation';
}
if (['aac', 'm4a', 'mp3', 'wav'].includes(ext)) {
return 'lucide:music';
}
if (['avi', 'mov', 'mp4', 'wmv'].includes(ext)) {
return 'lucide:video';
}
return 'lucide:file';
}
/**
* 获取文件类型样式类Tailwind CSS 渐变色)
*
* @param filename 文件名
* @returns Tailwind CSS 渐变类名
*/
export function getFileTypeClass(filename: null | string | undefined): string {
if (!filename) {
return 'from-gray-500 to-gray-700';
}
const ext = filename.split('.').pop()?.toLowerCase() || '';
if (isImage(ext)) {
return 'from-yellow-400 to-orange-500';
}
if (['pdf'].includes(ext)) {
return 'from-red-500 to-red-700';
}
if (['doc', 'docx'].includes(ext)) {
return 'from-blue-600 to-blue-800';
}
if (['xls', 'xlsx'].includes(ext)) {
return 'from-green-600 to-green-800';
}
if (['ppt', 'pptx'].includes(ext)) {
return 'from-orange-600 to-orange-800';
}
if (['aac', 'm4a', 'mp3', 'wav'].includes(ext)) {
return 'from-purple-500 to-purple-700';
}
if (['avi', 'mov', 'mp4', 'wmv'].includes(ext)) {
return 'from-red-500 to-red-700';
}
return 'from-gray-500 to-gray-700';
}

View File

@@ -0,0 +1,116 @@
export function bindMethods<T extends object>(instance: T): void {
const prototype = Object.getPrototypeOf(instance);
const propertyNames = Object.getOwnPropertyNames(prototype);
propertyNames.forEach((propertyName) => {
const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);
const propertyValue = instance[propertyName as keyof T];
if (
typeof propertyValue === 'function' &&
propertyName !== 'constructor' &&
descriptor &&
!descriptor.get &&
!descriptor.set
) {
instance[propertyName as keyof T] = propertyValue.bind(instance);
}
});
}
/**
* 获取嵌套对象的字段值
* @param obj - 要查找的对象
* @param path - 用于查找字段的路径,使用小数点分隔
* @returns 字段值,或者未找到时返回 undefined
*/
export function getNestedValue<T>(obj: T, path: string): any {
if (typeof path !== 'string' || path.length === 0) {
throw new Error('Path must be a non-empty string');
}
// 把路径字符串按 "." 分割成数组
const keys = path.split('.') as (number | string)[];
let current: any = obj;
for (const key of keys) {
if (current === null || current === undefined) {
return undefined;
}
current = current[key as keyof typeof current];
}
return current;
}
/**
* 获取链接的参数值(值类型)
* @param key 参数键名
* @param urlStr 链接地址,默认为当前浏览器的地址
*/
export function getUrlNumberValue(
key: string,
urlStr: string = location.href,
): number {
return Number(getUrlValue(key, urlStr));
}
/**
* 获取链接的参数值
* @param key 参数键名
* @param urlStr 链接地址,默认为当前浏览器的地址
*/
export function getUrlValue(
key: string,
urlStr: string = location.href,
): string {
if (!urlStr || !key) return '';
const url = new URL(decodeURIComponent(urlStr));
return url.searchParams.get(key) ?? '';
}
/**
* 将值复制到目标对象且以目标对象属性为准target: {a:1} source:{a:2,b:3} 结果为:{a:2}
* @param target 目标对象
* @param source 源对象
*/
export function copyValueToTarget(target: any, source: any) {
const newObj = Object.assign({}, target, source);
// 删除多余属性
Object.keys(newObj).forEach((key) => {
// 如果不是target中的属性则删除
if (!Object.keys(target).includes(key)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete newObj[key];
}
});
// 更新目标对象值
Object.assign(target, newObj);
}
/** 实现 groupBy 功能 */
export function groupBy(array: any[], key: string) {
const result: Record<string, any[]> = {};
for (const item of array) {
const groupKey = item[key];
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
}
return result;
}
/**
* 解析 JSON 字符串
*
* @param str
*/
export function jsonParse(str: string) {
try {
return JSON.parse(str);
} catch {
console.warn(`str[${str}] 不是一个 JSON 字符串`);
return str;
}
}

View File

@@ -0,0 +1,37 @@
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);
}
/**
* 在新窗口中打开路由。
* @param path
*/
function openRouteInNewWindow(path: string) {
const { hash, origin } = location;
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash && !fullPath.startsWith('/#') ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
}
export { openRouteInNewWindow, openWindow };

View File

@@ -0,0 +1,44 @@
{
"name": "@vben-core/typings",
"version": "5.5.9",
"homepage": "https://github.com/vbenjs/vue-vben-admin",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "packages/@vben-core/base/typings"
},
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm unbuild"
},
"files": [
"dist"
],
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"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": "catalog:",
"vue-router": "catalog:"
}
}

121
packages/@core/base/typings/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,121 @@
type LayoutType =
| 'full-content'
| 'header-mixed-nav'
| 'header-nav'
| 'header-sidebar-nav'
| 'mixed-nav'
| 'sidebar-mixed-nav'
| 'sidebar-nav';
type ThemeModeType = 'auto' | 'dark' | 'light';
/**
* 偏好设置按钮位置
* fixed 固定在右侧
* header 顶栏
* auto 自动
*/
type PreferencesButtonPositionType = 'auto' | 'fixed' | 'header';
type BuiltinThemeType =
| 'custom'
| 'deep-blue'
| 'deep-green'
| 'default'
| 'gray'
| 'green'
| 'neutral'
| 'orange'
| 'pink'
| 'red'
| 'rose'
| 'sky-blue'
| 'slate'
| 'stone'
| 'violet'
| 'yellow'
| 'zinc'
| (Record<never, never> & string);
type ContentCompactType = 'compact' | 'wide';
type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static';
type LayoutHeaderMenuAlignType = 'center' | 'end' | 'start';
/**
* 登录过期模式
* modal 弹窗模式
* page 页面模式
*/
type LoginExpiredModeType = 'modal' | 'page';
/**
* 面包屑样式
* background 背景
* normal 默认
*/
type BreadcrumbStyleType = 'background' | 'normal';
/**
* 权限模式
* backend 后端权限模式
* frontend 前端权限模式
* mixed 混合权限模式
*/
type AccessModeType = 'backend' | 'frontend' | 'mixed';
/**
* 导航风格
* plain 朴素
* rounded 圆润
*/
type NavigationStyleType = 'plain' | 'rounded';
/**
* 标签栏风格
* brisk 轻快
* card 卡片
* chrome 谷歌
* plain 朴素
*/
type TabsStyleType = 'brisk' | 'card' | 'chrome' | 'plain';
/**
* 页面切换动画
*/
type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up';
/**
* 页面切换动画
* panel-center 居中布局
* panel-left 居左布局
* panel-right 居右布局
*/
type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right';
/**
* 时区选项
*/
interface TimezoneOption {
label: string;
offset: number;
timezone: string;
}
export type {
AccessModeType,
AuthPageLayoutType,
BreadcrumbStyleType,
BuiltinThemeType,
ContentCompactType,
LayoutHeaderMenuAlignType,
LayoutHeaderModeType,
LayoutType,
LoginExpiredModeType,
NavigationStyleType,
PageTransitionType,
PreferencesButtonPositionType,
TabsStyleType,
ThemeModeType,
TimezoneOption,
};