提交新版本
This commit is contained in:
41
packages/@core/base/design/package.json
Normal file
41
packages/@core/base/design/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
packages/@core/base/design/src/css/ui.css
Normal file
101
packages/@core/base/design/src/css/ui.css
Normal 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);
|
||||
}
|
||||
}
|
||||
2
packages/@core/base/design/src/design-tokens/index.ts
Normal file
2
packages/@core/base/design/src/design-tokens/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import './default.css';
|
||||
import './dark.css';
|
||||
6
packages/@core/base/design/src/index.ts
Normal file
6
packages/@core/base/design/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import './design-tokens';
|
||||
|
||||
import './css/global.css';
|
||||
import './css/transition.css';
|
||||
import './css/nprogress.css';
|
||||
import './css/ui.css';
|
||||
41
packages/@core/base/icons/package.json
Normal file
41
packages/@core/base/icons/package.json
Normal 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:"
|
||||
}
|
||||
}
|
||||
76
packages/@core/base/icons/src/lucide.ts
Normal file
76
packages/@core/base/icons/src/lucide.ts
Normal 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';
|
||||
104
packages/@core/base/shared/package.json
Normal file
104
packages/@core/base/shared/package.json
Normal 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:"
|
||||
}
|
||||
}
|
||||
31
packages/@core/base/shared/src/constants/vben.ts
Normal file
31
packages/@core/base/shared/src/constants/vben.ts
Normal 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';
|
||||
143
packages/@core/base/shared/src/utils/__tests__/date.test.ts
Normal file
143
packages/@core/base/shared/src/utils/__tests__/date.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
99
packages/@core/base/shared/src/utils/date.ts
Normal file
99
packages/@core/base/shared/src/utils/date.ts
Normal 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;
|
||||
};
|
||||
267
packages/@core/base/shared/src/utils/encrypt.ts
Normal file
267
packages/@core/base/shared/src/utils/encrypt.ts
Normal 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);
|
||||
}
|
||||
199
packages/@core/base/shared/src/utils/formatNumber.ts
Normal file
199
packages/@core/base/shared/src/utils/formatNumber.ts
Normal 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);
|
||||
}
|
||||
25
packages/@core/base/shared/src/utils/index.ts
Normal file
25
packages/@core/base/shared/src/utils/index.ts
Normal 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';
|
||||
21
packages/@core/base/shared/src/utils/resources.ts
Normal file
21
packages/@core/base/shared/src/utils/resources.ts
Normal 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 };
|
||||
224
packages/@core/base/shared/src/utils/upload.ts
Normal file
224
packages/@core/base/shared/src/utils/upload.ts
Normal 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';
|
||||
}
|
||||
116
packages/@core/base/shared/src/utils/util.ts
Normal file
116
packages/@core/base/shared/src/utils/util.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
37
packages/@core/base/shared/src/utils/window.ts
Normal file
37
packages/@core/base/shared/src/utils/window.ts
Normal 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 };
|
||||
44
packages/@core/base/typings/package.json
Normal file
44
packages/@core/base/typings/package.json
Normal 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
121
packages/@core/base/typings/src/app.d.ts
vendored
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user