chore: init project

This commit is contained in:
vben
2024-05-19 21:20:42 +08:00
commit 399334ac57
630 changed files with 45623 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
# shared
全局共享包,请勿引入 workspace 依赖
- typings 共享类型
- toolkit 共享工具类

View File

@@ -0,0 +1,3 @@
# @vben-core/design-tokens
用于维护全局所有的 css 变量,它由 vite 插件在全局注入,不需要手动引入

View File

@@ -0,0 +1,45 @@
{
"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": {
"url": "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"
}
}
}
}

View File

@@ -0,0 +1,95 @@
.dark {
/* 基础背景颜色颜色 */
/* --color-background: 240 6% 18%; */
// --color-body: 220deg 13.04% 8%;
--color-body: 220deg 13.04% 8%;
--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;
/* ======================================== */
/* =============component & UI============= */
/* ======================================== */
/* --color-login-background: 219 42% 11%; */
/* 图标颜色 */
/* authentication */
--color-authentication-from: hsl(240deg 11% 6%);
--color-authentication-to: hsl(240deg 11% 6%);
color-scheme: dark;
}

View File

@@ -0,0 +1,103 @@
/* https://gavin-yyc.github.io/colorconvert/ */
:root {
--font-geist-sans: 'geist-sans', -apple-system, blinkmacsystemfont, 'Segoe UI',
roboto, helvetica, arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol';
/* 基础背景颜色颜色 */
/* --color-background: 210deg 25% 96.86%; */
--color-body: 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============= */
/* ======================================== */
/* --color-login-background: 0 0 100%; */
/* 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%;
// --color-menu-opened: 0deg 0% 100%;
/* authentication */
--color-authentication-from: hsl(231deg 61% 44%);
--color-authentication-to: hsl(218deg 70% 42%);
}

View File

@@ -0,0 +1,4 @@
import './default/index.scss';
import './dark/index.scss';
export {};

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"include": ["src"]
}

View File

@@ -0,0 +1,3 @@
import { defineConfig } from '@vben/vite-config';
export default defineConfig();

View 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'],
},
],
});

View File

@@ -0,0 +1,43 @@
{
"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": {
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
},
"scripts": {
"build": "pnpm unbuild",
"stub": "pnpm unbuild --stub",
"prepublishOnly": "npm run build"
},
"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"
}
}

View File

@@ -0,0 +1 @@
@import './scss/index';

View File

@@ -0,0 +1,96 @@
#app,
.ant-app,
body,
html {
width: 100%;
height: 100%;
overscroll-behavior: none;
}
*,
::after,
::before {
box-sizing: border-box;
border-color: hsl(var(--color-border));
border-style: solid;
border-width: 0;
}
body.invert {
filter: invert(100%);
}
body.grayscale {
--tw-grayscale: grayscale(100%);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast)
var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate)
var(--tw-sepia) var(--tw-drop-shadow);
}
html {
font-size: var(--font-size-base);
font-variation-settings: normal;
color: hsl(var(--color-foreground));
background-color: hsl(var(--color-background));
text-size-adjust: 100%;
font-synthesis-weight: none;
scroll-behavior: smooth;
text-rendering: optimizelegibility;
-webkit-tap-highlight-color: rgb(128 128 128 / 50%);
}
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;
}
// .collapse-transition {
// transition:
// height 0.2s ease-in-out,
// padding-top 0.2s ease-in-out,
// padding-bottom 0.2s ease-in-out;
// }
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;
}

View File

@@ -0,0 +1,5 @@
$namespace: 'vben' !default;
$common-separator: '-' !default;
$element-separator: '__' !default;
$modifier-separator: '--' !default;
$state-prefix: 'is' !default;

View File

@@ -0,0 +1,51 @@
$max-child: 5;
@for $i from 1 through $max-child {
* > .enter-x:nth-child(#{$i}) {
transform: translateX(50px);
}
* > .-enter-x:nth-child(#{$i}) {
transform: translateX(-50px);
}
* > .enter-x:nth-child(#{$i}),
* > .-enter-x:nth-child(#{$i}) {
// z-index: 10 - $i;
opacity: 0;
animation: enter-x-animation 0.3s ease-in-out 0.2s;
animation-delay: 0.1s * $i;
animation-fill-mode: forwards;
}
* > .enter-y:nth-child(#{$i}) {
transform: translateY(50px);
}
* > .-enter-y:nth-child(#{$i}) {
transform: translateY(-50px);
}
* > .enter-y:nth-child(#{$i}),
* > .-enter-y:nth-child(#{$i}) {
// z-index: 10 - $i;
opacity: 0;
animation: enter-y-animation 0.3s ease-in-out 0.2s;
animation-delay: 0.1s * $i;
animation-fill-mode: forwards;
}
}
@keyframes enter-x-animation {
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-y-animation {
to {
opacity: 1;
transform: translateY(0);
}
}

View 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;
}
}
}

View File

