第一基础版
This commit is contained in:
3
packages/@core/README.md
Normal file
3
packages/@core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# @vben-core
|
||||
|
||||
系统一些比较基础的SDK和UI组件库,该目录后续完善后,可能会迁移出去或者发布到npm,请勿将任何业务逻辑和业务包放在该目录。
|
||||
5
packages/@core/base/README.md
Normal file
5
packages/@core/base/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# base
|
||||
|
||||
基础共享包,请勿引入 workspace 依赖
|
||||
|
||||
-
|
||||
41
packages/@core/base/design/package.json
Normal file
41
packages/@core/base/design/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@vben-core/design",
|
||||
"version": "5.5.4",
|
||||
"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
packages/@core/base/design/src/css/global.css
Normal file
160
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;
|
||||
}
|
||||
59
packages/@core/base/design/src/css/nprogress.css
Normal file
59
packages/@core/base/design/src/css/nprogress.css
Normal file
@@ -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
packages/@core/base/design/src/css/transition.css
Normal file
236
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
packages/@core/base/design/src/css/ui.css
Normal file
87
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);
|
||||
}
|
||||
446
packages/@core/base/design/src/design-tokens/dark.css
Normal file
446
packages/@core/base/design/src/design-tokens/dark.css
Normal file
@@ -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%;
|
||||
}
|
||||
382
packages/@core/base/design/src/design-tokens/default.css
Normal file
382
packages/@core/base/design/src/design-tokens/default.css
Normal file
@@ -0,0 +1,382 @@
|
||||
: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%;
|
||||
}
|
||||
4
packages/@core/base/design/src/design-tokens/index.ts
Normal file
4
packages/@core/base/design/src/design-tokens/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import './default.css';
|
||||
import './dark.css';
|
||||
|
||||
export {};
|
||||
8
packages/@core/base/design/src/index.ts
Normal file
8
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 {};
|
||||
34
packages/@core/base/design/src/scss-bem/bem.scss
Normal file
34
packages/@core/base/design/src/scss-bem/bem.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
packages/@core/base/design/src/scss-bem/constants.scss
Normal file
5
packages/@core/base/design/src/scss-bem/constants.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
$namespace: 'vben' !default;
|
||||
$common-separator: '-' !default;
|
||||
$element-separator: '__' !default;
|
||||
$modifier-separator: '--' !default;
|
||||
$state-prefix: 'is' !default;
|
||||
6
packages/@core/base/design/tsconfig.json
Normal file
6
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
packages/@core/base/design/vite.config.mts
Normal file
9
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
packages/@core/base/icons/build.config.ts
Normal file
7
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
packages/@core/base/icons/package.json
Normal file
41
packages/@core/base/icons/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@vben-core/icons",
|
||||
"version": "5.5.4",
|
||||
"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:"
|
||||
}
|
||||
}
|
||||
14
packages/@core/base/icons/src/create-icon.ts
Normal file
14
packages/@core/base/icons/src/create-icon.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
function createIconifyIcon(icon: string) {
|
||||
return defineComponent({
|
||||
name: `Icon-${icon}`,
|
||||
setup(props, { attrs }) {
|
||||
return () => h(Icon, { icon, ...props, ...attrs });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { createIconifyIcon };
|
||||
11
packages/@core/base/icons/src/index.ts
Normal file
11
packages/@core/base/icons/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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';
|
||||
68
packages/@core/base/icons/src/lucide.ts
Normal file
68
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
packages/@core/base/icons/tsconfig.json
Normal file
6
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
packages/@core/base/shared/build.config.ts
Normal file
14
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
packages/@core/base/shared/package.json
Normal file
103
packages/@core/base/shared/package.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "@vben-core/shared",
|
||||
"version": "5.5.4",
|
||||
"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
packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts
vendored
Normal file
130
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
packages/@core/base/shared/src/cache/index.ts
vendored
Normal file
1
packages/@core/base/shared/src/cache/index.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from './storage-manager';
|
||||
118
packages/@core/base/shared/src/cache/storage-manager.ts
vendored
Normal file
118
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有带前缀的存储项
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整的存储键
|
||||
* @param key 原始键
|
||||
* @returns 带前缀的完整键
|
||||
*/
|
||||
private getFullKey(key: string): string {
|
||||
return `${this.prefix}-${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
export { StorageManager };
|
||||
17
packages/@core/base/shared/src/cache/types.ts
vendored
Normal file
17
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);
|
||||
});
|
||||
});
|
||||
9
packages/@core/base/shared/src/color/color.ts
Normal file
9
packages/@core/base/shared/src/color/color.ts
Normal file
@@ -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();
|
||||
}
|
||||
62
packages/@core/base/shared/src/color/convert.ts
Normal file
62
packages/@core/base/shared/src/color/convert.ts
Normal file
@@ -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,
|
||||
};
|
||||
45
packages/@core/base/shared/src/color/generator.ts
Normal file
45
packages/@core/base/shared/src/color/generator.ts
Normal file
@@ -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 };
|
||||
3
packages/@core/base/shared/src/color/index.ts
Normal file
3
packages/@core/base/shared/src/color/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './color';
|
||||
export * from './convert';
|
||||
export * from './generator';
|
||||
16
packages/@core/base/shared/src/constants/globals.ts
Normal file
16
packages/@core/base/shared/src/constants/globals.ts
Normal file
@@ -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';
|
||||
2
packages/@core/base/shared/src/constants/index.ts
Normal file
2
packages/@core/base/shared/src/constants/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './globals';
|
||||
export * from './vben';
|
||||
26
packages/@core/base/shared/src/constants/vben.ts
Normal file
26
packages/@core/base/shared/src/constants/vben.ts
Normal file
@@ -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
packages/@core/base/shared/src/global-state.ts
Normal file
45
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
packages/@core/base/shared/src/store.ts
Normal file
1
packages/@core/base/shared/src/store.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@tanstack/vue-store';
|
||||
53
packages/@core/base/shared/src/utils/__tests__/diff.test.ts
Normal file
53
packages/@core/base/shared/src/utils/__tests__/diff.test.ts
Normal file
@@ -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 });
|
||||
});
|
||||
});
|
||||
127
packages/@core/base/shared/src/utils/__tests__/dom.test.ts
Normal file
127
packages/@core/base/shared/src/utils/__tests__/dom.test.ts
Normal file
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
183
packages/@core/base/shared/src/utils/__tests__/inference.test.ts
Normal file
183
packages/@core/base/shared/src/utils/__tests__/inference.test.ts
Normal file
@@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
116
packages/@core/base/shared/src/utils/__tests__/letter.test.ts
Normal file
116
packages/@core/base/shared/src/utils/__tests__/letter.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
196
packages/@core/base/shared/src/utils/__tests__/tree.test.ts
Normal file
196
packages/@core/base/shared/src/utils/__tests__/tree.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { filterTree, mapTree, traverseTreeValues } from '../tree';
|
||||
|
||||
describe('traverseTreeValues', () => {
|
||||
interface Node {
|
||||
children?: Node[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
type NodeValue = string;
|
||||
|
||||
const sampleTree: Node[] = [
|
||||
{
|
||||
name: 'A',
|
||||
children: [
|
||||
{ name: 'B' },
|
||||
{
|
||||
name: 'C',
|
||||
children: [{ name: 'D' }, { name: 'E' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'F',
|
||||
children: [
|
||||
{ name: 'G' },
|
||||
{
|
||||
name: 'H',
|
||||
children: [{ name: 'I' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
it('traverses tree and returns all node values', () => {
|
||||
const values = traverseTreeValues<Node, NodeValue>(
|
||||
sampleTree,
|
||||
(node) => node.name,
|
||||
{
|
||||
childProps: 'children',
|
||||
},
|
||||
);
|
||||
expect(values).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']);
|
||||
});
|
||||
|
||||
it('handles empty tree', () => {
|
||||
const values = traverseTreeValues<Node, NodeValue>([], (node) => node.name);
|
||||
expect(values).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles tree with only root node', () => {
|
||||
const rootNode = { name: 'A' };
|
||||
const values = traverseTreeValues<Node, NodeValue>(
|
||||
[rootNode],
|
||||
(node) => node.name,
|
||||
);
|
||||
expect(values).toEqual(['A']);
|
||||
});
|
||||
|
||||
it('handles tree with only leaf nodes', () => {
|
||||
const leafNodes = [{ name: 'A' }, { name: 'B' }, { name: 'C' }];
|
||||
const values = traverseTreeValues<Node, NodeValue>(
|
||||
leafNodes,
|
||||
(node) => node.name,
|
||||
);
|
||||
expect(values).toEqual(['A', 'B', 'C']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterTree', () => {
|
||||
const tree = [
|
||||
{
|
||||
id: 1,
|
||||
children: [
|
||||
{ id: 2 },
|
||||
{ id: 3, children: [{ id: 4 }, { id: 5 }, { id: 6 }] },
|
||||
{ id: 7 },
|
||||
],
|
||||
},
|
||||
{ id: 8, children: [{ id: 9 }, { id: 10 }] },
|
||||
{ id: 11 },
|
||||
];
|
||||
|
||||
it('should return all nodes when condition is always true', () => {
|
||||
const result = filterTree(tree, () => true, { childProps: 'children' });
|
||||
expect(result).toEqual(tree);
|
||||
});
|
||||
|
||||
it('should return only root nodes when condition is always false', () => {
|
||||
const result = filterTree(tree, () => false);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return nodes with even id values', () => {
|
||||
const result = filterTree(tree, (node) => node.id % 2 === 0);
|
||||
expect(result).toEqual([{ id: 8, children: [{ id: 10 }] }]);
|
||||
});
|
||||
|
||||
it('should return nodes with odd id values and their ancestors', () => {
|
||||
const result = filterTree(tree, (node) => node.id % 2 === 1);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
children: [{ id: 3, children: [{ id: 5 }] }, { id: 7 }],
|
||||
},
|
||||
{ id: 11 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return nodes with "leaf" in their name', () => {
|
||||
const tree = [
|
||||
{
|
||||
name: 'root',
|
||||
children: [
|
||||
{ name: 'leaf 1' },
|
||||
{
|
||||
name: 'branch',
|
||||
children: [{ name: 'leaf 2' }, { name: 'leaf 3' }],
|
||||
},
|
||||
{ name: 'leaf 4' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = filterTree(
|
||||
tree,
|
||||
(node) => node.name.includes('leaf') || node.name === 'root',
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'root',
|
||||
children: [{ name: 'leaf 1' }, { name: 'leaf 4' }],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapTree', () => {
|
||||
it('map infinite depth tree using mapTree', () => {
|
||||
const tree = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'node1',
|
||||
children: [
|
||||
{ id: 2, name: 'node2' },
|
||||
{ id: 3, name: 'node3' },
|
||||
{
|
||||
id: 4,
|
||||
name: 'node4',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'node5',
|
||||
children: [
|
||||
{ id: 6, name: 'node6' },
|
||||
{ id: 7, name: 'node7' },
|
||||
],
|
||||
},
|
||||
{ id: 8, name: 'node8' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const newTree = mapTree(tree, (node) => ({
|
||||
...node,
|
||||
name: `${node.name}-new`,
|
||||
}));
|
||||
|
||||
expect(newTree).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'node1-new',
|
||||
children: [
|
||||
{ id: 2, name: 'node2-new' },
|
||||
{ id: 3, name: 'node3-new' },
|
||||
{
|
||||
id: 4,
|
||||
name: 'node4-new',
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'node5-new',
|
||||
children: [
|
||||
{ id: 6, name: 'node6-new' },
|
||||
{ id: 7, name: 'node7-new' },
|
||||
],
|
||||
},
|
||||
{ id: 8, name: 'node8-new' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
156
packages/@core/base/shared/src/utils/__tests__/util.test.ts
Normal file
156
packages/@core/base/shared/src/utils/__tests__/util.test.ts
Normal file
@@ -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 {
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(newValue: string) {
|
||||
this._value = newValue;
|
||||
}
|
||||
|
||||
private _value: string = 'test';
|
||||
|
||||
constructor() {
|
||||
bindMethods(this);
|
||||
}
|
||||
}
|
||||
|
||||
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
packages/@core/base/shared/src/utils/cn.ts
Normal file
10
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
packages/@core/base/shared/src/utils/date.ts
Normal file
26
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
packages/@core/base/shared/src/utils/diff.ts
Normal file
96
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
packages/@core/base/shared/src/utils/dom.ts
Normal file
95
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
packages/@core/base/shared/src/utils/download.ts
Normal file
157
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
packages/@core/base/shared/src/utils/index.ts
Normal file
20
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';
|
||||
164
packages/@core/base/shared/src/utils/inference.ts
Normal file
164
packages/@core/base/shared/src/utils/inference.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
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
packages/@core/base/shared/src/utils/letter.ts
Normal file
47
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
packages/@core/base/shared/src/utils/merge.ts
Normal file
10
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;
|
||||
}
|
||||
});
|
||||
43
packages/@core/base/shared/src/utils/nprogress.ts
Normal file
43
packages/@core/base/shared/src/utils/nprogress.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type NProgress from 'nprogress';
|
||||
|
||||
// 创建一个NProgress实例的变量,初始值为null
|
||||
let nProgressInstance: null | typeof NProgress = null;
|
||||
|
||||
/**
|
||||
* 动态加载NProgress库,并进行配置。
|
||||
* 此函数首先检查是否已经加载过NProgress库,如果已经加载过,则直接返回NProgress实例。
|
||||
* 否则,动态导入NProgress库,进行配置,然后返回NProgress实例。
|
||||
*
|
||||
* @returns NProgress实例的Promise对象。
|
||||
*/
|
||||
async function loadNprogress() {
|
||||
if (nProgressInstance) {
|
||||
return nProgressInstance;
|
||||
}
|
||||
nProgressInstance = await import('nprogress');
|
||||
nProgressInstance.configure({
|
||||
showSpinner: true,
|
||||
speed: 300,
|
||||
});
|
||||
return nProgressInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始显示进度条。
|
||||
* 此函数首先加载NProgress库,然后调用NProgress的start方法开始显示进度条。
|
||||
*/
|
||||
async function startProgress() {
|
||||
const nprogress = await loadNprogress();
|
||||
nprogress?.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止显示进度条,并隐藏进度条。
|
||||
* 此函数首先加载NProgress库,然后调用NProgress的done方法停止并隐藏进度条。
|
||||
*/
|
||||
async function stopProgress() {
|
||||
const nprogress = await loadNprogress();
|
||||
nprogress?.done();
|
||||
}
|
||||
|
||||
export { startProgress, stopProgress };
|
||||
50
packages/@core/base/shared/src/utils/state-handler.ts
Normal file
50
packages/@core/base/shared/src/utils/state-handler.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
export class StateHandler {
|
||||
private condition: boolean = false;
|
||||
private rejectCondition: (() => void) | null = null;
|
||||
private resolveCondition: (() => void) | null = 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 清理 resolve/reject 函数
|
||||
private clearPromises() {
|
||||
this.resolveCondition = null;
|
||||
this.rejectCondition = null;
|
||||
}
|
||||
}
|
||||
21
packages/@core/base/shared/src/utils/to.ts
Normal file
21
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
packages/@core/base/shared/src/utils/tree.ts
Normal file
97
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
packages/@core/base/shared/src/utils/unique.ts
Normal file
15
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 };
|
||||
35
packages/@core/base/shared/src/utils/update-css-variables.ts
Normal file
35
packages/@core/base/shared/src/utils/update-css-variables.ts
Normal file
@@ -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
packages/@core/base/shared/src/utils/util.ts
Normal file
44
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
packages/@core/base/shared/src/utils/window.ts
Normal file
37
packages/@core/base/shared/src/utils/window.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
interface OpenWindowOptions {
|
||||
noopener?: boolean;
|
||||
noreferrer?: boolean;
|
||||
target?: '_blank' | '_parent' | '_self' | '_top' | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新窗口打开URL。
|
||||
*
|
||||
* @param url - 需要打开的网址。
|
||||
* @param options - 打开窗口的选项。
|
||||
*/
|
||||
function openWindow(url: string, options: OpenWindowOptions = {}): void {
|
||||
// 解构并设置默认值
|
||||
const { noopener = true, noreferrer = true, target = '_blank' } = options;
|
||||
|
||||
// 基于选项创建特性字符串
|
||||
const features = [noopener && 'noopener=yes', noreferrer && 'noreferrer=yes']
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
|
||||
// 打开窗口
|
||||
window.open(url, target, features);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在新窗口中打开路由。
|
||||
* @param path
|
||||
*/
|
||||
function openRouteInNewWindow(path: string) {
|
||||
const { hash, origin } = location;
|
||||
const fullPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
|
||||
openWindow(url, { target: '_blank' });
|
||||
}
|
||||
|
||||
export { openRouteInNewWindow, openWindow };
|
||||
6
packages/@core/base/shared/tsconfig.json
Normal file
6
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"]
|
||||
}
|
||||
7
packages/@core/base/typings/build.config.ts
Normal file
7
packages/@core/base/typings/build.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: ['src/index'],
|
||||
});
|
||||
44
packages/@core/base/typings/package.json
Normal file
44
packages/@core/base/typings/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@vben-core/typings",
|
||||
"version": "5.5.4",
|
||||
"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:"
|
||||
}
|
||||
}
|
||||
110
packages/@core/base/typings/src/app.d.ts
vendored
Normal file
110
packages/@core/base/typings/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
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 前端权限模式
|
||||
*/
|
||||
type AccessModeType = 'backend' | 'frontend';
|
||||
|
||||
/**
|
||||
* 导航风格
|
||||
* 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,
|
||||
};
|
||||
35
packages/@core/base/typings/src/basic.d.ts
vendored
Normal file
35
packages/@core/base/typings/src/basic.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
interface BasicOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type SelectOption = BasicOption;
|
||||
|
||||
type TabOption = BasicOption;
|
||||
|
||||
interface BasicUserInfo {
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
avatar: string;
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
realName: string;
|
||||
/**
|
||||
* 用户角色
|
||||
*/
|
||||
roles?: string[];
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
username: string;
|
||||
}
|
||||
|
||||
type ClassType = Array<object | string> | object | string;
|
||||
|
||||
export type { BasicOption, BasicUserInfo, ClassType, SelectOption, TabOption };
|
||||
132
packages/@core/base/typings/src/helper.d.ts
vendored
Normal file
132
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
packages/@core/base/typings/src/index.ts
Normal file
6
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
packages/@core/base/typings/src/menu-record.ts
Normal file
76
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 = RouteRecordRaw & {
|
||||
parent?: string;
|
||||
parents?: string[];
|
||||
path?: any;
|
||||
};
|
||||
|
||||
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 };
|
||||
3
packages/@core/base/typings/src/tabs.ts
Normal file
3
packages/@core/base/typings/src/tabs.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
export type TabDefinition = RouteLocationNormalized;
|
||||
149
packages/@core/base/typings/src/vue-router.d.ts
vendored
Normal file
149
packages/@core/base/typings/src/vue-router.d.ts
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
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;
|
||||
/**
|
||||
* 当前路由的子级在菜单中不展现
|
||||
* @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;
|
||||
/**
|
||||
* 标题名称
|
||||
*/
|
||||
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
packages/@core/base/typings/tsconfig.json
Normal file
6
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
packages/@core/base/typings/vue-router.d.ts
vendored
Normal file
9
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
packages/@core/composables/build.config.ts
Normal file
7
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
packages/@core/composables/package.json
Normal file
47
packages/@core/composables/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "@vben-core/composables",
|
||||
"version": "5.5.4",
|
||||
"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
packages/@core/composables/src/index.ts
Normal file
13
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';
|
||||
7
packages/@core/composables/src/use-is-mobile.ts
Normal file
7
packages/@core/composables/src/use-is-mobile.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||
|
||||
export function useIsMobile() {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller('md');
|
||||
return { isMobile };
|
||||
}
|
||||
87
packages/@core/composables/src/use-layout-style.ts
Normal file
87
packages/@core/composables/src/use-layout-style.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import type { VisibleDomRect } from '@vben-core/shared/utils';
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } 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';
|
||||
|
||||
/**
|
||||
* @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
packages/@core/composables/src/use-namespace.ts
Normal file
106
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 };
|
||||
94
packages/@core/composables/src/use-priority-value.ts
Normal file
94
packages/@core/composables/src/use-priority-value.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
|
||||
import { computed, getCurrentInstance, unref, useAttrs, useSlots } from 'vue';
|
||||
|
||||
import {
|
||||
getFirstNonNullOrUndefined,
|
||||
kebabToCamelCase,
|
||||
} from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* 依次从插槽、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] };
|
||||
});
|
||||
}
|
||||
54
packages/@core/composables/src/use-scroll-lock.ts
Normal file
54
packages/@core/composables/src/use-scroll-lock.ts
Normal file
@@ -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
|
||||
27
packages/@core/composables/src/use-simple-locale/index.ts
Normal file
27
packages/@core/composables/src/use-simple-locale/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Locale } from './messages';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { createSharedComposable } from '@vueuse/core';
|
||||
|
||||
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,
|
||||
};
|
||||
});
|
||||
24
packages/@core/composables/src/use-simple-locale/messages.ts
Normal file
24
packages/@core/composables/src/use-simple-locale/messages.ts
Normal file
@@ -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
packages/@core/composables/src/use-sortable.ts
Normal file
29
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
packages/@core/composables/tsconfig.json
Normal file
6
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,122 @@
|
||||
// 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",
|
||||
"defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp",
|
||||
"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,
|
||||
},
|
||||
"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,
|
||||
},
|
||||
"header": {
|
||||
"enable": true,
|
||||
"hidden": false,
|
||||
"menuAlign": "start",
|
||||
"mode": "fixed",
|
||||
},
|
||||
"logo": {
|
||||
"enable": true,
|
||||
"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,
|
||||
"enable": true,
|
||||
"expandOnHover": true,
|
||||
"extraCollapse": false,
|
||||
"fixedButton": true,
|
||||
"hidden": false,
|
||||
"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,
|
||||
},
|
||||
}
|
||||
`;
|
||||
10
packages/@core/preferences/__tests__/config.test.ts
Normal file
10
packages/@core/preferences/__tests__/config.test.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
253
packages/@core/preferences/__tests__/preferences.test.ts
Normal file
253
packages/@core/preferences/__tests__/preferences.test.ts
Normal file
@@ -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
packages/@core/preferences/build.config.ts
Normal file
7
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
packages/@core/preferences/package.json
Normal file
37
packages/@core/preferences/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@vben-core/preferences",
|
||||
"version": "5.5.4",
|
||||
"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:"
|
||||
}
|
||||
}
|
||||
123
packages/@core/preferences/src/config.ts
Normal file
123
packages/@core/preferences/src/config.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Preferences } from './types';
|
||||
|
||||
const defaultPreferences: Preferences = {
|
||||
app: {
|
||||
accessMode: 'frontend',
|
||||
authPageLayout: 'panel-right',
|
||||
checkUpdatesInterval: 1,
|
||||
colorGrayMode: false,
|
||||
colorWeakMode: false,
|
||||
compact: false,
|
||||
contentCompact: 'wide',
|
||||
defaultAvatar:
|
||||
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
header: {
|
||||
enable: true,
|
||||
hidden: false,
|
||||
menuAlign: 'start',
|
||||
mode: 'fixed',
|
||||
},
|
||||
logo: {
|
||||
enable: true,
|
||||
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,
|
||||
enable: true,
|
||||
expandOnHover: true,
|
||||
extraCollapse: false,
|
||||
fixedButton: true,
|
||||
hidden: false,
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
export { defaultPreferences };
|
||||
88
packages/@core/preferences/src/constants.ts
Normal file
88
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
packages/@core/preferences/src/index.ts
Normal file
35
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';
|
||||
235
packages/@core/preferences/src/preferences.ts
Normal file
235
packages/@core/preferences/src/preferences.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import type { DeepPartial } from '@vben-core/typings';
|
||||
|
||||
import type { InitialOptions, Preferences } from './types';
|
||||
|
||||
import { markRaw, reactive, readonly, watch } from 'vue';
|
||||
|
||||
import { StorageManager } from '@vben-core/shared/cache';
|
||||
import { isMacOs, merge } from '@vben-core/shared/utils';
|
||||
|
||||
import {
|
||||
breakpointsTailwind,
|
||||
useBreakpoints,
|
||||
useDebounceFn,
|
||||
} from '@vueuse/core';
|
||||
|
||||
import { defaultPreferences } from './config';
|
||||
import { updateCSSVariables } from './update-css-variables';
|
||||
|
||||
const STORAGE_KEY = 'preferences';
|
||||
const STORAGE_KEY_LOCALE = `${STORAGE_KEY}-locale`;
|
||||
const STORAGE_KEY_THEME = `${STORAGE_KEY}-theme`;
|
||||
|
||||
class PreferenceManager {
|
||||
private cache: null | StorageManager = null;
|
||||
// private flattenedState: Flatten<Preferences>;
|
||||
private initialPreferences: Preferences = defaultPreferences;
|
||||
private isInitialized: boolean = false;
|
||||
private savePreferences: (preference: Preferences) => void;
|
||||
private state: Preferences = reactive<Preferences>({
|
||||
...this.loadPreferences(),
|
||||
});
|
||||
constructor() {
|
||||
this.cache = new StorageManager();
|
||||
|
||||
// 避免频繁的操作缓存
|
||||
this.savePreferences = useDebounceFn(
|
||||
(preference: Preferences) => this._savePreferences(preference),
|
||||
150,
|
||||
);
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
[STORAGE_KEY, STORAGE_KEY_LOCALE, STORAGE_KEY_THEME].forEach((key) => {
|
||||
this.cache?.removeItem(key);
|
||||
});
|
||||
}
|
||||
|
||||
public getInitialPreferences() {
|
||||
return this.initialPreferences;
|
||||
}
|
||||
|
||||
public getPreferences() {
|
||||
return readonly(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖偏好设置
|
||||
* overrides 要覆盖的偏好设置
|
||||
* namespace 命名空间
|
||||
*/
|
||||
public async initPreferences({ namespace, overrides }: InitialOptions) {
|
||||
// 是否初始化过
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
// 初始化存储管理器
|
||||
this.cache = new StorageManager({ prefix: namespace });
|
||||
// 合并初始偏好设置
|
||||
this.initialPreferences = merge({}, overrides, defaultPreferences);
|
||||
|
||||
// 加载并合并当前存储的偏好设置
|
||||
const mergedPreference = merge(
|
||||
{},
|
||||
// overrides,
|
||||
this.loadCachedPreferences() || {},
|
||||
this.initialPreferences,
|
||||
);
|
||||
|
||||
// 更新偏好设置
|
||||
this.updatePreferences(mergedPreference);
|
||||
|
||||
this.setupWatcher();
|
||||
|
||||
this.initPlatform();
|
||||
// 标记为已初始化
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置偏好设置
|
||||
* 偏好设置将被重置为初始值,并从 localStorage 中移除。
|
||||
*
|
||||
* @example
|
||||
* 假设 initialPreferences 为 { theme: 'light', language: 'en' }
|
||||
* 当前 state 为 { theme: 'dark', language: 'fr' }
|
||||
* this.resetPreferences();
|
||||
* 调用后,state 将被重置为 { theme: 'light', language: 'en' }
|
||||
* 并且 localStorage 中的对应项将被移除
|
||||
*/
|
||||
resetPreferences() {
|
||||
// 将状态重置为初始偏好设置
|
||||
Object.assign(this.state, this.initialPreferences);
|
||||
// 保存重置后的偏好设置
|
||||
this.savePreferences(this.state);
|
||||
// 从存储中移除偏好设置项
|
||||
[STORAGE_KEY, STORAGE_KEY_THEME, STORAGE_KEY_LOCALE].forEach((key) => {
|
||||
this.cache?.removeItem(key);
|
||||
});
|
||||
this.updatePreferences(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新偏好设置
|
||||
* @param updates - 要更新的偏好设置
|
||||
*/
|
||||
public updatePreferences(updates: DeepPartial<Preferences>) {
|
||||
const mergedState = merge({}, updates, markRaw(this.state));
|
||||
|
||||
Object.assign(this.state, mergedState);
|
||||
|
||||
// 根据更新的键值执行相应的操作
|
||||
this.handleUpdates(updates);
|
||||
this.savePreferences(this.state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存偏好设置
|
||||
* @param {Preferences} preference - 需要保存的偏好设置
|
||||
*/
|
||||
private _savePreferences(preference: Preferences) {
|
||||
this.cache?.setItem(STORAGE_KEY, preference);
|
||||
this.cache?.setItem(STORAGE_KEY_LOCALE, preference.app.locale);
|
||||
this.cache?.setItem(STORAGE_KEY_THEME, preference.theme.mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新的键值
|
||||
* 根据更新的键值执行相应的操作。
|
||||
* @param {DeepPartial<Preferences>} updates - 部分更新的偏好设置
|
||||
*/
|
||||
private handleUpdates(updates: DeepPartial<Preferences>) {
|
||||
const themeUpdates = updates.theme || {};
|
||||
const appUpdates = updates.app || {};
|
||||
if (themeUpdates && Object.keys(themeUpdates).length > 0) {
|
||||
updateCSSVariables(this.state);
|
||||
}
|
||||
|
||||
if (
|
||||
Reflect.has(appUpdates, 'colorGrayMode') ||
|
||||
Reflect.has(appUpdates, 'colorWeakMode')
|
||||
) {
|
||||
this.updateColorMode(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
private initPlatform() {
|
||||
const dom = document.documentElement;
|
||||
dom.dataset.platform = isMacOs() ? 'macOs' : 'window';
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存中加载偏好设置。如果缓存中没有找到对应的偏好设置,则返回默认偏好设置。
|
||||
*/
|
||||
private loadCachedPreferences() {
|
||||
return this.cache?.getItem<Preferences>(STORAGE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载偏好设置
|
||||
* @returns {Preferences} 加载的偏好设置
|
||||
*/
|
||||
private loadPreferences(): Preferences {
|
||||
return this.loadCachedPreferences() || { ...defaultPreferences };
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听状态和系统偏好设置的变化。
|
||||
*/
|
||||
private setupWatcher() {
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 监听断点,判断是否移动端
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller('md');
|
||||
watch(
|
||||
() => isMobile.value,
|
||||
(val) => {
|
||||
this.updatePreferences({
|
||||
app: { isMobile: val },
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 监听系统主题偏好设置变化
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', ({ matches: isDark }) => {
|
||||
// 如果偏好设置中主题模式为auto,则跟随系统更新
|
||||
if (this.state.theme.mode === 'auto') {
|
||||
this.updatePreferences({
|
||||
theme: { mode: isDark ? 'dark' : 'light' },
|
||||
});
|
||||
// 恢复为auto模式
|
||||
this.updatePreferences({
|
||||
theme: { mode: 'auto' },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新页面颜色模式(灰色、色弱)
|
||||
* @param preference
|
||||
*/
|
||||
private updateColorMode(preference: Preferences) {
|
||||
if (preference.app) {
|
||||
const { colorGrayMode, colorWeakMode } = preference.app;
|
||||
const dom = document.documentElement;
|
||||
const COLOR_WEAK = 'invert-mode';
|
||||
const COLOR_GRAY = 'grayscale-mode';
|
||||
colorWeakMode
|
||||
? dom.classList.add(COLOR_WEAK)
|
||||
: dom.classList.remove(COLOR_WEAK);
|
||||
colorGrayMode
|
||||
? dom.classList.add(COLOR_GRAY)
|
||||
: dom.classList.remove(COLOR_GRAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const preferencesManager = new PreferenceManager();
|
||||
export { PreferenceManager, preferencesManager };
|
||||
296
packages/@core/preferences/src/types.ts
Normal file
296
packages/@core/preferences/src/types.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import type {
|
||||
AccessModeType,
|
||||
AuthPageLayoutType,
|
||||
BreadcrumbStyleType,
|
||||
BuiltinThemeType,
|
||||
ContentCompactType,
|
||||
DeepPartial,
|
||||
LayoutHeaderMenuAlignType,
|
||||
LayoutHeaderModeType,
|
||||
LayoutType,
|
||||
LoginExpiredModeType,
|
||||
NavigationStyleType,
|
||||
PageTransitionType,
|
||||
PreferencesButtonPositionType,
|
||||
TabsStyleType,
|
||||
ThemeModeType,
|
||||
} from '@vben-core/typings';
|
||||
|
||||
type SupportedLanguagesType = 'en-US' | 'zh-CN';
|
||||
|
||||
interface AppPreferences {
|
||||
/** 权限模式 */
|
||||
accessMode: AccessModeType;
|
||||
/** 登录注册页面布局 */
|
||||
authPageLayout: AuthPageLayoutType;
|
||||
/** 检查更新轮询时间 */
|
||||
checkUpdatesInterval: number;
|
||||
/** 是否开启灰色模式 */
|
||||
colorGrayMode: boolean;
|
||||
/** 是否开启色弱模式 */
|
||||
colorWeakMode: boolean;
|
||||
/** 是否开启紧凑模式 */
|
||||
compact: boolean;
|
||||
/** 是否开启内容紧凑模式 */
|
||||
contentCompact: ContentCompactType;
|
||||
// /** 应用默认头像 */
|
||||
defaultAvatar: string;
|
||||
// /** 开启动态标题 */
|
||||
dynamicTitle: boolean;
|
||||
/** 是否开启检查更新 */
|
||||
enableCheckUpdates: boolean;
|
||||
/** 是否显示偏好设置 */
|
||||
enablePreferences: boolean;
|
||||
/**
|
||||
* @zh_CN 是否开启refreshToken
|
||||
*/
|
||||
enableRefreshToken: boolean;
|
||||
/** 是否移动端 */
|
||||
isMobile: boolean;
|
||||
/** 布局方式 */
|
||||
layout: LayoutType;
|
||||
/** 支持的语言 */
|
||||
locale: SupportedLanguagesType;
|
||||
/** 登录过期模式 */
|
||||
loginExpiredMode: LoginExpiredModeType;
|
||||
/** 应用名 */
|
||||
name: string;
|
||||
/** 偏好设置按钮位置 */
|
||||
preferencesButtonPosition: PreferencesButtonPositionType;
|
||||
/**
|
||||
* @zh_CN 是否开启水印
|
||||
*/
|
||||
watermark: boolean;
|
||||
}
|
||||
|
||||
interface BreadcrumbPreferences {
|
||||
/** 面包屑是否启用 */
|
||||
enable: boolean;
|
||||
/** 面包屑是否只有一个时隐藏 */
|
||||
hideOnlyOne: boolean;
|
||||
/** 面包屑首页图标是否可见 */
|
||||
showHome: boolean;
|
||||
/** 面包屑图标是否可见 */
|
||||
showIcon: boolean;
|
||||
/** 面包屑风格 */
|
||||
styleType: BreadcrumbStyleType;
|
||||
}
|
||||
|
||||
interface CopyrightPreferences {
|
||||
/** 版权公司名 */
|
||||
companyName: string;
|
||||
/** 版权公司名链接 */
|
||||
companySiteLink: string;
|
||||
/** 版权日期 */
|
||||
date: string;
|
||||
/** 版权是否可见 */
|
||||
enable: boolean;
|
||||
/** 备案号 */
|
||||
icp: string;
|
||||
/** 备案号链接 */
|
||||
icpLink: string;
|
||||
/** 设置面板是否显示*/
|
||||
settingShow?: boolean;
|
||||
}
|
||||
|
||||
interface FooterPreferences {
|
||||
/** 底栏是否可见 */
|
||||
enable: boolean;
|
||||
/** 底栏是否固定 */
|
||||
fixed: boolean;
|
||||
}
|
||||
|
||||
interface HeaderPreferences {
|
||||
/** 顶栏是否启用 */
|
||||
enable: boolean;
|
||||
/** 顶栏是否隐藏,css-隐藏 */
|
||||
hidden: boolean;
|
||||
/** 顶栏菜单位置 */
|
||||
menuAlign: LayoutHeaderMenuAlignType;
|
||||
/** header显示模式 */
|
||||
mode: LayoutHeaderModeType;
|
||||
}
|
||||
|
||||
interface LogoPreferences {
|
||||
/** logo是否可见 */
|
||||
enable: boolean;
|
||||
/** logo地址 */
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface NavigationPreferences {
|
||||
/** 导航菜单手风琴模式 */
|
||||
accordion: boolean;
|
||||
/** 导航菜单是否切割,只在 layout=mixed-nav 生效 */
|
||||
split: boolean;
|
||||
/** 导航菜单风格 */
|
||||
styleType: NavigationStyleType;
|
||||
}
|
||||
|
||||
interface SidebarPreferences {
|
||||
/** 点击目录时自动激活子菜单 */
|
||||
autoActivateChild: boolean;
|
||||
/** 侧边栏是否折叠 */
|
||||
collapsed: boolean;
|
||||
/** 侧边栏折叠按钮是否可见 */
|
||||
collapsedButton: boolean;
|
||||
/** 侧边栏折叠时,是否显示title */
|
||||
collapsedShowTitle: boolean;
|
||||
/** 侧边栏是否可见 */
|
||||
enable: boolean;
|
||||
/** 菜单自动展开状态 */
|
||||
expandOnHover: boolean;
|
||||
/** 侧边栏扩展区域是否折叠 */
|
||||
extraCollapse: boolean;
|
||||
/** 侧边栏固定按钮是否可见 */
|
||||
fixedButton: boolean;
|
||||
/** 侧边栏是否隐藏 - css */
|
||||
hidden: boolean;
|
||||
/** 侧边栏宽度 */
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface ShortcutKeyPreferences {
|
||||
/** 是否启用快捷键-全局 */
|
||||
enable: boolean;
|
||||
/** 是否启用全局锁屏快捷键 */
|
||||
globalLockScreen: boolean;
|
||||
/** 是否启用全局注销快捷键 */
|
||||
globalLogout: boolean;
|
||||
/** 是否启用全局偏好设置快捷键 */
|
||||
globalPreferences: boolean;
|
||||
/** 是否启用全局搜索快捷键 */
|
||||
globalSearch: boolean;
|
||||
}
|
||||
|
||||
interface TabbarPreferences {
|
||||
/** 是否开启多标签页拖拽 */
|
||||
draggable: boolean;
|
||||
/** 是否开启多标签页 */
|
||||
enable: boolean;
|
||||
/** 标签页高度 */
|
||||
height: number;
|
||||
/** 开启标签页缓存功能 */
|
||||
keepAlive: boolean;
|
||||
/** 限制最大数量 */
|
||||
maxCount: number;
|
||||
/** 是否点击中键时关闭标签 */
|
||||
middleClickToClose: boolean;
|
||||
/** 是否持久化标签 */
|
||||
persist: boolean;
|
||||
/** 是否开启多标签页图标 */
|
||||
showIcon: boolean;
|
||||
/** 显示最大化按钮 */
|
||||
showMaximize: boolean;
|
||||
/** 显示更多按钮 */
|
||||
showMore: boolean;
|
||||
/** 标签页风格 */
|
||||
styleType: TabsStyleType;
|
||||
/** 是否开启鼠标滚轮响应 */
|
||||
wheelable: boolean;
|
||||
}
|
||||
|
||||
interface ThemePreferences {
|
||||
/** 内置主题名 */
|
||||
builtinType: BuiltinThemeType;
|
||||
/** 错误色 */
|
||||
colorDestructive: string;
|
||||
/** 主题色 */
|
||||
colorPrimary: string;
|
||||
/** 成功色 */
|
||||
colorSuccess: string;
|
||||
/** 警告色 */
|
||||
colorWarning: string;
|
||||
/** 当前主题 */
|
||||
mode: ThemeModeType;
|
||||
/** 圆角 */
|
||||
radius: string;
|
||||
/** 是否开启半深色header(只在theme='light'时生效) */
|
||||
semiDarkHeader: boolean;
|
||||
/** 是否开启半深色菜单(只在theme='light'时生效) */
|
||||
semiDarkSidebar: boolean;
|
||||
}
|
||||
|
||||
interface TransitionPreferences {
|
||||
/** 页面切换动画是否启用 */
|
||||
enable: boolean;
|
||||
// /** 是否开启页面加载loading */
|
||||
loading: boolean;
|
||||
/** 页面切换动画 */
|
||||
name: PageTransitionType | string;
|
||||
/** 是否开启页面加载进度动画 */
|
||||
progress: boolean;
|
||||
}
|
||||
|
||||
interface WidgetPreferences {
|
||||
/** 是否启用全屏部件 */
|
||||
fullscreen: boolean;
|
||||
/** 是否启用全局搜索部件 */
|
||||
globalSearch: boolean;
|
||||
/** 是否启用语言切换部件 */
|
||||
languageToggle: boolean;
|
||||
/** 是否开启锁屏功能 */
|
||||
lockScreen: boolean;
|
||||
/** 是否显示通知部件 */
|
||||
notification: boolean;
|
||||
/** 显示刷新按钮 */
|
||||
refresh: boolean;
|
||||
/** 是否显示侧边栏显示/隐藏部件 */
|
||||
sidebarToggle: boolean;
|
||||
/** 是否显示主题切换部件 */
|
||||
themeToggle: boolean;
|
||||
}
|
||||
|
||||
interface Preferences {
|
||||
/** 全局配置 */
|
||||
app: AppPreferences;
|
||||
/** 顶栏配置 */
|
||||
breadcrumb: BreadcrumbPreferences;
|
||||
/** 版权配置 */
|
||||
copyright: CopyrightPreferences;
|
||||
/** 底栏配置 */
|
||||
footer: FooterPreferences;
|
||||
/** 面包屑配置 */
|
||||
header: HeaderPreferences;
|
||||
/** logo配置 */
|
||||
logo: LogoPreferences;
|
||||
/** 导航配置 */
|
||||
navigation: NavigationPreferences;
|
||||
/** 快捷键配置 */
|
||||
shortcutKeys: ShortcutKeyPreferences;
|
||||
/** 侧边栏配置 */
|
||||
sidebar: SidebarPreferences;
|
||||
/** 标签页配置 */
|
||||
tabbar: TabbarPreferences;
|
||||
/** 主题配置 */
|
||||
theme: ThemePreferences;
|
||||
/** 动画配置 */
|
||||
transition: TransitionPreferences;
|
||||
/** 功能配置 */
|
||||
widget: WidgetPreferences;
|
||||
}
|
||||
|
||||
type PreferencesKeys = keyof Preferences;
|
||||
|
||||
interface InitialOptions {
|
||||
namespace: string;
|
||||
overrides?: DeepPartial<Preferences>;
|
||||
}
|
||||
export type {
|
||||
AppPreferences,
|
||||
BreadcrumbPreferences,
|
||||
FooterPreferences,
|
||||
HeaderPreferences,
|
||||
InitialOptions,
|
||||
LogoPreferences,
|
||||
NavigationPreferences,
|
||||
Preferences,
|
||||
PreferencesKeys,
|
||||
ShortcutKeyPreferences,
|
||||
SidebarPreferences,
|
||||
SupportedLanguagesType,
|
||||
TabbarPreferences,
|
||||
ThemePreferences,
|
||||
TransitionPreferences,
|
||||
WidgetPreferences,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user