fix: 把 Yi.Vben5.Vue3 下的 packages 目录放出来
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -154,6 +154,10 @@ PublishScripts/
|
|||||||
*.nupkg
|
*.nupkg
|
||||||
# The packages folder can be ignored because of Package Restore
|
# The packages folder can be ignored because of Package Restore
|
||||||
**/packages/*
|
**/packages/*
|
||||||
|
|
||||||
|
# 把 Yi.Vben5.Vue3 下的 packages 目录重新放出来
|
||||||
|
!**/Yi.Vben5.Vue3/packages/
|
||||||
|
!**/Yi.Vben5.Vue3/packages/**
|
||||||
# except build/, which is used as an MSBuild target.
|
# except build/, which is used as an MSBuild target.
|
||||||
!**/packages/build/
|
!**/packages/build/
|
||||||
# Uncomment if necessary however generally it will be regenerated when needed
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
|||||||
3
Yi.Vben5.Vue3/packages/@core/README.md
Normal file
3
Yi.Vben5.Vue3/packages/@core/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# @vben-core
|
||||||
|
|
||||||
|
系统一些比较基础的SDK和UI组件库,该目录后续完善后,可能会迁移出去或者发布到npm,请勿将任何业务逻辑和业务包放在该目录。
|
||||||
5
Yi.Vben5.Vue3/packages/@core/base/README.md
Normal file
5
Yi.Vben5.Vue3/packages/@core/base/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# base
|
||||||
|
|
||||||
|
基础共享包,请勿引入 workspace 依赖
|
||||||
|
|
||||||
|
-
|
||||||
41
Yi.Vben5.Vue3/packages/@core/base/design/package.json
Normal file
41
Yi.Vben5.Vue3/packages/@core/base/design/package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/design",
|
||||||
|
"version": "5.5.7",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
Yi.Vben5.Vue3/packages/@core/base/design/src/css/global.css
Normal file
160
Yi.Vben5.Vue3/packages/@core/base/design/src/css/global.css
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before {
|
||||||
|
@apply border-border;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply text-foreground bg-background font-sans text-[100%];
|
||||||
|
|
||||||
|
font-variation-settings: normal;
|
||||||
|
line-height: 1.15;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
font-synthesis-weight: none;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
/* -webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale; */
|
||||||
|
}
|
||||||
|
|
||||||
|
#app,
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
@apply size-full;
|
||||||
|
|
||||||
|
/* scrollbar-gutter: stable; */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
/* pointer-events: auto !important; */
|
||||||
|
|
||||||
|
/* overflow: overlay; */
|
||||||
|
|
||||||
|
/* -webkit-font-smoothing: antialiased; */
|
||||||
|
|
||||||
|
/* -moz-osx-font-smoothing: grayscale; */
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:active,
|
||||||
|
a:hover,
|
||||||
|
a:link,
|
||||||
|
a:visited {
|
||||||
|
@apply no-underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root),
|
||||||
|
::view-transition-old(root) {
|
||||||
|
@apply animate-none mix-blend-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
@apply z-[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
@apply z-[2147483646];
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark::view-transition-old(root) {
|
||||||
|
@apply z-[2147483646];
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark::view-transition-new(root) {
|
||||||
|
@apply z-[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* input:-webkit-autofill {
|
||||||
|
@apply border-none;
|
||||||
|
|
||||||
|
box-shadow: 0 0 0 1000px transparent inset;
|
||||||
|
} */
|
||||||
|
|
||||||
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
|
@apply m-0 appearance-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 只有非mac下才进行调整,mac下使用默认滚动条 */
|
||||||
|
html:not([data-platform='macOs']) {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
@apply h-[10px] w-[10px];
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-border rounded-sm border-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply rounded-sm border-none bg-transparent shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.flex-center {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col-center {
|
||||||
|
@apply flex flex-col items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-box {
|
||||||
|
@apply outline-border relative cursor-pointer rounded-md p-1 outline outline-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-box::after {
|
||||||
|
@apply absolute left-1/2 top-1/2 z-20 h-0 w-[1px] rounded-sm opacity-0 outline outline-2 outline-transparent transition-all duration-300 content-[""];
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-box.outline-box-active {
|
||||||
|
@apply outline-primary outline outline-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-box.outline-box-active::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-box:not(.outline-box-active):hover::after {
|
||||||
|
@apply outline-primary left-0 top-0 h-full w-full p-1 opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vben-link {
|
||||||
|
@apply text-primary hover:text-primary-hover active:text-primary-active cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-box {
|
||||||
|
@apply bg-card text-card-foreground border-border rounded-xl border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html.invert-mode {
|
||||||
|
@apply invert;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.grayscale-mode {
|
||||||
|
@apply grayscale;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* Make clicks pass-through */
|
||||||
|
#nprogress {
|
||||||
|
@apply pointer-events-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
@apply bg-primary fixed left-0 top-0 z-[1031] h-[2px] w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fancy blur effect */
|
||||||
|
#nprogress .peg {
|
||||||
|
@apply absolute right-0 block h-full w-[100px];
|
||||||
|
|
||||||
|
box-shadow:
|
||||||
|
0 0 10px hsl(var(--primary)),
|
||||||
|
0 0 5px hsl(var(--primary));
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(3deg) translate(0, -4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove these to get rid of the spinner */
|
||||||
|
#nprogress .spinner {
|
||||||
|
@apply fixed right-4 top-4 z-[1031] block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .spinner-icon {
|
||||||
|
@apply border-t-primary border-l-primary size-4 rounded-full border-[2px] border-solid border-transparent;
|
||||||
|
|
||||||
|
animation: nprogress-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nprogress-custom-parent {
|
||||||
|
@apply relative overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nprogress-custom-parent #nprogress .spinner,
|
||||||
|
.nprogress-custom-parent #nprogress .bar {
|
||||||
|
@apply absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nprogress-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nprogress-spinner {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
236
Yi.Vben5.Vue3/packages/@core/base/design/src/css/transition.css
Normal file
236
Yi.Vben5.Vue3/packages/@core/base/design/src/css/transition.css
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
.slide-up-enter-active,
|
||||||
|
.slide-up-leave-active {
|
||||||
|
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-move {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-enter-from,
|
||||||
|
.slide-up-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-enter-active,
|
||||||
|
.slide-down-leave-active {
|
||||||
|
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-move {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-down-enter-from,
|
||||||
|
.slide-down-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-enter-active,
|
||||||
|
.slide-left-leave-active {
|
||||||
|
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-move {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-left-enter-from,
|
||||||
|
.slide-left-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-active,
|
||||||
|
.slide-right-leave-active {
|
||||||
|
transition: 0.25s cubic-bezier(0.25, 0.8, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-move {
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-right-enter-from,
|
||||||
|
.slide-right-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transition-enter-active,
|
||||||
|
.fade-transition-leave-active {
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-transition-enter-from,
|
||||||
|
.fade-transition-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-slide-leave-active,
|
||||||
|
.fade-slide-enter-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-slide-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-down-enter-active,
|
||||||
|
.fade-down-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.25s,
|
||||||
|
transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-down-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-down-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-leave-active,
|
||||||
|
.fade-scale-enter-active {
|
||||||
|
transition: all 0.28s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-up-enter-active,
|
||||||
|
.fade-up-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
transform 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-up-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-up-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-slide {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(30px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-up {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-down {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-slow {
|
||||||
|
animation: fade 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-slide-slow {
|
||||||
|
animation: fade-slide 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-up-slow {
|
||||||
|
animation: fade-up 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-down-slow {
|
||||||
|
animation: fade-down 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-transition {
|
||||||
|
transition:
|
||||||
|
0.2s height ease-in-out,
|
||||||
|
0.2s padding-top ease-in-out,
|
||||||
|
0.2s padding-bottom ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-transition-leave-active,
|
||||||
|
.collapse-transition-enter-active {
|
||||||
|
transition:
|
||||||
|
0.2s max-height ease-in-out,
|
||||||
|
0.2s padding-top ease-in-out,
|
||||||
|
0.2s margin-top ease-in-out;
|
||||||
|
}
|
||||||
87
Yi.Vben5.Vue3/packages/@core/base/design/src/css/ui.css
Normal file
87
Yi.Vben5.Vue3/packages/@core/base/design/src/css/ui.css
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
.side-content {
|
||||||
|
animation-duration: 0.2s;
|
||||||
|
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(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-left {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-right {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-popup {
|
||||||
|
z-index: var(--popup-z-index);
|
||||||
|
}
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
.dark,
|
||||||
|
.dark[data-theme='custom'],
|
||||||
|
.dark[data-theme='default'] {
|
||||||
|
/* Default background color of <body />...etc */
|
||||||
|
--background: 222.34deg 10.43% 12.27%;
|
||||||
|
|
||||||
|
/* 主体区域背景色 */
|
||||||
|
--background-deep: 220deg 13.06% 9%;
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
|
||||||
|
/* Background color for <Card /> */
|
||||||
|
--card: 222.34deg 10.43% 12.27%;
|
||||||
|
|
||||||
|
/* --card: 222.2 84% 4.9%; */
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
/* Background color for popovers such as <DropdownMenu />, <HoverCard />, <Popover /> */
|
||||||
|
|
||||||
|
/* --popover: 222.82deg 8.43% 12.27%; */
|
||||||
|
|
||||||
|
/* 弹出层的背景色与主题区域背景色太过接近 */
|
||||||
|
--popover: 0 0% 14.2%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
/* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */
|
||||||
|
|
||||||
|
/* --muted: 220deg 6.82% 17.25%; */
|
||||||
|
|
||||||
|
/* --muted-foreground: 215 20.2% 65.1%; */
|
||||||
|
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
|
||||||
|
/* 主题颜色 */
|
||||||
|
|
||||||
|
/* --primary: 245 82% 67%; */
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Used for destructive actions such as <Button variant="destructive"> */
|
||||||
|
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Used for success actions such as <message> */
|
||||||
|
|
||||||
|
--info: 180, 1.54%, 12.75%;
|
||||||
|
--info-foreground: 220, 4%, 58%;
|
||||||
|
|
||||||
|
/* Used for success actions such as <message> */
|
||||||
|
|
||||||
|
--success: 144 57% 58%;
|
||||||
|
--success-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Used for warning actions such as <message> */
|
||||||
|
|
||||||
|
--warning: 42 84% 61%;
|
||||||
|
--warning-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* 颜色次要 */
|
||||||
|
--secondary: 240 5% 17%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
|
||||||
|
--accent: 216 5% 19%;
|
||||||
|
--accent-dark: 240 0% 22%;
|
||||||
|
--accent-darker: 240 0% 26%;
|
||||||
|
--accent-lighter: 216 5% 12%;
|
||||||
|
--accent-hover: 216 5% 24%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Darker color */
|
||||||
|
--heavy: 216 5% 24%;
|
||||||
|
--heavy-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
|
/* Default border color */
|
||||||
|
--border: 240 3.7% 22%;
|
||||||
|
|
||||||
|
/* Border color for inputs such as <Input />, <Select />, <Textarea /> */
|
||||||
|
--input: 0deg 0% 100% / 10%;
|
||||||
|
--input-placeholder: 218deg 11% 65%;
|
||||||
|
--input-background: 0deg 0% 100% / 5%;
|
||||||
|
|
||||||
|
/* Used for focus ring */
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
/* 基本圆角大小 */
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* ============= Custom ============= */
|
||||||
|
|
||||||
|
/* 遮罩颜色 */
|
||||||
|
--overlay: 0deg 0% 0% / 40%;
|
||||||
|
--overlay-content: 0deg 0% 0% / 40%;
|
||||||
|
|
||||||
|
/* 基本文字大小 */
|
||||||
|
--font-size-base: 16px;
|
||||||
|
|
||||||
|
/* =============component & UI============= */
|
||||||
|
|
||||||
|
--sidebar: 222.34deg 10.43% 12.27%;
|
||||||
|
--sidebar-deep: 220deg 13.06% 9%;
|
||||||
|
--menu: var(--sidebar);
|
||||||
|
|
||||||
|
/* header */
|
||||||
|
--header: 222.34deg 10.43% 12.27%;
|
||||||
|
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='violet'],
|
||||||
|
[data-theme='violet'] .dark {
|
||||||
|
--background: 224 71.4% 4.1%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 210 20% 98%;
|
||||||
|
--card: 224 71.4% 4.1%;
|
||||||
|
--card-foreground: 210 20% 98%;
|
||||||
|
--popover: 224 71.4% 4.1%;
|
||||||
|
--popover-foreground: 210 20% 98%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 215 27.9% 16.9%;
|
||||||
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
--muted: 215 27.9% 16.9%;
|
||||||
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
|
--accent: 215 27.9% 16.9%;
|
||||||
|
--accent-foreground: 210 20% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 215 27.9% 16.9%;
|
||||||
|
--input: 215 27.9% 16.9%;
|
||||||
|
--ring: 263.4 70% 50.4%;
|
||||||
|
--sidebar: 224 71.4% 4.1%;
|
||||||
|
--sidebar-deep: 224 71.4% 4.1%;
|
||||||
|
--header: 224 71.4% 4.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='pink'],
|
||||||
|
[data-theme='pink'] .dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 0 0% 9%;
|
||||||
|
--card-foreground: 0 0% 95%;
|
||||||
|
--popover: 0 0% 9%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 15%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 346.8 77.2% 49.8%;
|
||||||
|
--sidebar: 20 14.3% 4.1%;
|
||||||
|
--sidebar-deep: 20 14.3% 4.1%;
|
||||||
|
--header: 20 14.3% 4.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='rose'],
|
||||||
|
[data-theme='rose'] .dark {
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary-foreground: 0 85.7% 97.3%;
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
--input: 0 0% 14.9%;
|
||||||
|
--ring: 0 72.2% 50.6%;
|
||||||
|
--sidebar: 0 0% 3.9%;
|
||||||
|
--sidebar-deep: 0 0% 3.9%;
|
||||||
|
--header: 0 0% 3.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='sky-blue'],
|
||||||
|
[data-theme='sky-blue'] .dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 224.3 76.3% 48%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-deep: 222.2 84% 4.9%;
|
||||||
|
--header: 222.2 84% 4.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='deep-blue'],
|
||||||
|
[data-theme='deep-blue'] .dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 224.3 76.3% 48%;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-deep: 222.2 84% 4.9%;
|
||||||
|
--header: 222.2 84% 4.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='green'],
|
||||||
|
[data-theme='green'] .dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 24 9.8% 6%;
|
||||||
|
--card-foreground: 0 0% 95%;
|
||||||
|
--popover: 0 0% 9%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 15%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 142.4 71.8% 29.2%;
|
||||||
|
--sidebar: 20 14.3% 4.1%;
|
||||||
|
--sidebar-deep: 20 14.3% 4.1%;
|
||||||
|
--header: 20 14.3% 4.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='deep-green'],
|
||||||
|
[data-theme='deep-green'] .dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 0 0% 95%;
|
||||||
|
--card: 24 9.8% 6%;
|
||||||
|
--card-foreground: 0 0% 95%;
|
||||||
|
--popover: 0 0% 9%;
|
||||||
|
--popover-foreground: 0 0% 95%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 15%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 0 85.7% 97.3%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 142.4 71.8% 29.2%;
|
||||||
|
--sidebar: 20 14.3% 4.1%;
|
||||||
|
--sidebar-deep: 20 14.3% 4.1%;
|
||||||
|
--header: 20 14.3% 4.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='orange'],
|
||||||
|
[data-theme='orange'] .dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
--destructive: 0 72.2% 50.6%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 20.5 90.2% 48.2%;
|
||||||
|
--sidebar: 20 14.3% 4.1%;
|
||||||
|
--sidebar-deep: 20 14.3% 4.1%;
|
||||||
|
--header: 20 14.3% 4.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='yellow'],
|
||||||
|
[data-theme='yellow'] .dark {
|
||||||
|
--background: 20 14.3% 4.1%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 60 9.1% 97.8%;
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 26 83.3% 14.1%;
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 35.5 91.7% 32.9%;
|
||||||
|
--sidebar: 20 14.3% 4.1%;
|
||||||
|
--sidebar-deep: 20 14.3% 4.1%;
|
||||||
|
--header: 20 14.3% 4.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='zinc'],
|
||||||
|
[data-theme='zinc'] .dark {
|
||||||
|
--background: 240 10% 3.9%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 240 10% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 240 3.7% 15.9%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 3.7% 15.9%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
--sidebar: 240 10% 3.9%;
|
||||||
|
--sidebar-deep: 240 10% 3.9%;
|
||||||
|
--header: 240 10% 3.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='neutral'],
|
||||||
|
[data-theme='neutral'] .dark {
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary-foreground: 0 0% 9%;
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
--input: 0 0% 14.9%;
|
||||||
|
--ring: 0 0% 83.1%;
|
||||||
|
--sidebar: 0 0% 3.9%;
|
||||||
|
--sidebar-deep: 0 0% 3.9%;
|
||||||
|
--header: 0 0% 3.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='slate'],
|
||||||
|
[data-theme='slate'] .dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9;
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--sidebar-deep: 222.2 84% 4.9%;
|
||||||
|
--header: 222.2 84% 4.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark[data-theme='gray'],
|
||||||
|
[data-theme='gray'] .dark {
|
||||||
|
--background: 224 71.4% 4.1%;
|
||||||
|
--background-deep: var(--background);
|
||||||
|
--foreground: 210 20% 98%;
|
||||||
|
--card: 224 71.4% 4.1%;
|
||||||
|
--card-foreground: 210 20% 98%;
|
||||||
|
--popover: 224 71.4% 4.1%;
|
||||||
|
--popover-foreground: 210 20% 98%;
|
||||||
|
--primary-foreground: 220.9 39.3% 11%;
|
||||||
|
--secondary: 215 27.9% 16.9%;
|
||||||
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
--muted: 215 27.9% 16.9%;
|
||||||
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
|
--accent: 215 27.9% 16.9%;
|
||||||
|
--accent-foreground: 210 20% 98%;
|
||||||
|
--destructive: 359.21 68.47% 56.47%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 215 27.9% 16.9%;
|
||||||
|
--input: 215 27.9% 16.9%;
|
||||||
|
--ring: 216 12.2% 83.9%;
|
||||||
|
--sidebar: 224 71.4% 4.1%;
|
||||||
|
--sidebar-deep: 224 71.4% 4.1%;
|
||||||
|
--header: 224 71.4% 4.1%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
:root {
|
||||||
|
/** 弹出层的基础层级 **/
|
||||||
|
--popup-z-index: 2000;
|
||||||
|
--font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto,
|
||||||
|
'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
|
|
||||||
|
/* Default background color of <body />...etc */
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
|
||||||
|
/* 主体区域背景色 */
|
||||||
|
--background-deep: 216 20.11% 95.47%;
|
||||||
|
--foreground: 210 6% 21%;
|
||||||
|
|
||||||
|
/* Background color for <Card /> */
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
/* Background color for popovers such as <DropdownMenu />, <HoverCard />, <Popover /> */
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
/* Muted backgrounds such as <TabsList />, <Skeleton /> and <Switch /> */
|
||||||
|
|
||||||
|
/* --muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%; */
|
||||||
|
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
|
/* 主题颜色 */
|
||||||
|
|
||||||
|
--primary: 212 100% 45%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Used for destructive actions such as <Button variant="destructive"> */
|
||||||
|
|
||||||
|
--destructive: 359.33 100% 65.1%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Used for success actions such as <message> */
|
||||||
|
|
||||||
|
--info: 240, 5%, 96%;
|
||||||
|
--info-foreground: 220, 4%, 58%;
|
||||||
|
|
||||||
|
/* Used for success actions such as <message> */
|
||||||
|
|
||||||
|
--success: 144 57% 58%;
|
||||||
|
--success-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Used for warning actions such as <message> */
|
||||||
|
|
||||||
|
--warning: 42 84% 61%;
|
||||||
|
--warning-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
/* Secondary colors for <Button /> */
|
||||||
|
|
||||||
|
--secondary: 240 5% 96%;
|
||||||
|
--secondary-foreground: 240 6% 10%;
|
||||||
|
|
||||||
|
/* Used for accents such as hover effects on <DropdownMenuItem>, <SelectItem>...etc */
|
||||||
|
--accent: 240 5% 96%;
|
||||||
|
--accent-dark: 216 14% 93%;
|
||||||
|
--accent-darker: 216 11% 91%;
|
||||||
|
--accent-lighter: 240 0% 98%;
|
||||||
|
--accent-hover: 200deg 10% 90%;
|
||||||
|
--accent-foreground: 240 6% 10%;
|
||||||
|
|
||||||
|
/* Darker color */
|
||||||
|
--heavy: 192deg 9.43% 89.61%;
|
||||||
|
--heavy-foreground: var(--accent-foreground);
|
||||||
|
|
||||||
|
/* Default border color */
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
|
||||||
|
/* Border color for inputs such as <Input />, <Select />, <Textarea /> */
|
||||||
|
--input: 240deg 5.88% 90%;
|
||||||
|
--input-placeholder: 217 10.6% 65%;
|
||||||
|
--input-background: 0 0% 100%;
|
||||||
|
|
||||||
|
/* Used for focus ring */
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
/* Border radius for card, input and buttons */
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* ============= custom ============= */
|
||||||
|
|
||||||
|
/* 遮罩颜色 */
|
||||||
|
--overlay: 0 0% 0% / 45%;
|
||||||
|
--overlay-content: 0 0% 95% / 45%;
|
||||||
|
|
||||||
|
/* 基本文字大小 */
|
||||||
|
--font-size-base: 16px;
|
||||||
|
|
||||||
|
/* =============component & UI============= */
|
||||||
|
|
||||||
|
/* menu */
|
||||||
|
--sidebar: 0 0% 100%;
|
||||||
|
--sidebar-deep: 0 0% 100%;
|
||||||
|
--menu: var(--sidebar);
|
||||||
|
|
||||||
|
/* header */
|
||||||
|
--header: 0 0% 100%;
|
||||||
|
|
||||||
|
accent-color: var(--primary);
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='violet'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 224 71.4% 4.1%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 224 71.4% 4.1%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 224 71.4% 4.1%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 220 14.3% 95.9%;
|
||||||
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
--muted: 220 14.3% 95.9%;
|
||||||
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
|
--accent: 220 14.3% 95.9%;
|
||||||
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 220 13% 91%;
|
||||||
|
--input: 220 13% 91%;
|
||||||
|
--ring: 262.1 83.3% 57.8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='pink'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 346.8 77.2% 49.8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='rose'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 346.8 77.2% 49.8%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='sky-blue'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='deep-blue'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='green'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 142.1 76.2% 36.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='deep-green'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary-foreground: 355.7 100% 97.3%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 142.1 76.2% 36.3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='orange'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 24.6 95% 53.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='yellow'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
--primary-foreground: 26 83.3% 14.1%;
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='zinc'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 5.9% 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='neutral'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 0 0% 3.9%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 0 0% 96.1%;
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
--muted: 0 0% 96.1%;
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
--accent: 0 0% 96.1%;
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
--input: 0 0% 89.8%;
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='slate'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='gray'] {
|
||||||
|
/* --background: 0 0% 100%; */
|
||||||
|
--foreground: 224 71.4% 4.1%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 224 71.4% 4.1%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 224 71.4% 4.1%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 220 14.3% 95.9%;
|
||||||
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
--muted: 220 14.3% 95.9%;
|
||||||
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
|
--accent: 220 14.3% 95.9%;
|
||||||
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 220 13% 91%;
|
||||||
|
--input: 220 13% 91%;
|
||||||
|
--ring: 224 71.4% 4.1%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import './default.css';
|
||||||
|
import './dark.css';
|
||||||
|
|
||||||
|
export {};
|
||||||
8
Yi.Vben5.Vue3/packages/@core/base/design/src/index.ts
Normal file
8
Yi.Vben5.Vue3/packages/@core/base/design/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import './design-tokens';
|
||||||
|
|
||||||
|
import './css/global.css';
|
||||||
|
import './css/transition.css';
|
||||||
|
import './css/nprogress.css';
|
||||||
|
import './css/ui.css';
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
@forward './constants';
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
$namespace: 'vben' !default;
|
||||||
|
$common-separator: '-' !default;
|
||||||
|
$element-separator: '__' !default;
|
||||||
|
$modifier-separator: '--' !default;
|
||||||
|
$state-prefix: 'is' !default;
|
||||||
6
Yi.Vben5.Vue3/packages/@core/base/design/tsconfig.json
Normal file
6
Yi.Vben5.Vue3/packages/@core/base/design/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/web.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
9
Yi.Vben5.Vue3/packages/@core/base/design/vite.config.mts
Normal file
9
Yi.Vben5.Vue3/packages/@core/base/design/vite.config.mts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from '@vben/vite-config';
|
||||||
|
|
||||||
|
export default defineConfig(async () => {
|
||||||
|
return {
|
||||||
|
vite: {
|
||||||
|
publicDir: 'src/scss-bem',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
7
Yi.Vben5.Vue3/packages/@core/base/icons/build.config.ts
Normal file
7
Yi.Vben5.Vue3/packages/@core/base/icons/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: ['src/index'],
|
||||||
|
});
|
||||||
41
Yi.Vben5.Vue3/packages/@core/base/icons/package.json
Normal file
41
Yi.Vben5.Vue3/packages/@core/base/icons/package.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/icons",
|
||||||
|
"version": "5.5.7",
|
||||||
|
"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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Yi.Vben5.Vue3/packages/@core/base/icons/src/create-icon.ts
Normal file
30
Yi.Vben5.Vue3/packages/@core/base/icons/src/create-icon.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { defineComponent, h } from 'vue';
|
||||||
|
|
||||||
|
import { addIcon, Icon, type IconifyIcon } from '@iconify/vue';
|
||||||
|
|
||||||
|
function createIconifyIcon(icon: string) {
|
||||||
|
return defineComponent({
|
||||||
|
name: `Icon-${icon}`,
|
||||||
|
setup(props, { attrs }) {
|
||||||
|
return () => h(Icon, { icon, ...props, ...attrs });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建离线图标
|
||||||
|
* @param icon 图标名称 建议与iconify的名称保持一致
|
||||||
|
* @param iconComponent 从@iconify/icon-xxx/xxx导入的图标
|
||||||
|
* @returns IconComponent
|
||||||
|
*/
|
||||||
|
function createIconifyOfflineIcon(icon: string, iconComponent: IconifyIcon) {
|
||||||
|
return defineComponent({
|
||||||
|
name: `Icon-${icon}`,
|
||||||
|
setup(props, { attrs }) {
|
||||||
|
addIcon(icon, iconComponent);
|
||||||
|
return () => h(Icon, { icon, ...props, ...attrs });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createIconifyIcon, createIconifyOfflineIcon };
|
||||||
16
Yi.Vben5.Vue3/packages/@core/base/icons/src/index.ts
Normal file
16
Yi.Vben5.Vue3/packages/@core/base/icons/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export * from './create-icon';
|
||||||
|
|
||||||
|
export * from './lucide';
|
||||||
|
|
||||||
|
export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue';
|
||||||
|
export {
|
||||||
|
addCollection,
|
||||||
|
addIcon,
|
||||||
|
Icon as IconifyIcon,
|
||||||
|
listIcons,
|
||||||
|
} from '@iconify/vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从@iconify/vue/dist/offline'导出的组件为离线ICON 不支持在线
|
||||||
|
* 从@iconify/vue'导出的组件为在能找到本地图标为离线 否则会在线获取(适用性更强)
|
||||||
|
*/
|
||||||
68
Yi.Vben5.Vue3/packages/@core/base/icons/src/lucide.ts
Normal file
68
Yi.Vben5.Vue3/packages/@core/base/icons/src/lucide.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
export {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ArrowLeftToLine,
|
||||||
|
ArrowRightLeft,
|
||||||
|
ArrowRightToLine,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowUpToLine,
|
||||||
|
Bell,
|
||||||
|
BookOpenText,
|
||||||
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
Circle,
|
||||||
|
CircleAlert,
|
||||||
|
CircleCheckBig,
|
||||||
|
CircleHelp,
|
||||||
|
CircleX,
|
||||||
|
Copy,
|
||||||
|
CornerDownLeft,
|
||||||
|
Ellipsis,
|
||||||
|
Expand,
|
||||||
|
ExternalLink,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
FoldHorizontal,
|
||||||
|
Fullscreen,
|
||||||
|
Github,
|
||||||
|
Grip,
|
||||||
|
GripVertical,
|
||||||
|
Menu as IconDefault,
|
||||||
|
Info,
|
||||||
|
InspectionPanel,
|
||||||
|
Languages,
|
||||||
|
LoaderCircle,
|
||||||
|
LockKeyhole,
|
||||||
|
LogOut,
|
||||||
|
MailCheck,
|
||||||
|
Maximize,
|
||||||
|
ArrowRightFromLine as MdiMenuClose,
|
||||||
|
ArrowLeftFromLine as MdiMenuOpen,
|
||||||
|
Menu,
|
||||||
|
Minimize,
|
||||||
|
Minimize2,
|
||||||
|
MoonStar,
|
||||||
|
Palette,
|
||||||
|
PanelLeft,
|
||||||
|
PanelRight,
|
||||||
|
Pin,
|
||||||
|
PinOff,
|
||||||
|
Plus,
|
||||||
|
RotateCw,
|
||||||
|
Search,
|
||||||
|
SearchX,
|
||||||
|
Settings,
|
||||||
|
Shrink,
|
||||||
|
Square,
|
||||||
|
SquareCheckBig,
|
||||||
|
SquareMinus,
|
||||||
|
Sun,
|
||||||
|
SunMoon,
|
||||||
|
SwatchBook,
|
||||||
|
UserRoundPen,
|
||||||
|
X,
|
||||||
|
} from 'lucide-vue-next';
|
||||||
6
Yi.Vben5.Vue3/packages/@core/base/icons/tsconfig.json
Normal file
6
Yi.Vben5.Vue3/packages/@core/base/icons/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/web.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
14
Yi.Vben5.Vue3/packages/@core/base/shared/build.config.ts
Normal file
14
Yi.Vben5.Vue3/packages/@core/base/shared/build.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: [
|
||||||
|
'src/store',
|
||||||
|
'src/constants/index',
|
||||||
|
'src/utils/index',
|
||||||
|
'src/color/index',
|
||||||
|
'src/cache/index',
|
||||||
|
'src/global-state',
|
||||||
|
],
|
||||||
|
});
|
||||||
103
Yi.Vben5.Vue3/packages/@core/base/shared/package.json
Normal file
103
Yi.Vben5.Vue3/packages/@core/base/shared/package.json
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/shared",
|
||||||
|
"version": "5.5.7",
|
||||||
|
"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:",
|
||||||
|
"dayjs": "catalog:",
|
||||||
|
"defu": "catalog:",
|
||||||
|
"lodash.clonedeep": "catalog:",
|
||||||
|
"lodash.get": "catalog:",
|
||||||
|
"lodash.isequal": "catalog:",
|
||||||
|
"lodash.set": "catalog:",
|
||||||
|
"nprogress": "catalog:",
|
||||||
|
"tailwind-merge": "catalog:",
|
||||||
|
"theme-colors": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash.clonedeep": "catalog:",
|
||||||
|
"@types/lodash.get": "catalog:",
|
||||||
|
"@types/lodash.isequal": "catalog:",
|
||||||
|
"@types/lodash.set": "catalog:",
|
||||||
|
"@types/nprogress": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
130
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts
vendored
Normal file
130
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts
vendored
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { StorageManager } from '../storage-manager';
|
||||||
|
|
||||||
|
describe('storageManager', () => {
|
||||||
|
let storageManager: StorageManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
localStorage.clear();
|
||||||
|
storageManager = new StorageManager({
|
||||||
|
prefix: 'test_',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set and get an item', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default value if item does not exist', () => {
|
||||||
|
const user = storageManager.getItem('nonexistent', {
|
||||||
|
age: 0,
|
||||||
|
name: 'Default User',
|
||||||
|
});
|
||||||
|
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove an item', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.removeItem('user');
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear all items with the prefix', () => {
|
||||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||||
|
storageManager.clear();
|
||||||
|
expect(storageManager.getItem('user1')).toBeNull();
|
||||||
|
expect(storageManager.getItem('user2')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear expired items', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||||
|
vi.advanceTimersByTime(1001); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not clear non-expired items', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||||
|
vi.advanceTimersByTime(5000); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle JSON parse errors gracefully', () => {
|
||||||
|
localStorage.setItem('test_user', '{ invalid JSON }');
|
||||||
|
const user = storageManager.getItem('user', {
|
||||||
|
age: 0,
|
||||||
|
name: 'Default User',
|
||||||
|
});
|
||||||
|
expect(user).toEqual({ age: 0, name: 'Default User' });
|
||||||
|
});
|
||||||
|
it('should return null for non-existent items without default value', () => {
|
||||||
|
const user = storageManager.getItem('nonexistent');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing items', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.setItem('user', { age: 25, name: 'Jane Doe' });
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 25, name: 'Jane Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle items without expiry correctly', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove expired items when accessed', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||||
|
vi.advanceTimersByTime(1001); // 快进时间
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove non-expired items when accessed', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
|
||||||
|
vi.advanceTimersByTime(5000); // 快进时间
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple items with different expiry times', () => {
|
||||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
|
||||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
|
||||||
|
vi.advanceTimersByTime(1500); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user1 = storageManager.getItem('user1');
|
||||||
|
const user2 = storageManager.getItem('user2');
|
||||||
|
expect(user1).toBeNull();
|
||||||
|
expect(user2).toEqual({ age: 25, name: 'Jane Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle items with no expiry', () => {
|
||||||
|
storageManager.setItem('user', { age: 30, name: 'John Doe' });
|
||||||
|
vi.advanceTimersByTime(10_000); // 快进时间
|
||||||
|
storageManager.clearExpiredItems();
|
||||||
|
const user = storageManager.getItem('user');
|
||||||
|
expect(user).toEqual({ age: 30, name: 'John Doe' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear all items correctly', () => {
|
||||||
|
storageManager.setItem('user1', { age: 30, name: 'John Doe' });
|
||||||
|
storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
|
||||||
|
storageManager.clear();
|
||||||
|
const user1 = storageManager.getItem('user1');
|
||||||
|
const user2 = storageManager.getItem('user2');
|
||||||
|
expect(user1).toBeNull();
|
||||||
|
expect(user2).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
1
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/index.ts
vendored
Normal file
1
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './storage-manager';
|
||||||
118
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/storage-manager.ts
vendored
Normal file
118
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/storage-manager.ts
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
type StorageType = 'localStorage' | 'sessionStorage';
|
||||||
|
|
||||||
|
interface StorageManagerOptions {
|
||||||
|
prefix?: string;
|
||||||
|
storageType?: StorageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StorageItem<T> {
|
||||||
|
expiry?: number;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StorageManager {
|
||||||
|
private prefix: string;
|
||||||
|
private storage: Storage;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
prefix = '',
|
||||||
|
storageType = 'localStorage',
|
||||||
|
}: StorageManagerOptions = {}) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.storage =
|
||||||
|
storageType === 'localStorage'
|
||||||
|
? window.localStorage
|
||||||
|
: window.sessionStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整的存储键
|
||||||
|
* @param key 原始键
|
||||||
|
* @returns 带前缀的完整键
|
||||||
|
*/
|
||||||
|
private getFullKey(key: string): string {
|
||||||
|
return `${this.prefix}-${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有带前缀的存储项
|
||||||
|
*/
|
||||||
|
clear(): void {
|
||||||
|
const keysToRemove: string[] = [];
|
||||||
|
for (let i = 0; i < this.storage.length; i++) {
|
||||||
|
const key = this.storage.key(i);
|
||||||
|
if (key && key.startsWith(this.prefix)) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToRemove.forEach((key) => this.storage.removeItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有过期的存储项
|
||||||
|
*/
|
||||||
|
clearExpiredItems(): void {
|
||||||
|
for (let i = 0; i < this.storage.length; i++) {
|
||||||
|
const key = this.storage.key(i);
|
||||||
|
if (key && key.startsWith(this.prefix)) {
|
||||||
|
const shortKey = key.replace(this.prefix, '');
|
||||||
|
this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取存储项
|
||||||
|
* @param key 键
|
||||||
|
* @param defaultValue 当项不存在或已过期时返回的默认值
|
||||||
|
* @returns 值,如果项已过期或解析错误则返回默认值
|
||||||
|
*/
|
||||||
|
getItem<T>(key: string, defaultValue: null | T = null): null | T {
|
||||||
|
const fullKey = this.getFullKey(key);
|
||||||
|
const itemStr = this.storage.getItem(fullKey);
|
||||||
|
if (!itemStr) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item: StorageItem<T> = JSON.parse(itemStr);
|
||||||
|
if (item.expiry && Date.now() > item.expiry) {
|
||||||
|
this.storage.removeItem(fullKey);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return item.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error parsing item with key "${fullKey}":`, error);
|
||||||
|
this.storage.removeItem(fullKey); // 如果解析失败,删除该项
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除存储项
|
||||||
|
* @param key 键
|
||||||
|
*/
|
||||||
|
removeItem(key: string): void {
|
||||||
|
const fullKey = this.getFullKey(key);
|
||||||
|
this.storage.removeItem(fullKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置存储项
|
||||||
|
* @param key 键
|
||||||
|
* @param value 值
|
||||||
|
* @param ttl 存活时间(毫秒)
|
||||||
|
*/
|
||||||
|
setItem<T>(key: string, value: T, ttl?: number): void {
|
||||||
|
const fullKey = this.getFullKey(key);
|
||||||
|
const expiry = ttl ? Date.now() + ttl : undefined;
|
||||||
|
const item: StorageItem<T> = { expiry, value };
|
||||||
|
try {
|
||||||
|
this.storage.setItem(fullKey, JSON.stringify(item));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error setting item with key "${fullKey}":`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StorageManager };
|
||||||
17
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/types.ts
vendored
Normal file
17
Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/types.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
type StorageType = 'localStorage' | 'sessionStorage';
|
||||||
|
|
||||||
|
interface StorageValue<T> {
|
||||||
|
data: T;
|
||||||
|
expiry: null | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IStorageCache {
|
||||||
|
clear(): void;
|
||||||
|
getItem<T>(key: string): null | T;
|
||||||
|
key(index: number): null | string;
|
||||||
|
length(): number;
|
||||||
|
removeItem(key: string): void;
|
||||||
|
setItem<T>(key: string, value: T, expiryInMinutes?: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { IStorageCache, StorageType, StorageValue };
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
convertToHsl,
|
||||||
|
convertToHslCssVar,
|
||||||
|
convertToRgb,
|
||||||
|
isValidColor,
|
||||||
|
} from '../convert';
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly convert color to RGB CSS variable format', () => {
|
||||||
|
const color = 'hsl(284, 100%, 50%)';
|
||||||
|
const expectedRgb = 'rgb(187, 0, 255)';
|
||||||
|
expect(convertToRgb(color)).toEqual(expectedRgb);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly convert color with alpha to RGBA CSS variable format', () => {
|
||||||
|
const color = 'hsla(284, 100%, 50%, 0.92)';
|
||||||
|
const expectedRgba = 'rgba(187, 0, 255, 0.92)';
|
||||||
|
expect(convertToRgb(color)).toEqual(expectedRgba);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { TinyColor } from '@ctrl/tinycolor';
|
||||||
|
|
||||||
|
export function isDarkColor(color: string) {
|
||||||
|
return new TinyColor(color).isDark();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLightColor(color: string) {
|
||||||
|
return new TinyColor(color).isLight();
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { TinyColor } from '@ctrl/tinycolor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将颜色转换为HSL格式。
|
||||||
|
*
|
||||||
|
* HSL是一种颜色模型,包括色相(Hue)、饱和度(Saturation)和亮度(Lightness)三个部分。
|
||||||
|
*
|
||||||
|
* @param {string} color 输入的颜色。
|
||||||
|
* @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 输入的颜色。
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将颜色转换为RGB颜色字符串
|
||||||
|
* TinyColor无法处理hsl内包含'deg'、'grad'、'rad'或'turn'的字符串
|
||||||
|
* 比如 hsl(231deg 98% 65%)将被解析为rgb(0, 0, 0)
|
||||||
|
* 这里在转换之前先将这些单位去掉
|
||||||
|
* @param str 表示HLS颜色值的字符串
|
||||||
|
* @returns 如果颜色值有效,则返回对应的RGB颜色字符串;如果无效,则返回rgb(0, 0, 0)
|
||||||
|
*/
|
||||||
|
function convertToRgb(str: string): string {
|
||||||
|
return new TinyColor(str.replaceAll(/deg|grad|rad|turn/g, '')).toRgbString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查颜色是否有效
|
||||||
|
* @param {string} color - 待检查的颜色
|
||||||
|
* 如果颜色有效返回true,否则返回false
|
||||||
|
*/
|
||||||
|
function isValidColor(color?: string) {
|
||||||
|
if (!color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return new TinyColor(color).isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
convertToHsl,
|
||||||
|
convertToHslCssVar,
|
||||||
|
convertToRgb,
|
||||||
|
isValidColor,
|
||||||
|
TinyColor,
|
||||||
|
};
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { getColors } from 'theme-colors';
|
||||||
|
|
||||||
|
import { convertToHslCssVar, TinyColor } from './convert';
|
||||||
|
|
||||||
|
interface ColorItem {
|
||||||
|
alias?: string;
|
||||||
|
color: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatorColorVariables(colorItems: ColorItem[]) {
|
||||||
|
const colorVariables: Record<string, string> = {};
|
||||||
|
|
||||||
|
colorItems.forEach(({ alias, color, name }) => {
|
||||||
|
if (color) {
|
||||||
|
const colorsMap = getColors(new TinyColor(color).toHexString());
|
||||||
|
|
||||||
|
let mainColor = colorsMap['500'];
|
||||||
|
|
||||||
|
const colorKeys = Object.keys(colorsMap);
|
||||||
|
|
||||||
|
colorKeys.forEach((key) => {
|
||||||
|
const colorValue = colorsMap[key];
|
||||||
|
|
||||||
|
if (colorValue) {
|
||||||
|
const hslColor = convertToHslCssVar(colorValue);
|
||||||
|
colorVariables[`--${name}-${key}`] = hslColor;
|
||||||
|
if (alias) {
|
||||||
|
colorVariables[`--${alias}-${key}`] = hslColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === '500') {
|
||||||
|
mainColor = hslColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (alias && mainColor) {
|
||||||
|
colorVariables[`--${alias}`] = mainColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return colorVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { generatorColorVariables };
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './color';
|
||||||
|
export * from './convert';
|
||||||
|
export * from './generator';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const DictEnum = {
|
||||||
|
SYS_COMMON_STATUS: 'sys_common_status',
|
||||||
|
SYS_DEVICE_TYPE: 'sys_device_type', // 设备类型
|
||||||
|
SYS_GRANT_TYPE: 'sys_grant_type', // 授权类型
|
||||||
|
SYS_NORMAL_DISABLE: 'sys_normal_disable',
|
||||||
|
SYS_NOTICE_STATUS: 'sys_notice_status', // 通知状态
|
||||||
|
SYS_NOTICE_TYPE: 'sys_notice_type_vben5', // 通知类型
|
||||||
|
SYS_OPER_TYPE: 'sys_oper_type', // 操作类型
|
||||||
|
SYS_OSS_ACCESS_POLICY: 'oss_access_policy', // oss权限桶类型
|
||||||
|
SYS_SHOW_HIDE: 'sys_show_hide', // 显示状态
|
||||||
|
SYS_USER_SEX: 'sys_user_sex', // 性别
|
||||||
|
SYS_YES_NO: 'sys_yes_no', // 是否
|
||||||
|
WF_BUSINESS_STATUS: 'wf_business_status', // 业务状态
|
||||||
|
WF_FORM_TYPE: 'wf_form_type', // 表单类型
|
||||||
|
WF_TASK_STATUS: 'wf_task_status', // 任务状态
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DictEnumKey = keyof typeof DictEnum;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/** layout content 组件的高度 */
|
||||||
|
export const CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT = `--vben-content-height`;
|
||||||
|
/** layout content 组件的宽度 */
|
||||||
|
export const CSS_VARIABLE_LAYOUT_CONTENT_WIDTH = `--vben-content-width`;
|
||||||
|
/** layout header 组件的高度 */
|
||||||
|
export const CSS_VARIABLE_LAYOUT_HEADER_HEIGHT = `--vben-header-height`;
|
||||||
|
/** layout footer 组件的高度 */
|
||||||
|
export const CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT = `--vben-footer-height`;
|
||||||
|
|
||||||
|
/** 内容区域的组件ID */
|
||||||
|
export const ELEMENT_ID_MAIN_CONTENT = `__vben_main_content`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 默认命名空间
|
||||||
|
*/
|
||||||
|
export const DEFAULT_NAMESPACE = 'vben';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './dict-enum';
|
||||||
|
export * from './globals';
|
||||||
|
export * from './vben';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* @zh_CN GITHUB 仓库地址
|
||||||
|
*/
|
||||||
|
export const VBEN_GITHUB_URL = 'https://github.com/vbenjs/vue-vben-admin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 文档地址
|
||||||
|
*/
|
||||||
|
export const VBEN_DOC_URL = 'https://doc.vben.pro';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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';
|
||||||
45
Yi.Vben5.Vue3/packages/@core/base/shared/src/global-state.ts
Normal file
45
Yi.Vben5.Vue3/packages/@core/base/shared/src/global-state.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* 全局复用的变量、组件、配置,各个模块之间共享
|
||||||
|
* 通过单例模式实现,单例必须注意不受请求影响,例如用户信息这些需要根据请求获取的。后续如果有ssr需求,也不会影响
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ComponentsState {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageState {
|
||||||
|
copyPreferencesSuccess?: (title: string, content?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGlobalSharedState {
|
||||||
|
components: ComponentsState;
|
||||||
|
message: MessageState;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlobalShareState {
|
||||||
|
#components: ComponentsState = {};
|
||||||
|
#message: MessageState = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义框架内部各个场景的消息提示
|
||||||
|
*/
|
||||||
|
public defineMessage({ copyPreferencesSuccess }: MessageState) {
|
||||||
|
this.#message = {
|
||||||
|
copyPreferencesSuccess,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getComponents(): ComponentsState {
|
||||||
|
return this.#components;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMessage(): MessageState {
|
||||||
|
return this.#message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setComponents(value: ComponentsState) {
|
||||||
|
this.#components = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const globalShareState = new GlobalShareState();
|
||||||
1
Yi.Vben5.Vue3/packages/@core/base/shared/src/store.ts
Normal file
1
Yi.Vben5.Vue3/packages/@core/base/shared/src/store.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from '@tanstack/vue-store';
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { diff } from '../diff';
|
||||||
|
|
||||||
|
describe('diff function', () => {
|
||||||
|
it('should return an empty object when comparing identical objects', () => {
|
||||||
|
const obj1 = { a: 1, b: { c: 2 } };
|
||||||
|
const obj2 = { a: 1, b: { c: 2 } };
|
||||||
|
expect(diff(obj1, obj2)).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect simple changes in primitive values', () => {
|
||||||
|
const obj1 = { a: 1, b: 2 };
|
||||||
|
const obj2 = { a: 1, b: 3 };
|
||||||
|
expect(diff(obj1, obj2)).toEqual({ b: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect nested object changes', () => {
|
||||||
|
const obj1 = { a: 1, b: { c: 2, d: 4 } };
|
||||||
|
const obj2 = { a: 1, b: { c: 3, d: 4 } };
|
||||||
|
expect(diff(obj1, obj2)).toEqual({ b: { c: 3 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle array changes', () => {
|
||||||
|
const obj1 = { a: [1, 2, 3], b: 2 };
|
||||||
|
const obj2 = { a: [1, 2, 4], b: 2 };
|
||||||
|
expect(diff(obj1, obj2)).toEqual({ a: [1, 2, 4] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle added keys', () => {
|
||||||
|
const obj1 = { a: 1 };
|
||||||
|
const obj2 = { a: 1, b: 2 };
|
||||||
|
expect(diff(obj1, obj2)).toEqual({ b: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removed keys', () => {
|
||||||
|
const obj1 = { a: 1, b: 2 };
|
||||||
|
const obj2 = { a: 1 };
|
||||||
|
expect(diff(obj1, obj2)).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle boolean value changes', () => {
|
||||||
|
const obj1 = { a: true, b: false };
|
||||||
|
const obj2 = { a: true, b: true };
|
||||||
|
expect(diff(obj1, obj2)).toEqual({ b: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null and undefined values', () => {
|
||||||
|
const obj1 = { a: null, b: undefined };
|
||||||
|
const obj2: any = { a: 1, b: undefined };
|
||||||
|
expect(diff(obj1, obj2)).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { getElementVisibleRect } from '../dom';
|
||||||
|
|
||||||
|
describe('getElementVisibleRect', () => {
|
||||||
|
// 设置浏览器视口尺寸的 mock
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(document.documentElement, 'clientHeight', 'get').mockReturnValue(
|
||||||
|
800,
|
||||||
|
);
|
||||||
|
vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800);
|
||||||
|
vi.spyOn(document.documentElement, 'clientWidth', 'get').mockReturnValue(
|
||||||
|
1000,
|
||||||
|
);
|
||||||
|
vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default rect if element is undefined', () => {
|
||||||
|
expect(getElementVisibleRect()).toEqual({
|
||||||
|
bottom: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return default rect if element is null', () => {
|
||||||
|
expect(getElementVisibleRect(null)).toEqual({
|
||||||
|
bottom: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct visible rect when element is fully visible', () => {
|
||||||
|
const element = {
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
bottom: 400,
|
||||||
|
height: 300,
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
width: 400,
|
||||||
|
}),
|
||||||
|
} as HTMLElement;
|
||||||
|
|
||||||
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 400,
|
||||||
|
height: 300,
|
||||||
|
left: 200,
|
||||||
|
right: 600,
|
||||||
|
top: 100,
|
||||||
|
width: 400,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct visible rect when element is partially off-screen at the top', () => {
|
||||||
|
const element = {
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
bottom: 200,
|
||||||
|
height: 250,
|
||||||
|
left: 100,
|
||||||
|
right: 500,
|
||||||
|
top: -50,
|
||||||
|
width: 400,
|
||||||
|
}),
|
||||||
|
} as HTMLElement;
|
||||||
|
|
||||||
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 200,
|
||||||
|
height: 200,
|
||||||
|
left: 100,
|
||||||
|
right: 500,
|
||||||
|
top: 0,
|
||||||
|
width: 400,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct visible rect when element is partially off-screen at the right', () => {
|
||||||
|
const element = {
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
bottom: 400,
|
||||||
|
height: 300,
|
||||||
|
left: 800,
|
||||||
|
right: 1200,
|
||||||
|
top: 100,
|
||||||
|
width: 400,
|
||||||
|
}),
|
||||||
|
} as HTMLElement;
|
||||||
|
|
||||||
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 400,
|
||||||
|
height: 300,
|
||||||
|
left: 800,
|
||||||
|
right: 1000,
|
||||||
|
top: 100,
|
||||||
|
width: 200,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all zeros when element is completely off-screen', () => {
|
||||||
|
const element = {
|
||||||
|
getBoundingClientRect: () => ({
|
||||||
|
bottom: 1200,
|
||||||
|
height: 300,
|
||||||
|
left: 1100,
|
||||||
|
right: 1400,
|
||||||
|
top: 900,
|
||||||
|
width: 300,
|
||||||
|
}),
|
||||||
|
} as HTMLElement;
|
||||||
|
|
||||||
|
expect(getElementVisibleRect(element)).toEqual({
|
||||||
|
bottom: 800,
|
||||||
|
height: 0,
|
||||||
|
left: 1100,
|
||||||
|
right: 1000,
|
||||||
|
top: 900,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFirstNonNullOrUndefined,
|
||||||
|
isBoolean,
|
||||||
|
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('isBoolean', () => {
|
||||||
|
it('should return true for boolean values', () => {
|
||||||
|
expect(isBoolean(true)).toBe(true);
|
||||||
|
expect(isBoolean(false)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for non-boolean values', () => {
|
||||||
|
expect(isBoolean(null)).toBe(false);
|
||||||
|
expect(isBoolean(42)).toBe(false);
|
||||||
|
expect(isBoolean('string')).toBe(false);
|
||||||
|
expect(isBoolean({})).toBe(false);
|
||||||
|
expect(isBoolean([])).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(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getFirstNonNullOrUndefined', () => {
|
||||||
|
describe('getFirstNonNullOrUndefined', () => {
|
||||||
|
it('should return the first non-null and non-undefined value for a number array', () => {
|
||||||
|
expect(getFirstNonNullOrUndefined<number>(undefined, null, 0, 42)).toBe(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
expect(getFirstNonNullOrUndefined<number>(null, undefined, 42, 123)).toBe(
|
||||||
|
42,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the first non-null and non-undefined value for a string array', () => {
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<string>(undefined, null, '', 'hello'),
|
||||||
|
).toBe('');
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<string>(null, undefined, 'test', 'world'),
|
||||||
|
).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if all values are null or undefined', () => {
|
||||||
|
expect(getFirstNonNullOrUndefined(undefined, null)).toBeUndefined();
|
||||||
|
expect(getFirstNonNullOrUndefined(null)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with a single value', () => {
|
||||||
|
expect(getFirstNonNullOrUndefined(42)).toBe(42);
|
||||||
|
expect(getFirstNonNullOrUndefined()).toBeUndefined();
|
||||||
|
expect(getFirstNonNullOrUndefined(null)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed types correctly', () => {
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<number | object | string>(
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
'test',
|
||||||
|
123,
|
||||||
|
{ key: 'value' },
|
||||||
|
),
|
||||||
|
).toBe('test');
|
||||||
|
expect(
|
||||||
|
getFirstNonNullOrUndefined<number | object | string>(
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
[1, 2, 3],
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
kebabToCamelCase,
|
||||||
|
toCamelCase,
|
||||||
|
toLowerCaseFirstLetter,
|
||||||
|
} from '../letter';
|
||||||
|
|
||||||
|
describe('capitalizeFirstLetter', () => {
|
||||||
|
it('should capitalize the first letter of a string', () => {
|
||||||
|
expect(capitalizeFirstLetter('hello')).toBe('Hello');
|
||||||
|
expect(capitalizeFirstLetter('world')).toBe('World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty strings', () => {
|
||||||
|
expect(capitalizeFirstLetter('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single character strings', () => {
|
||||||
|
expect(capitalizeFirstLetter('a')).toBe('A');
|
||||||
|
expect(capitalizeFirstLetter('b')).toBe('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change the case of other characters', () => {
|
||||||
|
expect(capitalizeFirstLetter('hElLo')).toBe('HElLo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toLowerCaseFirstLetter', () => {
|
||||||
|
it('should convert the first letter to lowercase', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('CommonAppName')).toBe('commonAppName');
|
||||||
|
expect(toLowerCaseFirstLetter('AnotherKeyExample')).toBe(
|
||||||
|
'anotherKeyExample',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same string if the first letter is already lowercase', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('alreadyLowerCase')).toBe('alreadyLowerCase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty strings', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single character strings', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('A')).toBe('a');
|
||||||
|
expect(toLowerCaseFirstLetter('a')).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with only one uppercase letter', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('A')).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with special characters', () => {
|
||||||
|
expect(toLowerCaseFirstLetter('!Special')).toBe('!Special');
|
||||||
|
expect(toLowerCaseFirstLetter('123Number')).toBe('123Number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toCamelCase', () => {
|
||||||
|
it('should return the key if parentKey is empty', () => {
|
||||||
|
expect(toCamelCase('child', '')).toBe('child');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should combine parentKey and key in camel case', () => {
|
||||||
|
expect(toCamelCase('child', 'parent')).toBe('parentChild');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty key and parentKey', () => {
|
||||||
|
expect(toCamelCase('', '')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle key with capital letters', () => {
|
||||||
|
expect(toCamelCase('Child', 'parent')).toBe('parentChild');
|
||||||
|
expect(toCamelCase('Child', 'Parent')).toBe('ParentChild');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('kebabToCamelCase', () => {
|
||||||
|
it('should convert kebab-case to camelCase correctly', () => {
|
||||||
|
expect(kebabToCamelCase('my-component-name')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple consecutive hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('my--component--name')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim leading and trailing hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('-my-component-name-')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve the case of the first word', () => {
|
||||||
|
expect(kebabToCamelCase('My-component-name')).toBe('MyComponentName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert a single word correctly', () => {
|
||||||
|
expect(kebabToCamelCase('component')).toBe('component');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty string if input is empty', () => {
|
||||||
|
expect(kebabToCamelCase('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with no hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('mycomponentname')).toBe('mycomponentname');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle strings with only hyphens', () => {
|
||||||
|
expect(kebabToCamelCase('---')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed case inputs', () => {
|
||||||
|
expect(kebabToCamelCase('my-Component-Name')).toBe('myComponentName');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { StateHandler } from '../state-handler';
|
||||||
|
|
||||||
|
describe('stateHandler', () => {
|
||||||
|
it('should resolve when condition is set to true', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
|
||||||
|
// 模拟异步设置 condition 为 true
|
||||||
|
setTimeout(() => {
|
||||||
|
handler.setConditionTrue(); // 明确触发 condition 为 true
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 等待条件被设置为 true
|
||||||
|
await handler.waitForCondition();
|
||||||
|
expect(handler.isConditionTrue()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve immediately if condition is already true', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
handler.setConditionTrue(); // 提前设置为 true
|
||||||
|
|
||||||
|
// 立即 resolve,因为 condition 已经是 true
|
||||||
|
await handler.waitForCondition();
|
||||||
|
expect(handler.isConditionTrue()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject when condition is set to false after waiting', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
|
||||||
|
// 模拟异步设置 condition 为 false
|
||||||
|
setTimeout(() => {
|
||||||
|
handler.setConditionFalse(); // 明确触发 condition 为 false
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
// 等待过程中,期望 Promise 被 reject
|
||||||
|
await expect(handler.waitForCondition()).rejects.toThrow();
|
||||||
|
expect(handler.isConditionTrue()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset condition to false', () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
handler.setConditionTrue(); // 设置为 true
|
||||||
|
handler.reset(); // 重置为 false
|
||||||
|
|
||||||
|
expect(handler.isConditionTrue()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve when condition is set to true after reset', async () => {
|
||||||
|
const handler = new StateHandler();
|
||||||
|
handler.reset(); // 确保初始为 false
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
handler.setConditionTrue(); // 重置后设置为 true
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
await handler.waitForCondition();
|
||||||
|
expect(handler.isConditionTrue()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { uniqueByField } from '../unique';
|
||||||
|
|
||||||
|
describe('uniqueByField', () => {
|
||||||
|
it('should return an array with unique items based on id field', () => {
|
||||||
|
const items = [
|
||||||
|
{ id: 1, name: 'Item 1' },
|
||||||
|
{ id: 2, name: 'Item 2' },
|
||||||
|
{ id: 3, name: 'Item 3' },
|
||||||
|
{ id: 1, name: 'Duplicate Item' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const uniqueItems = uniqueByField(items, 'id');
|
||||||
|
|
||||||
|
expect(uniqueItems).toHaveLength(3);
|
||||||
|
expect(uniqueItems).toEqual([
|
||||||
|
{ id: 1, name: 'Item 1' },
|
||||||
|
{ id: 2, name: 'Item 2' },
|
||||||
|
{ id: 3, name: 'Item 3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array when input array is empty', () => {
|
||||||
|
const items: any[] = []; // Empty array
|
||||||
|
|
||||||
|
const uniqueItems = uniqueByField(items, 'id');
|
||||||
|
|
||||||
|
// Assert expected results
|
||||||
|
expect(uniqueItems).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle arrays with only one item correctly', () => {
|
||||||
|
const items = [{ id: 1, name: 'Item 1' }];
|
||||||
|
|
||||||
|
const uniqueItems = uniqueByField(items, 'id');
|
||||||
|
|
||||||
|
// Assert expected results
|
||||||
|
expect(uniqueItems).toHaveLength(1);
|
||||||
|
expect(uniqueItems).toEqual([{ id: 1, name: 'Item 1' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve the order of the first occurrence of each item', () => {
|
||||||
|
const items = [
|
||||||
|
{ id: 2, name: 'Item 2' },
|
||||||
|
{ id: 1, name: 'Item 1' },
|
||||||
|
{ id: 3, name: 'Item 3' },
|
||||||
|
{ id: 1, name: 'Duplicate Item' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const uniqueItems = uniqueByField(items, 'id');
|
||||||
|
|
||||||
|
// Assert expected results (order of first occurrences preserved)
|
||||||
|
expect(uniqueItems).toEqual([
|
||||||
|
{ id: 2, name: 'Item 2' },
|
||||||
|
{ id: 1, name: 'Item 1' },
|
||||||
|
{ id: 3, name: 'Item 3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { updateCSSVariables } from '../update-css-variables';
|
||||||
|
|
||||||
|
it('updateCSSVariables should update CSS variables in :root selector', () => {
|
||||||
|
// 模拟初始的内联样式表内容
|
||||||
|
const initialStyleContent = ':root { --primaryColor: red; }';
|
||||||
|
document.head.innerHTML = `<style id="custom-styles">${initialStyleContent}</style>`;
|
||||||
|
|
||||||
|
// 要更新的CSS变量和它们的新值
|
||||||
|
const updatedVariables = {
|
||||||
|
fontSize: '16px',
|
||||||
|
primaryColor: 'blue',
|
||||||
|
secondaryColor: 'green',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用函数来更新CSS变量
|
||||||
|
updateCSSVariables(updatedVariables, 'custom-styles');
|
||||||
|
|
||||||
|
// 获取更新后的样式内容
|
||||||
|
const styleElement = document.querySelector('#custom-styles');
|
||||||
|
const updatedStyleContent = styleElement ? styleElement.textContent : '';
|
||||||
|
|
||||||
|
// 检查更新后的样式内容是否包含正确的更新值
|
||||||
|
expect(
|
||||||
|
updatedStyleContent?.includes('primaryColor: blue;') &&
|
||||||
|
updatedStyleContent?.includes('secondaryColor: green;') &&
|
||||||
|
updatedStyleContent?.includes('fontSize: 16px;'),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { bindMethods, getNestedValue } from '../util';
|
||||||
|
|
||||||
|
class TestClass {
|
||||||
|
public value: string;
|
||||||
|
|
||||||
|
constructor(value: string) {
|
||||||
|
this.value = value;
|
||||||
|
bindMethods(this); // 调用通用方法
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(newValue: string) {
|
||||||
|
this.value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('bindMethods', () => {
|
||||||
|
it('should bind methods to the instance correctly', () => {
|
||||||
|
const instance = new TestClass('initial');
|
||||||
|
|
||||||
|
// 解构方法
|
||||||
|
const { getValue } = instance;
|
||||||
|
|
||||||
|
// 检查 getValue 是否能正确调用,并且 this 绑定了 instance
|
||||||
|
expect(getValue()).toBe('initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bind multiple methods', () => {
|
||||||
|
const instance = new TestClass('initial');
|
||||||
|
|
||||||
|
const { getValue, setValue } = instance;
|
||||||
|
|
||||||
|
// 检查 getValue 和 setValue 方法是否正确绑定了 this
|
||||||
|
setValue('newValue');
|
||||||
|
expect(getValue()).toBe('newValue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not bind non-function properties', () => {
|
||||||
|
const instance = new TestClass('initial');
|
||||||
|
|
||||||
|
// 检查普通属性是否保持原样
|
||||||
|
expect(instance.value).toBe('initial');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not bind constructor method', () => {
|
||||||
|
const instance = new TestClass('test');
|
||||||
|
|
||||||
|
// 检查 constructor 是否没有被绑定
|
||||||
|
expect(instance.constructor.name).toBe('TestClass');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not bind getter/setter properties', () => {
|
||||||
|
class TestWithGetterSetter {
|
||||||
|
private _value: string = 'test';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
bindMethods(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(newValue: string) {
|
||||||
|
this._value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new TestWithGetterSetter();
|
||||||
|
const { value } = instance;
|
||||||
|
|
||||||
|
// Getter 和 setter 不应被绑定
|
||||||
|
expect(value).toBe('test');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getNestedValue', () => {
|
||||||
|
interface UserProfile {
|
||||||
|
age: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserSettings {
|
||||||
|
theme: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
user: {
|
||||||
|
profile: UserProfile;
|
||||||
|
settings: UserSettings;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Data = {
|
||||||
|
user: {
|
||||||
|
profile: {
|
||||||
|
age: 25,
|
||||||
|
name: 'Alice',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
theme: 'dark',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should get a nested value when the path is valid', () => {
|
||||||
|
const result = getNestedValue(data, 'user.profile.name');
|
||||||
|
expect(result).toBe('Alice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for non-existent property', () => {
|
||||||
|
const result = getNestedValue(data, 'user.profile.gender');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when accessing a non-existent deep path', () => {
|
||||||
|
const result = getNestedValue(data, 'user.nonexistent.field');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if a middle level is undefined', () => {
|
||||||
|
const result = getNestedValue({ user: undefined }, 'user.profile.name');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct value for a nested setting', () => {
|
||||||
|
const result = getNestedValue(data, 'user.settings.theme');
|
||||||
|
expect(result).toBe('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work for a single-level path', () => {
|
||||||
|
const result = getNestedValue({ a: 1, b: 2 }, 'b');
|
||||||
|
expect(result).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the entire object if path is empty', () => {
|
||||||
|
expect(() => getNestedValue(data, '')()).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle paths with array indexes', () => {
|
||||||
|
const complexData = { list: [{ name: 'Item1' }, { name: 'Item2' }] };
|
||||||
|
const result = getNestedValue(complexData, 'list.1.name');
|
||||||
|
expect(result).toBe('Item2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when accessing an out-of-bounds array index', () => {
|
||||||
|
const complexData = { list: [{ name: 'Item1' }] };
|
||||||
|
const result = getNestedValue(complexData, 'list.2.name');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { openWindow } from '../window';
|
||||||
|
|
||||||
|
describe('openWindow', () => {
|
||||||
|
// 保存原始的 window.open 函数
|
||||||
|
let originalOpen: typeof window.open;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalOpen = window.open;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
window.open = originalOpen;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call window.open with correct arguments', () => {
|
||||||
|
const url = 'https://example.com';
|
||||||
|
const options = { noopener: true, noreferrer: true, target: '_blank' };
|
||||||
|
|
||||||
|
window.open = vi.fn();
|
||||||
|
|
||||||
|
// 调用函数
|
||||||
|
openWindow(url, options);
|
||||||
|
|
||||||
|
// 验证 window.open 是否被正确地调用
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
url,
|
||||||
|
options.target,
|
||||||
|
'noopener=yes,noreferrer=yes',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
10
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/cn.ts
Normal file
10
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/cn.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { ClassValue } from 'clsx';
|
||||||
|
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export { cn };
|
||||||
26
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/date.ts
Normal file
26
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/date.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export function formatDate(time: number | string, format = 'YYYY-MM-DD') {
|
||||||
|
try {
|
||||||
|
const date = dayjs(time);
|
||||||
|
if (!date.isValid()) {
|
||||||
|
throw new Error('Invalid date');
|
||||||
|
}
|
||||||
|
return date.format(format);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error formatting date: ${error}`);
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(time: number | string) {
|
||||||
|
return formatDate(time, '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);
|
||||||
|
}
|
||||||
96
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/diff.ts
Normal file
96
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/diff.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
type DiffResult<T> = Partial<{
|
||||||
|
[K in keyof T]: T[K] extends object ? DiffResult<T[K]> : T[K];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function diff<T extends Record<string, any>>(obj1: T, obj2: T): DiffResult<T> {
|
||||||
|
function findDifferences(o1: any, o2: any): any {
|
||||||
|
if (Array.isArray(o1) && Array.isArray(o2)) {
|
||||||
|
if (!arraysEqual(o1, o2)) {
|
||||||
|
return o2;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof o1 === 'object' &&
|
||||||
|
typeof o2 === 'object' &&
|
||||||
|
o1 !== null &&
|
||||||
|
o2 !== null
|
||||||
|
) {
|
||||||
|
const diffResult: any = {};
|
||||||
|
|
||||||
|
const keys = new Set([...Object.keys(o1), ...Object.keys(o2)]);
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const valueDiff = findDifferences(o1[key], o2[key]);
|
||||||
|
if (valueDiff !== undefined) {
|
||||||
|
diffResult[key] = valueDiff;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(diffResult).length > 0 ? diffResult : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return o1 === o2 ? undefined : o2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return findDifferences(obj1, obj2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { arraysEqual, diff };
|
||||||
95
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/dom.ts
Normal file
95
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/dom.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export interface VisibleDomRect {
|
||||||
|
bottom: number;
|
||||||
|
height: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元素可见信息
|
||||||
|
* @param element
|
||||||
|
*/
|
||||||
|
export function getElementVisibleRect(
|
||||||
|
element?: HTMLElement | null | undefined,
|
||||||
|
): VisibleDomRect {
|
||||||
|
if (!element) {
|
||||||
|
return {
|
||||||
|
bottom: 0,
|
||||||
|
height: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const viewHeight = Math.max(
|
||||||
|
document.documentElement.clientHeight,
|
||||||
|
window.innerHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
const top = Math.max(rect.top, 0);
|
||||||
|
const bottom = Math.min(rect.bottom, viewHeight);
|
||||||
|
|
||||||
|
const viewWidth = Math.max(
|
||||||
|
document.documentElement.clientWidth,
|
||||||
|
window.innerWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
const left = Math.max(rect.left, 0);
|
||||||
|
const right = Math.min(rect.right, viewWidth);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bottom,
|
||||||
|
height: Math.max(0, bottom - top),
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
top,
|
||||||
|
width: Math.max(0, right - left),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScrollbarWidth() {
|
||||||
|
const scrollDiv = document.createElement('div');
|
||||||
|
|
||||||
|
scrollDiv.style.visibility = 'hidden';
|
||||||
|
scrollDiv.style.overflow = 'scroll';
|
||||||
|
scrollDiv.style.position = 'absolute';
|
||||||
|
scrollDiv.style.top = '-9999px';
|
||||||
|
|
||||||
|
document.body.append(scrollDiv);
|
||||||
|
|
||||||
|
const innerDiv = document.createElement('div');
|
||||||
|
scrollDiv.append(innerDiv);
|
||||||
|
|
||||||
|
const scrollbarWidth = scrollDiv.offsetWidth - innerDiv.offsetWidth;
|
||||||
|
|
||||||
|
scrollDiv.remove();
|
||||||
|
return scrollbarWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function needsScrollbar() {
|
||||||
|
const doc = document.documentElement;
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
// 检查 body 的 overflow-y 样式
|
||||||
|
const overflowY = window.getComputedStyle(body).overflowY;
|
||||||
|
|
||||||
|
// 如果明确设置了需要滚动条的样式
|
||||||
|
if (overflowY === 'scroll' || overflowY === 'auto') {
|
||||||
|
return doc.scrollHeight > window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在其他情况下,根据 scrollHeight 和 innerHeight 比较判断
|
||||||
|
return doc.scrollHeight > window.innerHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function triggerWindowResize(): void {
|
||||||
|
// 创建一个新的 resize 事件
|
||||||
|
const resizeEvent = new Event('resize');
|
||||||
|
|
||||||
|
// 触发 window 的 resize 事件
|
||||||
|
window.dispatchEvent(resizeEvent);
|
||||||
|
}
|
||||||
157
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/download.ts
Normal file
157
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/download.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { openWindow } from './window';
|
||||||
|
|
||||||
|
interface DownloadOptions<T = string> {
|
||||||
|
fileName?: string;
|
||||||
|
source: T;
|
||||||
|
target?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_FILENAME = 'downloaded_file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 URL 下载文件,支持跨域
|
||||||
|
* @throws {Error} - 当下载失败时抛出错误
|
||||||
|
*/
|
||||||
|
export async function downloadFileFromUrl({
|
||||||
|
fileName,
|
||||||
|
source,
|
||||||
|
target = '_blank',
|
||||||
|
}: DownloadOptions): Promise<void> {
|
||||||
|
if (!source || typeof source !== 'string') {
|
||||||
|
throw new Error('Invalid URL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome');
|
||||||
|
const isSafari = window.navigator.userAgent.toLowerCase().includes('safari');
|
||||||
|
|
||||||
|
if (/iP/.test(window.navigator.userAgent)) {
|
||||||
|
console.error('Your browser does not support download!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChrome || isSafari) {
|
||||||
|
triggerDownload(source, resolveFileName(source, fileName));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!source.includes('?')) {
|
||||||
|
source += '?download';
|
||||||
|
}
|
||||||
|
|
||||||
|
openWindow(source, { target });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Base64 下载文件
|
||||||
|
*/
|
||||||
|
export function downloadFileFromBase64({ fileName, source }: DownloadOptions) {
|
||||||
|
if (!source || typeof source !== 'string') {
|
||||||
|
throw new Error('Invalid Base64 data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedFileName = fileName || DEFAULT_FILENAME;
|
||||||
|
triggerDownload(source, resolvedFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过图片 URL 下载图片文件
|
||||||
|
*/
|
||||||
|
export async function downloadFileFromImageUrl({
|
||||||
|
fileName,
|
||||||
|
source,
|
||||||
|
}: DownloadOptions) {
|
||||||
|
const base64 = await urlToBase64(source);
|
||||||
|
downloadFileFromBase64({ fileName, source: base64 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Blob 下载文件
|
||||||
|
*/
|
||||||
|
export function downloadFileFromBlob({
|
||||||
|
fileName = DEFAULT_FILENAME,
|
||||||
|
source,
|
||||||
|
}: DownloadOptions<Blob>): void {
|
||||||
|
if (!(source instanceof Blob)) {
|
||||||
|
throw new TypeError('Invalid Blob data.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(source);
|
||||||
|
triggerDownload(url, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件,支持 Blob、字符串和其他 BlobPart 类型
|
||||||
|
*/
|
||||||
|
export function downloadFileFromBlobPart({
|
||||||
|
fileName = DEFAULT_FILENAME,
|
||||||
|
source,
|
||||||
|
}: DownloadOptions<BlobPart>): void {
|
||||||
|
// 如果 data 不是 Blob,则转换为 Blob
|
||||||
|
const blob =
|
||||||
|
source instanceof Blob
|
||||||
|
? source
|
||||||
|
: new Blob([source], { type: 'application/octet-stream' });
|
||||||
|
|
||||||
|
// 创建对象 URL 并触发下载
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
triggerDownload(url, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* img url to base64
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
export function urlToBase64(url: string, mineType?: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null;
|
||||||
|
const ctx = canvas?.getContext('2d');
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = '';
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
if (!canvas || !ctx) {
|
||||||
|
return reject(new Error('Failed to create canvas.'));
|
||||||
|
}
|
||||||
|
canvas.height = img.height;
|
||||||
|
canvas.width = img.width;
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
const dataURL = canvas.toDataURL(mineType || 'image/png');
|
||||||
|
canvas = null;
|
||||||
|
resolve(dataURL);
|
||||||
|
});
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用下载触发函数
|
||||||
|
* @param href - 文件下载的 URL
|
||||||
|
* @param fileName - 下载文件的名称,如果未提供则自动识别
|
||||||
|
* @param revokeDelay - 清理 URL 的延迟时间 (毫秒)
|
||||||
|
*/
|
||||||
|
export function triggerDownload(
|
||||||
|
href: string,
|
||||||
|
fileName: string | undefined,
|
||||||
|
revokeDelay: number = 100,
|
||||||
|
): void {
|
||||||
|
const defaultFileName = 'downloaded_file';
|
||||||
|
const finalFileName = fileName || defaultFileName;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = href;
|
||||||
|
link.download = finalFileName;
|
||||||
|
link.style.display = 'none';
|
||||||
|
|
||||||
|
if (link.download === undefined) {
|
||||||
|
link.setAttribute('target', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.append(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
// 清理临时 URL 以释放内存
|
||||||
|
setTimeout(() => URL.revokeObjectURL(href), revokeDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFileName(url: string, fileName?: string): string {
|
||||||
|
return fileName || url.slice(url.lastIndexOf('/') + 1) || DEFAULT_FILENAME;
|
||||||
|
}
|
||||||
20
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/index.ts
Normal file
20
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/index.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export * from './cn';
|
||||||
|
export * from './date';
|
||||||
|
export * from './diff';
|
||||||
|
export * from './dom';
|
||||||
|
export * from './download';
|
||||||
|
export * from './inference';
|
||||||
|
export * from './letter';
|
||||||
|
export * from './merge';
|
||||||
|
export * from './nprogress';
|
||||||
|
export * from './state-handler';
|
||||||
|
export * from './to';
|
||||||
|
export * from './tree';
|
||||||
|
export * from './unique';
|
||||||
|
export * from './update-css-variables';
|
||||||
|
export * from './util';
|
||||||
|
export * from './window';
|
||||||
|
export { default as cloneDeep } from 'lodash.clonedeep';
|
||||||
|
export { default as get } from 'lodash.get';
|
||||||
|
export { default as isEqual } from 'lodash.isequal';
|
||||||
|
export { default as set } from 'lodash.set';
|
||||||
165
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/inference.ts
Normal file
165
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/inference.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// eslint-disable-next-line vue/prefer-import-from-vue
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查传入的值是否为boolean
|
||||||
|
* @param value
|
||||||
|
* @returns 如果值是布尔值,返回true,否则返回false。
|
||||||
|
*/
|
||||||
|
function isBoolean(value: unknown): value is boolean {
|
||||||
|
return typeof value === 'boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查传入的值是否为空。
|
||||||
|
*
|
||||||
|
* 以下情况将被认为是空:
|
||||||
|
* - 值为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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查传入的值是否为数字
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
function isNumber(value: any): value is number {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first value in the provided list that is neither `null` nor `undefined`.
|
||||||
|
*
|
||||||
|
* This function iterates over the input values and returns the first one that is
|
||||||
|
* not strictly equal to `null` or `undefined`. If all values are either `null` or
|
||||||
|
* `undefined`, it returns `undefined`.
|
||||||
|
*
|
||||||
|
* @template T - The type of the input values.
|
||||||
|
* @param {...(T | null | undefined)[]} values - A list of values to evaluate.
|
||||||
|
* @returns {T | undefined} - The first value that is not `null` or `undefined`, or `undefined` if none are found.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Returns 42 because it is the first non-null, non-undefined value.
|
||||||
|
* getFirstNonNullOrUndefined(undefined, null, 42, 'hello'); // 42
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Returns 'hello' because it is the first non-null, non-undefined value.
|
||||||
|
* getFirstNonNullOrUndefined(null, undefined, 'hello', 123); // 'hello'
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Returns undefined because all values are either null or undefined.
|
||||||
|
* getFirstNonNullOrUndefined(undefined, null); // undefined
|
||||||
|
*/
|
||||||
|
function getFirstNonNullOrUndefined<T>(
|
||||||
|
...values: (null | T | undefined)[]
|
||||||
|
): T | undefined {
|
||||||
|
for (const value of values) {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getFirstNonNullOrUndefined,
|
||||||
|
isBoolean,
|
||||||
|
isEmpty,
|
||||||
|
isFunction,
|
||||||
|
isHttpUrl,
|
||||||
|
isMacOs,
|
||||||
|
isNumber,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
isUndefined,
|
||||||
|
isWindow,
|
||||||
|
isWindowsOs,
|
||||||
|
};
|
||||||
47
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/letter.ts
Normal file
47
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/letter.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 将字符串的首字母大写
|
||||||
|
* @param string
|
||||||
|
*/
|
||||||
|
function capitalizeFirstLetter(string: string): string {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串的首字母转换为小写。
|
||||||
|
*
|
||||||
|
* @param str 要转换的字符串
|
||||||
|
* @returns 首字母小写的字符串
|
||||||
|
*/
|
||||||
|
function toLowerCaseFirstLetter(str: string): string {
|
||||||
|
if (!str) return str; // 如果字符串为空,直接返回
|
||||||
|
return str.charAt(0).toLowerCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成驼峰命名法的键名
|
||||||
|
* @param key
|
||||||
|
* @param parentKey
|
||||||
|
*/
|
||||||
|
function toCamelCase(key: string, parentKey: string): string {
|
||||||
|
if (!parentKey) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return parentKey + key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function kebabToCamelCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.split('-')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((word, index) =>
|
||||||
|
index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1),
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
capitalizeFirstLetter,
|
||||||
|
kebabToCamelCase,
|
||||||
|
toCamelCase,
|
||||||
|
toLowerCaseFirstLetter,
|
||||||
|
};
|
||||||
10
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/merge.ts
Normal file
10
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/merge.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createDefu } from 'defu';
|
||||||
|
|
||||||
|
export { createDefu as createMerge, defu as merge } from 'defu';
|
||||||
|
|
||||||
|
export const mergeWithArrayOverride = createDefu((originObj, key, updates) => {
|
||||||
|
if (Array.isArray(originObj[key]) && Array.isArray(updates)) {
|
||||||
|
originObj[key] = updates;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
export class StateHandler {
|
||||||
|
private condition: boolean = false;
|
||||||
|
private rejectCondition: (() => void) | null = null;
|
||||||
|
private resolveCondition: (() => void) | null = null;
|
||||||
|
|
||||||
|
// 清理 resolve/reject 函数
|
||||||
|
private clearPromises() {
|
||||||
|
this.resolveCondition = null;
|
||||||
|
this.rejectCondition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isConditionTrue(): boolean {
|
||||||
|
return this.condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.condition = false;
|
||||||
|
this.clearPromises();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发状态为 false 时,reject
|
||||||
|
setConditionFalse() {
|
||||||
|
this.condition = false;
|
||||||
|
if (this.rejectCondition) {
|
||||||
|
this.rejectCondition();
|
||||||
|
this.clearPromises();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发状态为 true 时,resolve
|
||||||
|
setConditionTrue() {
|
||||||
|
this.condition = true;
|
||||||
|
if (this.resolveCondition) {
|
||||||
|
this.resolveCondition();
|
||||||
|
this.clearPromises();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回一个 Promise,等待 condition 变为 true
|
||||||
|
waitForCondition(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (this.condition) {
|
||||||
|
resolve(); // 如果 condition 已经为 true,立即 resolve
|
||||||
|
} else {
|
||||||
|
this.resolveCondition = resolve;
|
||||||
|
this.rejectCondition = reject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/to.ts
Normal file
21
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/to.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* @param { Readonly<Promise> } promise
|
||||||
|
* @param {object=} errorExt - Additional Information you can pass to the err object
|
||||||
|
* @return { Promise }
|
||||||
|
*/
|
||||||
|
export async function to<T, U = Error>(
|
||||||
|
promise: Readonly<Promise<T>>,
|
||||||
|
errorExt?: object,
|
||||||
|
): Promise<[null, T] | [U, undefined]> {
|
||||||
|
try {
|
||||||
|
const data = await promise;
|
||||||
|
const result: [null, T] = [null, data];
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (errorExt) {
|
||||||
|
const parsedError = Object.assign({}, error, errorExt);
|
||||||
|
return [parsedError as U, undefined];
|
||||||
|
}
|
||||||
|
return [error as U, undefined];
|
||||||
|
}
|
||||||
|
}
|
||||||
97
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/tree.ts
Normal file
97
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/tree.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
interface TreeConfigOptions {
|
||||||
|
// 子属性的名称,默认为'children'
|
||||||
|
childProps: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN 遍历树形结构,并返回所有节点中指定的值。
|
||||||
|
* @param tree 树形结构数组
|
||||||
|
* @param getValue 获取节点值的函数
|
||||||
|
* @param options 作为子节点数组的可选属性名称。
|
||||||
|
* @returns 所有节点中指定的值的数组
|
||||||
|
*/
|
||||||
|
function traverseTreeValues<T, V>(
|
||||||
|
tree: T[],
|
||||||
|
getValue: (node: T) => V,
|
||||||
|
options?: TreeConfigOptions,
|
||||||
|
): V[] {
|
||||||
|
const result: V[] = [];
|
||||||
|
const { childProps } = options || {
|
||||||
|
childProps: 'children',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dfs = (treeNode: T) => {
|
||||||
|
const value = getValue(treeNode);
|
||||||
|
result.push(value);
|
||||||
|
const children = (treeNode as Record<string, any>)?.[childProps];
|
||||||
|
if (!children) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (children.length > 0) {
|
||||||
|
for (const child of children) {
|
||||||
|
dfs(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const treeNode of tree) {
|
||||||
|
dfs(treeNode);
|
||||||
|
}
|
||||||
|
return result.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件过滤给定树结构的节点,并以原有顺序返回所有匹配节点的数组。
|
||||||
|
* @param tree 要过滤的树结构的根节点数组。
|
||||||
|
* @param filter 用于匹配每个节点的条件。
|
||||||
|
* @param options 作为子节点数组的可选属性名称。
|
||||||
|
* @returns 包含所有匹配节点的数组。
|
||||||
|
*/
|
||||||
|
function filterTree<T extends Record<string, any>>(
|
||||||
|
tree: T[],
|
||||||
|
filter: (node: T) => boolean,
|
||||||
|
options?: TreeConfigOptions,
|
||||||
|
): T[] {
|
||||||
|
const { childProps } = options || {
|
||||||
|
childProps: 'children',
|
||||||
|
};
|
||||||
|
|
||||||
|
const _filterTree = (nodes: T[]): T[] => {
|
||||||
|
return nodes.filter((node: Record<string, any>) => {
|
||||||
|
if (filter(node as T)) {
|
||||||
|
if (node[childProps]) {
|
||||||
|
node[childProps] = _filterTree(node[childProps]);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return _filterTree(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据条件重新映射给定树结构的节
|
||||||
|
* @param tree 要过滤的树结构的根节点数组。
|
||||||
|
* @param mapper 用于map每个节点的条件。
|
||||||
|
* @param options 作为子节点数组的可选属性名称。
|
||||||
|
*/
|
||||||
|
function mapTree<T, V extends Record<string, any>>(
|
||||||
|
tree: T[],
|
||||||
|
mapper: (node: T) => V,
|
||||||
|
options?: TreeConfigOptions,
|
||||||
|
): V[] {
|
||||||
|
const { childProps } = options || {
|
||||||
|
childProps: 'children',
|
||||||
|
};
|
||||||
|
return tree.map((node) => {
|
||||||
|
const mapperNode: Record<string, any> = mapper(node);
|
||||||
|
if (mapperNode[childProps]) {
|
||||||
|
mapperNode[childProps] = mapTree(mapperNode[childProps], mapper, options);
|
||||||
|
}
|
||||||
|
return mapperNode as V;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { filterTree, mapTree, traverseTreeValues };
|
||||||
15
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/unique.ts
Normal file
15
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/unique.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 根据指定字段对对象数组进行去重
|
||||||
|
* @param arr 要去重的对象数组
|
||||||
|
* @param key 去重依据的字段名
|
||||||
|
* @returns 去重后的对象数组
|
||||||
|
*/
|
||||||
|
function uniqueByField<T>(arr: T[], key: keyof T): T[] {
|
||||||
|
const seen = new Map<any, T>();
|
||||||
|
return arr.filter((item) => {
|
||||||
|
const value = item[key];
|
||||||
|
return seen.has(value) ? false : (seen.set(value, item), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { uniqueByField };
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 更新 CSS 变量的函数
|
||||||
|
* @param variables 要更新的 CSS 变量与其新值的映射
|
||||||
|
*/
|
||||||
|
function updateCSSVariables(
|
||||||
|
variables: { [key: string]: string },
|
||||||
|
id = '__vben-styles__',
|
||||||
|
): void {
|
||||||
|
// 获取或创建内联样式表元素
|
||||||
|
const styleElement =
|
||||||
|
document.querySelector(`#${id}`) || document.createElement('style');
|
||||||
|
|
||||||
|
styleElement.id = id;
|
||||||
|
|
||||||
|
// 构建要更新的 CSS 变量的样式文本
|
||||||
|
let cssText = ':root {';
|
||||||
|
for (const key in variables) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(variables, key)) {
|
||||||
|
cssText += `${key}: ${variables[key]};`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cssText += '}';
|
||||||
|
|
||||||
|
// 将样式文本赋值给内联样式表
|
||||||
|
styleElement.textContent = cssText;
|
||||||
|
|
||||||
|
// 将内联样式表添加到文档头部
|
||||||
|
if (!document.querySelector(`#${id}`)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.head.append(styleElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { updateCSSVariables };
|
||||||
44
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/util.ts
Normal file
44
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/util.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
37
Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/window.ts
Normal file
37
Yi.Vben5.Vue3/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}`;
|
||||||
|
openWindow(url, { target: '_blank' });
|
||||||
|
}
|
||||||
|
|
||||||
|
export { openRouteInNewWindow, openWindow };
|
||||||
6
Yi.Vben5.Vue3/packages/@core/base/shared/tsconfig.json
Normal file
6
Yi.Vben5.Vue3/packages/@core/base/shared/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/library.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: ['src/index'],
|
||||||
|
});
|
||||||
44
Yi.Vben5.Vue3/packages/@core/base/typings/package.json
Normal file
44
Yi.Vben5.Vue3/packages/@core/base/typings/package.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/typings",
|
||||||
|
"version": "5.5.7",
|
||||||
|
"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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Yi.Vben5.Vue3/packages/@core/base/typings/src/app.d.ts
vendored
Normal file
111
Yi.Vben5.Vue3/packages/@core/base/typings/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AccessModeType,
|
||||||
|
AuthPageLayoutType,
|
||||||
|
BreadcrumbStyleType,
|
||||||
|
BuiltinThemeType,
|
||||||
|
ContentCompactType,
|
||||||
|
LayoutHeaderMenuAlignType,
|
||||||
|
LayoutHeaderModeType,
|
||||||
|
LayoutType,
|
||||||
|
LoginExpiredModeType,
|
||||||
|
NavigationStyleType,
|
||||||
|
PageTransitionType,
|
||||||
|
PreferencesButtonPositionType,
|
||||||
|
TabsStyleType,
|
||||||
|
ThemeModeType,
|
||||||
|
};
|
||||||
43
Yi.Vben5.Vue3/packages/@core/base/typings/src/basic.d.ts
vendored
Normal file
43
Yi.Vben5.Vue3/packages/@core/base/typings/src/basic.d.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
interface BasicOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectOption = BasicOption;
|
||||||
|
|
||||||
|
type TabOption = BasicOption;
|
||||||
|
|
||||||
|
interface BasicUserInfo {
|
||||||
|
/**
|
||||||
|
* 头像
|
||||||
|
*/
|
||||||
|
avatar: string;
|
||||||
|
/**
|
||||||
|
* 邮箱
|
||||||
|
*/
|
||||||
|
email: string;
|
||||||
|
/**
|
||||||
|
* 用户权限
|
||||||
|
*/
|
||||||
|
permissions: string[];
|
||||||
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
|
realName: string;
|
||||||
|
/**
|
||||||
|
* 用户角色
|
||||||
|
*/
|
||||||
|
roles: string[];
|
||||||
|
/**
|
||||||
|
* 用户id
|
||||||
|
*/
|
||||||
|
userId: number | string;
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClassType = Array<object | string> | object | string;
|
||||||
|
|
||||||
|
export type { BasicOption, BasicUserInfo, ClassType, SelectOption, TabOption };
|
||||||
132
Yi.Vben5.Vue3/packages/@core/base/typings/src/helper.d.ts
vendored
Normal file
132
Yi.Vben5.Vue3/packages/@core/base/typings/src/helper.d.ts
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import type { ComputedRef, MaybeRef } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深层递归所有属性为可选
|
||||||
|
*/
|
||||||
|
type DeepPartial<T> = T extends object
|
||||||
|
? {
|
||||||
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
|
}
|
||||||
|
: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深层递归所有属性为只读
|
||||||
|
*/
|
||||||
|
type DeepReadonly<T> = {
|
||||||
|
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任意类型的异步函数
|
||||||
|
*/
|
||||||
|
|
||||||
|
type AnyPromiseFunction<T extends any[] = any[], R = void> = (
|
||||||
|
...arg: T
|
||||||
|
) => PromiseLike<R>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任意类型的普通函数
|
||||||
|
*/
|
||||||
|
type AnyNormalFunction<T extends any[] = any[], R = void> = (...arg: T) => R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任意类型的函数
|
||||||
|
*/
|
||||||
|
type AnyFunction<T extends any[] = any[], R = void> =
|
||||||
|
| AnyNormalFunction<T, R>
|
||||||
|
| AnyPromiseFunction<T, R>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* T | null 包装
|
||||||
|
*/
|
||||||
|
type Nullable<T> = null | T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* T | Not null 包装
|
||||||
|
*/
|
||||||
|
type NonNullable<T> = T extends null | undefined ? never : T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串类型对象
|
||||||
|
*/
|
||||||
|
type Recordable<T> = Record<string, T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字符串类型对象(只读)
|
||||||
|
*/
|
||||||
|
interface ReadonlyRecordable<T = any> {
|
||||||
|
readonly [key: string]: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setTimeout 返回值类型
|
||||||
|
*/
|
||||||
|
type TimeoutHandle = ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setInterval 返回值类型
|
||||||
|
*/
|
||||||
|
type IntervalHandle = ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 也许它是一个计算的 ref,或者一个 getter 函数
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
type MaybeReadonlyRef<T> = (() => T) | ComputedRef<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 也许它是一个 ref,或者一个普通值,或者一个 getter 函数
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
type MaybeComputedRef<T> = MaybeReadonlyRef<T> | MaybeRef<T>;
|
||||||
|
|
||||||
|
type Merge<O extends object, T extends object> = {
|
||||||
|
[K in keyof O | keyof T]: K extends keyof T
|
||||||
|
? T[K]
|
||||||
|
: K extends keyof O
|
||||||
|
? O[K]
|
||||||
|
: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* T = [
|
||||||
|
* { name: string; age: number; },
|
||||||
|
* { sex: 'male' | 'female'; age: string }
|
||||||
|
* ]
|
||||||
|
* =>
|
||||||
|
* MergeAll<T> = {
|
||||||
|
* name: string;
|
||||||
|
* sex: 'male' | 'female';
|
||||||
|
* age: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
type MergeAll<
|
||||||
|
T extends object[],
|
||||||
|
R extends object = Record<string, any>,
|
||||||
|
> = T extends [infer F extends object, ...infer Rest extends object[]]
|
||||||
|
? MergeAll<Rest, Merge<R, F>>
|
||||||
|
: R;
|
||||||
|
|
||||||
|
type EmitType = (name: Name, ...args: any[]) => void;
|
||||||
|
|
||||||
|
type MaybePromise<T> = Promise<T> | T;
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AnyFunction,
|
||||||
|
AnyNormalFunction,
|
||||||
|
AnyPromiseFunction,
|
||||||
|
DeepPartial,
|
||||||
|
DeepReadonly,
|
||||||
|
EmitType,
|
||||||
|
IntervalHandle,
|
||||||
|
MaybeComputedRef,
|
||||||
|
MaybePromise,
|
||||||
|
MaybeReadonlyRef,
|
||||||
|
Merge,
|
||||||
|
MergeAll,
|
||||||
|
NonNullable,
|
||||||
|
Nullable,
|
||||||
|
ReadonlyRecordable,
|
||||||
|
Recordable,
|
||||||
|
TimeoutHandle,
|
||||||
|
};
|
||||||
6
Yi.Vben5.Vue3/packages/@core/base/typings/src/index.ts
Normal file
6
Yi.Vben5.Vue3/packages/@core/base/typings/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export type * from './app';
|
||||||
|
export type * from './basic';
|
||||||
|
export type * from './helper';
|
||||||
|
export type * from './menu-record';
|
||||||
|
export type * from './tabs';
|
||||||
|
export type * from './vue-router';
|
||||||
76
Yi.Vben5.Vue3/packages/@core/base/typings/src/menu-record.ts
Normal file
76
Yi.Vben5.Vue3/packages/@core/base/typings/src/menu-record.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Component } from 'vue';
|
||||||
|
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 {
|
||||||
|
/**
|
||||||
|
* 激活时的图标名
|
||||||
|
*/
|
||||||
|
activeIcon?: string;
|
||||||
|
/**
|
||||||
|
* 子菜单
|
||||||
|
*/
|
||||||
|
children?: MenuRecordRaw[];
|
||||||
|
/**
|
||||||
|
* 是否禁用菜单
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disabled?: boolean;
|
||||||
|
/**
|
||||||
|
* 图标名
|
||||||
|
*/
|
||||||
|
icon?: Component | string;
|
||||||
|
/**
|
||||||
|
* 菜单名
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* 排序号
|
||||||
|
*/
|
||||||
|
order?: number;
|
||||||
|
/**
|
||||||
|
* 父级路径
|
||||||
|
*/
|
||||||
|
parent?: string;
|
||||||
|
/**
|
||||||
|
* 所有父级路径
|
||||||
|
*/
|
||||||
|
parents?: string[];
|
||||||
|
/**
|
||||||
|
* 菜单路径,唯一,可当作key
|
||||||
|
*/
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* 是否显示菜单
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
show?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { ExRouteRecordRaw, MenuRecordBadgeRaw, MenuRecordRaw };
|
||||||
8
Yi.Vben5.Vue3/packages/@core/base/typings/src/tabs.ts
Normal file
8
Yi.Vben5.Vue3/packages/@core/base/typings/src/tabs.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { RouteLocationNormalized } from 'vue-router';
|
||||||
|
|
||||||
|
export interface TabDefinition extends RouteLocationNormalized {
|
||||||
|
/**
|
||||||
|
* 标签页的key
|
||||||
|
*/
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
159
Yi.Vben5.Vue3/packages/@core/base/typings/src/vue-router.d.ts
vendored
Normal file
159
Yi.Vben5.Vue3/packages/@core/base/typings/src/vue-router.d.ts
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import type { Component } from 'vue';
|
||||||
|
import type { Router, RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
interface RouteMeta {
|
||||||
|
/**
|
||||||
|
* 激活图标(菜单/tab)
|
||||||
|
*/
|
||||||
|
activeIcon?: string;
|
||||||
|
/**
|
||||||
|
* 当前激活的菜单,有时候不想激活现有菜单,需要激活父级菜单时使用
|
||||||
|
*/
|
||||||
|
activePath?: string;
|
||||||
|
/**
|
||||||
|
* 是否固定标签页
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
affixTab?: boolean;
|
||||||
|
/**
|
||||||
|
* 固定标签页的顺序
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
affixTabOrder?: number;
|
||||||
|
/**
|
||||||
|
* 需要特定的角色标识才可以访问
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
authority?: string[];
|
||||||
|
/**
|
||||||
|
* 徽标
|
||||||
|
*/
|
||||||
|
badge?: string;
|
||||||
|
/**
|
||||||
|
* 徽标类型
|
||||||
|
*/
|
||||||
|
badgeType?: 'dot' | 'normal';
|
||||||
|
/**
|
||||||
|
* 徽标颜色
|
||||||
|
*/
|
||||||
|
badgeVariants?:
|
||||||
|
| 'default'
|
||||||
|
| 'destructive'
|
||||||
|
| 'primary'
|
||||||
|
| 'success'
|
||||||
|
| 'warning'
|
||||||
|
| string;
|
||||||
|
/**
|
||||||
|
* 路由的完整路径作为key(默认true)
|
||||||
|
*/
|
||||||
|
fullPathKey?: boolean;
|
||||||
|
/**
|
||||||
|
* 当前路由的子级在菜单中不展现
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideChildrenInMenu?: boolean;
|
||||||
|
/**
|
||||||
|
* 当前路由在面包屑中不展现
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideInBreadcrumb?: boolean;
|
||||||
|
/**
|
||||||
|
* 当前路由在菜单中不展现
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideInMenu?: boolean;
|
||||||
|
/**
|
||||||
|
* 当前路由在标签页不展现
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
hideInTab?: boolean;
|
||||||
|
/**
|
||||||
|
* 图标(菜单/tab)
|
||||||
|
*/
|
||||||
|
icon?: Component | string;
|
||||||
|
/**
|
||||||
|
* iframe 地址
|
||||||
|
*/
|
||||||
|
iframeSrc?: string;
|
||||||
|
/**
|
||||||
|
* 忽略权限,直接可以访问
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
ignoreAccess?: boolean;
|
||||||
|
/**
|
||||||
|
* 开启KeepAlive缓存
|
||||||
|
*/
|
||||||
|
keepAlive?: boolean;
|
||||||
|
/**
|
||||||
|
* 外链-跳转路径
|
||||||
|
*/
|
||||||
|
link?: string;
|
||||||
|
/**
|
||||||
|
* 路由是否已经加载过
|
||||||
|
*/
|
||||||
|
loaded?: boolean;
|
||||||
|
/**
|
||||||
|
* 标签页最大打开数量
|
||||||
|
* @default -1
|
||||||
|
*/
|
||||||
|
maxNumOfOpenTab?: number;
|
||||||
|
/**
|
||||||
|
* 菜单可以看到,但是访问会被重定向到403
|
||||||
|
*/
|
||||||
|
menuVisibleWithForbidden?: boolean;
|
||||||
|
/**
|
||||||
|
* 不使用基础布局(仅在顶级生效)
|
||||||
|
*/
|
||||||
|
noBasicLayout?: boolean;
|
||||||
|
/**
|
||||||
|
* 在新窗口打开
|
||||||
|
*/
|
||||||
|
openInNewWindow?: boolean;
|
||||||
|
/**
|
||||||
|
* 用于路由->菜单排序
|
||||||
|
*/
|
||||||
|
order?: number;
|
||||||
|
/**
|
||||||
|
* 菜单所携带的参数
|
||||||
|
*/
|
||||||
|
query?: Recordable;
|
||||||
|
/**
|
||||||
|
* 管理员切换租户 该页面是否需要重定向到首页
|
||||||
|
* 用于区分带路由参数的页面 比如/oss/:id 这种路由是需要回到首页的
|
||||||
|
* 默认false
|
||||||
|
*/
|
||||||
|
requireHomeRedirect?: boolean;
|
||||||
|
/**
|
||||||
|
* 标题名称
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义递归类型以将 RouteRecordRaw 的 component 属性更改为 string
|
||||||
|
type RouteRecordStringComponent<T = string> = Omit<
|
||||||
|
RouteRecordRaw,
|
||||||
|
'children' | 'component'
|
||||||
|
> & {
|
||||||
|
children?: RouteRecordStringComponent<T>[];
|
||||||
|
component: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ComponentRecordType = Record<string, () => Promise<Component>>;
|
||||||
|
|
||||||
|
interface GenerateMenuAndRoutesOptions {
|
||||||
|
fetchMenuListAsync?: () => Promise<RouteRecordStringComponent[]>;
|
||||||
|
forbiddenComponent?: RouteRecordRaw['component'];
|
||||||
|
layoutMap?: ComponentRecordType;
|
||||||
|
pageMap?: ComponentRecordType;
|
||||||
|
roles?: string[];
|
||||||
|
router: Router;
|
||||||
|
routes: RouteRecordRaw[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ComponentRecordType,
|
||||||
|
GenerateMenuAndRoutesOptions,
|
||||||
|
RouteMeta,
|
||||||
|
RouteRecordRaw,
|
||||||
|
RouteRecordStringComponent,
|
||||||
|
};
|
||||||
6
Yi.Vben5.Vue3/packages/@core/base/typings/tsconfig.json
Normal file
6
Yi.Vben5.Vue3/packages/@core/base/typings/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/library.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
9
Yi.Vben5.Vue3/packages/@core/base/typings/vue-router.d.ts
vendored
Normal file
9
Yi.Vben5.Vue3/packages/@core/base/typings/vue-router.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* eslint-disable no-restricted-imports */
|
||||||
|
import type { RouteMeta as IRouteMeta } from '@vben-core/typings';
|
||||||
|
|
||||||
|
import 'vue-router';
|
||||||
|
|
||||||
|
declare module 'vue-router' {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||||
|
interface RouteMeta extends IRouteMeta {}
|
||||||
|
}
|
||||||
7
Yi.Vben5.Vue3/packages/@core/composables/build.config.ts
Normal file
7
Yi.Vben5.Vue3/packages/@core/composables/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: ['src/index'],
|
||||||
|
});
|
||||||
47
Yi.Vben5.Vue3/packages/@core/composables/package.json
Normal file
47
Yi.Vben5.Vue3/packages/@core/composables/package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/composables",
|
||||||
|
"version": "5.5.7",
|
||||||
|
"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/@core/composables"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm unbuild"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"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": {
|
||||||
|
"@vben-core/shared": "workspace:*",
|
||||||
|
"@vueuse/core": "catalog:",
|
||||||
|
"radix-vue": "catalog:",
|
||||||
|
"sortablejs": "catalog:",
|
||||||
|
"vue": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/sortablejs": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { SortableOptions } from 'sortablejs';
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { useSortable } from '../use-sortable';
|
||||||
|
|
||||||
|
describe('useSortable', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mock('sortablejs/modular/sortable.complete.esm.js', () => ({
|
||||||
|
default: {
|
||||||
|
create: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
it('should call Sortable.create with the correct options', async () => {
|
||||||
|
// Create a mock element
|
||||||
|
const mockElement = document.createElement('div') as HTMLDivElement;
|
||||||
|
|
||||||
|
// Define custom options
|
||||||
|
const customOptions: SortableOptions = {
|
||||||
|
group: 'test-group',
|
||||||
|
sort: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the useSortable function
|
||||||
|
const { initializeSortable } = useSortable(mockElement, customOptions);
|
||||||
|
|
||||||
|
// Initialize sortable
|
||||||
|
await initializeSortable();
|
||||||
|
|
||||||
|
// Import sortablejs to access the mocked create function
|
||||||
|
const Sortable = await import(
|
||||||
|
'sortablejs/modular/sortable.complete.esm.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that Sortable.create was called with the correct parameters
|
||||||
|
expect(Sortable.default.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(Sortable.default.create).toHaveBeenCalledWith(
|
||||||
|
mockElement,
|
||||||
|
expect.objectContaining({
|
||||||
|
animation: 300,
|
||||||
|
delay: 400,
|
||||||
|
delayOnTouchOnly: true,
|
||||||
|
...customOptions,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
Yi.Vben5.Vue3/packages/@core/composables/src/index.ts
Normal file
13
Yi.Vben5.Vue3/packages/@core/composables/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export * from './use-is-mobile';
|
||||||
|
export * from './use-layout-style';
|
||||||
|
export * from './use-namespace';
|
||||||
|
export * from './use-priority-value';
|
||||||
|
export * from './use-scroll-lock';
|
||||||
|
export * from './use-simple-locale';
|
||||||
|
export * from './use-sortable';
|
||||||
|
export {
|
||||||
|
useEmitAsProps,
|
||||||
|
useForwardExpose,
|
||||||
|
useForwardProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||||
|
|
||||||
|
export function useIsMobile() {
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||||
|
const isMobile = breakpoints.smaller('md');
|
||||||
|
return { isMobile };
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { VisibleDomRect } from '@vben-core/shared/utils';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
||||||
|
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
|
||||||
|
CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT,
|
||||||
|
CSS_VARIABLE_LAYOUT_HEADER_HEIGHT,
|
||||||
|
} from '@vben-core/shared/constants';
|
||||||
|
import { getElementVisibleRect } from '@vben-core/shared/utils';
|
||||||
|
import { useCssVar, useDebounceFn } from '@vueuse/core';
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh_CN content style
|
||||||
|
*/
|
||||||
|
export function useLayoutContentStyle() {
|
||||||
|
let resizeObserver: null | ResizeObserver = null;
|
||||||
|
const contentElement = ref<HTMLDivElement | null>(null);
|
||||||
|
const visibleDomRect = ref<null | VisibleDomRect>(null);
|
||||||
|
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
||||||
|
const contentWidth = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_WIDTH);
|
||||||
|
|
||||||
|
const overlayStyle = computed((): CSSProperties => {
|
||||||
|
const { height, left, top, width } = visibleDomRect.value ?? {};
|
||||||
|
return {
|
||||||
|
height: `${height}px`,
|
||||||
|
left: `${left}px`,
|
||||||
|
position: 'fixed',
|
||||||
|
top: `${top}px`,
|
||||||
|
width: `${width}px`,
|
||||||
|
zIndex: 150,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const debouncedCalcHeight = useDebounceFn(
|
||||||
|
(_entries: ResizeObserverEntry[]) => {
|
||||||
|
visibleDomRect.value = getElementVisibleRect(contentElement.value);
|
||||||
|
contentHeight.value = `${visibleDomRect.value.height}px`;
|
||||||
|
contentWidth.value = `${visibleDomRect.value.width}px`;
|
||||||
|
},
|
||||||
|
16,
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (contentElement.value && !resizeObserver) {
|
||||||
|
resizeObserver = new ResizeObserver(debouncedCalcHeight);
|
||||||
|
resizeObserver.observe(contentElement.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { contentElement, overlayStyle, visibleDomRect };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLayoutHeaderStyle() {
|
||||||
|
const headerHeight = useCssVar(CSS_VARIABLE_LAYOUT_HEADER_HEIGHT);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getLayoutHeaderHeight: () => {
|
||||||
|
return Number.parseInt(`${headerHeight.value}`, 10);
|
||||||
|
},
|
||||||
|
setLayoutHeaderHeight: (height: number) => {
|
||||||
|
headerHeight.value = `${height}px`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLayoutFooterStyle() {
|
||||||
|
const footerHeight = useCssVar(CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getLayoutFooterHeight: () => {
|
||||||
|
return Number.parseInt(`${footerHeight.value}`, 10);
|
||||||
|
},
|
||||||
|
setLayoutFooterHeight: (height: number) => {
|
||||||
|
footerHeight.value = `${height}px`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
106
Yi.Vben5.Vue3/packages/@core/composables/src/use-namespace.ts
Normal file
106
Yi.Vben5.Vue3/packages/@core/composables/src/use-namespace.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { DEFAULT_NAMESPACE } from '@vben-core/shared/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 = DEFAULT_NAMESPACE;
|
||||||
|
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 };
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFirstNonNullOrUndefined,
|
||||||
|
kebabToCamelCase,
|
||||||
|
} from '@vben-core/shared/utils';
|
||||||
|
import { computed, getCurrentInstance, unref, useAttrs, useSlots } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 依次从插槽、attrs、props、state 中获取值
|
||||||
|
* @param key
|
||||||
|
* @param props
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
export function usePriorityValue<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
S extends Record<string, any>,
|
||||||
|
K extends keyof T = keyof T,
|
||||||
|
>(key: K, props: T, state: Readonly<Ref<NoInfer<S>>> | undefined) {
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const slots = useSlots();
|
||||||
|
const attrs = useAttrs() as T;
|
||||||
|
|
||||||
|
const value = computed((): T[K] => {
|
||||||
|
// props不管有没有传,都会有默认值,会影响这里的顺序,
|
||||||
|
// 通过判断原始props是否有值来判断是否传入
|
||||||
|
const rawProps = (instance?.vnode?.props || {}) as T;
|
||||||
|
|
||||||
|
const standardRawProps = {} as T;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rawProps)) {
|
||||||
|
standardRawProps[kebabToCamelCase(key) as K] = value;
|
||||||
|
}
|
||||||
|
const propsKey =
|
||||||
|
standardRawProps?.[key] === undefined ? undefined : props[key];
|
||||||
|
|
||||||
|
// slot可以关闭
|
||||||
|
return getFirstNonNullOrUndefined(
|
||||||
|
slots[key as string],
|
||||||
|
attrs[key],
|
||||||
|
propsKey,
|
||||||
|
state?.value?.[key as keyof S],
|
||||||
|
) as T[K];
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取state中的值(每个值都是ref)
|
||||||
|
* @param props
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
export function usePriorityValues<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,
|
||||||
|
>(props: T, state: S | undefined) {
|
||||||
|
const result: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;
|
||||||
|
|
||||||
|
(Object.keys(props) as (keyof T)[]).forEach((key) => {
|
||||||
|
result[key] = usePriorityValue(key as keyof typeof props, props, state);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量获取state中的值(集中在一个computed,用于透传)
|
||||||
|
* @param props
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
|
export function useForwardPriorityValues<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,
|
||||||
|
>(props: T, state: S | undefined) {
|
||||||
|
const computedResult: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;
|
||||||
|
|
||||||
|
(Object.keys(props) as (keyof T)[]).forEach((key) => {
|
||||||
|
computedResult[key] = usePriorityValue(
|
||||||
|
key as keyof typeof props,
|
||||||
|
props,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const unwrapResult: Record<string, any> = {};
|
||||||
|
Object.keys(props).forEach((key) => {
|
||||||
|
unwrapResult[key] = unref(computedResult[key]);
|
||||||
|
});
|
||||||
|
return unwrapResult as { [K in keyof T]: T[K] };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { getScrollbarWidth, needsScrollbar } from '@vben-core/shared/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useScrollLock as _useScrollLock,
|
||||||
|
tryOnBeforeUnmount,
|
||||||
|
tryOnMounted,
|
||||||
|
} from '@vueuse/core';
|
||||||
|
|
||||||
|
export const SCROLL_FIXED_CLASS = `_scroll__fixed_`;
|
||||||
|
|
||||||
|
export function useScrollLock() {
|
||||||
|
const isLocked = _useScrollLock(document.body);
|
||||||
|
const scrollbarWidth = getScrollbarWidth();
|
||||||
|
|
||||||
|
tryOnMounted(() => {
|
||||||
|
if (!needsScrollbar()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
||||||
|
|
||||||
|
const layoutFixedNodes = document.querySelectorAll<HTMLElement>(
|
||||||
|
`.${SCROLL_FIXED_CLASS}`,
|
||||||
|
);
|
||||||
|
const nodes = [...layoutFixedNodes];
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
node.dataset.transition = node.style.transition;
|
||||||
|
node.style.transition = 'none';
|
||||||
|
node.style.paddingRight = `${scrollbarWidth}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isLocked.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
tryOnBeforeUnmount(() => {
|
||||||
|
if (!needsScrollbar()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isLocked.value = false;
|
||||||
|
const layoutFixedNodes = document.querySelectorAll<HTMLElement>(
|
||||||
|
`.${SCROLL_FIXED_CLASS}`,
|
||||||
|
);
|
||||||
|
const nodes = [...layoutFixedNodes];
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
node.style.paddingRight = '';
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
node.style.transition = node.dataset.transition || '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.body.style.paddingRight = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Simple i18n
|
||||||
|
|
||||||
|
Simple i18 implementation
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Locale } from './messages';
|
||||||
|
|
||||||
|
import { createSharedComposable } from '@vueuse/core';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { getMessages } from './messages';
|
||||||
|
|
||||||
|
export const useSimpleLocale = createSharedComposable(() => {
|
||||||
|
const currentLocale = ref<Locale>('zh-CN');
|
||||||
|
|
||||||
|
const setSimpleLocale = (locale: Locale) => {
|
||||||
|
currentLocale.value = locale;
|
||||||
|
};
|
||||||
|
|
||||||
|
const $t = computed(() => {
|
||||||
|
const localeMessages = getMessages(currentLocale.value);
|
||||||
|
return (key: string) => {
|
||||||
|
return localeMessages[key] || key;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
$t,
|
||||||
|
currentLocale,
|
||||||
|
setSimpleLocale,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export type Locale = 'en-US' | 'zh-CN';
|
||||||
|
|
||||||
|
export const messages: Record<Locale, Record<string, string>> = {
|
||||||
|
'en-US': {
|
||||||
|
cancel: 'Cancel',
|
||||||
|
collapse: 'Collapse',
|
||||||
|
confirm: 'Confirm',
|
||||||
|
expand: 'Expand',
|
||||||
|
prompt: 'Prompt',
|
||||||
|
reset: 'Reset',
|
||||||
|
submit: 'Submit',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
cancel: '取消',
|
||||||
|
collapse: '收起',
|
||||||
|
confirm: '确认',
|
||||||
|
expand: '展开',
|
||||||
|
prompt: '提示',
|
||||||
|
reset: '重置',
|
||||||
|
submit: '提交',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessages = (locale: Locale) => messages[locale];
|
||||||
29
Yi.Vben5.Vue3/packages/@core/composables/src/use-sortable.ts
Normal file
29
Yi.Vben5.Vue3/packages/@core/composables/src/use-sortable.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import type { SortableOptions } from 'sortablejs';
|
||||||
|
import type Sortable from 'sortablejs';
|
||||||
|
|
||||||
|
function useSortable<T extends HTMLElement>(
|
||||||
|
sortableContainer: T,
|
||||||
|
options: SortableOptions = {},
|
||||||
|
) {
|
||||||
|
const initializeSortable = async () => {
|
||||||
|
const Sortable = await import(
|
||||||
|
// @ts-expect-error - This is a dynamic import
|
||||||
|
'sortablejs/modular/sortable.complete.esm.js'
|
||||||
|
);
|
||||||
|
const sortable = Sortable?.default?.create?.(sortableContainer, {
|
||||||
|
animation: 300,
|
||||||
|
delay: 400,
|
||||||
|
delayOnTouchOnly: true,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return sortable as Sortable;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
initializeSortable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useSortable };
|
||||||
|
|
||||||
|
export type { Sortable };
|
||||||
6
Yi.Vben5.Vue3/packages/@core/composables/tsconfig.json
Normal file
6
Yi.Vben5.Vue3/packages/@core/composables/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@vben/tsconfig/library.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`defaultPreferences immutability test > should not modify the config object 1`] = `
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"accessMode": "frontend",
|
||||||
|
"authPageLayout": "panel-right",
|
||||||
|
"checkUpdatesInterval": 1,
|
||||||
|
"colorGrayMode": false,
|
||||||
|
"colorWeakMode": false,
|
||||||
|
"compact": false,
|
||||||
|
"contentCompact": "wide",
|
||||||
|
"contentCompactWidth": 1200,
|
||||||
|
"contentPadding": 0,
|
||||||
|
"contentPaddingBottom": 0,
|
||||||
|
"contentPaddingLeft": 0,
|
||||||
|
"contentPaddingRight": 0,
|
||||||
|
"contentPaddingTop": 0,
|
||||||
|
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
||||||
|
"defaultHomePath": "/analytics",
|
||||||
|
"dynamicTitle": true,
|
||||||
|
"enableCheckUpdates": true,
|
||||||
|
"enablePreferences": true,
|
||||||
|
"enableRefreshToken": false,
|
||||||
|
"isMobile": false,
|
||||||
|
"layout": "sidebar-nav",
|
||||||
|
"locale": "zh-CN",
|
||||||
|
"loginExpiredMode": "page",
|
||||||
|
"name": "Vben Admin",
|
||||||
|
"preferencesButtonPosition": "auto",
|
||||||
|
"watermark": false,
|
||||||
|
"zIndex": 200,
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"enable": true,
|
||||||
|
"hideOnlyOne": false,
|
||||||
|
"showHome": false,
|
||||||
|
"showIcon": true,
|
||||||
|
"styleType": "normal",
|
||||||
|
},
|
||||||
|
"copyright": {
|
||||||
|
"companyName": "Vben",
|
||||||
|
"companySiteLink": "https://www.vben.pro",
|
||||||
|
"date": "2024",
|
||||||
|
"enable": true,
|
||||||
|
"icp": "",
|
||||||
|
"icpLink": "",
|
||||||
|
"settingShow": true,
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"enable": false,
|
||||||
|
"fixed": false,
|
||||||
|
"height": 32,
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"enable": true,
|
||||||
|
"height": 50,
|
||||||
|
"hidden": false,
|
||||||
|
"menuAlign": "start",
|
||||||
|
"mode": "fixed",
|
||||||
|
},
|
||||||
|
"logo": {
|
||||||
|
"enable": true,
|
||||||
|
"fit": "contain",
|
||||||
|
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"accordion": true,
|
||||||
|
"split": true,
|
||||||
|
"styleType": "rounded",
|
||||||
|
},
|
||||||
|
"shortcutKeys": {
|
||||||
|
"enable": true,
|
||||||
|
"globalLockScreen": true,
|
||||||
|
"globalLogout": true,
|
||||||
|
"globalPreferences": true,
|
||||||
|
"globalSearch": true,
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"autoActivateChild": false,
|
||||||
|
"collapseWidth": 60,
|
||||||
|
"collapsed": false,
|
||||||
|
"collapsedButton": true,
|
||||||
|
"collapsedShowTitle": false,
|
||||||
|
"enable": true,
|
||||||
|
"expandOnHover": true,
|
||||||
|
"extraCollapse": false,
|
||||||
|
"extraCollapsedWidth": 60,
|
||||||
|
"fixedButton": true,
|
||||||
|
"hidden": false,
|
||||||
|
"mixedWidth": 80,
|
||||||
|
"width": 224,
|
||||||
|
},
|
||||||
|
"tabbar": {
|
||||||
|
"draggable": true,
|
||||||
|
"enable": true,
|
||||||
|
"height": 38,
|
||||||
|
"keepAlive": true,
|
||||||
|
"maxCount": 0,
|
||||||
|
"middleClickToClose": false,
|
||||||
|
"persist": true,
|
||||||
|
"showIcon": true,
|
||||||
|
"showMaximize": true,
|
||||||
|
"showMore": true,
|
||||||
|
"styleType": "chrome",
|
||||||
|
"wheelable": true,
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"builtinType": "default",
|
||||||
|
"colorDestructive": "hsl(348 100% 61%)",
|
||||||
|
"colorPrimary": "hsl(212 100% 45%)",
|
||||||
|
"colorSuccess": "hsl(144 57% 58%)",
|
||||||
|
"colorWarning": "hsl(42 84% 61%)",
|
||||||
|
"mode": "dark",
|
||||||
|
"radius": "0.5",
|
||||||
|
"semiDarkHeader": false,
|
||||||
|
"semiDarkSidebar": false,
|
||||||
|
},
|
||||||
|
"transition": {
|
||||||
|
"enable": true,
|
||||||
|
"loading": true,
|
||||||
|
"name": "fade-slide",
|
||||||
|
"progress": true,
|
||||||
|
},
|
||||||
|
"widget": {
|
||||||
|
"fullscreen": true,
|
||||||
|
"globalSearch": true,
|
||||||
|
"languageToggle": true,
|
||||||
|
"lockScreen": true,
|
||||||
|
"notification": true,
|
||||||
|
"refresh": true,
|
||||||
|
"sidebarToggle": true,
|
||||||
|
"themeToggle": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { defaultPreferences } from '../src/config';
|
||||||
|
|
||||||
|
describe('defaultPreferences immutability test', () => {
|
||||||
|
// 创建快照,确保默认配置对象不被修改
|
||||||
|
it('should not modify the config object', () => {
|
||||||
|
expect(defaultPreferences).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { defaultPreferences } from '../src/config';
|
||||||
|
import { PreferenceManager } from '../src/preferences';
|
||||||
|
import { isDarkTheme } from '../src/update-css-variables';
|
||||||
|
|
||||||
|
describe('preferences', () => {
|
||||||
|
let preferenceManager: PreferenceManager;
|
||||||
|
|
||||||
|
// 模拟 window.matchMedia 方法
|
||||||
|
vi.stubGlobal(
|
||||||
|
'matchMedia',
|
||||||
|
vi.fn().mockImplementation((query) => ({
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
addListener: vi.fn(), // Deprecated
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
matches: query === '(prefers-color-scheme: dark)',
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(), // Deprecated
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
beforeEach(() => {
|
||||||
|
preferenceManager = new PreferenceManager();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads default preferences if no saved preferences found', () => {
|
||||||
|
const preferences = preferenceManager.getPreferences();
|
||||||
|
expect(preferences).toEqual(defaultPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes preferences with overrides', async () => {
|
||||||
|
const overrides: any = {
|
||||||
|
app: {
|
||||||
|
locale: 'en-US',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await preferenceManager.initPreferences({
|
||||||
|
namespace: 'testNamespace',
|
||||||
|
overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待防抖动操作完成
|
||||||
|
// await new Promise((resolve) => setTimeout(resolve, 300)); // 等待100毫秒
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
...defaultPreferences,
|
||||||
|
app: {
|
||||||
|
...defaultPreferences.app,
|
||||||
|
...overrides.app,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences()).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates theme mode correctly', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
theme: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates color modes correctly', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { colorGrayMode: true, colorWeakMode: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().app.colorGrayMode).toBe(true);
|
||||||
|
expect(preferenceManager.getPreferences().app.colorWeakMode).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets preferences to default', () => {
|
||||||
|
// 先更新一些偏好设置
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
theme: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 然后重置偏好设置
|
||||||
|
preferenceManager.resetPreferences();
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates isMobile correctly', () => {
|
||||||
|
// 模拟移动端状态
|
||||||
|
vi.stubGlobal(
|
||||||
|
'matchMedia',
|
||||||
|
vi.fn().mockImplementation((query) => ({
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
addListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
matches: query === '(max-width: 768px)',
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { isMobile: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().app.isMobile).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the locale preference correctly', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { locale: 'en-US' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates the sidebar width correctly', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
sidebar: { width: 200 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().sidebar.width).toBe(200);
|
||||||
|
});
|
||||||
|
it('updates the sidebar collapse state correctly', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
sidebar: { collapsed: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().sidebar.collapsed).toBe(true);
|
||||||
|
});
|
||||||
|
it('updates the navigation style type correctly', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
navigation: { styleType: 'flat' },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().navigation.styleType).toBe(
|
||||||
|
'flat',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets preferences to default correctly', () => {
|
||||||
|
// 先更新一些偏好设置
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { locale: 'en-US' },
|
||||||
|
sidebar: { collapsed: true, width: 200 },
|
||||||
|
theme: {
|
||||||
|
mode: 'light',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 然后重置偏好设置
|
||||||
|
preferenceManager.resetPreferences();
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences()).toEqual(defaultPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not update undefined preferences', () => {
|
||||||
|
const originalPreferences = preferenceManager.getPreferences();
|
||||||
|
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { nonexistentField: 'value' },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reverts to default when a preference field is deleted', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { locale: 'en-US' },
|
||||||
|
});
|
||||||
|
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { locale: undefined },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().app.locale).toBe('en-US');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores updates with invalid preference value types', () => {
|
||||||
|
const originalPreferences = preferenceManager.getPreferences();
|
||||||
|
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { isMobile: 'true' as unknown as boolean }, // 错误类型
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences()).toEqual(originalPreferences);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges nested preference objects correctly', () => {
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
app: { name: 'New App Name' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
...defaultPreferences,
|
||||||
|
app: {
|
||||||
|
...defaultPreferences.app,
|
||||||
|
name: 'New App Name',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences()).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies updates immediately after initialization', async () => {
|
||||||
|
const overrides: any = {
|
||||||
|
app: {
|
||||||
|
locale: 'en-US',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await preferenceManager.initPreferences(overrides);
|
||||||
|
|
||||||
|
preferenceManager.updatePreferences({
|
||||||
|
theme: { mode: 'light' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(preferenceManager.getPreferences().theme.mode).toBe('light');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDarkTheme', () => {
|
||||||
|
it('should return true for dark theme', () => {
|
||||||
|
expect(isDarkTheme('dark')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for light theme', () => {
|
||||||
|
expect(isDarkTheme('light')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return system preference for auto theme', () => {
|
||||||
|
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
addListener: vi.fn(), // Deprecated
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
matches: query === '(prefers-color-scheme: dark)',
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(), // Deprecated
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(isDarkTheme('auto')).toBe(true);
|
||||||
|
expect(window.matchMedia).toHaveBeenCalledWith(
|
||||||
|
'(prefers-color-scheme: dark)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
7
Yi.Vben5.Vue3/packages/@core/preferences/build.config.ts
Normal file
7
Yi.Vben5.Vue3/packages/@core/preferences/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineBuildConfig } from 'unbuild';
|
||||||
|
|
||||||
|
export default defineBuildConfig({
|
||||||
|
clean: true,
|
||||||
|
declaration: true,
|
||||||
|
entries: ['src/index'],
|
||||||
|
});
|
||||||
37
Yi.Vben5.Vue3/packages/@core/preferences/package.json
Normal file
37
Yi.Vben5.Vue3/packages/@core/preferences/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@vben-core/preferences",
|
||||||
|
"version": "5.5.7",
|
||||||
|
"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/@core/preferences"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"#build": "pnpm unbuild"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"**/*.css"
|
||||||
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
|
"default": "./src/index.ts",
|
||||||
|
"#default": "./dist/index.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vben-core/shared": "workspace:*",
|
||||||
|
"@vben-core/typings": "workspace:*",
|
||||||
|
"@vueuse/core": "catalog:",
|
||||||
|
"vue": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Yi.Vben5.Vue3/packages/@core/preferences/src/config.ts
Normal file
138
Yi.Vben5.Vue3/packages/@core/preferences/src/config.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import type { Preferences } from './types';
|
||||||
|
|
||||||
|
const defaultPreferences: Preferences = {
|
||||||
|
app: {
|
||||||
|
accessMode: 'frontend',
|
||||||
|
authPageLayout: 'panel-right',
|
||||||
|
checkUpdatesInterval: 1,
|
||||||
|
colorGrayMode: false,
|
||||||
|
colorWeakMode: false,
|
||||||
|
compact: false,
|
||||||
|
contentCompact: 'wide',
|
||||||
|
contentCompactWidth: 1200,
|
||||||
|
contentPadding: 0,
|
||||||
|
contentPaddingBottom: 0,
|
||||||
|
contentPaddingLeft: 0,
|
||||||
|
contentPaddingRight: 0,
|
||||||
|
contentPaddingTop: 0,
|
||||||
|
defaultAvatar:
|
||||||
|
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||||
|
defaultHomePath: '/analytics',
|
||||||
|
dynamicTitle: true,
|
||||||
|
enableCheckUpdates: true,
|
||||||
|
enablePreferences: true,
|
||||||
|
enableRefreshToken: false,
|
||||||
|
isMobile: false,
|
||||||
|
layout: 'sidebar-nav',
|
||||||
|
locale: 'zh-CN',
|
||||||
|
loginExpiredMode: 'page',
|
||||||
|
name: 'Vben Admin',
|
||||||
|
preferencesButtonPosition: 'auto',
|
||||||
|
watermark: false,
|
||||||
|
zIndex: 200,
|
||||||
|
},
|
||||||
|
breadcrumb: {
|
||||||
|
enable: true,
|
||||||
|
hideOnlyOne: false,
|
||||||
|
showHome: false,
|
||||||
|
showIcon: true,
|
||||||
|
styleType: 'normal',
|
||||||
|
},
|
||||||
|
copyright: {
|
||||||
|
companyName: 'Vben',
|
||||||
|
companySiteLink: 'https://www.vben.pro',
|
||||||
|
date: '2024',
|
||||||
|
enable: true,
|
||||||
|
icp: '',
|
||||||
|
icpLink: '',
|
||||||
|
settingShow: true,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
enable: false,
|
||||||
|
fixed: false,
|
||||||
|
height: 32,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
enable: true,
|
||||||
|
height: 50,
|
||||||
|
hidden: false,
|
||||||
|
menuAlign: 'start',
|
||||||
|
mode: 'fixed',
|
||||||
|
},
|
||||||
|
|
||||||
|
logo: {
|
||||||
|
enable: true,
|
||||||
|
fit: 'contain',
|
||||||
|
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
accordion: true,
|
||||||
|
split: true,
|
||||||
|
styleType: 'rounded',
|
||||||
|
},
|
||||||
|
shortcutKeys: {
|
||||||
|
enable: true,
|
||||||
|
globalLockScreen: true,
|
||||||
|
globalLogout: true,
|
||||||
|
globalPreferences: true,
|
||||||
|
globalSearch: true,
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
autoActivateChild: false,
|
||||||
|
collapsed: false,
|
||||||
|
collapsedButton: true,
|
||||||
|
collapsedShowTitle: false,
|
||||||
|
collapseWidth: 60,
|
||||||
|
enable: true,
|
||||||
|
expandOnHover: true,
|
||||||
|
extraCollapse: false,
|
||||||
|
extraCollapsedWidth: 60,
|
||||||
|
fixedButton: true,
|
||||||
|
hidden: false,
|
||||||
|
mixedWidth: 80,
|
||||||
|
width: 224,
|
||||||
|
},
|
||||||
|
tabbar: {
|
||||||
|
draggable: true,
|
||||||
|
enable: true,
|
||||||
|
height: 38,
|
||||||
|
keepAlive: true,
|
||||||
|
maxCount: 0,
|
||||||
|
middleClickToClose: false,
|
||||||
|
persist: true,
|
||||||
|
showIcon: true,
|
||||||
|
showMaximize: true,
|
||||||
|
showMore: true,
|
||||||
|
styleType: 'chrome',
|
||||||
|
wheelable: true,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
builtinType: 'default',
|
||||||
|
colorDestructive: 'hsl(348 100% 61%)',
|
||||||
|
colorPrimary: 'hsl(212 100% 45%)',
|
||||||
|
colorSuccess: 'hsl(144 57% 58%)',
|
||||||
|
colorWarning: 'hsl(42 84% 61%)',
|
||||||
|
mode: 'auto',
|
||||||
|
radius: '0.5',
|
||||||
|
semiDarkHeader: false,
|
||||||
|
semiDarkSidebar: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
enable: true,
|
||||||
|
loading: true,
|
||||||
|
name: 'fade-slide',
|
||||||
|
progress: true,
|
||||||
|
},
|
||||||
|
widget: {
|
||||||
|
fullscreen: true,
|
||||||
|
globalSearch: true,
|
||||||
|
languageToggle: true,
|
||||||
|
lockScreen: false,
|
||||||
|
notification: true,
|
||||||
|
refresh: true,
|
||||||
|
sidebarToggle: true,
|
||||||
|
themeToggle: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { defaultPreferences };
|
||||||
88
Yi.Vben5.Vue3/packages/@core/preferences/src/constants.ts
Normal file
88
Yi.Vben5.Vue3/packages/@core/preferences/src/constants.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import type { BuiltinThemeType } from '@vben-core/typings';
|
||||||
|
|
||||||
|
interface BuiltinThemePreset {
|
||||||
|
color: string;
|
||||||
|
darkPrimaryColor?: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
type: BuiltinThemeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BUILT_IN_THEME_PRESETS: BuiltinThemePreset[] = [
|
||||||
|
{
|
||||||
|
color: 'hsl(212 100% 45%)',
|
||||||
|
type: 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(245 82% 67%)',
|
||||||
|
type: 'violet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(347 77% 60%)',
|
||||||
|
type: 'pink',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(42 84% 61%)',
|
||||||
|
type: 'yellow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(231 98% 65%)',
|
||||||
|
type: 'sky-blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(161 90% 43%)',
|
||||||
|
type: 'green',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(240 5% 26%)',
|
||||||
|
darkPrimaryColor: 'hsl(0 0% 98%)',
|
||||||
|
primaryColor: 'hsl(240 5.9% 10%)',
|
||||||
|
type: 'zinc',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
color: 'hsl(181 84% 32%)',
|
||||||
|
type: 'deep-green',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
color: 'hsl(211 91% 39%)',
|
||||||
|
type: 'deep-blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(18 89% 40%)',
|
||||||
|
type: 'orange',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(0 75% 42%)',
|
||||||
|
type: 'rose',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
color: 'hsl(0 0% 25%)',
|
||||||
|
darkPrimaryColor: 'hsl(0 0% 98%)',
|
||||||
|
primaryColor: 'hsl(240 5.9% 10%)',
|
||||||
|
type: 'neutral',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(215 25% 27%)',
|
||||||
|
darkPrimaryColor: 'hsl(0 0% 98%)',
|
||||||
|
primaryColor: 'hsl(240 5.9% 10%)',
|
||||||
|
type: 'slate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'hsl(217 19% 27%)',
|
||||||
|
darkPrimaryColor: 'hsl(0 0% 98%)',
|
||||||
|
primaryColor: 'hsl(240 5.9% 10%)',
|
||||||
|
type: 'gray',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: '',
|
||||||
|
type: 'custom',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const COLOR_PRESETS = [...BUILT_IN_THEME_PRESETS].slice(0, 7);
|
||||||
|
|
||||||
|
export { BUILT_IN_THEME_PRESETS };
|
||||||
|
|
||||||
|
export type { BuiltinThemePreset };
|
||||||
35
Yi.Vben5.Vue3/packages/@core/preferences/src/index.ts
Normal file
35
Yi.Vben5.Vue3/packages/@core/preferences/src/index.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { Preferences } from './types';
|
||||||
|
|
||||||
|
import { preferencesManager } from './preferences';
|
||||||
|
|
||||||
|
// 偏好设置(带有层级关系)
|
||||||
|
const preferences: Preferences =
|
||||||
|
preferencesManager.getPreferences.apply(preferencesManager);
|
||||||
|
|
||||||
|
// 更新偏好设置
|
||||||
|
const updatePreferences =
|
||||||
|
preferencesManager.updatePreferences.bind(preferencesManager);
|
||||||
|
|
||||||
|
// 重置偏好设置
|
||||||
|
const resetPreferences =
|
||||||
|
preferencesManager.resetPreferences.bind(preferencesManager);
|
||||||
|
|
||||||
|
const clearPreferencesCache =
|
||||||
|
preferencesManager.clearCache.bind(preferencesManager);
|
||||||
|
|
||||||
|
// 初始化偏好设置
|
||||||
|
const initPreferences =
|
||||||
|
preferencesManager.initPreferences.bind(preferencesManager);
|
||||||
|
|
||||||
|
export {
|
||||||
|
clearPreferencesCache,
|
||||||
|
initPreferences,
|
||||||
|
preferences,
|
||||||
|
preferencesManager,
|
||||||
|
resetPreferences,
|
||||||
|
updatePreferences,
|
||||||
|
};
|
||||||
|
|
||||||
|
export * from './constants';
|
||||||
|
export type * from './types';
|
||||||
|
export * from './use-preferences';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user