@@ -0,0 +1,8 @@
/** css 样式重置 */
@import 'modern-normalize/modern-normalize.css';
/** 元素进入动画 */
@import './common/entry';
@import './common/base';
@import './transition';
@import './nprogress';

View File

@@ -0,0 +1,78 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
position: fixed;
top: 0;
left: 0;
z-index: 1031;
width: 100%;
height: 2px;
background: hsl(var(--color-primary));
}
/* Fancy blur effect */
#nprogress .peg {
position: absolute;
right: 0;
display: block;
width: 100px;
height: 100%;
box-shadow:
0 0 10px hsl(var(--color-primary)),
0 0 5px hsl(var(--color-primary));
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
position: fixed;
top: 15px;
right: 15px;
z-index: 1031;
display: block;
}
#nprogress .spinner-icon {
box-sizing: border-box;
width: 18px;
height: 18px;
border: solid 2px transparent;
border-top-color: hsl(var(--color-primary));
border-left-color: hsl(var(--color-primary));
border-radius: 50%;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
position: relative;
overflow: hidden;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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%);
}

View File

@@ -0,0 +1,4 @@
@import './slide';
@import './fade';
@import './animation';
@import './collapse';

View 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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,34 @@
@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;
&: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-active {
@apply outline-primary outline outline-2;
&:after {
display: none;
}
}
&:not(.outline-box-active):hover:after {
@apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
}
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"include": ["src"]
}

View File

@@ -0,0 +1,32 @@
{
"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/iconify"
},
"bugs": {
"url": "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"
}
}

View File

@@ -0,0 +1,12 @@
import { Icon } from '@iconify/vue';
import { defineComponent, h } from 'vue';
function createIcon(name: string) {
return defineComponent({
setup(props, { attrs }) {
return () => h(Icon, { icon: name, ...props, ...attrs });
},
});
}
export { createIcon };

View File

@@ -0,0 +1,5 @@
export * from './factory';
export * from './material';
export * from './mdi';
export * from '@iconify/vue';

View File

@@ -0,0 +1,75 @@
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 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');

View 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',
);

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,53 @@
{
"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": {
"url": "https://github.com/vbenjs/vue-vben-admin/issues"
},
"scripts": {
"stub": "pnpm unbuild --stub",
"build": "pnpm unbuild"
},
"files": [
"dist"
],
"sideEffects": false,
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"imports": {
"#*": "./src/*"
},
"exports": {
".": {
"development": "./src/index.ts",
"types": "./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"
}
}

View 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);
});
});

View 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 };

View 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');
});
});

View 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 };

View 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 });
});
});

View 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 };

View 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);
});
});

View 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 };

View File

@@ -0,0 +1,10 @@
export * from './color';
export * from './date';
export * from './diff';
export * from './hash';
export * from './inference';
export * from './merge';
export * from './namespace';
export * from './nprogress';
export * from './tree';
export * from './window';

View 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);
});
});

View 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,
};

View File

@@ -0,0 +1 @@
export { defu as merge } from 'defu';

View 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 };

View 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 };

View 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' },
],
},
],
},
]);
});
});

View 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 };

View File

@@ -0,0 +1,33 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { openWindow } from './window'; // 假设你的函数在 'openWindow' 文件中
describe('generateUUID', () => {
// 保存原始的 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',
);
});
});

View 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 };

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"include": ["src"]
}

View File

@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
clean: true,
declaration: true,
entries: ['src/index'],
});

View File

@@ -0,0 +1,88 @@
import 'vue-router';
declare module 'vue-router' {
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;
/**
* 路由是否已经加载过
*/
loaded?: boolean;
/**
* 用于路由->菜单排序
*/
orderNo?: number;
/**
* 标题名称
*/
title: string;
}
}

View File

@@ -0,0 +1,50 @@
{
"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": {
"url": "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"
},
"./global": {
"types": "./global.d.ts"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"vue": "^3.4.27",
"vue-router": "^4.3.2"
}
}

View File

@@ -0,0 +1,44 @@
interface RoleInfo {
/** 角色名 */
roleName: string;
/** 角色值 */
value: string;
}
/** 用户信息 */
interface UserInfo {
/**
* 头像
*/
avatar: string;
/**
* 用户描述
*/
desc: string;
/**
* 首页地址
*/
homePath: string;
/**
* 用户昵称
*/
realName: string;
/**
* 用户角色信息
*/
roles: RoleInfo[];
/**
* accessToken
*/
token: string;
/**
* 用户id
*/
userId: string;
/**
* 用户名
*/
username: string;
}
export type { RoleInfo, UserInfo };

View File

@@ -0,0 +1,6 @@
export * from './access';
export * from './menu-record';
export * from './preference';
export * from './tabs';
export * from './tools';
export * from './ui';

View 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;
/**
* 排序号
*/
orderNo?: number;
/**
* 父级路径
*/
parent?: string;
/**
* 所有父级路径
*/
parents?: string[];
/**
* 菜单路径唯一可当作key
*/
path: string;
/**
* 是否显示菜单
* @default true
*/
show?: boolean;
}
export type { ExRouteRecordRaw, MenuRecordBadgeRaw, MenuRecordRaw };

View File

@@ -0,0 +1,130 @@
type LayoutType =
| 'full-content'
| 'header-nav'
| 'mixed-nav'
| 'side-mixed-nav'
| 'side-nav';
type BreadcrumbStyle = 'background' | 'normal';
type NavigationStyle = 'normal' | 'rounded';
type ThemeType = 'auto' | 'dark' | 'light';
type ContentCompactType = 'compact' | 'wide';
type LayoutHeaderMode = 'auto' | 'auto-scroll' | 'fixed' | 'static';
type PageTransitionType = 'fade-slide';
type AuthPageLayout = 'panel-center' | 'panel-left' | 'panel-right';
type SupportLocale = 'en-US' | 'zh-CN';
interface Language {
key: SupportLocale;
text: string;
}
interface Preference {
/** 应用名 */
appName: string;
/** 登录注册页面布局 */
authPageLayout: AuthPageLayout;
/** 面包屑是否只有一个时隐藏 */
breadcrumbHideOnlyOne: boolean;
/** 面包屑首页图标是否可见 */
breadcrumbHome: boolean;
/** 面包屑图标是否可见 */
breadcrumbIcon: boolean;
/** 面包屑类型 */
breadcrumbStyle: BreadcrumbStyle;
/** 面包屑是否可见 */
breadcrumbVisible: boolean;
/** 是否开启灰色模式 */
colorGrayMode: boolean;
/** 主题色 */
colorPrimary: string;
/** 是否开启色弱模式 */
colorWeakMode: boolean;
/** 是否开启紧凑模式 */
compact: boolean;
/** 是否开启内容紧凑模式 */
contentCompact: ContentCompactType;
/** 页脚Copyright */
copyright: string;
/** 应用默认头像 */
defaultAvatar: string;
/** 页脚是否固定 */
footerFixed: boolean;
/** 页脚是否可见 */
footerVisible: boolean;
/** header显示模式 */
headerMode: LayoutHeaderMode;
/** 顶栏是否可见 */
headerVisible: boolean;
/** 是否移动端 */
isMobile: boolean;
/** 开启标签页缓存功能 */
keepAlive: boolean;
/** 布局方式 */
layout: LayoutType;
/** 支持的语言 */
locale: SupportLocale;
/** 应用Logo */
logo: string;
/** logo是否可见 */
logoVisible: boolean;
/** 导航菜单风格 */
navigationStyle: NavigationStyle;
/** 是否开启页面加载进度条 */
pageProgress: boolean;
/** 页面切换动画 */
pageTransition: PageTransitionType;
/** 页面切换动画是否启用 */
pageTransitionEnable: boolean;
/** 是否开启半深色菜单只在theme='light'时生效) */
semiDarkMenu: boolean;
/** 侧边栏是否折叠 */
sideCollapse: boolean;
/** 侧边栏折叠时是否显示title */
sideCollapseShowTitle: boolean;
/** 菜单自动展开状态 */
sideExpandOnHover: boolean;
/** 侧边栏扩展区域是否折叠 */
sideExtraCollapse: boolean;
/** 侧边栏是否可见 */
sideVisible: boolean;
/** 侧边栏宽度 */
sideWidth: number;
/** 是否开启多标签页图标 */
tabsIcon: boolean;
/** 是否开启多标签页 */
tabsVisible: boolean;
/** 当前主题 */
theme: ThemeType;
}
// 这些属性是静态的,不会随着用户的操作而改变
interface StaticPreference {
/** 主题色预设 */
colorPrimaryPresets: string[];
/** 支持的语言 */
supportLanguages: Language[];
}
type PreferenceKeys = keyof Preference;
export type {
AuthPageLayout,
BreadcrumbStyle,
ContentCompactType,
LayoutHeaderMode,
LayoutType,
PageTransitionType,
Preference,
PreferenceKeys,
StaticPreference,
SupportLocale,
ThemeType,
};

View File

@@ -0,0 +1,3 @@
import type { RouteLocationNormalized } from 'vue-router';
export type TabItem = RouteLocationNormalized;

View File

@@ -0,0 +1,89 @@
import { type ComputedRef, type MaybeRef } from 'vue';
/**
* 深度部分类型
*/
type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
/**
* 任意类型的异步函数
*/
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>;
export {
type AnyFunction,
type AnyNormalFunction,
type AnyPromiseFunction,
type DeepPartial,
type IntervalHandle,
type MaybeComputedRef,
type MaybeReadonlyRef,
type NonNullable,
type Nullable,
type ReadonlyRecordable,
type Recordable,
type TimeoutHandle,
};

View File

@@ -0,0 +1,6 @@
interface SelectListItem {
label: string;
value: string;
}
export type { SelectListItem };

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"include": ["src"]
}