From 176af225729e09d157622b67bc8f4f939489b662 Mon Sep 17 00:00:00 2001 From: dubai Date: Sun, 4 Jan 2026 14:10:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=8A=8A=20Yi.Vben5.Vue3=20=E4=B8=8B?= =?UTF-8?q?=E7=9A=84=20packages=20=E7=9B=AE=E5=BD=95=E6=94=BE=E5=87=BA?= =?UTF-8?q?=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + Yi.Vben5.Vue3/packages/@core/README.md | 3 + Yi.Vben5.Vue3/packages/@core/base/README.md | 5 + .../packages/@core/base/design/package.json | 41 + .../@core/base/design/src/css/global.css | 160 + .../@core/base/design/src/css/nprogress.css | 59 + .../@core/base/design/src/css/transition.css | 236 + .../packages/@core/base/design/src/css/ui.css | 87 + .../base/design/src/design-tokens/dark.css | 446 ++ .../base/design/src/design-tokens/default.css | 381 ++ .../base/design/src/design-tokens/index.ts | 4 + .../packages/@core/base/design/src/index.ts | 8 + .../@core/base/design/src/scss-bem/bem.scss | 34 + .../base/design/src/scss-bem/constants.scss | 5 + .../packages/@core/base/design/tsconfig.json | 6 + .../@core/base/design/vite.config.mts | 9 + .../packages/@core/base/icons/build.config.ts | 7 + .../packages/@core/base/icons/package.json | 41 + .../@core/base/icons/src/create-icon.ts | 30 + .../packages/@core/base/icons/src/index.ts | 16 + .../packages/@core/base/icons/src/lucide.ts | 68 + .../packages/@core/base/icons/tsconfig.json | 6 + .../@core/base/shared/build.config.ts | 14 + .../packages/@core/base/shared/package.json | 103 + .../cache/__tests__/storage-manager.test.ts | 130 + .../@core/base/shared/src/cache/index.ts | 1 + .../base/shared/src/cache/storage-manager.ts | 118 + .../@core/base/shared/src/cache/types.ts | 17 + .../src/color/__tests__/convert.test.ts | 58 + .../@core/base/shared/src/color/color.ts | 9 + .../@core/base/shared/src/color/convert.ts | 62 + .../@core/base/shared/src/color/generator.ts | 45 + .../@core/base/shared/src/color/index.ts | 3 + .../base/shared/src/constants/dict-enum.ts | 18 + .../base/shared/src/constants/globals.ts | 16 + .../@core/base/shared/src/constants/index.ts | 3 + .../@core/base/shared/src/constants/vben.ts | 26 + .../@core/base/shared/src/global-state.ts | 45 + .../packages/@core/base/shared/src/store.ts | 1 + .../shared/src/utils/__tests__/diff.test.ts | 53 + .../shared/src/utils/__tests__/dom.test.ts | 127 + .../src/utils/__tests__/inference.test.ts | 183 + .../shared/src/utils/__tests__/letter.test.ts | 116 + .../src/utils/__tests__/state-handler.test.ts | 60 + .../shared/src/utils/__tests__/tree.test.ts | 196 + .../shared/src/utils/__tests__/unique.test.ts | 60 + .../__tests__/update-css-variables.test.ts | 30 + .../shared/src/utils/__tests__/util.test.ts | 156 + .../shared/src/utils/__tests__/window.test.ts | 33 + .../@core/base/shared/src/utils/cn.ts | 10 + .../@core/base/shared/src/utils/date.ts | 26 + .../@core/base/shared/src/utils/diff.ts | 96 + .../@core/base/shared/src/utils/dom.ts | 95 + .../@core/base/shared/src/utils/download.ts | 157 + .../@core/base/shared/src/utils/index.ts | 20 + .../@core/base/shared/src/utils/inference.ts | 165 + .../@core/base/shared/src/utils/letter.ts | 47 + .../@core/base/shared/src/utils/merge.ts | 10 + .../@core/base/shared/src/utils/nprogress.ts | 43 + .../base/shared/src/utils/state-handler.ts | 50 + .../@core/base/shared/src/utils/to.ts | 21 + .../@core/base/shared/src/utils/tree.ts | 97 + .../@core/base/shared/src/utils/unique.ts | 15 + .../shared/src/utils/update-css-variables.ts | 35 + .../@core/base/shared/src/utils/util.ts | 44 + .../@core/base/shared/src/utils/window.ts | 37 + .../packages/@core/base/shared/tsconfig.json | 6 + .../@core/base/typings/build.config.ts | 7 + .../packages/@core/base/typings/package.json | 44 + .../packages/@core/base/typings/src/app.d.ts | 111 + .../@core/base/typings/src/basic.d.ts | 43 + .../@core/base/typings/src/helper.d.ts | 132 + .../packages/@core/base/typings/src/index.ts | 6 + .../@core/base/typings/src/menu-record.ts | 76 + .../packages/@core/base/typings/src/tabs.ts | 8 + .../@core/base/typings/src/vue-router.d.ts | 159 + .../packages/@core/base/typings/tsconfig.json | 6 + .../@core/base/typings/vue-router.d.ts | 9 + .../@core/composables/build.config.ts | 7 + .../packages/@core/composables/package.json | 47 + .../src/__tests__/use-sortable.test.ts | 48 + .../packages/@core/composables/src/index.ts | 13 + .../@core/composables/src/use-is-mobile.ts | 7 + .../@core/composables/src/use-layout-style.ts | 84 + .../@core/composables/src/use-namespace.ts | 106 + .../composables/src/use-priority-value.ts | 93 + .../@core/composables/src/use-scroll-lock.ts | 54 + .../src/use-simple-locale/README.md | 3 + .../src/use-simple-locale/index.ts | 26 + .../src/use-simple-locale/messages.ts | 24 + .../@core/composables/src/use-sortable.ts | 29 + .../packages/@core/composables/tsconfig.json | 6 + .../__snapshots__/config.test.ts.snap | 136 + .../preferences/__tests__/config.test.ts | 10 + .../preferences/__tests__/preferences.test.ts | 253 + .../@core/preferences/build.config.ts | 7 + .../packages/@core/preferences/package.json | 37 + .../packages/@core/preferences/src/config.ts | 138 + .../@core/preferences/src/constants.ts | 88 + .../packages/@core/preferences/src/index.ts | 35 + .../@core/preferences/src/preferences.ts | 235 + .../packages/@core/preferences/src/types.ts | 324 ++ .../preferences/src/update-css-variables.ts | 116 + .../@core/preferences/src/use-preferences.ts | 253 + .../packages/@core/preferences/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/@core/ui-kit/README.md | 3 + .../ui-kit/form-ui/__tests__/form-api.test.ts | 189 + .../@core/ui-kit/form-ui/build.config.ts | 21 + .../@core/ui-kit/form-ui/package.json | 52 + .../@core/ui-kit/form-ui/postcss.config.mjs | 1 + .../form-ui/src/components/form-actions.vue | 160 + .../@core/ui-kit/form-ui/src/config.ts | 87 + .../@core/ui-kit/form-ui/src/form-api.ts | 596 +++ .../ui-kit/form-ui/src/form-render/context.ts | 24 + .../form-ui/src/form-render/dependencies.ts | 124 + .../form-ui/src/form-render/expandable.ts | 105 + .../form-ui/src/form-render/form-field.vue | 394 ++ .../form-ui/src/form-render/form-label.vue | 31 + .../ui-kit/form-ui/src/form-render/form.vue | 165 + .../ui-kit/form-ui/src/form-render/helper.ts | 60 + .../ui-kit/form-ui/src/form-render/index.ts | 3 + .../@core/ui-kit/form-ui/src/index.ts | 12 + .../@core/ui-kit/form-ui/src/types.ts | 442 ++ .../ui-kit/form-ui/src/use-form-context.ts | 109 + .../@core/ui-kit/form-ui/src/use-vben-form.ts | 50 + .../@core/ui-kit/form-ui/src/vben-form.vue | 77 + .../ui-kit/form-ui/src/vben-use-form.vue | 148 + .../@core/ui-kit/form-ui/tailwind.config.mjs | 1 + .../@core/ui-kit/form-ui/tsconfig.json | 6 + .../@core/ui-kit/layout-ui/build.config.ts | 21 + .../@core/ui-kit/layout-ui/package.json | 48 + .../@core/ui-kit/layout-ui/postcss.config.mjs | 1 + .../ui-kit/layout-ui/src/components/index.ts | 5 + .../src/components/layout-content.vue | 62 + .../src/components/layout-footer.vue | 44 + .../src/components/layout-header.vue | 77 + .../src/components/layout-sidebar.vue | 322 ++ .../src/components/layout-tabbar.vue | 30 + .../layout-ui/src/components/widgets/index.ts | 2 + .../widgets/sidebar-collapse-button.vue | 19 + .../widgets/sidebar-fixed-button.vue | 19 + .../ui-kit/layout-ui/src/hooks/use-layout.ts | 53 + .../@core/ui-kit/layout-ui/src/index.ts | 2 + .../@core/ui-kit/layout-ui/src/vben-layout.ts | 175 + .../ui-kit/layout-ui/src/vben-layout.vue | 616 +++ .../ui-kit/layout-ui/tailwind.config.mjs | 1 + .../@core/ui-kit/layout-ui/tsconfig.json | 6 + .../packages/@core/ui-kit/menu-ui/README.md | 1 + .../@core/ui-kit/menu-ui/build.config.ts | 26 + .../@core/ui-kit/menu-ui/package.json | 48 + .../@core/ui-kit/menu-ui/postcss.config.mjs | 1 + .../src/components/collapse-transition.vue | 96 + .../ui-kit/menu-ui/src/components/index.ts | 4 + .../menu-ui/src/components/menu-badge-dot.vue | 28 + .../menu-ui/src/components/menu-badge.vue | 57 + .../menu-ui/src/components/menu-item.vue | 122 + .../ui-kit/menu-ui/src/components/menu.vue | 872 ++++ .../src/components/normal-menu/index.ts | 2 + .../src/components/normal-menu/normal-menu.ts | 27 + .../components/normal-menu/normal-menu.vue | 161 + .../src/components/sub-menu-content.vue | 105 + .../menu-ui/src/components/sub-menu.vue | 275 + .../@core/ui-kit/menu-ui/src/hooks/index.ts | 2 + .../menu-ui/src/hooks/use-menu-context.ts | 55 + .../menu-ui/src/hooks/use-menu-scroll.ts | 46 + .../ui-kit/menu-ui/src/hooks/use-menu.ts | 48 + .../@core/ui-kit/menu-ui/src/index.ts | 4 + .../@core/ui-kit/menu-ui/src/menu.vue | 32 + .../@core/ui-kit/menu-ui/src/sub-menu.vue | 71 + .../@core/ui-kit/menu-ui/src/types.ts | 144 + .../@core/ui-kit/menu-ui/src/utils/index.ts | 52 + .../@core/ui-kit/menu-ui/tailwind.config.mjs | 1 + .../@core/ui-kit/menu-ui/tsconfig.json | 6 + .../@core/ui-kit/popup-ui/build.config.ts | 21 + .../@core/ui-kit/popup-ui/package.json | 48 + .../@core/ui-kit/popup-ui/postcss.config.mjs | 1 + .../ui-kit/popup-ui/src/alert/AlertBuilder.ts | 244 + .../@core/ui-kit/popup-ui/src/alert/alert.ts | 99 + .../@core/ui-kit/popup-ui/src/alert/alert.vue | 210 + .../@core/ui-kit/popup-ui/src/alert/index.ts | 14 + .../src/drawer/__tests__/drawer-api.test.ts | 116 + .../ui-kit/popup-ui/src/drawer/drawer-api.ts | 193 + .../ui-kit/popup-ui/src/drawer/drawer.ts | 179 + .../ui-kit/popup-ui/src/drawer/drawer.vue | 332 ++ .../@core/ui-kit/popup-ui/src/drawer/index.ts | 3 + .../ui-kit/popup-ui/src/drawer/use-drawer.ts | 142 + .../@core/ui-kit/popup-ui/src/index.ts | 3 + .../src/modal/__tests__/modal-api.test.ts | 117 + .../@core/ui-kit/popup-ui/src/modal/index.ts | 3 + .../ui-kit/popup-ui/src/modal/modal-api.ts | 202 + .../@core/ui-kit/popup-ui/src/modal/modal.ts | 189 + .../@core/ui-kit/popup-ui/src/modal/modal.vue | 358 ++ .../popup-ui/src/modal/use-modal-draggable.ts | 133 + .../ui-kit/popup-ui/src/modal/use-modal.ts | 151 + .../@core/ui-kit/popup-ui/tailwind.config.mjs | 1 + .../@core/ui-kit/popup-ui/tsconfig.json | 6 + .../@core/ui-kit/shadcn-ui/build.config.ts | 27 + .../@core/ui-kit/shadcn-ui/components.json | 16 + .../@core/ui-kit/shadcn-ui/package.json | 54 + .../@core/ui-kit/shadcn-ui/postcss.config.mjs | 1 + .../src/components/avatar/avatar.vue | 76 + .../shadcn-ui/src/components/avatar/index.ts | 1 + .../src/components/back-top/back-top.vue | 43 + .../src/components/back-top/backtop.ts | 38 + .../src/components/back-top/index.ts | 1 + .../src/components/back-top/use-backtop.ts | 45 + .../breadcrumb/breadcrumb-background.vue | 109 + .../components/breadcrumb/breadcrumb-view.vue | 39 + .../src/components/breadcrumb/breadcrumb.vue | 98 + .../src/components/breadcrumb/index.ts | 3 + .../src/components/breadcrumb/types.ts | 16 + .../src/components/button/button-group.vue | 98 + .../shadcn-ui/src/components/button/button.ts | 53 + .../src/components/button/button.vue | 42 + .../components/button/check-button-group.vue | 196 + .../src/components/button/icon-button.vue | 68 + .../shadcn-ui/src/components/button/index.ts | 5 + .../src/components/checkbox/checkbox.vue | 26 + .../src/components/checkbox/index.ts | 1 + .../components/context-menu/context-menu.vue | 97 + .../src/components/context-menu/index.ts | 3 + .../src/components/context-menu/interface.ts | 38 + .../count-to-animator/count-to-animator.vue | 128 + .../src/components/count-to-animator/index.ts | 1 + .../dropdown-menu/dropdown-menu.vue | 49 + .../dropdown-menu/dropdown-radio-menu.vue | 52 + .../src/components/dropdown-menu/index.ts | 4 + .../src/components/dropdown-menu/interface.ts | 32 + .../expandable-arrow/expandable-arrow.vue | 31 + .../src/components/expandable-arrow/index.ts | 1 + .../components/full-screen/full-screen.vue | 28 + .../src/components/full-screen/index.ts | 1 + .../src/components/hover-card/hover-card.vue | 55 + .../src/components/hover-card/index.ts | 2 + .../shadcn-ui/src/components/icon/icon.vue | 34 + .../shadcn-ui/src/components/icon/index.ts | 1 + .../ui-kit/shadcn-ui/src/components/index.ts | 24 + .../src/components/input-captcha/index.ts | 1 + .../input-captcha/input-captcha.vue | 86 + .../src/components/input-password/index.ts | 1 + .../input-password/input-password.vue | 57 + .../input-password/password-strength.vue | 66 + .../shadcn-ui/src/components/logo/index.ts | 1 + .../shadcn-ui/src/components/logo/logo.vue | 73 + .../src/components/pin-input/index.ts | 3 + .../src/components/pin-input/input.vue | 120 + .../src/components/pin-input/types.ts | 30 + .../shadcn-ui/src/components/popover/index.ts | 1 + .../src/components/popover/popover.vue | 60 + .../src/components/render-content/index.ts | 1 + .../render-content/render-content.vue | 56 + .../src/components/scrollbar/index.ts | 1 + .../src/components/scrollbar/scrollbar.vue | 165 + .../src/components/segmented/index.ts | 3 + .../src/components/segmented/segmented.vue | 59 + .../components/segmented/tabs-indicator.vue | 35 + .../src/components/segmented/types.ts | 6 + .../shadcn-ui/src/components/select/index.ts | 1 + .../src/components/select/select.vue | 59 + .../src/components/spine-text/index.ts | 1 + .../src/components/spine-text/spine-text.vue | 49 + .../shadcn-ui/src/components/spinner/index.ts | 2 + .../src/components/spinner/loading.vue | 140 + .../src/components/spinner/spinner.vue | 137 + .../src/components/tooltip/help-tooltip.vue | 31 + .../shadcn-ui/src/components/tooltip/index.ts | 2 + .../src/components/tooltip/tooltip.vue | 42 + .../@core/ui-kit/shadcn-ui/src/index.ts | 3 + .../shadcn-ui/src/ui/accordion/Accordion.vue | 16 + .../src/ui/accordion/AccordionContent.vue | 26 + .../src/ui/accordion/AccordionItem.vue | 23 + .../src/ui/accordion/AccordionTrigger.vue | 37 + .../shadcn-ui/src/ui/accordion/index.ts | 4 + .../src/ui/alert-dialog/AlertDialog.vue | 16 + .../src/ui/alert-dialog/AlertDialogAction.vue | 13 + .../src/ui/alert-dialog/AlertDialogCancel.vue | 13 + .../ui/alert-dialog/AlertDialogContent.vue | 101 + .../alert-dialog/AlertDialogDescription.vue | 28 + .../ui/alert-dialog/AlertDialogOverlay.vue | 8 + .../src/ui/alert-dialog/AlertDialogTitle.vue | 30 + .../shadcn-ui/src/ui/alert-dialog/index.ts | 6 + .../ui-kit/shadcn-ui/src/ui/avatar/Avatar.vue | 26 + .../src/ui/avatar/AvatarFallback.vue | 13 + .../shadcn-ui/src/ui/avatar/AvatarImage.vue | 11 + .../ui-kit/shadcn-ui/src/ui/avatar/avatar.ts | 22 + .../ui-kit/shadcn-ui/src/ui/avatar/index.ts | 4 + .../ui-kit/shadcn-ui/src/ui/badge/Badge.vue | 18 + .../ui-kit/shadcn-ui/src/ui/badge/badge.ts | 25 + .../ui-kit/shadcn-ui/src/ui/badge/index.ts | 3 + .../src/ui/breadcrumb/Breadcrumb.vue | 11 + .../src/ui/breadcrumb/BreadcrumbEllipsis.vue | 22 + .../src/ui/breadcrumb/BreadcrumbItem.vue | 17 + .../src/ui/breadcrumb/BreadcrumbLink.vue | 20 + .../src/ui/breadcrumb/BreadcrumbList.vue | 20 + .../src/ui/breadcrumb/BreadcrumbPage.vue | 18 + .../src/ui/breadcrumb/BreadcrumbSeparator.vue | 21 + .../shadcn-ui/src/ui/breadcrumb/index.ts | 7 + .../ui-kit/shadcn-ui/src/ui/button/Button.vue | 31 + .../ui-kit/shadcn-ui/src/ui/button/button.ts | 34 + .../ui-kit/shadcn-ui/src/ui/button/index.ts | 5 + .../ui-kit/shadcn-ui/src/ui/button/types.ts | 20 + .../ui-kit/shadcn-ui/src/ui/card/Card.vue | 20 + .../shadcn-ui/src/ui/card/CardContent.vue | 13 + .../shadcn-ui/src/ui/card/CardDescription.vue | 13 + .../shadcn-ui/src/ui/card/CardFooter.vue | 13 + .../shadcn-ui/src/ui/card/CardHeader.vue | 13 + .../shadcn-ui/src/ui/card/CardTitle.vue | 13 + .../ui-kit/shadcn-ui/src/ui/card/index.ts | 6 + .../shadcn-ui/src/ui/checkbox/Checkbox.vue | 47 + .../ui-kit/shadcn-ui/src/ui/checkbox/index.ts | 1 + .../src/ui/context-menu/ContextMenu.vue | 18 + .../context-menu/ContextMenuCheckboxItem.vue | 45 + .../ui/context-menu/ContextMenuContent.vue | 43 + .../src/ui/context-menu/ContextMenuGroup.vue | 13 + .../src/ui/context-menu/ContextMenuItem.vue | 35 + .../src/ui/context-menu/ContextMenuLabel.vue | 32 + .../src/ui/context-menu/ContextMenuPortal.vue | 13 + .../ui/context-menu/ContextMenuRadioGroup.vue | 19 + .../ui/context-menu/ContextMenuRadioItem.vue | 45 + .../ui/context-menu/ContextMenuSeparator.vue | 22 + .../ui/context-menu/ContextMenuShortcut.vue | 17 + .../src/ui/context-menu/ContextMenuSub.vue | 16 + .../ui/context-menu/ContextMenuSubContent.vue | 35 + .../ui/context-menu/ContextMenuSubTrigger.vue | 39 + .../ui/context-menu/ContextMenuTrigger.vue | 15 + .../shadcn-ui/src/ui/context-menu/index.ts | 14 + .../ui-kit/shadcn-ui/src/ui/dialog/Dialog.vue | 16 + .../shadcn-ui/src/ui/dialog/DialogClose.vue | 13 + .../shadcn-ui/src/ui/dialog/DialogContent.vue | 125 + .../src/ui/dialog/DialogDescription.vue | 26 + .../shadcn-ui/src/ui/dialog/DialogFooter.vue | 15 + .../shadcn-ui/src/ui/dialog/DialogHeader.vue | 15 + .../shadcn-ui/src/ui/dialog/DialogOverlay.vue | 11 + .../src/ui/dialog/DialogScrollContent.vue | 69 + .../shadcn-ui/src/ui/dialog/DialogTitle.vue | 28 + .../shadcn-ui/src/ui/dialog/DialogTrigger.vue | 13 + .../ui-kit/shadcn-ui/src/ui/dialog/index.ts | 9 + .../src/ui/dropdown-menu/DropdownMenu.vue | 18 + .../DropdownMenuCheckboxItem.vue | 45 + .../ui/dropdown-menu/DropdownMenuContent.vue | 48 + .../ui/dropdown-menu/DropdownMenuGroup.vue | 13 + .../src/ui/dropdown-menu/DropdownMenuItem.vue | 34 + .../ui/dropdown-menu/DropdownMenuLabel.vue | 30 + .../dropdown-menu/DropdownMenuRadioGroup.vue | 19 + .../dropdown-menu/DropdownMenuRadioItem.vue | 46 + .../dropdown-menu/DropdownMenuSeparator.vue | 26 + .../ui/dropdown-menu/DropdownMenuShortcut.vue | 13 + .../src/ui/dropdown-menu/DropdownMenuSub.vue | 16 + .../dropdown-menu/DropdownMenuSubContent.vue | 35 + .../dropdown-menu/DropdownMenuSubTrigger.vue | 33 + .../ui/dropdown-menu/DropdownMenuTrigger.vue | 15 + .../shadcn-ui/src/ui/dropdown-menu/index.ts | 16 + .../shadcn-ui/src/ui/form/FormControl.vue | 19 + .../shadcn-ui/src/ui/form/FormDescription.vue | 20 + .../ui-kit/shadcn-ui/src/ui/form/FormItem.vue | 20 + .../shadcn-ui/src/ui/form/FormLabel.vue | 18 + .../shadcn-ui/src/ui/form/FormMessage.vue | 18 + .../ui-kit/shadcn-ui/src/ui/form/index.ts | 11 + .../shadcn-ui/src/ui/form/injectionKeys.ts | 4 + .../shadcn-ui/src/ui/form/useFormField.ts | 38 + .../shadcn-ui/src/ui/hover-card/HoverCard.vue | 16 + .../src/ui/hover-card/HoverCardContent.vue | 40 + .../src/ui/hover-card/HoverCardTrigger.vue | 13 + .../shadcn-ui/src/ui/hover-card/index.ts | 3 + .../@core/ui-kit/shadcn-ui/src/ui/index.ts | 31 + .../ui-kit/shadcn-ui/src/ui/input/Input.vue | 37 + .../ui-kit/shadcn-ui/src/ui/input/index.ts | 1 + .../ui-kit/shadcn-ui/src/ui/label/Label.vue | 29 + .../ui-kit/shadcn-ui/src/ui/label/index.ts | 1 + .../src/ui/number-field/NumberField.vue | 24 + .../ui/number-field/NumberFieldContent.vue | 20 + .../ui/number-field/NumberFieldDecrement.vue | 35 + .../ui/number-field/NumberFieldIncrement.vue | 35 + .../src/ui/number-field/NumberFieldInput.vue | 16 + .../shadcn-ui/src/ui/number-field/index.ts | 5 + .../src/ui/pagination/PaginationEllipsis.vue | 27 + .../src/ui/pagination/PaginationFirst.vue | 33 + .../src/ui/pagination/PaginationLast.vue | 33 + .../src/ui/pagination/PaginationNext.vue | 33 + .../src/ui/pagination/PaginationPrev.vue | 33 + .../shadcn-ui/src/ui/pagination/index.ts | 10 + .../shadcn-ui/src/ui/pin-input/PinInput.vue | 26 + .../src/ui/pin-input/PinInputGroup.vue | 23 + .../src/ui/pin-input/PinInputInput.vue | 28 + .../src/ui/pin-input/PinInputSeparator.vue | 17 + .../shadcn-ui/src/ui/pin-input/index.ts | 4 + .../shadcn-ui/src/ui/popover/Popover.vue | 16 + .../src/ui/popover/PopoverContent.vue | 44 + .../src/ui/popover/PopoverTrigger.vue | 13 + .../ui-kit/shadcn-ui/src/ui/popover/index.ts | 4 + .../src/ui/radio-group/RadioGroup.vue | 24 + .../src/ui/radio-group/RadioGroupItem.vue | 38 + .../shadcn-ui/src/ui/radio-group/index.ts | 2 + .../src/ui/resizable/ResizableHandle.vue | 47 + .../src/ui/resizable/ResizablePanelGroup.vue | 34 + .../shadcn-ui/src/ui/resizable/index.ts | 3 + .../src/ui/scroll-area/ScrollArea.vue | 48 + .../src/ui/scroll-area/ScrollBar.vue | 38 + .../shadcn-ui/src/ui/scroll-area/index.ts | 2 + .../ui-kit/shadcn-ui/src/ui/select/Select.vue | 16 + .../shadcn-ui/src/ui/select/SelectContent.vue | 67 + .../shadcn-ui/src/ui/select/SelectGroup.vue | 21 + .../shadcn-ui/src/ui/select/SelectItem.vue | 45 + .../src/ui/select/SelectItemText.vue | 13 + .../shadcn-ui/src/ui/select/SelectLabel.vue | 14 + .../src/ui/select/SelectScrollDownButton.vue | 31 + .../src/ui/select/SelectScrollUpButton.vue | 31 + .../src/ui/select/SelectSeparator.vue | 22 + .../shadcn-ui/src/ui/select/SelectTrigger.vue | 37 + .../shadcn-ui/src/ui/select/SelectValue.vue | 13 + .../ui-kit/shadcn-ui/src/ui/select/index.ts | 11 + .../shadcn-ui/src/ui/separator/Separator.vue | 42 + .../shadcn-ui/src/ui/separator/index.ts | 1 + .../ui-kit/shadcn-ui/src/ui/sheet/Sheet.vue | 16 + .../shadcn-ui/src/ui/sheet/SheetClose.vue | 13 + .../shadcn-ui/src/ui/sheet/SheetContent.vue | 107 + .../src/ui/sheet/SheetDescription.vue | 24 + .../shadcn-ui/src/ui/sheet/SheetFooter.vue | 15 + .../shadcn-ui/src/ui/sheet/SheetHeader.vue | 11 + .../shadcn-ui/src/ui/sheet/SheetOverlay.vue | 11 + .../shadcn-ui/src/ui/sheet/SheetTitle.vue | 24 + .../shadcn-ui/src/ui/sheet/SheetTrigger.vue | 13 + .../ui-kit/shadcn-ui/src/ui/sheet/index.ts | 10 + .../ui-kit/shadcn-ui/src/ui/sheet/sheet.ts | 24 + .../ui-kit/shadcn-ui/src/ui/switch/Switch.vue | 39 + .../ui-kit/shadcn-ui/src/ui/switch/index.ts | 1 + .../ui-kit/shadcn-ui/src/ui/tabs/Tabs.vue | 16 + .../shadcn-ui/src/ui/tabs/TabsContent.vue | 29 + .../ui-kit/shadcn-ui/src/ui/tabs/TabsList.vue | 29 + .../shadcn-ui/src/ui/tabs/TabsTrigger.vue | 31 + .../ui-kit/shadcn-ui/src/ui/tabs/index.ts | 5 + .../shadcn-ui/src/ui/textarea/Textarea.vue | 32 + .../ui-kit/shadcn-ui/src/ui/textarea/index.ts | 1 + .../src/ui/toggle-group/ToggleGroup.vue | 42 + .../src/ui/toggle-group/ToggleGroupItem.vue | 46 + .../shadcn-ui/src/ui/toggle-group/index.ts | 2 + .../ui-kit/shadcn-ui/src/ui/toggle/Toggle.vue | 45 + .../ui-kit/shadcn-ui/src/ui/toggle/index.ts | 2 + .../ui-kit/shadcn-ui/src/ui/toggle/toggle.ts | 27 + .../shadcn-ui/src/ui/tooltip/Tooltip.vue | 16 + .../src/ui/tooltip/TooltipContent.vue | 48 + .../src/ui/tooltip/TooltipProvider.vue | 13 + .../src/ui/tooltip/TooltipTrigger.vue | 13 + .../ui-kit/shadcn-ui/src/ui/tooltip/index.ts | 4 + .../ui-kit/shadcn-ui/src/ui/tree/index.ts | 2 + .../ui-kit/shadcn-ui/src/ui/tree/tree.vue | 384 ++ .../ui-kit/shadcn-ui/src/ui/tree/types.ts | 42 + .../ui-kit/shadcn-ui/tailwind.config.mjs | 1 + .../@core/ui-kit/shadcn-ui/tsconfig.json | 12 + .../@core/ui-kit/tabs-ui/build.config.ts | 21 + .../@core/ui-kit/tabs-ui/package.json | 47 + .../@core/ui-kit/tabs-ui/postcss.config.mjs | 1 + .../ui-kit/tabs-ui/src/components/index.ts | 2 + .../src/components/tabs-chrome/tabs.vue | 208 + .../tabs-ui/src/components/tabs/tabs.vue | 148 + .../tabs-ui/src/components/widgets/index.ts | 2 + .../src/components/widgets/tool-more.vue | 18 + .../src/components/widgets/tool-screen.vue | 19 + .../@core/ui-kit/tabs-ui/src/index.ts | 3 + .../@core/ui-kit/tabs-ui/src/tabs-view.vue | 106 + .../@core/ui-kit/tabs-ui/src/types.ts | 73 + .../@core/ui-kit/tabs-ui/src/use-tabs-drag.ts | 123 + .../tabs-ui/src/use-tabs-view-scroll.ts | 202 + .../@core/ui-kit/tabs-ui/tailwind.config.mjs | 1 + .../@core/ui-kit/tabs-ui/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/constants/README.md | 19 + Yi.Vben5.Vue3/packages/constants/package.json | 25 + Yi.Vben5.Vue3/packages/constants/src/core.ts | 28 + Yi.Vben5.Vue3/packages/constants/src/index.ts | 2 + .../packages/constants/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/effects/README.md | 10 + .../packages/effects/access/package.json | 29 + .../effects/access/src/access-control.vue | 78 + .../packages/effects/access/src/accessible.ts | 171 + .../packages/effects/access/src/directive.ts | 42 + .../packages/effects/access/src/index.ts | 4 + .../packages/effects/access/src/use-access.ts | 115 + .../packages/effects/access/tsconfig.json | 6 + .../packages/effects/common-ui/package.json | 66 + .../api-component/api-component.vue | 288 ++ .../src/components/api-component/index.ts | 1 + .../captcha/hooks/useCaptchaPoints.ts | 19 + .../common-ui/src/components/captcha/index.ts | 6 + .../captcha/point-selection-captcha/index.vue | 175 + .../point-selection-captcha-card.vue | 82 + .../captcha/slider-captcha/index.vue | 241 + .../slider-captcha/slider-captcha-action.vue | 63 + .../slider-captcha/slider-captcha-bar.vue | 40 + .../slider-captcha/slider-captcha-content.vue | 52 + .../captcha/slider-rotate-captcha/index.vue | 213 + .../common-ui/src/components/captcha/types.ts | 174 + .../components/code-mirror/code-mirror.vue | 68 + .../src/components/code-mirror/data.ts | 24 + .../src/components/code-mirror/index.ts | 2 + .../src/components/col-page/col-page.vue | 107 + .../src/components/col-page/index.ts | 2 + .../src/components/col-page/types.ts | 26 + .../src/components/count-to/count-to.vue | 123 + .../src/components/count-to/index.ts | 2 + .../src/components/count-to/types.ts | 53 + .../ellipsis-text/ellipsis-text.vue | 232 + .../src/components/ellipsis-text/index.ts | 1 + .../components/icon-picker/icon-picker.vue | 326 ++ .../src/components/icon-picker/icons.ts | 56 + .../src/components/icon-picker/index.ts | 1 + .../effects/common-ui/src/components/index.ts | 35 + .../src/components/json-preview/index.ts | 1 + .../components/json-preview/json-preview.vue | 18 + .../src/components/json-viewer/index.ts | 3 + .../src/components/json-viewer/index.vue | 116 + .../src/components/json-viewer/style.scss | 98 + .../src/components/json-viewer/types.ts | 44 + .../src/components/loading/directive.ts | 132 + .../common-ui/src/components/loading/index.ts | 3 + .../src/components/loading/loading.vue | 39 + .../src/components/loading/spinner.vue | 28 + .../src/components/markdown/editor.vue | 136 + .../src/components/markdown/index.ts | 2 + .../src/components/markdown/preview.vue | 102 + .../components/page/__tests__/page.test.ts | 88 + .../common-ui/src/components/page/index.ts | 2 + .../common-ui/src/components/page/page.vue | 106 + .../common-ui/src/components/page/types.ts | 17 + .../common-ui/src/components/resize/index.ts | 1 + .../src/components/resize/resize.vue | 1122 ++++ .../src/components/tippy/directive.ts | 100 + .../common-ui/src/components/tippy/index.ts | 67 + .../packages/effects/common-ui/src/index.ts | 3 + .../effects/common-ui/src/ui/about/about.ts | 14 + .../effects/common-ui/src/ui/about/about.vue | 181 + .../effects/common-ui/src/ui/about/index.ts | 1 + .../src/ui/authentication/auth-title.vue | 13 + .../src/ui/authentication/code-login.vue | 119 + .../src/ui/authentication/forget-password.vue | 114 + .../common-ui/src/ui/authentication/index.ts | 12 + .../ui/authentication/login-expired-modal.vue | 95 + .../common-ui/src/ui/authentication/login.vue | 187 + .../src/ui/authentication/qrcode-login.vue | 92 + .../src/ui/authentication/register.vue | 121 + .../ui/authentication/third-party-login.vue | 43 + .../common-ui/src/ui/authentication/types.ts | 117 + .../analysis/analysis-chart-card.vue | 24 + .../analysis/analysis-charts-tabs.vue | 40 + .../dashboard/analysis/analysis-overview.vue | 55 + .../src/ui/dashboard/analysis/index.ts | 3 + .../common-ui/src/ui/dashboard/index.ts | 3 + .../common-ui/src/ui/dashboard/typing.ts | 48 + .../src/ui/dashboard/workbench/index.ts | 5 + .../dashboard/workbench/workbench-header.vue | 46 + .../dashboard/workbench/workbench-project.vue | 65 + .../workbench/workbench-quick-nav.vue | 56 + .../ui/dashboard/workbench/workbench-todo.vue | 63 + .../dashboard/workbench/workbench-trends.vue | 64 + .../common-ui/src/ui/fallback/fallback.ts | 25 + .../common-ui/src/ui/fallback/fallback.vue | 162 + .../src/ui/fallback/icons/icon-403.vue | 151 + .../src/ui/fallback/icons/icon-404.vue | 154 + .../src/ui/fallback/icons/icon-500.vue | 215 + .../ui/fallback/icons/icon-coming-soon.vue | 262 + .../src/ui/fallback/icons/icon-offline.vue | 112 + .../src/ui/fallback/icons/warning.svg | 1 + .../common-ui/src/ui/fallback/index.ts | 2 + .../effects/common-ui/src/ui/index.ts | 4 + .../packages/effects/common-ui/tsconfig.json | 6 + .../packages/effects/hooks/README.md | 19 + .../packages/effects/hooks/package.json | 33 + .../packages/effects/hooks/src/index.ts | 9 + .../effects/hooks/src/use-app-config.ts | 43 + .../effects/hooks/src/use-content-maximize.ts | 24 + .../effects/hooks/src/use-design-tokens.ts | 321 ++ .../effects/hooks/src/use-hover-toggle.ts | 163 + .../effects/hooks/src/use-pagination.ts | 58 + .../packages/effects/hooks/src/use-refresh.ts | 16 + .../packages/effects/hooks/src/use-tabs.ts | 132 + .../effects/hooks/src/use-watermark.ts | 84 + .../packages/effects/hooks/tsconfig.json | 9 + .../packages/effects/layouts/package.json | 43 + .../src/authentication/authentication.vue | 164 + .../layouts/src/authentication/form.vue | 33 + .../src/authentication/icons/slogan.vue | 4568 +++++++++++++++++ .../layouts/src/authentication/index.ts | 2 + .../layouts/src/authentication/toolbar.vue | 49 + .../layouts/src/authentication/types.ts | 1 + .../effects/layouts/src/basic/README.md | 7 + .../src/basic/content/content-spinner.vue | 12 + .../layouts/src/basic/content/content.vue | 148 + .../layouts/src/basic/content/index.ts | 2 + .../src/basic/content/use-content-spinner.ts | 50 + .../layouts/src/basic/copyright/copyright.vue | 48 + .../layouts/src/basic/copyright/index.ts | 1 + .../layouts/src/basic/footer/footer.vue | 11 + .../effects/layouts/src/basic/footer/index.ts | 1 + .../layouts/src/basic/header/header.vue | 183 + .../effects/layouts/src/basic/header/index.ts | 1 + .../effects/layouts/src/basic/index.ts | 1 + .../effects/layouts/src/basic/layout.vue | 385 ++ .../layouts/src/basic/menu/extra-menu.vue | 41 + .../effects/layouts/src/basic/menu/index.ts | 5 + .../effects/layouts/src/basic/menu/menu.vue | 45 + .../layouts/src/basic/menu/mixed-menu.vue | 43 + .../layouts/src/basic/menu/use-extra-menu.ts | 133 + .../layouts/src/basic/menu/use-mixed-menu.ts | 172 + .../layouts/src/basic/menu/use-navigation.ts | 63 + .../effects/layouts/src/basic/tabbar/index.ts | 2 + .../layouts/src/basic/tabbar/tabbar.vue | 75 + .../layouts/src/basic/tabbar/use-tabbar.ts | 227 + .../layouts/src/iframe/iframe-router-view.vue | 84 + .../layouts/src/iframe/iframe-view.vue | 3 + .../effects/layouts/src/iframe/index.ts | 2 + .../packages/effects/layouts/src/index.ts | 4 + .../layouts/src/widgets/breadcrumb.vue | 71 + .../widgets/check-updates/check-updates.vue | 134 + .../src/widgets/check-updates/index.ts | 1 + .../layouts/src/widgets/color-toggle.vue | 63 + .../widgets/global-search/global-search.vue | 157 + .../src/widgets/global-search/index.ts | 1 + .../widgets/global-search/search-panel.vue | 288 ++ .../effects/layouts/src/widgets/index.ts | 11 + .../layouts/src/widgets/language-toggle.vue | 38 + .../layouts/src/widgets/layout-toggle.vue | 61 + .../layouts/src/widgets/lock-screen/index.ts | 2 + .../widgets/lock-screen/lock-screen-modal.vue | 100 + .../src/widgets/lock-screen/lock-screen.vue | 156 + .../layouts/src/widgets/notification/index.ts | 3 + .../src/widgets/notification/notification.vue | 185 + .../layouts/src/widgets/notification/types.ts | 10 + .../src/widgets/preferences/blocks/block.vue | 22 + .../preferences/blocks/checkbox-item.vue | 63 + .../preferences/blocks/general/animation.vue | 51 + .../preferences/blocks/general/general.vue | 31 + .../src/widgets/preferences/blocks/index.ts | 19 + .../widgets/preferences/blocks/input-item.vue | 50 + .../preferences/blocks/layout/breadcrumb.vue | 56 + .../preferences/blocks/layout/content.vue | 52 + .../preferences/blocks/layout/copyright.vue | 44 + .../preferences/blocks/layout/footer.vue | 17 + .../preferences/blocks/layout/header.vue | 74 + .../preferences/blocks/layout/layout.vue | 109 + .../preferences/blocks/layout/navigation.vue | 45 + .../preferences/blocks/layout/sidebar.vue | 100 + .../preferences/blocks/layout/tabbar.vue | 94 + .../preferences/blocks/layout/widget.vue | 71 + .../preferences/blocks/number-field-item.vue | 74 + .../preferences/blocks/select-item.vue | 66 + .../blocks/shortcut-keys/global.vue | 50 + .../preferences/blocks/switch-item.vue | 53 + .../preferences/blocks/theme/builtin.vue | 161 + .../preferences/blocks/theme/color-mode.vue | 26 + .../preferences/blocks/theme/radius.vue | 38 + .../preferences/blocks/theme/theme.vue | 82 + .../preferences/blocks/toggle-item.vue | 46 + .../preferences/icons/content-compact.vue | 119 + .../preferences/icons/full-content.vue | 50 + .../preferences/icons/header-mixed-nav.vue | 202 + .../widgets/preferences/icons/header-nav.vue | 119 + .../preferences/icons/header-sidebar-nav.vue | 177 + .../src/widgets/preferences/icons/index.ts | 12 + .../widgets/preferences/icons/mixed-nav.vue | 161 + .../src/widgets/preferences/icons/setting.vue | 12 + .../preferences/icons/sidebar-mixed-nav.vue | 173 + .../widgets/preferences/icons/sidebar-nav.vue | 153 + .../layouts/src/widgets/preferences/index.ts | 3 + .../preferences/preferences-button.vue | 19 + .../preferences/preferences-drawer.vue | 449 ++ .../src/widgets/preferences/preferences.vue | 70 + .../preferences/use-open-preferences.ts | 16 + .../layouts/src/widgets/theme-toggle/index.ts | 1 + .../src/widgets/theme-toggle/theme-button.vue | 185 + .../src/widgets/theme-toggle/theme-toggle.vue | 82 + .../src/widgets/user-dropdown/index.ts | 1 + .../widgets/user-dropdown/user-dropdown.vue | 272 + .../packages/effects/layouts/tsconfig.json | 6 + .../packages/effects/plugins/README.md | 28 + .../packages/effects/plugins/package.json | 47 + .../plugins/src/echarts/echarts-ui.vue | 15 + .../effects/plugins/src/echarts/echarts.ts | 70 + .../effects/plugins/src/echarts/index.ts | 3 + .../plugins/src/echarts/use-echarts.ts | 124 + .../effects/plugins/src/motion/index.ts | 8 + .../effects/plugins/src/motion/types.ts | 26 + .../effects/plugins/src/vxe-table/api.ts | 128 + .../effects/plugins/src/vxe-table/extends.ts | 80 + .../effects/plugins/src/vxe-table/index.ts | 10 + .../effects/plugins/src/vxe-table/init.ts | 131 + .../effects/plugins/src/vxe-table/style.css | 125 + .../effects/plugins/src/vxe-table/types.ts | 93 + .../plugins/src/vxe-table/use-vxe-grid.ts | 50 + .../plugins/src/vxe-table/use-vxe-grid.vue | 484 ++ .../packages/effects/plugins/tsconfig.json | 6 + .../packages/effects/request/package.json | 32 + .../packages/effects/request/src/index.ts | 3 + .../request/src/request-client/index.ts | 3 + .../request-client/modules/downloader.test.ts | 86 + .../src/request-client/modules/downloader.ts | 41 + .../src/request-client/modules/interceptor.ts | 40 + .../request-client/modules/uploader.test.ts | 118 + .../src/request-client/modules/uploader.ts | 42 + .../src/request-client/preset-interceptors.ts | 165 + .../src/request-client/request-client.test.ts | 99 + .../src/request-client/request-client.ts | 196 + .../request/src/request-client/types.ts | 109 + .../packages/effects/request/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/icons/README.md | 19 + Yi.Vben5.Vue3/packages/icons/package.json | 60 + .../icons/src/iconify-offline/index.ts | 186 + .../icons/src/iconify-offline/menu-icons.ts | 120 + .../packages/icons/src/iconify/index.ts | 18 + .../packages/icons/src/icons/empty-icon.vue | 27 + Yi.Vben5.Vue3/packages/icons/src/index.ts | 5 + .../icons/src/svg/icons/antdv-logo.svg | 29 + .../packages/icons/src/svg/icons/avatar-1.svg | 1 + .../packages/icons/src/svg/icons/avatar-2.svg | 1 + .../packages/icons/src/svg/icons/avatar-3.svg | 1 + .../packages/icons/src/svg/icons/avatar-4.svg | 1 + .../packages/icons/src/svg/icons/bell.svg | 1 + .../packages/icons/src/svg/icons/cake.svg | 1 + .../packages/icons/src/svg/icons/card.svg | 1 + .../packages/icons/src/svg/icons/download.svg | 1 + .../packages/icons/src/svg/icons/max-key.svg | 12 + .../packages/icons/src/svg/icons/message.svg | 1 + .../packages/icons/src/svg/icons/qq.svg | 1 + .../icons/src/svg/icons/snail-job.svg | 27 + .../packages/icons/src/svg/icons/topiam.svg | 29 + .../packages/icons/src/svg/icons/wechat.svg | 1 + Yi.Vben5.Vue3/packages/icons/src/svg/index.ts | 37 + Yi.Vben5.Vue3/packages/icons/src/svg/load.ts | 61 + Yi.Vben5.Vue3/packages/icons/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/locales/package.json | 28 + Yi.Vben5.Vue3/packages/locales/src/i18n.ts | 146 + Yi.Vben5.Vue3/packages/locales/src/index.ts | 30 + .../src/langs/en-US/authentication.json | 56 + .../locales/src/langs/en-US/common.json | 24 + .../locales/src/langs/en-US/preferences.json | 189 + .../packages/locales/src/langs/en-US/ui.json | 105 + .../src/langs/zh-CN/authentication.json | 56 + .../locales/src/langs/zh-CN/common.json | 24 + .../locales/src/langs/zh-CN/preferences.json | 189 + .../packages/locales/src/langs/zh-CN/ui.json | 105 + Yi.Vben5.Vue3/packages/locales/src/typing.ts | 25 + Yi.Vben5.Vue3/packages/locales/tsconfig.json | 6 + .../packages/preferences/package.json | 26 + .../packages/preferences/src/index.ts | 17 + .../packages/preferences/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/stores/package.json | 32 + Yi.Vben5.Vue3/packages/stores/shim-pinia.d.ts | 9 + Yi.Vben5.Vue3/packages/stores/src/index.ts | 3 + .../stores/src/modules/access.test.ts | 46 + .../packages/stores/src/modules/access.ts | 129 + .../packages/stores/src/modules/index.ts | 3 + .../stores/src/modules/tabbar.test.ts | 300 ++ .../packages/stores/src/modules/tabbar.ts | 658 +++ .../packages/stores/src/modules/user.test.ts | 37 + .../packages/stores/src/modules/user.ts | 72 + Yi.Vben5.Vue3/packages/stores/src/setup.ts | 60 + Yi.Vben5.Vue3/packages/stores/tsconfig.json | 5 + Yi.Vben5.Vue3/packages/styles/README.md | 19 + Yi.Vben5.Vue3/packages/styles/package.json | 34 + .../packages/styles/src/antd/index.css | 133 + .../packages/styles/src/ele/index.css | 44 + .../packages/styles/src/global/index.scss | 1 + Yi.Vben5.Vue3/packages/styles/src/index.ts | 1 + .../packages/styles/src/naive/index.css | 20 + Yi.Vben5.Vue3/packages/styles/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/types/README.md | 20 + Yi.Vben5.Vue3/packages/types/global.d.ts | 48 + Yi.Vben5.Vue3/packages/types/package.json | 27 + Yi.Vben5.Vue3/packages/types/src/index.ts | 2 + Yi.Vben5.Vue3/packages/types/src/user.ts | 11 + Yi.Vben5.Vue3/packages/types/tsconfig.json | 6 + Yi.Vben5.Vue3/packages/utils/README.md | 19 + Yi.Vben5.Vue3/packages/utils/package.json | 28 + .../helpers/__tests__/enum-options.test.ts | 19 + .../__tests__/find-menu-by-path.test.ts | 88 + .../helpers/__tests__/generate-menus.test.ts | 233 + .../generate-routes-frontend.test.ts | 105 + .../__tests__/merge-route-modules.test.ts | 68 + .../utils/src/helpers/enum-options.ts | 47 + .../utils/src/helpers/find-menu-by-path.ts | 37 + .../utils/src/helpers/generate-menus.ts | 90 + .../src/helpers/generate-routes-backend.ts | 89 + .../src/helpers/generate-routes-frontend.ts | 58 + .../utils/src/helpers/get-popup-container.ts | 66 + .../packages/utils/src/helpers/index.ts | 14 + .../utils/src/helpers/merge-route-modules.ts | 28 + .../packages/utils/src/helpers/mitt.ts | 135 + .../packages/utils/src/helpers/request.ts | 24 + .../utils/src/helpers/reset-routes.ts | 31 + .../packages/utils/src/helpers/safe.ts | 10 + .../packages/utils/src/helpers/tree.ts | 400 ++ .../src/helpers/unmount-global-loading.ts | 31 + .../packages/utils/src/helpers/uuid.ts | 42 + Yi.Vben5.Vue3/packages/utils/src/index.ts | 5 + Yi.Vben5.Vue3/packages/utils/tsconfig.json | 9 + 793 files changed, 52888 insertions(+) create mode 100644 Yi.Vben5.Vue3/packages/@core/README.md create mode 100644 Yi.Vben5.Vue3/packages/@core/base/README.md create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/css/global.css create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/css/nprogress.css create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/css/transition.css create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/css/ui.css create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/dark.css create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/default.css create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/bem.scss create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/constants.scss create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/design/vite.config.mts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/icons/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/icons/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/icons/src/create-icon.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/icons/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/icons/src/lucide.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/icons/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/storage-manager.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/color/__tests__/convert.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/color/color.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/color/convert.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/color/generator.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/color/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/dict-enum.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/globals.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/vben.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/global-state.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/store.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/diff.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/dom.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/inference.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/letter.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/state-handler.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/tree.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/unique.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/util.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/window.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/cn.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/date.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/diff.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/dom.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/download.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/inference.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/letter.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/merge.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/nprogress.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/state-handler.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/to.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/tree.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/unique.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/update-css-variables.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/util.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/window.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/shared/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/src/app.d.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/src/basic.d.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/src/helper.d.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/src/menu-record.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/src/tabs.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/src/vue-router.d.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/base/typings/vue-router.d.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/__tests__/use-sortable.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-is-mobile.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-layout-style.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-namespace.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-priority-value.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-scroll-lock.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/README.md create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/messages.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/src/use-sortable.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/composables/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/__tests__/config.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/__tests__/preferences.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/src/config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/src/constants.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/src/preferences.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/src/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/src/update-css-variables.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/src/use-preferences.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/preferences/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/README.md create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/postcss.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/components/form-actions.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-api.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/context.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/expandable.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/helper.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-form-context.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-vben-form.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-form.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-use-form.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tailwind.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/postcss.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-header.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-tabbar.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-collapse-button.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-fixed-button.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tailwind.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/README.md create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/postcss.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/collapse-transition.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge-dot.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-item.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu-content.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-context.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/sub-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/utils/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tailwind.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/postcss.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/use-drawer.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/modal/__tests__/modal-api.test.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/modal/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/modal/modal-api.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/modal/modal.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/modal/modal.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/modal/use-modal-draggable.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/modal/use-modal.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/tailwind.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/components.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/postcss.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/avatar/avatar.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/avatar/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/back-top/back-top.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/back-top/backtop.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/back-top/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/back-top/use-backtop.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-background.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb-view.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/breadcrumb.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/breadcrumb/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/button/button-group.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/button/button.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/button/button.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/button/check-button-group.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/button/icon-button.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/button/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/checkbox/checkbox.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/checkbox/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/context-menu/context-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/context-menu/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/context-menu/interface.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/count-to-animator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/count-to-animator/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/dropdown-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/dropdown-radio-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/dropdown-menu/interface.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/expandable-arrow/expandable-arrow.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/expandable-arrow/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/full-screen/full-screen.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/full-screen/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/hover-card/hover-card.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/hover-card/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/icon/icon.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/icon/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/input-captcha/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/input-captcha/input-captcha.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/input-password/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/input-password/input-password.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/input-password/password-strength.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/logo/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/logo/logo.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/pin-input/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/pin-input/input.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/pin-input/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/popover/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/popover/popover.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/render-content/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/scrollbar/scrollbar.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/segmented/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/segmented/tabs-indicator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/segmented/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/select/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/select/select.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/spine-text/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/spine-text/spine-text.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/spinner/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/spinner/loading.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/spinner/spinner.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/tooltip/help-tooltip.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/tooltip/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/components/tooltip/tooltip.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/accordion/Accordion.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/accordion/AccordionContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/accordion/AccordionItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/accordion/AccordionTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/accordion/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialog.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogAction.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogCancel.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogDescription.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogOverlay.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/AlertDialogTitle.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/alert-dialog/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/avatar/Avatar.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/avatar/AvatarFallback.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/avatar/AvatarImage.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/avatar/avatar.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/avatar/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/badge/Badge.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/badge/badge.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/badge/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/Breadcrumb.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbEllipsis.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbLink.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbList.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbPage.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/BreadcrumbSeparator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/breadcrumb/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/button/Button.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/button/button.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/button/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/button/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/card/Card.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/card/CardContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/card/CardDescription.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/card/CardFooter.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/card/CardHeader.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/card/CardTitle.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/card/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/checkbox/Checkbox.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/checkbox/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuCheckboxItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuLabel.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuPortal.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuRadioGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuRadioItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSeparator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuShortcut.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSub.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSubContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuSubTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/ContextMenuTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/context-menu/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/Dialog.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogClose.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogDescription.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogFooter.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogHeader.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogOverlay.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogScrollContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogTitle.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/DialogTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dialog/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenu.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuCheckboxItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuLabel.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuRadioGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuRadioItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSeparator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuShortcut.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSub.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSubContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuSubTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/DropdownMenuTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/dropdown-menu/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/FormControl.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/FormDescription.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/FormItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/FormLabel.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/FormMessage.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/injectionKeys.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/form/useFormField.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/HoverCard.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/HoverCardContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/HoverCardTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/hover-card/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/input/Input.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/input/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/label/Label.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/label/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberField.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldDecrement.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldIncrement.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/number-field/NumberFieldInput.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/number-field/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationEllipsis.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationFirst.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationLast.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationNext.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pagination/PaginationPrev.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pagination/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInput.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInputGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInputInput.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/PinInputSeparator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/pin-input/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/popover/Popover.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/popover/PopoverContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/popover/PopoverTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/popover/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/radio-group/RadioGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/radio-group/RadioGroupItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/radio-group/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/resizable/ResizableHandle.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/resizable/ResizablePanelGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/resizable/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/scroll-area/ScrollArea.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/scroll-area/ScrollBar.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/scroll-area/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/Select.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectItemText.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectLabel.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectScrollDownButton.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectScrollUpButton.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectSeparator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/SelectValue.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/select/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/separator/Separator.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/separator/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/Sheet.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetClose.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetDescription.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetFooter.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetHeader.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetOverlay.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetTitle.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/SheetTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/sheet/sheet.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/switch/Switch.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/switch/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tabs/Tabs.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsList.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tabs/TabsTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tabs/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/textarea/Textarea.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/textarea/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroup.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/ToggleGroupItem.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/toggle-group/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/Toggle.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/toggle/toggle.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/Tooltip.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipContent.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipProvider.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/TooltipTrigger.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tooltip/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tree/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tree/tree.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/src/ui/tree/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/tailwind.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/shadcn-ui/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/build.config.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/package.json create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/postcss.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/components/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/components/tabs-chrome/tabs.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/components/tabs/tabs.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/components/widgets/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-more.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/components/widgets/tool-screen.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/tabs-view.vue create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/types.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/use-tabs-drag.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/src/use-tabs-view-scroll.ts create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/tailwind.config.mjs create mode 100644 Yi.Vben5.Vue3/packages/@core/ui-kit/tabs-ui/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/constants/README.md create mode 100644 Yi.Vben5.Vue3/packages/constants/package.json create mode 100644 Yi.Vben5.Vue3/packages/constants/src/core.ts create mode 100644 Yi.Vben5.Vue3/packages/constants/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/constants/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/effects/README.md create mode 100644 Yi.Vben5.Vue3/packages/effects/access/package.json create mode 100644 Yi.Vben5.Vue3/packages/effects/access/src/access-control.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/access/src/accessible.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/access/src/directive.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/access/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/access/src/use-access.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/access/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/package.json create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/api-component/api-component.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/api-component/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/hooks/useCaptchaPoints.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/point-selection-captcha/index.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/point-selection-captcha/point-selection-captcha-card.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/slider-captcha/index.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-action.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-bar.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/slider-captcha/slider-captcha-content.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/slider-rotate-captcha/index.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/captcha/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/code-mirror/code-mirror.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/code-mirror/data.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/code-mirror/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/col-page/col-page.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/col-page/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/col-page/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/count-to/count-to.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/count-to/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/count-to/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/ellipsis-text/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/icon-picker/icon-picker.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/icon-picker/icons.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/icon-picker/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/json-preview/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/json-preview/json-preview.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/json-viewer/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/json-viewer/index.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/json-viewer/style.scss create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/json-viewer/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/loading/directive.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/loading/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/loading/loading.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/loading/spinner.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/markdown/editor.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/markdown/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/markdown/preview.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/page/__tests__/page.test.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/page/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/page/page.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/page/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/resize/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/resize/resize.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/tippy/directive.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/components/tippy/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/about/about.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/about/about.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/about/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/auth-title.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/code-login.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/forget-password.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/login-expired-modal.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/login.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/qrcode-login.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/register.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/third-party-login.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/authentication/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-chart-card.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-charts-tabs.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/analysis/analysis-overview.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/analysis/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/typing.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/workbench/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-header.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-project.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-quick-nav.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-todo.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/dashboard/workbench/workbench-trends.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/fallback.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/fallback.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/icons/icon-403.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/icons/icon-404.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/icons/icon-500.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/icons/icon-coming-soon.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/icons/icon-offline.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/icons/warning.svg create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/fallback/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/src/ui/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/common-ui/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/README.md create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/package.json create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-app-config.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-content-maximize.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-design-tokens.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-hover-toggle.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-pagination.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-refresh.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-tabs.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/src/use-watermark.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/hooks/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/package.json create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/authentication/authentication.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/authentication/form.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/authentication/icons/slogan.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/authentication/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/authentication/toolbar.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/authentication/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/README.md create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/content/content-spinner.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/content/content.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/content/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/content/use-content-spinner.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/copyright/copyright.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/copyright/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/footer/footer.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/footer/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/header/header.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/header/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/layout.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/menu/extra-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/menu/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/menu/menu.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/menu/mixed-menu.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/menu/use-extra-menu.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/menu/use-mixed-menu.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/menu/use-navigation.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/tabbar/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/tabbar/tabbar.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/basic/tabbar/use-tabbar.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/iframe/iframe-router-view.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/iframe/iframe-view.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/iframe/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/breadcrumb.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/check-updates/check-updates.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/check-updates/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/color-toggle.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/global-search/global-search.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/global-search/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/global-search/search-panel.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/language-toggle.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/layout-toggle.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/lock-screen/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/lock-screen/lock-screen-modal.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/lock-screen/lock-screen.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/notification/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/notification/notification.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/notification/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/block.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/checkbox-item.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/general/animation.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/general/general.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/input-item.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/breadcrumb.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/content.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/copyright.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/footer.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/header.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/layout.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/navigation.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/sidebar.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/tabbar.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/layout/widget.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/number-field-item.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/select-item.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/shortcut-keys/global.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/switch-item.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/theme/builtin.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/theme/color-mode.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/theme/radius.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/theme/theme.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/blocks/toggle-item.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/content-compact.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/full-content.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/header-mixed-nav.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/header-nav.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/header-sidebar-nav.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/mixed-nav.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/setting.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/sidebar-mixed-nav.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/icons/sidebar-nav.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/preferences-button.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/preferences.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/preferences/use-open-preferences.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/theme-toggle/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/theme-toggle/theme-toggle.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/user-dropdown/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/src/widgets/user-dropdown/user-dropdown.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/layouts/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/README.md create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/package.json create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/echarts/echarts-ui.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/echarts/echarts.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/echarts/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/echarts/use-echarts.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/motion/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/motion/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/api.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/extends.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/init.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/style.css create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/use-vxe-grid.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/src/vxe-table/use-vxe-grid.vue create mode 100644 Yi.Vben5.Vue3/packages/effects/plugins/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/effects/request/package.json create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/index.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/modules/downloader.test.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/modules/downloader.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/modules/interceptor.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/modules/uploader.test.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/modules/uploader.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/preset-interceptors.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/request-client.test.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/request-client.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/src/request-client/types.ts create mode 100644 Yi.Vben5.Vue3/packages/effects/request/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/icons/README.md create mode 100644 Yi.Vben5.Vue3/packages/icons/package.json create mode 100644 Yi.Vben5.Vue3/packages/icons/src/iconify-offline/index.ts create mode 100644 Yi.Vben5.Vue3/packages/icons/src/iconify-offline/menu-icons.ts create mode 100644 Yi.Vben5.Vue3/packages/icons/src/iconify/index.ts create mode 100644 Yi.Vben5.Vue3/packages/icons/src/icons/empty-icon.vue create mode 100644 Yi.Vben5.Vue3/packages/icons/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/antdv-logo.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/avatar-1.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/avatar-2.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/avatar-3.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/avatar-4.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/bell.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/cake.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/card.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/download.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/max-key.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/message.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/qq.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/snail-job.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/topiam.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/icons/wechat.svg create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/index.ts create mode 100644 Yi.Vben5.Vue3/packages/icons/src/svg/load.ts create mode 100644 Yi.Vben5.Vue3/packages/icons/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/locales/package.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/i18n.ts create mode 100644 Yi.Vben5.Vue3/packages/locales/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/en-US/authentication.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/en-US/common.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/en-US/preferences.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/en-US/ui.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/zh-CN/authentication.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/zh-CN/common.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/zh-CN/preferences.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/langs/zh-CN/ui.json create mode 100644 Yi.Vben5.Vue3/packages/locales/src/typing.ts create mode 100644 Yi.Vben5.Vue3/packages/locales/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/preferences/package.json create mode 100644 Yi.Vben5.Vue3/packages/preferences/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/preferences/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/stores/package.json create mode 100644 Yi.Vben5.Vue3/packages/stores/shim-pinia.d.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/modules/access.test.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/modules/access.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/modules/index.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/modules/tabbar.test.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/modules/tabbar.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/modules/user.test.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/modules/user.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/src/setup.ts create mode 100644 Yi.Vben5.Vue3/packages/stores/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/styles/README.md create mode 100644 Yi.Vben5.Vue3/packages/styles/package.json create mode 100644 Yi.Vben5.Vue3/packages/styles/src/antd/index.css create mode 100644 Yi.Vben5.Vue3/packages/styles/src/ele/index.css create mode 100644 Yi.Vben5.Vue3/packages/styles/src/global/index.scss create mode 100644 Yi.Vben5.Vue3/packages/styles/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/styles/src/naive/index.css create mode 100644 Yi.Vben5.Vue3/packages/styles/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/types/README.md create mode 100644 Yi.Vben5.Vue3/packages/types/global.d.ts create mode 100644 Yi.Vben5.Vue3/packages/types/package.json create mode 100644 Yi.Vben5.Vue3/packages/types/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/types/src/user.ts create mode 100644 Yi.Vben5.Vue3/packages/types/tsconfig.json create mode 100644 Yi.Vben5.Vue3/packages/utils/README.md create mode 100644 Yi.Vben5.Vue3/packages/utils/package.json create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/__tests__/enum-options.test.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/__tests__/find-menu-by-path.test.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/__tests__/generate-menus.test.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/__tests__/generate-routes-frontend.test.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/__tests__/merge-route-modules.test.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/enum-options.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/find-menu-by-path.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/generate-menus.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/generate-routes-backend.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/generate-routes-frontend.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/get-popup-container.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/index.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/merge-route-modules.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/mitt.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/request.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/reset-routes.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/safe.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/tree.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/unmount-global-loading.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/helpers/uuid.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/src/index.ts create mode 100644 Yi.Vben5.Vue3/packages/utils/tsconfig.json diff --git a/.gitignore b/.gitignore index 8ed69463..4989d1a4 100644 --- a/.gitignore +++ b/.gitignore @@ -154,6 +154,10 @@ PublishScripts/ *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* + +# 把 Yi.Vben5.Vue3 下的 packages 目录重新放出来 +!**/Yi.Vben5.Vue3/packages/ +!**/Yi.Vben5.Vue3/packages/** # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed diff --git a/Yi.Vben5.Vue3/packages/@core/README.md b/Yi.Vben5.Vue3/packages/@core/README.md new file mode 100644 index 00000000..8eb201dc --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/README.md @@ -0,0 +1,3 @@ +# @vben-core + +系统一些比较基础的SDK和UI组件库,该目录后续完善后,可能会迁移出去或者发布到npm,请勿将任何业务逻辑和业务包放在该目录。 diff --git a/Yi.Vben5.Vue3/packages/@core/base/README.md b/Yi.Vben5.Vue3/packages/@core/base/README.md new file mode 100644 index 00000000..cc745b4e --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/README.md @@ -0,0 +1,5 @@ +# base + +基础共享包,请勿引入 workspace 依赖 + +- diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/package.json b/Yi.Vben5.Vue3/packages/@core/base/design/package.json new file mode 100644 index 00000000..33e92474 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vben-core/design", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/base/design" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm vite build", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist", + "src" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + "./bem": { + "development": "./src/scss-bem/bem.scss", + "default": "./dist/bem.scss" + }, + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/design.css" + } + }, + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.mjs" + } + } + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/css/global.css b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/global.css new file mode 100644 index 00000000..d1999098 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/global.css @@ -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; +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/css/nprogress.css b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/nprogress.css new file mode 100644 index 00000000..3503dab2 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/nprogress.css @@ -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); + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/css/transition.css b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/transition.css new file mode 100644 index 00000000..c1cb0e48 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/transition.css @@ -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; +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/css/ui.css b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/ui.css new file mode 100644 index 00000000..f7119c8b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/css/ui.css @@ -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); +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/dark.css b/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/dark.css new file mode 100644 index 00000000..38810415 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/dark.css @@ -0,0 +1,446 @@ +.dark, +.dark[data-theme='custom'], +.dark[data-theme='default'] { + /* Default background color of ...etc */ + --background: 222.34deg 10.43% 12.27%; + + /* 主体区域背景色 */ + --background-deep: 220deg 13.06% 9%; + --foreground: 0 0% 95%; + + /* Background color for */ + --card: 222.34deg 10.43% 12.27%; + + /* --card: 222.2 84% 4.9%; */ + --card-foreground: 210 40% 98%; + + /* Background color for popovers such as , , */ + + /* --popover: 222.82deg 8.43% 12.27%; */ + + /* 弹出层的背景色与主题区域背景色太过接近 */ + --popover: 0 0% 14.2%; + --popover-foreground: 210 40% 98%; + + /* Muted backgrounds such as , and */ + + /* --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 */ + + --destructive: 359.21 68.47% 56.47%; + --destructive-foreground: 0 0% 98%; + + /* Used for success actions such as */ + + --info: 180, 1.54%, 12.75%; + --info-foreground: 220, 4%, 58%; + + /* Used for success actions such as */ + + --success: 144 57% 58%; + --success-foreground: 0 0% 98%; + + /* Used for warning actions such as */ + + --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 , ...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: 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%; +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/default.css b/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/default.css new file mode 100644 index 00000000..6fb71f17 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/default.css @@ -0,0 +1,381 @@ +:root { + /** 弹出层的基础层级 **/ + --popup-z-index: 2000; + --font-family: -apple-system, blinkmacsystemfont, 'Segoe UI', roboto, + 'Helvetica Neue', arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + + /* Default background color of ...etc */ + --background: 0 0% 100%; + + /* 主体区域背景色 */ + --background-deep: 216 20.11% 95.47%; + --foreground: 210 6% 21%; + + /* Background color for */ + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + /* Background color for popovers such as , , */ + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + /* Muted backgrounds such as , and */ + + /* --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 */ + + --destructive: 359.33 100% 65.1%; + --destructive-foreground: 0 0% 98%; + + /* Used for success actions such as */ + + --info: 240, 5%, 96%; + --info-foreground: 220, 4%, 58%; + + /* Used for success actions such as */ + + --success: 144 57% 58%; + --success-foreground: 0 0% 98%; + + /* Used for warning actions such as */ + + --warning: 42 84% 61%; + --warning-foreground: 0 0% 98%; + + /* Secondary colors for */ + + --secondary: 240 5% 96%; + --secondary-foreground: 240 6% 10%; + + /* Used for accents such as hover effects on , ...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: 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%; +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/index.ts b/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/index.ts new file mode 100644 index 00000000..2d031d81 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/design-tokens/index.ts @@ -0,0 +1,4 @@ +import './default.css'; +import './dark.css'; + +export {}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/index.ts b/Yi.Vben5.Vue3/packages/@core/base/design/src/index.ts new file mode 100644 index 00000000..d7c05340 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/index.ts @@ -0,0 +1,8 @@ +import './design-tokens'; + +import './css/global.css'; +import './css/transition.css'; +import './css/nprogress.css'; +import './css/ui.css'; + +export {}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/bem.scss b/Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/bem.scss new file mode 100644 index 00000000..3910a305 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/bem.scss @@ -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; + } + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/constants.scss b/Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/constants.scss new file mode 100644 index 00000000..87c24efc --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/src/scss-bem/constants.scss @@ -0,0 +1,5 @@ +$namespace: 'vben' !default; +$common-separator: '-' !default; +$element-separator: '__' !default; +$modifier-separator: '--' !default; +$state-prefix: 'is' !default; diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/base/design/tsconfig.json new file mode 100644 index 00000000..ce1a891f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/design/vite.config.mts b/Yi.Vben5.Vue3/packages/@core/base/design/vite.config.mts new file mode 100644 index 00000000..935929d8 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/design/vite.config.mts @@ -0,0 +1,9 @@ +import { defineConfig } from '@vben/vite-config'; + +export default defineConfig(async () => { + return { + vite: { + publicDir: 'src/scss-bem', + }, + }; +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/icons/build.config.ts b/Yi.Vben5.Vue3/packages/@core/base/icons/build.config.ts new file mode 100644 index 00000000..97e572c5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/icons/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/icons/package.json b/Yi.Vben5.Vue3/packages/@core/base/icons/package.json new file mode 100644 index 00000000..9a349883 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/icons/package.json @@ -0,0 +1,41 @@ +{ + "name": "@vben-core/icons", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/base/icons" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild" + }, + "files": [ + "dist" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + } + }, + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@iconify/vue": "catalog:", + "lucide-vue-next": "catalog:", + "vue": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/icons/src/create-icon.ts b/Yi.Vben5.Vue3/packages/@core/base/icons/src/create-icon.ts new file mode 100644 index 00000000..9d9ba32c --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/icons/src/create-icon.ts @@ -0,0 +1,30 @@ +import { defineComponent, h } from 'vue'; + +import { addIcon, Icon, type IconifyIcon } from '@iconify/vue'; + +function createIconifyIcon(icon: string) { + return defineComponent({ + name: `Icon-${icon}`, + setup(props, { attrs }) { + return () => h(Icon, { icon, ...props, ...attrs }); + }, + }); +} + +/** + * 创建离线图标 + * @param icon 图标名称 建议与iconify的名称保持一致 + * @param iconComponent 从@iconify/icon-xxx/xxx导入的图标 + * @returns IconComponent + */ +function createIconifyOfflineIcon(icon: string, iconComponent: IconifyIcon) { + return defineComponent({ + name: `Icon-${icon}`, + setup(props, { attrs }) { + addIcon(icon, iconComponent); + return () => h(Icon, { icon, ...props, ...attrs }); + }, + }); +} + +export { createIconifyIcon, createIconifyOfflineIcon }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/icons/src/index.ts b/Yi.Vben5.Vue3/packages/@core/base/icons/src/index.ts new file mode 100644 index 00000000..2e3f54f9 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/icons/src/index.ts @@ -0,0 +1,16 @@ +export * from './create-icon'; + +export * from './lucide'; + +export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue'; +export { + addCollection, + addIcon, + Icon as IconifyIcon, + listIcons, +} from '@iconify/vue'; + +/** + * 从@iconify/vue/dist/offline'导出的组件为离线ICON 不支持在线 + * 从@iconify/vue'导出的组件为在能找到本地图标为离线 否则会在线获取(适用性更强) + */ diff --git a/Yi.Vben5.Vue3/packages/@core/base/icons/src/lucide.ts b/Yi.Vben5.Vue3/packages/@core/base/icons/src/lucide.ts new file mode 100644 index 00000000..70e6a426 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/icons/src/lucide.ts @@ -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'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/icons/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/base/icons/tsconfig.json new file mode 100644 index 00000000..ce1a891f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/icons/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/build.config.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/build.config.ts new file mode 100644 index 00000000..98e2209f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/build.config.ts @@ -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', + ], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/package.json b/Yi.Vben5.Vue3/packages/@core/base/shared/package.json new file mode 100644 index 00000000..b02cc6e4 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/package.json @@ -0,0 +1,103 @@ +{ + "name": "@vben-core/shared", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/base/shared" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild", + "stub": "pnpm unbuild --stub" + }, + "files": [ + "dist" + ], + "sideEffects": false, + "exports": { + "./constants": { + "types": "./src/constants/index.ts", + "development": "./src/constants/index.ts", + "default": "./dist/constants/index.mjs" + }, + "./utils": { + "types": "./src/utils/index.ts", + "development": "./src/utils/index.ts", + "default": "./dist/utils/index.mjs" + }, + "./color": { + "types": "./src/color/index.ts", + "development": "./src/color/index.ts", + "default": "./dist/color/index.mjs" + }, + "./cache": { + "types": "./src/cache/index.ts", + "development": "./src/cache/index.ts", + "default": "./dist/cache/index.mjs" + }, + "./store": { + "types": "./src/store.ts", + "development": "./src/store.ts", + "default": "./dist/store.mjs" + }, + "./global-state": { + "types": "./src/global-state.ts", + "development": "./src/global-state.ts", + "default": "./dist/global-state.mjs" + } + }, + "publishConfig": { + "exports": { + "./constants": { + "types": "./dist/constants/index.d.ts", + "default": "./dist/constants/index.mjs" + }, + "./utils": { + "types": "./dist/utils/index.d.ts", + "default": "./dist/utils/index.mjs" + }, + "./color": { + "types": "./dist/color/index.d.ts", + "default": "./dist/color/index.mjs" + }, + "./cache": { + "types": "./dist/cache/index.d.ts", + "default": "./dist/cache/index.mjs" + }, + "./store": { + "types": "./dist/store.d.ts", + "default": "./dist/store.mjs" + }, + "./global-state": { + "types": "./dist/global-state.d.ts", + "default": "./dist/global-state.mjs" + } + } + }, + "dependencies": { + "@ctrl/tinycolor": "catalog:", + "@tanstack/vue-store": "catalog:", + "@vue/shared": "catalog:", + "clsx": "catalog:", + "dayjs": "catalog:", + "defu": "catalog:", + "lodash.clonedeep": "catalog:", + "lodash.get": "catalog:", + "lodash.isequal": "catalog:", + "lodash.set": "catalog:", + "nprogress": "catalog:", + "tailwind-merge": "catalog:", + "theme-colors": "catalog:" + }, + "devDependencies": { + "@types/lodash.clonedeep": "catalog:", + "@types/lodash.get": "catalog:", + "@types/lodash.isequal": "catalog:", + "@types/lodash.set": "catalog:", + "@types/nprogress": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts new file mode 100644 index 00000000..a5abe5a7 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/__tests__/storage-manager.test.ts @@ -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(); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/index.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/index.ts new file mode 100644 index 00000000..8c44d7f1 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/index.ts @@ -0,0 +1 @@ +export * from './storage-manager'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/storage-manager.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/storage-manager.ts new file mode 100644 index 00000000..611cdb83 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/storage-manager.ts @@ -0,0 +1,118 @@ +type StorageType = 'localStorage' | 'sessionStorage'; + +interface StorageManagerOptions { + prefix?: string; + storageType?: StorageType; +} + +interface StorageItem { + expiry?: number; + value: T; +} + +class StorageManager { + private prefix: string; + private storage: Storage; + + constructor({ + prefix = '', + storageType = 'localStorage', + }: StorageManagerOptions = {}) { + this.prefix = prefix; + this.storage = + storageType === 'localStorage' + ? window.localStorage + : window.sessionStorage; + } + + /** + * 获取完整的存储键 + * @param key 原始键 + * @returns 带前缀的完整键 + */ + private getFullKey(key: string): string { + return `${this.prefix}-${key}`; + } + + /** + * 清除所有带前缀的存储项 + */ + clear(): void { + const keysToRemove: string[] = []; + for (let i = 0; i < this.storage.length; i++) { + const key = this.storage.key(i); + if (key && key.startsWith(this.prefix)) { + keysToRemove.push(key); + } + } + keysToRemove.forEach((key) => this.storage.removeItem(key)); + } + + /** + * 清除所有过期的存储项 + */ + clearExpiredItems(): void { + for (let i = 0; i < this.storage.length; i++) { + const key = this.storage.key(i); + if (key && key.startsWith(this.prefix)) { + const shortKey = key.replace(this.prefix, ''); + this.getItem(shortKey); // 调用 getItem 方法检查并移除过期项 + } + } + } + + /** + * 获取存储项 + * @param key 键 + * @param defaultValue 当项不存在或已过期时返回的默认值 + * @returns 值,如果项已过期或解析错误则返回默认值 + */ + getItem(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 = 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(key: string, value: T, ttl?: number): void { + const fullKey = this.getFullKey(key); + const expiry = ttl ? Date.now() + ttl : undefined; + const item: StorageItem = { expiry, value }; + try { + this.storage.setItem(fullKey, JSON.stringify(item)); + } catch (error) { + console.error(`Error setting item with key "${fullKey}":`, error); + } + } +} + +export { StorageManager }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/types.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/types.ts new file mode 100644 index 00000000..d8939208 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/cache/types.ts @@ -0,0 +1,17 @@ +type StorageType = 'localStorage' | 'sessionStorage'; + +interface StorageValue { + data: T; + expiry: null | number; +} + +interface IStorageCache { + clear(): void; + getItem(key: string): null | T; + key(index: number): null | string; + length(): number; + removeItem(key: string): void; + setItem(key: string, value: T, expiryInMinutes?: number): void; +} + +export type { IStorageCache, StorageType, StorageValue }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/__tests__/convert.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/__tests__/convert.test.ts new file mode 100644 index 00000000..fc4256c5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/__tests__/convert.test.ts @@ -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); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/color.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/color.ts new file mode 100644 index 00000000..e3cefdb2 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/color.ts @@ -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(); +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/convert.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/convert.ts new file mode 100644 index 00000000..dd7dff20 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/convert.ts @@ -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, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/generator.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/generator.ts new file mode 100644 index 00000000..ac7bd8ad --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/generator.ts @@ -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 = {}; + + 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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/index.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/index.ts new file mode 100644 index 00000000..9fade3d2 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/color/index.ts @@ -0,0 +1,3 @@ +export * from './color'; +export * from './convert'; +export * from './generator'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/dict-enum.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/dict-enum.ts new file mode 100644 index 00000000..ef13dc94 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/dict-enum.ts @@ -0,0 +1,18 @@ +export const DictEnum = { + SYS_COMMON_STATUS: 'sys_common_status', + SYS_DEVICE_TYPE: 'sys_device_type', // 设备类型 + SYS_GRANT_TYPE: 'sys_grant_type', // 授权类型 + SYS_NORMAL_DISABLE: 'sys_normal_disable', + SYS_NOTICE_STATUS: 'sys_notice_status', // 通知状态 + SYS_NOTICE_TYPE: 'sys_notice_type_vben5', // 通知类型 + SYS_OPER_TYPE: 'sys_oper_type', // 操作类型 + SYS_OSS_ACCESS_POLICY: 'oss_access_policy', // oss权限桶类型 + SYS_SHOW_HIDE: 'sys_show_hide', // 显示状态 + SYS_USER_SEX: 'sys_user_sex', // 性别 + SYS_YES_NO: 'sys_yes_no', // 是否 + WF_BUSINESS_STATUS: 'wf_business_status', // 业务状态 + WF_FORM_TYPE: 'wf_form_type', // 表单类型 + WF_TASK_STATUS: 'wf_task_status', // 任务状态 +} as const; + +export type DictEnumKey = keyof typeof DictEnum; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/globals.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/globals.ts new file mode 100644 index 00000000..3c699570 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/globals.ts @@ -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'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/index.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/index.ts new file mode 100644 index 00000000..6e818083 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/index.ts @@ -0,0 +1,3 @@ +export * from './dict-enum'; +export * from './globals'; +export * from './vben'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/vben.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/vben.ts new file mode 100644 index 00000000..0f6cbbe1 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/constants/vben.ts @@ -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'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/global-state.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/global-state.ts new file mode 100644 index 00000000..2d71356b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/global-state.ts @@ -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(); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/store.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/store.ts new file mode 100644 index 00000000..4b03afb0 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/store.ts @@ -0,0 +1 @@ +export * from '@tanstack/vue-store'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/diff.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/diff.test.ts new file mode 100644 index 00000000..cb3227b5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/diff.test.ts @@ -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 }); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/dom.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/dom.test.ts new file mode 100644 index 00000000..df51268a --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/dom.test.ts @@ -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, + }); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/inference.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/inference.test.ts new file mode 100644 index 00000000..7fda4650 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/inference.test.ts @@ -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(undefined, null, 0, 42)).toBe( + 0, + ); + expect(getFirstNonNullOrUndefined(null, undefined, 42, 123)).toBe( + 42, + ); + }); + + it('should return the first non-null and non-undefined value for a string array', () => { + expect( + getFirstNonNullOrUndefined(undefined, null, '', 'hello'), + ).toBe(''); + expect( + getFirstNonNullOrUndefined(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( + undefined, + null, + 'test', + 123, + { key: 'value' }, + ), + ).toBe('test'); + expect( + getFirstNonNullOrUndefined( + null, + undefined, + [1, 2, 3], + 'string', + ), + ).toEqual([1, 2, 3]); + }); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/letter.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/letter.test.ts new file mode 100644 index 00000000..a4aebaf7 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/letter.test.ts @@ -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'); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/state-handler.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/state-handler.test.ts new file mode 100644 index 00000000..d11be068 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/state-handler.test.ts @@ -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); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/tree.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/tree.test.ts new file mode 100644 index 00000000..afe43cc5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/tree.test.ts @@ -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( + 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) => node.name); + expect(values).toEqual([]); + }); + + it('handles tree with only root node', () => { + const rootNode = { name: 'A' }; + const values = traverseTreeValues( + [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( + 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' }, + ], + }, + ], + }, + ]); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/unique.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/unique.test.ts new file mode 100644 index 00000000..0aa9d61f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/unique.test.ts @@ -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' }, + ]); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts new file mode 100644 index 00000000..4a9cdadc --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/update-css-variables.test.ts @@ -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 = ``; + + // 要更新的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); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/util.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/util.test.ts new file mode 100644 index 00000000..0d87b318 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/util.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it } from 'vitest'; + +import { bindMethods, getNestedValue } from '../util'; + +class TestClass { + public value: string; + + constructor(value: string) { + this.value = value; + bindMethods(this); // 调用通用方法 + } + + getValue() { + return this.value; + } + + setValue(newValue: string) { + this.value = newValue; + } +} + +describe('bindMethods', () => { + it('should bind methods to the instance correctly', () => { + const instance = new TestClass('initial'); + + // 解构方法 + const { getValue } = instance; + + // 检查 getValue 是否能正确调用,并且 this 绑定了 instance + expect(getValue()).toBe('initial'); + }); + + it('should bind multiple methods', () => { + const instance = new TestClass('initial'); + + const { getValue, setValue } = instance; + + // 检查 getValue 和 setValue 方法是否正确绑定了 this + setValue('newValue'); + expect(getValue()).toBe('newValue'); + }); + + it('should not bind non-function properties', () => { + const instance = new TestClass('initial'); + + // 检查普通属性是否保持原样 + expect(instance.value).toBe('initial'); + }); + + it('should not bind constructor method', () => { + const instance = new TestClass('test'); + + // 检查 constructor 是否没有被绑定 + expect(instance.constructor.name).toBe('TestClass'); + }); + + it('should not bind getter/setter properties', () => { + class TestWithGetterSetter { + private _value: string = 'test'; + + constructor() { + bindMethods(this); + } + + get value() { + return this._value; + } + + set value(newValue: string) { + this._value = newValue; + } + } + + const instance = new TestWithGetterSetter(); + const { value } = instance; + + // Getter 和 setter 不应被绑定 + expect(value).toBe('test'); + }); +}); + +describe('getNestedValue', () => { + interface UserProfile { + age: number; + name: string; + } + + interface UserSettings { + theme: string; + } + + interface Data { + user: { + profile: UserProfile; + settings: UserSettings; + }; + } + + const data: Data = { + user: { + profile: { + age: 25, + name: 'Alice', + }, + settings: { + theme: 'dark', + }, + }, + }; + + it('should get a nested value when the path is valid', () => { + const result = getNestedValue(data, 'user.profile.name'); + expect(result).toBe('Alice'); + }); + + it('should return undefined for non-existent property', () => { + const result = getNestedValue(data, 'user.profile.gender'); + expect(result).toBeUndefined(); + }); + + it('should return undefined when accessing a non-existent deep path', () => { + const result = getNestedValue(data, 'user.nonexistent.field'); + expect(result).toBeUndefined(); + }); + + it('should return undefined if a middle level is undefined', () => { + const result = getNestedValue({ user: undefined }, 'user.profile.name'); + expect(result).toBeUndefined(); + }); + + it('should return the correct value for a nested setting', () => { + const result = getNestedValue(data, 'user.settings.theme'); + expect(result).toBe('dark'); + }); + + it('should work for a single-level path', () => { + const result = getNestedValue({ a: 1, b: 2 }, 'b'); + expect(result).toBe(2); + }); + + it('should return the entire object if path is empty', () => { + expect(() => getNestedValue(data, '')()).toThrow(); + }); + + it('should handle paths with array indexes', () => { + const complexData = { list: [{ name: 'Item1' }, { name: 'Item2' }] }; + const result = getNestedValue(complexData, 'list.1.name'); + expect(result).toBe('Item2'); + }); + + it('should return undefined when accessing an out-of-bounds array index', () => { + const complexData = { list: [{ name: 'Item1' }] }; + const result = getNestedValue(complexData, 'list.2.name'); + expect(result).toBeUndefined(); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/window.test.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/window.test.ts new file mode 100644 index 00000000..ebb04bb0 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/__tests__/window.test.ts @@ -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', + ); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/cn.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/cn.ts new file mode 100644 index 00000000..3a2f9773 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/cn.ts @@ -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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/date.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/date.ts new file mode 100644 index 00000000..3736b9ad --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/date.ts @@ -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); +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/diff.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/diff.ts new file mode 100644 index 00000000..449214d7 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/diff.ts @@ -0,0 +1,96 @@ +// type Diff = T; + +// 比较两个数组是否相等 + +function arraysEqual(a: T[], b: T[]): boolean { + if (a.length !== b.length) return false; + const counter = new Map(); + 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(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( +// oldObj: T, +// newObj: T, +// ignoreFields: (keyof T)[] = [], +// ): { [K in keyof T]?: Diff } | null { +// const difference: { [K in keyof T]?: Diff } = {}; + +// 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 = Partial<{ + [K in keyof T]: T[K] extends object ? DiffResult : T[K]; +}>; + +function diff>(obj1: T, obj2: T): DiffResult { + 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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/dom.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/dom.ts new file mode 100644 index 00000000..69617176 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/dom.ts @@ -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); +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/download.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/download.ts new file mode 100644 index 00000000..6f38ee5b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/download.ts @@ -0,0 +1,157 @@ +import { openWindow } from './window'; + +interface DownloadOptions { + fileName?: string; + source: T; + target?: string; +} + +const DEFAULT_FILENAME = 'downloaded_file'; + +/** + * 通过 URL 下载文件,支持跨域 + * @throws {Error} - 当下载失败时抛出错误 + */ +export async function downloadFileFromUrl({ + fileName, + source, + target = '_blank', +}: DownloadOptions): Promise { + 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): 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): 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 { + 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; +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/index.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/index.ts new file mode 100644 index 00000000..925af1c1 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/index.ts @@ -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'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/inference.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/inference.ts new file mode 100644 index 00000000..195d071b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/inference.ts @@ -0,0 +1,165 @@ +// eslint-disable-next-line vue/prefer-import-from-vue +import { isFunction, isObject, isString } from '@vue/shared'; + +/** + * 检查传入的值是否为undefined。 + * + * @param {unknown} value 要检查的值。 + * @returns {boolean} 如果值是undefined,返回true,否则返回false。 + */ +function isUndefined(value?: unknown): value is undefined { + return value === undefined; +} + +/** + * 检查传入的值是否为boolean + * @param value + * @returns 如果值是布尔值,返回true,否则返回false。 + */ +function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} + +/** + * 检查传入的值是否为空。 + * + * 以下情况将被认为是空: + * - 值为null。 + * - 值为undefined。 + * - 值为一个空字符串。 + * - 值为一个长度为0的数组。 + * - 值为一个没有元素的Map或Set。 + * - 值为一个没有属性的对象。 + * + * @param {T} value 要检查的值。 + * @returns {boolean} 如果值为空,返回true,否则返回false。 + */ +function isEmpty(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( + ...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, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/letter.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/letter.ts new file mode 100644 index 00000000..65a1c229 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/letter.ts @@ -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, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/merge.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/merge.ts new file mode 100644 index 00000000..4bf79eb5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/merge.ts @@ -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; + } +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/nprogress.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/nprogress.ts new file mode 100644 index 00000000..8e8fe2ef --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/nprogress.ts @@ -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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/state-handler.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/state-handler.ts new file mode 100644 index 00000000..cef1bd8f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/state-handler.ts @@ -0,0 +1,50 @@ +export class StateHandler { + private condition: boolean = false; + private rejectCondition: (() => void) | null = null; + private resolveCondition: (() => void) | null = null; + + // 清理 resolve/reject 函数 + private clearPromises() { + this.resolveCondition = null; + this.rejectCondition = null; + } + + isConditionTrue(): boolean { + return this.condition; + } + + reset() { + this.condition = false; + this.clearPromises(); + } + + // 触发状态为 false 时,reject + setConditionFalse() { + this.condition = false; + if (this.rejectCondition) { + this.rejectCondition(); + this.clearPromises(); + } + } + + // 触发状态为 true 时,resolve + setConditionTrue() { + this.condition = true; + if (this.resolveCondition) { + this.resolveCondition(); + this.clearPromises(); + } + } + + // 返回一个 Promise,等待 condition 变为 true + waitForCondition(): Promise { + return new Promise((resolve, reject) => { + if (this.condition) { + resolve(); // 如果 condition 已经为 true,立即 resolve + } else { + this.resolveCondition = resolve; + this.rejectCondition = reject; + } + }); + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/to.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/to.ts new file mode 100644 index 00000000..6f254051 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/to.ts @@ -0,0 +1,21 @@ +/** + * @param { Readonly } promise + * @param {object=} errorExt - Additional Information you can pass to the err object + * @return { Promise } + */ +export async function to( + promise: Readonly>, + 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]; + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/tree.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/tree.ts new file mode 100644 index 00000000..09a9481c --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/tree.ts @@ -0,0 +1,97 @@ +interface TreeConfigOptions { + // 子属性的名称,默认为'children' + childProps: string; +} + +/** + * @zh_CN 遍历树形结构,并返回所有节点中指定的值。 + * @param tree 树形结构数组 + * @param getValue 获取节点值的函数 + * @param options 作为子节点数组的可选属性名称。 + * @returns 所有节点中指定的值的数组 + */ +function traverseTreeValues( + 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)?.[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>( + tree: T[], + filter: (node: T) => boolean, + options?: TreeConfigOptions, +): T[] { + const { childProps } = options || { + childProps: 'children', + }; + + const _filterTree = (nodes: T[]): T[] => { + return nodes.filter((node: Record) => { + 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>( + tree: T[], + mapper: (node: T) => V, + options?: TreeConfigOptions, +): V[] { + const { childProps } = options || { + childProps: 'children', + }; + return tree.map((node) => { + const mapperNode: Record = mapper(node); + if (mapperNode[childProps]) { + mapperNode[childProps] = mapTree(mapperNode[childProps], mapper, options); + } + return mapperNode as V; + }); +} + +export { filterTree, mapTree, traverseTreeValues }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/unique.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/unique.ts new file mode 100644 index 00000000..e81f972c --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/unique.ts @@ -0,0 +1,15 @@ +/** + * 根据指定字段对对象数组进行去重 + * @param arr 要去重的对象数组 + * @param key 去重依据的字段名 + * @returns 去重后的对象数组 + */ +function uniqueByField(arr: T[], key: keyof T): T[] { + const seen = new Map(); + return arr.filter((item) => { + const value = item[key]; + return seen.has(value) ? false : (seen.set(value, item), true); + }); +} + +export { uniqueByField }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/update-css-variables.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/update-css-variables.ts new file mode 100644 index 00000000..657deaa7 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/update-css-variables.ts @@ -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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/util.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/util.ts new file mode 100644 index 00000000..885eeaa6 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/util.ts @@ -0,0 +1,44 @@ +export function bindMethods(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(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; +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/window.ts b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/window.ts new file mode 100644 index 00000000..4608f4be --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/src/utils/window.ts @@ -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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/shared/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/base/shared/tsconfig.json new file mode 100644 index 00000000..f6860a32 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/shared/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/library.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/build.config.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/build.config.ts new file mode 100644 index 00000000..97e572c5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/package.json b/Yi.Vben5.Vue3/packages/@core/base/typings/package.json new file mode 100644 index 00000000..e2ab1870 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/package.json @@ -0,0 +1,44 @@ +{ + "name": "@vben-core/typings", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/base/typings" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild" + }, + "files": [ + "dist" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + }, + "./vue-router": { + "types": "./vue-router.d.ts" + } + }, + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "vue": "catalog:", + "vue-router": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/src/app.d.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/src/app.d.ts new file mode 100644 index 00000000..d2e86aec --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/src/app.d.ts @@ -0,0 +1,111 @@ +type LayoutType = + | 'full-content' + | 'header-mixed-nav' + | 'header-nav' + | 'header-sidebar-nav' + | 'mixed-nav' + | 'sidebar-mixed-nav' + | 'sidebar-nav'; + +type ThemeModeType = 'auto' | 'dark' | 'light'; + +/** + * 偏好设置按钮位置 + * fixed 固定在右侧 + * header 顶栏 + * auto 自动 + */ +type PreferencesButtonPositionType = 'auto' | 'fixed' | 'header'; + +type BuiltinThemeType = + | 'custom' + | 'deep-blue' + | 'deep-green' + | 'default' + | 'gray' + | 'green' + | 'neutral' + | 'orange' + | 'pink' + | 'red' + | 'rose' + | 'sky-blue' + | 'slate' + | 'stone' + | 'violet' + | 'yellow' + | 'zinc' + | (Record & string); + +type ContentCompactType = 'compact' | 'wide'; + +type LayoutHeaderModeType = 'auto' | 'auto-scroll' | 'fixed' | 'static'; +type LayoutHeaderMenuAlignType = 'center' | 'end' | 'start'; + +/** + * 登录过期模式 + * modal 弹窗模式 + * page 页面模式 + */ +type LoginExpiredModeType = 'modal' | 'page'; + +/** + * 面包屑样式 + * background 背景 + * normal 默认 + */ +type BreadcrumbStyleType = 'background' | 'normal'; + +/** + * 权限模式 + * backend 后端权限模式 + * frontend 前端权限模式 + * mixed 混合权限模式 + */ +type AccessModeType = 'backend' | 'frontend' | 'mixed'; + +/** + * 导航风格 + * plain 朴素 + * rounded 圆润 + */ +type NavigationStyleType = 'plain' | 'rounded'; + +/** + * 标签栏风格 + * brisk 轻快 + * card 卡片 + * chrome 谷歌 + * plain 朴素 + */ +type TabsStyleType = 'brisk' | 'card' | 'chrome' | 'plain'; + +/** + * 页面切换动画 + */ +type PageTransitionType = 'fade' | 'fade-down' | 'fade-slide' | 'fade-up'; + +/** + * 页面切换动画 + * panel-center 居中布局 + * panel-left 居左布局 + * panel-right 居右布局 + */ +type AuthPageLayoutType = 'panel-center' | 'panel-left' | 'panel-right'; + +export type { + AccessModeType, + AuthPageLayoutType, + BreadcrumbStyleType, + BuiltinThemeType, + ContentCompactType, + LayoutHeaderMenuAlignType, + LayoutHeaderModeType, + LayoutType, + LoginExpiredModeType, + NavigationStyleType, + PageTransitionType, + PreferencesButtonPositionType, + TabsStyleType, + ThemeModeType, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/src/basic.d.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/src/basic.d.ts new file mode 100644 index 00000000..75e958dc --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/src/basic.d.ts @@ -0,0 +1,43 @@ +interface BasicOption { + label: string; + value: string; +} + +type SelectOption = BasicOption; + +type TabOption = BasicOption; + +interface BasicUserInfo { + /** + * 头像 + */ + avatar: string; + /** + * 邮箱 + */ + email: string; + /** + * 用户权限 + */ + permissions: string[]; + /** + * 用户昵称 + */ + realName: string; + /** + * 用户角色 + */ + roles: string[]; + /** + * 用户id + */ + userId: number | string; + /** + * 用户名 + */ + username: string; +} + +type ClassType = Array | object | string; + +export type { BasicOption, BasicUserInfo, ClassType, SelectOption, TabOption }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/src/helper.d.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/src/helper.d.ts new file mode 100644 index 00000000..96d4f37b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/src/helper.d.ts @@ -0,0 +1,132 @@ +import type { ComputedRef, MaybeRef } from 'vue'; + +/** + * 深层递归所有属性为可选 + */ +type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + +/** + * 深层递归所有属性为只读 + */ +type DeepReadonly = { + readonly [P in keyof T]: T[P] extends object ? DeepReadonly : T[P]; +}; + +/** + * 任意类型的异步函数 + */ + +type AnyPromiseFunction = ( + ...arg: T +) => PromiseLike; + +/** + * 任意类型的普通函数 + */ +type AnyNormalFunction = (...arg: T) => R; + +/** + * 任意类型的函数 + */ +type AnyFunction = + | AnyNormalFunction + | AnyPromiseFunction; + +/** + * T | null 包装 + */ +type Nullable = null | T; + +/** + * T | Not null 包装 + */ +type NonNullable = T extends null | undefined ? never : T; + +/** + * 字符串类型对象 + */ +type Recordable = Record; + +/** + * 字符串类型对象(只读) + */ +interface ReadonlyRecordable { + readonly [key: string]: T; +} + +/** + * setTimeout 返回值类型 + */ +type TimeoutHandle = ReturnType; + +/** + * setInterval 返回值类型 + */ +type IntervalHandle = ReturnType; + +/** + * 也许它是一个计算的 ref,或者一个 getter 函数 + * + */ +type MaybeReadonlyRef = (() => T) | ComputedRef; + +/** + * 也许它是一个 ref,或者一个普通值,或者一个 getter 函数 + * + */ +type MaybeComputedRef = MaybeReadonlyRef | MaybeRef; + +type Merge = { + [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 = { + * name: string; + * sex: 'male' | 'female'; + * age: string + * } + */ +type MergeAll< + T extends object[], + R extends object = Record, +> = T extends [infer F extends object, ...infer Rest extends object[]] + ? MergeAll> + : R; + +type EmitType = (name: Name, ...args: any[]) => void; + +type MaybePromise = Promise | T; + +export type { + AnyFunction, + AnyNormalFunction, + AnyPromiseFunction, + DeepPartial, + DeepReadonly, + EmitType, + IntervalHandle, + MaybeComputedRef, + MaybePromise, + MaybeReadonlyRef, + Merge, + MergeAll, + NonNullable, + Nullable, + ReadonlyRecordable, + Recordable, + TimeoutHandle, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/src/index.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/src/index.ts new file mode 100644 index 00000000..33c35661 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/src/index.ts @@ -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'; diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/src/menu-record.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/src/menu-record.ts new file mode 100644 index 00000000..abcde813 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/src/menu-record.ts @@ -0,0 +1,76 @@ +import type { Component } from 'vue'; +import type { RouteRecordRaw } from 'vue-router'; + +/** + * 扩展路由原始对象 + */ +type ExRouteRecordRaw = { + parent?: string; + parents?: string[]; + path?: any; +} & RouteRecordRaw; + +interface MenuRecordBadgeRaw { + /** + * 徽标 + */ + badge?: string; + /** + * 徽标类型 + */ + badgeType?: 'dot' | 'normal'; + /** + * 徽标颜色 + */ + badgeVariants?: 'destructive' | 'primary' | string; +} + +/** + * 菜单原始对象 + */ +interface MenuRecordRaw extends MenuRecordBadgeRaw { + /** + * 激活时的图标名 + */ + activeIcon?: string; + /** + * 子菜单 + */ + children?: MenuRecordRaw[]; + /** + * 是否禁用菜单 + * @default false + */ + disabled?: boolean; + /** + * 图标名 + */ + icon?: Component | string; + /** + * 菜单名 + */ + name: string; + /** + * 排序号 + */ + order?: number; + /** + * 父级路径 + */ + parent?: string; + /** + * 所有父级路径 + */ + parents?: string[]; + /** + * 菜单路径,唯一,可当作key + */ + path: string; + /** + * 是否显示菜单 + * @default true + */ + show?: boolean; +} + +export type { ExRouteRecordRaw, MenuRecordBadgeRaw, MenuRecordRaw }; diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/src/tabs.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/src/tabs.ts new file mode 100644 index 00000000..58f7d264 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/src/tabs.ts @@ -0,0 +1,8 @@ +import type { RouteLocationNormalized } from 'vue-router'; + +export interface TabDefinition extends RouteLocationNormalized { + /** + * 标签页的key + */ + key?: string; +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/src/vue-router.d.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/src/vue-router.d.ts new file mode 100644 index 00000000..10c97f62 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/src/vue-router.d.ts @@ -0,0 +1,159 @@ +import type { Component } from 'vue'; +import type { Router, RouteRecordRaw } from 'vue-router'; + +interface RouteMeta { + /** + * 激活图标(菜单/tab) + */ + activeIcon?: string; + /** + * 当前激活的菜单,有时候不想激活现有菜单,需要激活父级菜单时使用 + */ + activePath?: string; + /** + * 是否固定标签页 + * @default false + */ + affixTab?: boolean; + /** + * 固定标签页的顺序 + * @default 0 + */ + affixTabOrder?: number; + /** + * 需要特定的角色标识才可以访问 + * @default [] + */ + authority?: string[]; + /** + * 徽标 + */ + badge?: string; + /** + * 徽标类型 + */ + badgeType?: 'dot' | 'normal'; + /** + * 徽标颜色 + */ + badgeVariants?: + | 'default' + | 'destructive' + | 'primary' + | 'success' + | 'warning' + | string; + /** + * 路由的完整路径作为key(默认true) + */ + fullPathKey?: boolean; + /** + * 当前路由的子级在菜单中不展现 + * @default false + */ + hideChildrenInMenu?: boolean; + /** + * 当前路由在面包屑中不展现 + * @default false + */ + hideInBreadcrumb?: boolean; + /** + * 当前路由在菜单中不展现 + * @default false + */ + hideInMenu?: boolean; + /** + * 当前路由在标签页不展现 + * @default false + */ + hideInTab?: boolean; + /** + * 图标(菜单/tab) + */ + icon?: Component | string; + /** + * iframe 地址 + */ + iframeSrc?: string; + /** + * 忽略权限,直接可以访问 + * @default false + */ + ignoreAccess?: boolean; + /** + * 开启KeepAlive缓存 + */ + keepAlive?: boolean; + /** + * 外链-跳转路径 + */ + link?: string; + /** + * 路由是否已经加载过 + */ + loaded?: boolean; + /** + * 标签页最大打开数量 + * @default -1 + */ + maxNumOfOpenTab?: number; + /** + * 菜单可以看到,但是访问会被重定向到403 + */ + menuVisibleWithForbidden?: boolean; + /** + * 不使用基础布局(仅在顶级生效) + */ + noBasicLayout?: boolean; + /** + * 在新窗口打开 + */ + openInNewWindow?: boolean; + /** + * 用于路由->菜单排序 + */ + order?: number; + /** + * 菜单所携带的参数 + */ + query?: Recordable; + /** + * 管理员切换租户 该页面是否需要重定向到首页 + * 用于区分带路由参数的页面 比如/oss/:id 这种路由是需要回到首页的 + * 默认false + */ + requireHomeRedirect?: boolean; + /** + * 标题名称 + */ + title: string; +} + +// 定义递归类型以将 RouteRecordRaw 的 component 属性更改为 string +type RouteRecordStringComponent = Omit< + RouteRecordRaw, + 'children' | 'component' +> & { + children?: RouteRecordStringComponent[]; + component: T; +}; + +type ComponentRecordType = Record Promise>; + +interface GenerateMenuAndRoutesOptions { + fetchMenuListAsync?: () => Promise; + forbiddenComponent?: RouteRecordRaw['component']; + layoutMap?: ComponentRecordType; + pageMap?: ComponentRecordType; + roles?: string[]; + router: Router; + routes: RouteRecordRaw[]; +} + +export type { + ComponentRecordType, + GenerateMenuAndRoutesOptions, + RouteMeta, + RouteRecordRaw, + RouteRecordStringComponent, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/base/typings/tsconfig.json new file mode 100644 index 00000000..f6860a32 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/library.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/base/typings/vue-router.d.ts b/Yi.Vben5.Vue3/packages/@core/base/typings/vue-router.d.ts new file mode 100644 index 00000000..4874bcd7 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/base/typings/vue-router.d.ts @@ -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 {} +} diff --git a/Yi.Vben5.Vue3/packages/@core/composables/build.config.ts b/Yi.Vben5.Vue3/packages/@core/composables/build.config.ts new file mode 100644 index 00000000..97e572c5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/composables/package.json b/Yi.Vben5.Vue3/packages/@core/composables/package.json new file mode 100644 index 00000000..08db5106 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/package.json @@ -0,0 +1,47 @@ +{ + "name": "@vben-core/composables", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@core/composables" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild" + }, + "files": [ + "dist" + ], + "sideEffects": false, + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + } + }, + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@vben-core/shared": "workspace:*", + "@vueuse/core": "catalog:", + "radix-vue": "catalog:", + "sortablejs": "catalog:", + "vue": "catalog:" + }, + "devDependencies": { + "@types/sortablejs": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/__tests__/use-sortable.test.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/__tests__/use-sortable.test.ts new file mode 100644 index 00000000..e7ba1f13 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/__tests__/use-sortable.test.ts @@ -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, + }), + ); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/index.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/index.ts new file mode 100644 index 00000000..146ff005 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/index.ts @@ -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'; diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-is-mobile.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-is-mobile.ts new file mode 100644 index 00000000..e35909f1 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-is-mobile.ts @@ -0,0 +1,7 @@ +import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'; + +export function useIsMobile() { + const breakpoints = useBreakpoints(breakpointsTailwind); + const isMobile = breakpoints.smaller('md'); + return { isMobile }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-layout-style.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-layout-style.ts new file mode 100644 index 00000000..eba597d0 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-layout-style.ts @@ -0,0 +1,84 @@ +import type { VisibleDomRect } from '@vben-core/shared/utils'; +import type { CSSProperties } from 'vue'; + +import { + CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT, + CSS_VARIABLE_LAYOUT_CONTENT_WIDTH, + CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT, + CSS_VARIABLE_LAYOUT_HEADER_HEIGHT, +} from '@vben-core/shared/constants'; +import { getElementVisibleRect } from '@vben-core/shared/utils'; +import { useCssVar, useDebounceFn } from '@vueuse/core'; +import { computed, onMounted, onUnmounted, ref } from 'vue'; + +/** + * @zh_CN content style + */ +export function useLayoutContentStyle() { + let resizeObserver: null | ResizeObserver = null; + const contentElement = ref(null); + const visibleDomRect = ref(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`; + }, + }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-namespace.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-namespace.ts new file mode 100644 index 00000000..b22cf036 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-namespace.ts @@ -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) => { + const styles: Record = {}; + for (const key in object) { + if (object[key]) { + styles[`--${namespace}-${key}`] = object[key]; + } + } + return styles; + }; + // with block + const cssVarBlock = (object: Record) => { + const styles: Record = {}; + 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; + +export type { UseNamespaceReturn }; +export { useNamespace }; diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-priority-value.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-priority-value.ts new file mode 100644 index 00000000..2c6bda13 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-priority-value.ts @@ -0,0 +1,93 @@ +import type { ComputedRef, Ref } from 'vue'; + +import { + getFirstNonNullOrUndefined, + kebabToCamelCase, +} from '@vben-core/shared/utils'; +import { computed, getCurrentInstance, unref, useAttrs, useSlots } from 'vue'; + +/** + * 依次从插槽、attrs、props、state 中获取值 + * @param key + * @param props + * @param state + */ +export function usePriorityValue< + T extends Record, + S extends Record, + K extends keyof T = keyof T, +>(key: K, props: T, state: Readonly>> | 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, + S extends Ref> = Readonly, NoInfer>>, +>(props: T, state: S | undefined) { + const result: { [K in keyof T]: ComputedRef } = {} 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, + S extends Ref> = Readonly, NoInfer>>, +>(props: T, state: S | undefined) { + const computedResult: { [K in keyof T]: ComputedRef } = {} 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 = {}; + Object.keys(props).forEach((key) => { + unwrapResult[key] = unref(computedResult[key]); + }); + return unwrapResult as { [K in keyof T]: T[K] }; + }); +} diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-scroll-lock.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-scroll-lock.ts new file mode 100644 index 00000000..d1c14975 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-scroll-lock.ts @@ -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( + `.${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( + `.${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 = ''; + }); +} diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/README.md b/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/README.md new file mode 100644 index 00000000..c0a676d3 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/README.md @@ -0,0 +1,3 @@ +# Simple i18n + +Simple i18 implementation diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/index.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/index.ts new file mode 100644 index 00000000..e344fdf3 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/index.ts @@ -0,0 +1,26 @@ +import type { Locale } from './messages'; + +import { createSharedComposable } from '@vueuse/core'; +import { computed, ref } from 'vue'; + +import { getMessages } from './messages'; + +export const useSimpleLocale = createSharedComposable(() => { + const currentLocale = ref('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, + }; +}); diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/messages.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/messages.ts new file mode 100644 index 00000000..efc5c3cc --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-simple-locale/messages.ts @@ -0,0 +1,24 @@ +export type Locale = 'en-US' | 'zh-CN'; + +export const messages: Record> = { + '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]; diff --git a/Yi.Vben5.Vue3/packages/@core/composables/src/use-sortable.ts b/Yi.Vben5.Vue3/packages/@core/composables/src/use-sortable.ts new file mode 100644 index 00000000..57f87a64 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/src/use-sortable.ts @@ -0,0 +1,29 @@ +import type { SortableOptions } from 'sortablejs'; +import type Sortable from 'sortablejs'; + +function useSortable( + 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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/composables/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/composables/tsconfig.json new file mode 100644 index 00000000..f6860a32 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/composables/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/library.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap b/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap new file mode 100644 index 00000000..1cccbbb2 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/__snapshots__/config.test.ts.snap @@ -0,0 +1,136 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`defaultPreferences immutability test > should not modify the config object 1`] = ` +{ + "app": { + "accessMode": "frontend", + "authPageLayout": "panel-right", + "checkUpdatesInterval": 1, + "colorGrayMode": false, + "colorWeakMode": false, + "compact": false, + "contentCompact": "wide", + "contentCompactWidth": 1200, + "contentPadding": 0, + "contentPaddingBottom": 0, + "contentPaddingLeft": 0, + "contentPaddingRight": 0, + "contentPaddingTop": 0, + "defaultAvatar": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp", + "defaultHomePath": "/analytics", + "dynamicTitle": true, + "enableCheckUpdates": true, + "enablePreferences": true, + "enableRefreshToken": false, + "isMobile": false, + "layout": "sidebar-nav", + "locale": "zh-CN", + "loginExpiredMode": "page", + "name": "Vben Admin", + "preferencesButtonPosition": "auto", + "watermark": false, + "zIndex": 200, + }, + "breadcrumb": { + "enable": true, + "hideOnlyOne": false, + "showHome": false, + "showIcon": true, + "styleType": "normal", + }, + "copyright": { + "companyName": "Vben", + "companySiteLink": "https://www.vben.pro", + "date": "2024", + "enable": true, + "icp": "", + "icpLink": "", + "settingShow": true, + }, + "footer": { + "enable": false, + "fixed": false, + "height": 32, + }, + "header": { + "enable": true, + "height": 50, + "hidden": false, + "menuAlign": "start", + "mode": "fixed", + }, + "logo": { + "enable": true, + "fit": "contain", + "source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp", + }, + "navigation": { + "accordion": true, + "split": true, + "styleType": "rounded", + }, + "shortcutKeys": { + "enable": true, + "globalLockScreen": true, + "globalLogout": true, + "globalPreferences": true, + "globalSearch": true, + }, + "sidebar": { + "autoActivateChild": false, + "collapseWidth": 60, + "collapsed": false, + "collapsedButton": true, + "collapsedShowTitle": false, + "enable": true, + "expandOnHover": true, + "extraCollapse": false, + "extraCollapsedWidth": 60, + "fixedButton": true, + "hidden": false, + "mixedWidth": 80, + "width": 224, + }, + "tabbar": { + "draggable": true, + "enable": true, + "height": 38, + "keepAlive": true, + "maxCount": 0, + "middleClickToClose": false, + "persist": true, + "showIcon": true, + "showMaximize": true, + "showMore": true, + "styleType": "chrome", + "wheelable": true, + }, + "theme": { + "builtinType": "default", + "colorDestructive": "hsl(348 100% 61%)", + "colorPrimary": "hsl(212 100% 45%)", + "colorSuccess": "hsl(144 57% 58%)", + "colorWarning": "hsl(42 84% 61%)", + "mode": "dark", + "radius": "0.5", + "semiDarkHeader": false, + "semiDarkSidebar": false, + }, + "transition": { + "enable": true, + "loading": true, + "name": "fade-slide", + "progress": true, + }, + "widget": { + "fullscreen": true, + "globalSearch": true, + "languageToggle": true, + "lockScreen": true, + "notification": true, + "refresh": true, + "sidebarToggle": true, + "themeToggle": true, + }, +} +`; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/config.test.ts b/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/config.test.ts new file mode 100644 index 00000000..f7c9bb36 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/config.test.ts @@ -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(); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/preferences.test.ts b/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/preferences.test.ts new file mode 100644 index 00000000..37657d89 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/__tests__/preferences.test.ts @@ -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)', + ); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/build.config.ts b/Yi.Vben5.Vue3/packages/@core/preferences/build.config.ts new file mode 100644 index 00000000..97e572c5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/build.config.ts @@ -0,0 +1,7 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: ['src/index'], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/package.json b/Yi.Vben5.Vue3/packages/@core/preferences/package.json new file mode 100644 index 00000000..726b473d --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/package.json @@ -0,0 +1,37 @@ +{ + "name": "@vben-core/preferences", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@core/preferences" + }, + "license": "MIT", + "type": "module", + "scripts": { + "#build": "pnpm unbuild" + }, + "files": [ + "dist", + "src" + ], + "sideEffects": [ + "**/*.css" + ], + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./src/index.ts", + "#default": "./dist/index.mjs" + } + }, + "dependencies": { + "@vben-core/shared": "workspace:*", + "@vben-core/typings": "workspace:*", + "@vueuse/core": "catalog:", + "vue": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/src/config.ts b/Yi.Vben5.Vue3/packages/@core/preferences/src/config.ts new file mode 100644 index 00000000..67af5a6d --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/src/config.ts @@ -0,0 +1,138 @@ +import type { Preferences } from './types'; + +const defaultPreferences: Preferences = { + app: { + accessMode: 'frontend', + authPageLayout: 'panel-right', + checkUpdatesInterval: 1, + colorGrayMode: false, + colorWeakMode: false, + compact: false, + contentCompact: 'wide', + contentCompactWidth: 1200, + contentPadding: 0, + contentPaddingBottom: 0, + contentPaddingLeft: 0, + contentPaddingRight: 0, + contentPaddingTop: 0, + defaultAvatar: + 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp', + defaultHomePath: '/analytics', + dynamicTitle: true, + enableCheckUpdates: true, + enablePreferences: true, + enableRefreshToken: false, + isMobile: false, + layout: 'sidebar-nav', + locale: 'zh-CN', + loginExpiredMode: 'page', + name: 'Vben Admin', + preferencesButtonPosition: 'auto', + watermark: false, + zIndex: 200, + }, + breadcrumb: { + enable: true, + hideOnlyOne: false, + showHome: false, + showIcon: true, + styleType: 'normal', + }, + copyright: { + companyName: 'Vben', + companySiteLink: 'https://www.vben.pro', + date: '2024', + enable: true, + icp: '', + icpLink: '', + settingShow: true, + }, + footer: { + enable: false, + fixed: false, + height: 32, + }, + header: { + enable: true, + height: 50, + hidden: false, + menuAlign: 'start', + mode: 'fixed', + }, + + logo: { + enable: true, + fit: 'contain', + source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp', + }, + navigation: { + accordion: true, + split: true, + styleType: 'rounded', + }, + shortcutKeys: { + enable: true, + globalLockScreen: true, + globalLogout: true, + globalPreferences: true, + globalSearch: true, + }, + sidebar: { + autoActivateChild: false, + collapsed: false, + collapsedButton: true, + collapsedShowTitle: false, + collapseWidth: 60, + enable: true, + expandOnHover: true, + extraCollapse: false, + extraCollapsedWidth: 60, + fixedButton: true, + hidden: false, + mixedWidth: 80, + width: 224, + }, + tabbar: { + draggable: true, + enable: true, + height: 38, + keepAlive: true, + maxCount: 0, + middleClickToClose: false, + persist: true, + showIcon: true, + showMaximize: true, + showMore: true, + styleType: 'chrome', + wheelable: true, + }, + theme: { + builtinType: 'default', + colorDestructive: 'hsl(348 100% 61%)', + colorPrimary: 'hsl(212 100% 45%)', + colorSuccess: 'hsl(144 57% 58%)', + colorWarning: 'hsl(42 84% 61%)', + mode: 'auto', + radius: '0.5', + semiDarkHeader: false, + semiDarkSidebar: false, + }, + transition: { + enable: true, + loading: true, + name: 'fade-slide', + progress: true, + }, + widget: { + fullscreen: true, + globalSearch: true, + languageToggle: true, + lockScreen: false, + notification: true, + refresh: true, + sidebarToggle: true, + themeToggle: true, + }, +}; + +export { defaultPreferences }; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/src/constants.ts b/Yi.Vben5.Vue3/packages/@core/preferences/src/constants.ts new file mode 100644 index 00000000..430ee3b0 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/src/constants.ts @@ -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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/src/index.ts b/Yi.Vben5.Vue3/packages/@core/preferences/src/index.ts new file mode 100644 index 00000000..33444e13 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/src/index.ts @@ -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'; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/src/preferences.ts b/Yi.Vben5.Vue3/packages/@core/preferences/src/preferences.ts new file mode 100644 index 00000000..23c5f8be --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/src/preferences.ts @@ -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; + private initialPreferences: Preferences = defaultPreferences; + private isInitialized: boolean = false; + private savePreferences: (preference: Preferences) => void; + private state: Preferences = reactive({ + ...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) { + 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} updates - 部分更新的偏好设置 + */ + private handleUpdates(updates: DeepPartial) { + 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(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 }; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/src/types.ts b/Yi.Vben5.Vue3/packages/@core/preferences/src/types.ts new file mode 100644 index 00000000..e640edb5 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/src/types.ts @@ -0,0 +1,324 @@ +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; + /** 内容紧凑宽度 */ + contentCompactWidth: number; + /** 内容内边距 */ + contentPadding: number; + /** 内容底部内边距 */ + contentPaddingBottom: number; + /** 内容左侧内边距 */ + contentPaddingLeft: number; + /** 内容右侧内边距 */ + contentPaddingRight: number; + /** 内容顶部内边距 */ + contentPaddingTop: number; + // /** 应用默认头像 */ + defaultAvatar: string; + /** 默认首页地址 */ + defaultHomePath: 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; + /** z-index */ + zIndex: number; +} + +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; + /** 底栏高度 */ + height: number; +} + +interface HeaderPreferences { + /** 顶栏是否启用 */ + enable: boolean; + /** 顶栏高度 */ + height: number; + /** 顶栏是否隐藏,css-隐藏 */ + hidden: boolean; + /** 顶栏菜单位置 */ + menuAlign: LayoutHeaderMenuAlignType; + /** header显示模式 */ + mode: LayoutHeaderModeType; +} + +interface LogoPreferences { + /** logo是否可见 */ + enable: boolean; + /** logo图片适应方式 */ + fit: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'; + /** 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; + /** 侧边栏折叠宽度 */ + collapseWidth: number; + /** 侧边栏是否可见 */ + enable: boolean; + /** 菜单自动展开状态 */ + expandOnHover: boolean; + /** 侧边栏扩展区域是否折叠 */ + extraCollapse: boolean; + /** 侧边栏扩展区域折叠宽度 */ + extraCollapsedWidth: number; + /** 侧边栏固定按钮是否可见 */ + fixedButton: boolean; + /** 侧边栏是否隐藏 - css */ + hidden: boolean; + /** 混合侧边栏宽度 */ + mixedWidth: number; + /** 侧边栏宽度 */ + 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; +} +export type { + AppPreferences, + BreadcrumbPreferences, + FooterPreferences, + HeaderPreferences, + InitialOptions, + LogoPreferences, + NavigationPreferences, + Preferences, + PreferencesKeys, + ShortcutKeyPreferences, + SidebarPreferences, + SupportedLanguagesType, + TabbarPreferences, + ThemePreferences, + TransitionPreferences, + WidgetPreferences, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/src/update-css-variables.ts b/Yi.Vben5.Vue3/packages/@core/preferences/src/update-css-variables.ts new file mode 100644 index 00000000..0d3466a0 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/src/update-css-variables.ts @@ -0,0 +1,116 @@ +import type { Preferences } from './types'; + +import { generatorColorVariables } from '@vben-core/shared/color'; +import { updateCSSVariables as executeUpdateCSSVariables } from '@vben-core/shared/utils'; + +import { BUILT_IN_THEME_PRESETS } from './constants'; + +/** + * 更新主题的 CSS 变量以及其他 CSS 变量 + * @param preferences - 当前偏好设置对象,它的主题值将被用来设置文档的主题。 + */ +function updateCSSVariables(preferences: Preferences) { + // 当修改到颜色变量时,更新 css 变量 + const root = document.documentElement; + if (!root) { + return; + } + + const theme = preferences?.theme ?? {}; + + const { builtinType, mode, radius } = theme; + + // html 设置 dark 类 + if (Reflect.has(theme, 'mode')) { + const dark = isDarkTheme(mode); + root.classList.toggle('dark', dark); + } + + // html 设置 data-theme=[builtinType] + if (Reflect.has(theme, 'builtinType')) { + const rootTheme = root.dataset.theme; + if (rootTheme !== builtinType) { + root.dataset.theme = builtinType; + } + } + + // 获取当前的内置主题 + const currentBuiltType = [...BUILT_IN_THEME_PRESETS].find( + (item) => item.type === builtinType, + ); + + let builtinTypeColorPrimary: string | undefined = ''; + + if (currentBuiltType) { + const isDark = isDarkTheme(preferences.theme.mode); + // 设置不同主题的主要颜色 + const color = isDark + ? currentBuiltType.darkPrimaryColor || currentBuiltType.primaryColor + : currentBuiltType.primaryColor; + builtinTypeColorPrimary = color || currentBuiltType.color; + } + + // 如果内置主题颜色和自定义颜色都不存在,则不更新主题颜色 + if ( + builtinTypeColorPrimary || + Reflect.has(theme, 'colorPrimary') || + Reflect.has(theme, 'colorDestructive') || + Reflect.has(theme, 'colorSuccess') || + Reflect.has(theme, 'colorWarning') + ) { + // preferences.theme.colorPrimary = builtinTypeColorPrimary || colorPrimary; + updateMainColorVariables(preferences); + } + + // 更新圆角 + if (Reflect.has(theme, 'radius')) { + document.documentElement.style.setProperty('--radius', `${radius}rem`); + } +} + +/** + * 更新主要的 CSS 变量 + * @param preference - 当前偏好设置对象,它的颜色值将被转换成 HSL 格式并设置为 CSS 变量。 + */ +function updateMainColorVariables(preference: Preferences) { + if (!preference.theme) { + return; + } + const { colorDestructive, colorPrimary, colorSuccess, colorWarning } = + preference.theme; + + const colorVariables = generatorColorVariables([ + { color: colorPrimary, name: 'primary' }, + { alias: 'warning', color: colorWarning, name: 'yellow' }, + { alias: 'success', color: colorSuccess, name: 'green' }, + { alias: 'destructive', color: colorDestructive, name: 'red' }, + ]); + + // 要设置的 CSS 变量映射 + const colorMappings = { + '--green-500': '--success', + '--primary-500': '--primary', + '--red-500': '--destructive', + '--yellow-500': '--warning', + }; + + // 统一处理颜色变量的更新 + Object.entries(colorMappings).forEach(([sourceVar, targetVar]) => { + const colorValue = colorVariables[sourceVar]; + if (colorValue) { + document.documentElement.style.setProperty(targetVar, colorValue); + } + }); + + executeUpdateCSSVariables(colorVariables); +} + +function isDarkTheme(theme: string) { + let dark = theme === 'dark'; + if (theme === 'auto') { + dark = window.matchMedia('(prefers-color-scheme: dark)').matches; + } + return dark; +} + +export { isDarkTheme, updateCSSVariables }; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/src/use-preferences.ts b/Yi.Vben5.Vue3/packages/@core/preferences/src/use-preferences.ts new file mode 100644 index 00000000..94f28bef --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/src/use-preferences.ts @@ -0,0 +1,253 @@ +import { diff } from '@vben-core/shared/utils'; +import { computed } from 'vue'; + +import { preferencesManager } from './preferences'; +import { isDarkTheme } from './update-css-variables'; + +function usePreferences() { + const preferences = preferencesManager.getPreferences(); + const initialPreferences = preferencesManager.getInitialPreferences(); + /** + * @zh_CN 计算偏好设置的变化 + */ + const diffPreference = computed(() => { + return diff(initialPreferences, preferences); + }); + + const appPreferences = computed(() => preferences.app); + + const shortcutKeysPreferences = computed(() => preferences.shortcutKeys); + + /** + * @zh_CN 判断是否为暗黑模式 + * @param preferences - 当前偏好设置对象,它的主题值将被用来判断是否为暗黑模式。 + * @returns 如果主题为暗黑模式,返回 true,否则返回 false。 + */ + const isDark = computed(() => { + return isDarkTheme(preferences.theme.mode); + }); + + const locale = computed(() => { + return preferences.app.locale; + }); + + const isMobile = computed(() => { + return appPreferences.value.isMobile; + }); + + const theme = computed(() => { + return isDark.value ? 'dark' : 'light'; + }); + + /** + * @zh_CN 布局方式 + */ + const layout = computed(() => + isMobile.value ? 'sidebar-nav' : appPreferences.value.layout, + ); + + /** + * @zh_CN 是否显示顶栏 + */ + const isShowHeaderNav = computed(() => { + return preferences.header.enable; + }); + + /** + * @zh_CN 是否全屏显示content,不需要侧边、底部、顶部、tab区域 + */ + const isFullContent = computed( + () => appPreferences.value.layout === 'full-content', + ); + + /** + * @zh_CN 是否侧边导航模式 + */ + const isSideNav = computed( + () => appPreferences.value.layout === 'sidebar-nav', + ); + + /** + * @zh_CN 是否侧边混合模式 + */ + const isSideMixedNav = computed( + () => appPreferences.value.layout === 'sidebar-mixed-nav', + ); + + /** + * @zh_CN 是否为头部导航模式 + */ + const isHeaderNav = computed( + () => appPreferences.value.layout === 'header-nav', + ); + + /** + * @zh_CN 是否为头部混合导航模式 + */ + const isHeaderMixedNav = computed( + () => appPreferences.value.layout === 'header-mixed-nav', + ); + + /** + * @zh_CN 是否为顶部通栏+侧边导航模式 + */ + const isHeaderSidebarNav = computed( + () => appPreferences.value.layout === 'header-sidebar-nav', + ); + + /** + * @zh_CN 是否为混合导航模式 + */ + const isMixedNav = computed( + () => appPreferences.value.layout === 'mixed-nav', + ); + + /** + * @zh_CN 是否包含侧边导航模式 + */ + const isSideMode = computed(() => { + return ( + isMixedNav.value || + isSideMixedNav.value || + isSideNav.value || + isHeaderMixedNav.value || + isHeaderSidebarNav.value + ); + }); + + const sidebarCollapsed = computed(() => { + return preferences.sidebar.collapsed; + }); + + /** + * @zh_CN 是否开启keep-alive + * 在tabs可见以及开启keep-alive的情况下才开启 + */ + const keepAlive = computed( + () => preferences.tabbar.enable && preferences.tabbar.keepAlive, + ); + + /** + * @zh_CN 登录注册页面布局是否为左侧 + */ + const authPanelLeft = computed(() => { + return appPreferences.value.authPageLayout === 'panel-left'; + }); + + /** + * @zh_CN 登录注册页面布局是否为左侧 + */ + const authPanelRight = computed(() => { + return appPreferences.value.authPageLayout === 'panel-right'; + }); + + /** + * @zh_CN 登录注册页面布局是否为中间 + */ + const authPanelCenter = computed(() => { + return appPreferences.value.authPageLayout === 'panel-center'; + }); + + /** + * @zh_CN 内容是否已经最大化 + * 排除 full-content模式 + */ + const contentIsMaximize = computed(() => { + const headerIsHidden = preferences.header.hidden; + const sidebarIsHidden = preferences.sidebar.hidden; + return headerIsHidden && sidebarIsHidden && !isFullContent.value; + }); + + /** + * @zh_CN 是否启用全局搜索快捷键 + */ + const globalSearchShortcutKey = computed(() => { + const { enable, globalSearch } = shortcutKeysPreferences.value; + return enable && globalSearch; + }); + + /** + * @zh_CN 是否启用全局注销快捷键 + */ + const globalLogoutShortcutKey = computed(() => { + const { enable, globalLogout } = shortcutKeysPreferences.value; + return enable && globalLogout; + }); + + const globalLockScreenShortcutKey = computed(() => { + const { enable, globalLockScreen } = shortcutKeysPreferences.value; + return enable && globalLockScreen; + }); + + /** + * @zh_CN 偏好设置按钮位置 + */ + const preferencesButtonPosition = computed(() => { + const { enablePreferences, preferencesButtonPosition } = preferences.app; + + // 如果没有启用偏好设置按钮 + if (!enablePreferences) { + return { + fixed: false, + header: false, + }; + } + + const { header, sidebar } = preferences; + const headerHidden = header.hidden; + const sidebarHidden = sidebar.hidden; + + const contentIsMaximize = headerHidden && sidebarHidden; + + const isHeaderPosition = preferencesButtonPosition === 'header'; + + // 如果设置了固定位置 + if (preferencesButtonPosition !== 'auto') { + return { + fixed: preferencesButtonPosition === 'fixed', + header: isHeaderPosition, + }; + } + + // 如果是全屏模式或者没有固定在顶部, + const fixed = + contentIsMaximize || + isFullContent.value || + isMobile.value || + !isShowHeaderNav.value; + + return { + fixed, + header: !fixed, + }; + }); + + return { + authPanelCenter, + authPanelLeft, + authPanelRight, + contentIsMaximize, + diffPreference, + globalLockScreenShortcutKey, + globalLogoutShortcutKey, + globalSearchShortcutKey, + isDark, + isFullContent, + isHeaderMixedNav, + isHeaderNav, + isHeaderSidebarNav, + isMixedNav, + isMobile, + isSideMixedNav, + isSideMode, + isSideNav, + keepAlive, + layout, + locale, + preferencesButtonPosition, + sidebarCollapsed, + theme, + }; +} + +export { usePreferences }; diff --git a/Yi.Vben5.Vue3/packages/@core/preferences/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/preferences/tsconfig.json new file mode 100644 index 00000000..bbb80506 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/preferences/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src", "__tests__"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/README.md b/Yi.Vben5.Vue3/packages/@core/ui-kit/README.md new file mode 100644 index 00000000..a1746f62 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/README.md @@ -0,0 +1,3 @@ +# ui-kit + +用于管理公共组件、不同UI组件库封装的组件 diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts new file mode 100644 index 00000000..31aa3554 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts @@ -0,0 +1,189 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { FormApi } from '../src/form-api'; + +describe('formApi', () => { + let formApi: FormApi; + + beforeEach(() => { + formApi = new FormApi(); + }); + + it('should initialize with default state', () => { + expect(formApi.state).toEqual( + expect.objectContaining({ + actionWrapperClass: '', + collapsed: false, + collapsedRows: 1, + commonConfig: {}, + handleReset: undefined, + handleSubmit: undefined, + layout: 'horizontal', + resetButtonOptions: {}, + schema: [], + showCollapseButton: false, + showDefaultActions: true, + submitButtonOptions: {}, + wrapperClass: 'grid-cols-1', + }), + ); + expect(formApi.isMounted).toBe(false); + }); + + it('should mount form actions', async () => { + const formActions: any = { + meta: {}, + resetForm: vi.fn(), + setFieldValue: vi.fn(), + setValues: vi.fn(), + submitForm: vi.fn(), + validate: vi.fn(), + values: { name: 'test' }, + }; + + await formApi.mount(formActions); + expect(formApi.isMounted).toBe(true); + expect(formApi.form).toEqual(formActions); + }); + + it('should get values from form', async () => { + const formActions: any = { + meta: {}, + values: { name: 'test' }, + }; + + await formApi.mount(formActions); + const values = await formApi.getValues(); + expect(values).toEqual({ name: 'test' }); + }); + + it('should set field value', async () => { + const setFieldValueMock = vi.fn(); + const formActions: any = { + meta: {}, + setFieldValue: setFieldValueMock, + values: { name: 'test' }, + }; + + await formApi.mount(formActions); + await formApi.setFieldValue('name', 'new value'); + expect(setFieldValueMock).toHaveBeenCalledWith( + 'name', + 'new value', + undefined, + ); + }); + + it('should reset form', async () => { + const resetFormMock = vi.fn(); + const formActions: any = { + meta: {}, + resetForm: resetFormMock, + values: { name: 'test' }, + }; + + await formApi.mount(formActions); + await formApi.resetForm(); + expect(resetFormMock).toHaveBeenCalled(); + }); + + it('should call handleSubmit on submit', async () => { + const handleSubmitMock = vi.fn(); + const formActions: any = { + meta: {}, + submitForm: vi.fn().mockResolvedValue(true), + values: { name: 'test' }, + }; + + const state = { + handleSubmit: handleSubmitMock, + }; + + formApi.setState(state); + await formApi.mount(formActions); + + const result = await formApi.submitForm(); + expect(formActions.submitForm).toHaveBeenCalled(); + expect(handleSubmitMock).toHaveBeenCalledWith({ name: 'test' }); + expect(result).toEqual({ name: 'test' }); + }); + + it('should unmount form and reset state', () => { + formApi.unmount(); + expect(formApi.isMounted).toBe(false); + }); + + it('should validate form', async () => { + const validateMock = vi.fn().mockResolvedValue(true); + const formActions: any = { + meta: {}, + validate: validateMock, + }; + + await formApi.mount(formActions); + const isValid = await formApi.validate(); + expect(validateMock).toHaveBeenCalled(); + expect(isValid).toBe(true); + }); +}); + +describe('updateSchema', () => { + let instance: FormApi; + + beforeEach(() => { + instance = new FormApi(); + instance.state = { + schema: [ + { component: 'text', fieldName: 'name' }, + { component: 'number', fieldName: 'age', label: 'Age' }, + ], + }; + }); + + it('should update the schema correctly when fieldName matches', () => { + const newSchema = [ + { component: 'text', fieldName: 'name' }, + { component: 'number', fieldName: 'age', label: 'Age' }, + ]; + + instance.updateSchema(newSchema); + + expect(instance.state?.schema?.[0]?.component).toBe('text'); + expect(instance.state?.schema?.[1]?.label).toBe('Age'); + }); + + it('should log an error if fieldName is missing in some items', () => { + const newSchema: any[] = [ + { component: 'textarea', fieldName: 'name' }, + { component: 'number' }, + ]; + + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + instance.updateSchema(newSchema); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'All items in the schema array must have a valid `fieldName` property to be updated', + ); + }); + + it('should not update schema if fieldName does not match', () => { + const newSchema = [{ component: 'textarea', fieldName: 'unknown' }]; + + instance.updateSchema(newSchema); + + expect(instance.state?.schema?.[0]?.component).toBe('text'); + expect(instance.state?.schema?.[1]?.component).toBe('number'); + }); + + it('should not update schema if updatedMap is empty', () => { + const newSchema: any[] = [{ component: 'textarea' }]; + + instance.updateSchema(newSchema); + + expect(instance.state?.schema?.[0]?.component).toBe('text'); + expect(instance.state?.schema?.[1]?.component).toBe('number'); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/build.config.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/build.config.ts new file mode 100644 index 00000000..18eaa604 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/build.config.ts @@ -0,0 +1,21 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: [ + { + builder: 'mkdist', + input: './src', + loaders: ['vue'], + pattern: ['**/*.vue'], + }, + { + builder: 'mkdist', + format: 'esm', + input: './src', + loaders: ['js'], + pattern: ['**/*.ts'], + }, + ], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/package.json b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/package.json new file mode 100644 index 00000000..36ae1678 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/package.json @@ -0,0 +1,52 @@ +{ + "name": "@vben-core/form-ui", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/uikit/form-ui" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist" + ], + "sideEffects": [ + "**/*.css" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + } + }, + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@vben-core/composables": "workspace:*", + "@vben-core/icons": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", + "@vben-core/typings": "workspace:*", + "@vee-validate/zod": "catalog:", + "@vueuse/core": "catalog:", + "vee-validate": "catalog:", + "vue": "catalog:", + "zod": "catalog:", + "zod-defaults": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/postcss.config.mjs b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/postcss.config.mjs new file mode 100644 index 00000000..3d807045 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/components/form-actions.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/components/form-actions.vue new file mode 100644 index 00000000..c5a3f9c2 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/components/form-actions.vue @@ -0,0 +1,160 @@ + + + + + + + + + {{ submitButtonOptions.content }} + + + + + + + + {{ resetButtonOptions.content }} + + + + + + + + {{ submitButtonOptions.content }} + + + + + + + + {{ collapsed ? $t('expand') : $t('collapse') }} + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/config.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/config.ts new file mode 100644 index 00000000..ecce1adc --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/config.ts @@ -0,0 +1,87 @@ +import type { Component } from 'vue'; + +import type { + BaseFormComponentType, + FormCommonConfig, + VbenFormAdapterOptions, +} from './types'; + +import { + VbenButton, + VbenCheckbox, + Input as VbenInput, + VbenInputCaptcha, + VbenInputPassword, + VbenPinInput, + VbenSelect, +} from '@vben-core/shadcn-ui'; +import { globalShareState } from '@vben-core/shared/global-state'; +import { defineRule } from 'vee-validate'; +import { h } from 'vue'; + +const DEFAULT_MODEL_PROP_NAME = 'modelValue'; + +export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {}; + +export const COMPONENT_MAP: Record = { + DefaultButton: h(VbenButton, { size: 'sm', variant: 'outline' }), + PrimaryButton: h(VbenButton, { size: 'sm', variant: 'default' }), + VbenCheckbox, + VbenInput, + VbenInputCaptcha, + VbenInputPassword, + VbenPinInput, + VbenSelect, +}; + +export const COMPONENT_BIND_EVENT_MAP: Partial< + Record +> = { + VbenCheckbox: 'checked', +}; + +export function setupVbenForm< + T extends BaseFormComponentType = BaseFormComponentType, +>(options: VbenFormAdapterOptions) { + const { config, defineRules } = options; + + const { + disabledOnChangeListener = true, + disabledOnInputListener = true, + emptyStateValue = undefined, + } = (config || {}) as FormCommonConfig; + + Object.assign(DEFAULT_FORM_COMMON_CONFIG, { + disabledOnChangeListener, + disabledOnInputListener, + emptyStateValue, + }); + + if (defineRules) { + for (const key of Object.keys(defineRules)) { + defineRule(key, defineRules[key as never]); + } + } + + const baseModelPropName = + config?.baseModelPropName ?? DEFAULT_MODEL_PROP_NAME; + const modelPropNameMap = config?.modelPropNameMap as + | Record + | undefined; + + const components = globalShareState.getComponents(); + + for (const component of Object.keys(components)) { + const key = component as BaseFormComponentType; + COMPONENT_MAP[key] = components[component as never]; + + if (baseModelPropName !== DEFAULT_MODEL_PROP_NAME) { + COMPONENT_BIND_EVENT_MAP[key] = baseModelPropName; + } + + // 覆盖特殊组件的modelPropName + if (modelPropNameMap && modelPropNameMap[key]) { + COMPONENT_BIND_EVENT_MAP[key] = modelPropNameMap[key]; + } + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-api.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-api.ts new file mode 100644 index 00000000..e097a465 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-api.ts @@ -0,0 +1,596 @@ +import type { + FormState, + GenericObject, + ResetFormOpts, + ValidationOptions, +} from 'vee-validate'; + +import type { ComponentPublicInstance } from 'vue'; + +import type { Recordable } from '@vben-core/typings'; + +import type { FormActions, FormSchema, VbenFormProps } from './types'; + +import { isRef, toRaw } from 'vue'; + +import { Store } from '@vben-core/shared/store'; +import { + bindMethods, + createMerge, + formatDate, + isDate, + isDayjsObject, + isFunction, + isObject, + mergeWithArrayOverride, + StateHandler, +} from '@vben-core/shared/utils'; + +function getDefaultState(): VbenFormProps { + return { + actionWrapperClass: '', + collapsed: false, + collapsedRows: 1, + collapseTriggerResize: false, + commonConfig: {}, + handleReset: undefined, + handleSubmit: undefined, + handleValuesChange: undefined, + layout: 'horizontal', + resetButtonOptions: {}, + schema: [], + showCollapseButton: false, + showDefaultActions: true, + submitButtonOptions: {}, + submitOnChange: false, + submitOnEnter: false, + wrapperClass: 'grid-cols-1', + }; +} + +export class FormApi { + // private api: Pick; + public form = {} as FormActions; + isMounted = false; + + public state: null | VbenFormProps = null; + stateHandler: StateHandler; + + public store: Store; + + /** + * 组件实例映射 + */ + private componentRefMap: Map = new Map(); + + // 最后一次点击提交时的表单值 + private latestSubmissionValues: null | Recordable = null; + + private prevState: null | VbenFormProps = null; + + constructor(options: VbenFormProps = {}) { + const { ...storeState } = options; + + const defaultState = getDefaultState(); + + this.store = new Store( + { + ...defaultState, + ...storeState, + }, + { + onUpdate: () => { + this.prevState = this.state; + this.state = this.store.state; + this.updateState(); + }, + }, + ); + + this.state = this.store.state; + this.stateHandler = new StateHandler(); + bindMethods(this); + } + + /** + * 获取字段组件实例 + * @param fieldName 字段名 + * @returns 组件实例 + */ + getFieldComponentRef( + fieldName: string, + ): T | undefined { + let target = this.componentRefMap.has(fieldName) + ? (this.componentRefMap.get(fieldName) as ComponentPublicInstance) + : undefined; + if ( + target && + target.$.type.name === 'AsyncComponentWrapper' && + target.$.subTree.ref + ) { + if (Array.isArray(target.$.subTree.ref)) { + if ( + target.$.subTree.ref.length > 0 && + isRef(target.$.subTree.ref[0]?.r) + ) { + target = target.$.subTree.ref[0]?.r.value as ComponentPublicInstance; + } + } else if (isRef(target.$.subTree.ref.r)) { + target = target.$.subTree.ref.r.value as ComponentPublicInstance; + } + } + return target as T; + } + + /** + * 获取当前聚焦的字段,如果没有聚焦的字段则返回undefined + */ + getFocusedField() { + for (const fieldName of this.componentRefMap.keys()) { + const ref = this.getFieldComponentRef(fieldName); + if (ref) { + let el: HTMLElement | null = null; + if (ref instanceof HTMLElement) { + el = ref; + } else if (ref.$el instanceof HTMLElement) { + el = ref.$el; + } + if (!el) { + continue; + } + if ( + el === document.activeElement || + el.contains(document.activeElement) + ) { + return fieldName; + } + } + } + return undefined; + } + + getLatestSubmissionValues() { + return this.latestSubmissionValues || {}; + } + + getState() { + return this.state; + } + + async getValues>() { + const form = await this.getForm(); + return (form.values ? this.handleRangeTimeValue(form.values) : {}) as T; + } + + async isFieldValid(fieldName: string) { + const form = await this.getForm(); + return form.isFieldValid(fieldName); + } + + merge(formApi: FormApi) { + const chain = [this, formApi]; + const proxy = new Proxy(formApi, { + get(target: any, prop: any) { + if (prop === 'merge') { + return (nextFormApi: FormApi) => { + chain.push(nextFormApi); + return proxy; + }; + } + if (prop === 'submitAllForm') { + return async (needMerge: boolean = true) => { + try { + const results = await Promise.all( + chain.map(async (api) => { + const validateResult = await api.validate(); + if (!validateResult.valid) { + return; + } + const rawValues = toRaw((await api.getValues()) || {}); + return rawValues; + }), + ); + if (needMerge) { + const mergedResults = Object.assign({}, ...results); + return mergedResults; + } + return results; + } catch (error) { + console.error('Validation error:', error); + } + }; + } + return target[prop]; + }, + }); + + return proxy; + } + + mount(formActions: FormActions, componentRefMap: Map) { + if (!this.isMounted) { + Object.assign(this.form, formActions); + this.stateHandler.setConditionTrue(); + this.setLatestSubmissionValues({ + ...toRaw(this.handleRangeTimeValue(this.form.values)), + }); + this.componentRefMap = componentRefMap; + this.isMounted = true; + } + } + + /** + * 根据字段名移除表单项 + * @param fields + */ + async removeSchemaByFields(fields: string[]) { + const fieldSet = new Set(fields); + const schema = this.state?.schema ?? []; + + const filterSchema = schema.filter((item) => !fieldSet.has(item.fieldName)); + + this.setState({ + schema: filterSchema, + }); + } + + /** + * 重置表单 + */ + async resetForm( + state?: Partial> | undefined, + opts?: Partial, + ) { + const form = await this.getForm(); + return form.resetForm(state, opts); + } + + async resetValidate() { + const form = await this.getForm(); + const fields = Object.keys(form.errors.value); + fields.forEach((field) => { + form.setFieldError(field, undefined); + }); + } + + async setFieldValue(field: string, value: any, shouldValidate?: boolean) { + const form = await this.getForm(); + form.setFieldValue(field, value, shouldValidate); + } + + setLatestSubmissionValues(values: null | Recordable) { + this.latestSubmissionValues = { ...toRaw(values) }; + } + + setState( + stateOrFn: + | ((prev: VbenFormProps) => Partial) + | Partial, + ) { + if (isFunction(stateOrFn)) { + this.store.setState((prev) => { + return mergeWithArrayOverride(stateOrFn(prev), prev); + }); + } else { + this.store.setState((prev) => mergeWithArrayOverride(stateOrFn, prev)); + } + } + + /** + * 设置表单值 + * @param fields record + * @param filterFields 过滤不在schema中定义的字段 默认为true + * @param shouldValidate + */ + async setValues( + fields: Record, + filterFields: boolean = true, + shouldValidate: boolean = false, + ) { + const form = await this.getForm(); + if (!filterFields) { + form.setValues(fields, shouldValidate); + return; + } + + /** + * 合并算法有待改进,目前的算法不支持object类型的值。 + * antd的日期时间相关组件的值类型为dayjs对象 + * element-plus的日期时间相关组件的值类型可能为Date对象 + * 以上两种类型需要排除深度合并 + */ + const fieldMergeFn = createMerge((obj, key, value) => { + if (key in obj) { + obj[key] = + !Array.isArray(obj[key]) && + isObject(obj[key]) && + !isDayjsObject(obj[key]) && + !isDate(obj[key]) + ? fieldMergeFn(obj[key], value) + : value; + } + return true; + }); + const filteredFields = fieldMergeFn(fields, form.values); + this.handleStringToArrayFields(filteredFields); + form.setValues(filteredFields, shouldValidate); + } + + async submitForm(e?: Event) { + e?.preventDefault(); + e?.stopPropagation(); + const form = await this.getForm(); + await form.submitForm(); + const rawValues = toRaw(await this.getValues()); + this.handleArrayToStringFields(rawValues); + await this.state?.handleSubmit?.(rawValues); + + return rawValues; + } + + unmount() { + this.form?.resetForm?.(); + // this.state = null; + this.latestSubmissionValues = null; + this.isMounted = false; + this.stateHandler.reset(); + } + + updateSchema(schema: Partial[]) { + const updated: Partial[] = [...schema]; + const hasField = updated.every( + (item) => Reflect.has(item, 'fieldName') && item.fieldName, + ); + + if (!hasField) { + console.error( + 'All items in the schema array must have a valid `fieldName` property to be updated', + ); + return; + } + const currentSchema = [...(this.state?.schema ?? [])]; + + const updatedMap: Record = {}; + + updated.forEach((item) => { + if (item.fieldName) { + updatedMap[item.fieldName] = item; + } + }); + + currentSchema.forEach((schema, index) => { + const updatedData = updatedMap[schema.fieldName]; + if (updatedData) { + currentSchema[index] = mergeWithArrayOverride( + updatedData, + schema, + ) as FormSchema; + } + }); + this.setState({ schema: currentSchema }); + } + + async validate(opts?: Partial) { + const form = await this.getForm(); + + const validateResult = await form.validate(opts); + + if (Object.keys(validateResult?.errors ?? {}).length > 0) { + console.error('validate error', validateResult?.errors); + } + return validateResult; + } + + async validateAndSubmitForm() { + const form = await this.getForm(); + const { valid } = await form.validate(); + if (!valid) { + return; + } + return await this.submitForm(); + } + + async validateField(fieldName: string, opts?: Partial) { + const form = await this.getForm(); + const validateResult = await form.validateField(fieldName, opts); + + if (Object.keys(validateResult?.errors ?? {}).length > 0) { + console.error('validate error', validateResult?.errors); + } + return validateResult; + } + + private async getForm() { + if (!this.isMounted) { + // 等待form挂载 + await this.stateHandler.waitForCondition(); + } + if (!this.form?.meta) { + throw new Error(' is not mounted'); + } + return this.form; + } + + private handleArrayToStringFields = (originValues: Record) => { + const arrayToStringFields = this.state?.arrayToStringFields; + if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) { + return; + } + + const processFields = (fields: string[], separator: string = ',') => { + this.processFields(fields, separator, originValues, (value, sep) => + Array.isArray(value) ? value.join(sep) : value, + ); + }; + + // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2'] + if (arrayToStringFields.every((item) => typeof item === 'string')) { + const lastItem = + arrayToStringFields[arrayToStringFields.length - 1] || ''; + const fields = + lastItem.length === 1 + ? arrayToStringFields.slice(0, -1) + : arrayToStringFields; + const separator = lastItem.length === 1 ? lastItem : ','; + processFields(fields, separator); + return; + } + + // 处理嵌套数组格式 [['field1'], ';'] + arrayToStringFields.forEach((fieldConfig) => { + if (Array.isArray(fieldConfig)) { + const [fields, separator = ','] = fieldConfig; + // 根据类型定义,fields 应该始终是字符串数组 + if (!Array.isArray(fields)) { + console.warn( + `Invalid field configuration: fields should be an array of strings, got ${typeof fields}`, + ); + return; + } + processFields(fields, separator); + } + }); + }; + + private handleRangeTimeValue = (originValues: Record) => { + const values = { ...originValues }; + const fieldMappingTime = this.state?.fieldMappingTime; + + this.handleStringToArrayFields(values); + + if (!fieldMappingTime || !Array.isArray(fieldMappingTime)) { + return values; + } + + fieldMappingTime.forEach( + ([field, [startTimeKey, endTimeKey], format = 'YYYY-MM-DD']) => { + if (startTimeKey && endTimeKey && values[field] === null) { + Reflect.deleteProperty(values, startTimeKey); + Reflect.deleteProperty(values, endTimeKey); + // delete values[startTimeKey]; + // delete values[endTimeKey]; + } + + if (!values[field]) { + Reflect.deleteProperty(values, field); + // delete values[field]; + return; + } + + const [startTime, endTime] = values[field]; + if (format === null) { + values[startTimeKey] = startTime; + values[endTimeKey] = endTime; + } else if (isFunction(format)) { + values[startTimeKey] = format(startTime, startTimeKey); + values[endTimeKey] = format(endTime, endTimeKey); + } else { + const [startTimeFormat, endTimeFormat] = Array.isArray(format) + ? format + : [format, format]; + + values[startTimeKey] = startTime + ? formatDate(startTime, startTimeFormat) + : undefined; + values[endTimeKey] = endTime + ? formatDate(endTime, endTimeFormat) + : undefined; + } + // delete values[field]; + Reflect.deleteProperty(values, field); + }, + ); + return values; + }; + + private handleStringToArrayFields = (originValues: Record) => { + const arrayToStringFields = this.state?.arrayToStringFields; + if (!arrayToStringFields || !Array.isArray(arrayToStringFields)) { + return; + } + + const processFields = (fields: string[], separator: string = ',') => { + this.processFields(fields, separator, originValues, (value, sep) => { + if (typeof value !== 'string') { + return value; + } + // 处理空字符串的情况 + if (value === '') { + return []; + } + // 处理复杂分隔符的情况 + const escapedSeparator = sep.replaceAll( + /[.*+?^${}()|[\]\\]/g, + String.raw`\$&`, + ); + return value.split(new RegExp(escapedSeparator)); + }); + }; + + // 处理简单数组格式 ['field1', 'field2', ';'] 或 ['field1', 'field2'] + if (arrayToStringFields.every((item) => typeof item === 'string')) { + const lastItem = + arrayToStringFields[arrayToStringFields.length - 1] || ''; + const fields = + lastItem.length === 1 + ? arrayToStringFields.slice(0, -1) + : arrayToStringFields; + const separator = lastItem.length === 1 ? lastItem : ','; + processFields(fields, separator); + return; + } + + // 处理嵌套数组格式 [['field1'], ';'] + arrayToStringFields.forEach((fieldConfig) => { + if (Array.isArray(fieldConfig)) { + const [fields, separator = ','] = fieldConfig; + if (Array.isArray(fields)) { + processFields(fields, separator); + } else if (typeof originValues[fields] === 'string') { + const value = originValues[fields]; + if (value === '') { + originValues[fields] = []; + } else { + const escapedSeparator = separator.replaceAll( + /[.*+?^${}()|[\]\\]/g, + String.raw`\$&`, + ); + originValues[fields] = value.split(new RegExp(escapedSeparator)); + } + } + } + }); + }; + + private processFields = ( + fields: string[], + separator: string, + originValues: Record, + transformFn: (value: any, separator: string) => any, + ) => { + fields.forEach((field) => { + const value = originValues[field]; + if (value === undefined || value === null) { + return; + } + originValues[field] = transformFn(value, separator); + }); + }; + + private updateState() { + const currentSchema = this.state?.schema ?? []; + const prevSchema = this.prevState?.schema ?? []; + // 进行了删除schema操作 + if (currentSchema.length < prevSchema.length) { + const currentFields = new Set( + currentSchema.map((item) => item.fieldName), + ); + const deletedSchema = prevSchema.filter( + (item) => !currentFields.has(item.fieldName), + ); + for (const schema of deletedSchema) { + this.form?.setFieldValue?.(schema.fieldName, undefined); + } + } + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/context.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/context.ts new file mode 100644 index 00000000..af4b15cd --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/context.ts @@ -0,0 +1,24 @@ +import type { FormRenderProps } from '../types'; + +import { computed } from 'vue'; + +import { createContext } from '@vben-core/shadcn-ui'; + +export const [injectRenderFormProps, provideFormRenderProps] = + createContext('FormRenderProps'); + +export const useFormContext = () => { + const formRenderProps = injectRenderFormProps(); + + const isVertical = computed(() => formRenderProps.layout === 'vertical'); + + const componentMap = computed(() => formRenderProps.componentMap); + const componentBindEventMap = computed( + () => formRenderProps.componentBindEventMap, + ); + return { + componentBindEventMap, + componentMap, + isVertical, + }; +}; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts new file mode 100644 index 00000000..9881db14 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts @@ -0,0 +1,124 @@ +import type { + FormItemDependencies, + FormSchemaRuleType, + MaybeComponentProps, +} from '../types'; + +import { computed, ref, watch } from 'vue'; + +import { isBoolean, isFunction } from '@vben-core/shared/utils'; + +import { useFormValues } from 'vee-validate'; + +import { injectRenderFormProps } from './context'; + +export default function useDependencies( + getDependencies: () => FormItemDependencies | undefined, +) { + const values = useFormValues(); + + const formRenderProps = injectRenderFormProps(); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const formApi = formRenderProps.form!; + + if (!values) { + throw new Error('useDependencies should be used within '); + } + + const isIf = ref(true); + const isDisabled = ref(false); + const isShow = ref(true); + const isRequired = ref(false); + const dynamicComponentProps = ref({}); + const dynamicRules = ref(); + + const triggerFieldValues = computed(() => { + // 该字段可能会被多个字段触发 + const triggerFields = getDependencies()?.triggerFields ?? []; + return triggerFields.map((dep) => { + return values.value[dep]; + }); + }); + + const resetConditionState = () => { + isDisabled.value = false; + isIf.value = true; + isShow.value = true; + isRequired.value = false; + dynamicRules.value = undefined; + dynamicComponentProps.value = {}; + }; + + watch( + [triggerFieldValues, getDependencies], + async ([_values, dependencies]) => { + if (!dependencies || !dependencies?.triggerFields?.length) { + return; + } + resetConditionState(); + const { + componentProps, + disabled, + if: whenIf, + required, + rules, + show, + trigger, + } = dependencies; + + // 1. 优先判断if,如果if为false,则不渲染dom,后续判断也不再执行 + const formValues = values.value; + + if (isFunction(whenIf)) { + isIf.value = !!(await whenIf(formValues, formApi)); + // 不渲染 + if (!isIf.value) return; + } else if (isBoolean(whenIf)) { + isIf.value = whenIf; + if (!isIf.value) return; + } + + // 2. 判断show,如果show为false,则隐藏 + if (isFunction(show)) { + isShow.value = !!(await show(formValues, formApi)); + if (!isShow.value) return; + } else if (isBoolean(show)) { + isShow.value = show; + if (!isShow.value) return; + } + + if (isFunction(componentProps)) { + dynamicComponentProps.value = await componentProps(formValues, formApi); + } + + if (isFunction(rules)) { + dynamicRules.value = await rules(formValues, formApi); + } + + if (isFunction(disabled)) { + isDisabled.value = !!(await disabled(formValues, formApi)); + } else if (isBoolean(disabled)) { + isDisabled.value = disabled; + } + + if (isFunction(required)) { + isRequired.value = !!(await required(formValues, formApi)); + } + + if (isFunction(trigger)) { + await trigger(formValues, formApi); + } + }, + { deep: true, immediate: true }, + ); + + return { + dynamicComponentProps, + dynamicRules, + isDisabled, + isIf, + isRequired, + isShow, + }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/expandable.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/expandable.ts new file mode 100644 index 00000000..9383064c --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/expandable.ts @@ -0,0 +1,105 @@ +import type { FormRenderProps } from '../types'; + +import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'; + +import { + breakpointsTailwind, + useBreakpoints, + useElementVisibility, +} from '@vueuse/core'; + +/** + * 动态计算行数 + */ +export function useExpandable(props: FormRenderProps) { + const wrapperRef = useTemplateRef('wrapperRef'); + const isVisible = useElementVisibility(wrapperRef); + const rowMapping = ref>({}); + // 是否已经计算过一次 + const isCalculated = ref(false); + + const breakpoints = useBreakpoints(breakpointsTailwind); + + const keepFormItemIndex = computed(() => { + const rows = props.collapsedRows ?? 1; + const mapping = rowMapping.value; + let maxItem = 0; + for (let index = 1; index <= rows; index++) { + maxItem += mapping?.[index] ?? 0; + } + // 保持一行 + return maxItem - 1 || 1; + }); + + watch( + [ + () => props.showCollapseButton, + () => breakpoints.active().value, + () => props.schema?.length, + () => isVisible.value, + ], + async ([val]) => { + if (val) { + await nextTick(); + rowMapping.value = {}; + isCalculated.value = false; + await calculateRowMapping(); + } + }, + ); + + async function calculateRowMapping() { + if (!props.showCollapseButton) { + return; + } + + await nextTick(); + if (!wrapperRef.value) { + return; + } + // 小屏幕不计算 + // if (breakpoints.smaller('sm').value) { + // // 保持一行 + // rowMapping.value = { 1: 2 }; + // return; + // } + + const formItems = [...wrapperRef.value.children]; + + const container = wrapperRef.value; + const containerStyles = window.getComputedStyle(container); + const rowHeights = containerStyles + .getPropertyValue('grid-template-rows') + .split(' '); + + const containerRect = container?.getBoundingClientRect(); + + formItems.forEach((el) => { + const itemRect = el.getBoundingClientRect(); + + // 计算元素在第几行 + const itemTop = itemRect.top - containerRect.top; + let rowStart = 0; + let cumulativeHeight = 0; + + for (const [i, rowHeight] of rowHeights.entries()) { + cumulativeHeight += Number.parseFloat(rowHeight); + if (itemTop < cumulativeHeight) { + rowStart = i + 1; + break; + } + } + if (rowStart > (props?.collapsedRows ?? 1)) { + return; + } + rowMapping.value[rowStart] = (rowMapping.value[rowStart] ?? 0) + 1; + isCalculated.value = true; + }); + } + + onMounted(() => { + calculateRowMapping(); + }); + + return { isCalculated, keepFormItemIndex, wrapperRef }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue new file mode 100644 index 00000000..fd5fb5ef --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue new file mode 100644 index 00000000..3c4fdd0a --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form-label.vue @@ -0,0 +1,31 @@ + + + + + * + + + + + {{ help }} + + + + : + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form.vue new file mode 100644 index 00000000..ff827c42 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/form.vue @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/helper.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/helper.ts new file mode 100644 index 00000000..cdb3999f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/helper.ts @@ -0,0 +1,60 @@ +import type { + AnyZodObject, + ZodDefault, + ZodEffects, + ZodNumber, + ZodString, + ZodTypeAny, +} from 'zod'; + +import { isObject, isString } from '@vben-core/shared/utils'; + +/** + * Get the lowest level Zod type. + * This will unpack optionals, refinements, etc. + */ +export function getBaseRules< + ChildType extends AnyZodObject | ZodTypeAny = ZodTypeAny, +>(schema: ChildType | ZodEffects): ChildType | null { + if (!schema || isString(schema)) return null; + if ('innerType' in schema._def) + return getBaseRules(schema._def.innerType as ChildType); + + if ('schema' in schema._def) + return getBaseRules(schema._def.schema as ChildType); + + return schema as ChildType; +} + +/** + * Search for a "ZodDefault" in the Zod stack and return its value. + */ +export function getDefaultValueInZodStack(schema: ZodTypeAny): any { + if (!schema || isString(schema)) { + return; + } + const typedSchema = schema as unknown as ZodDefault; + + if (typedSchema._def.typeName === 'ZodDefault') + return typedSchema._def.defaultValue(); + + if ('innerType' in typedSchema._def) { + return getDefaultValueInZodStack( + typedSchema._def.innerType as unknown as ZodTypeAny, + ); + } + if ('schema' in typedSchema._def) { + return getDefaultValueInZodStack( + (typedSchema._def as any).schema as ZodTypeAny, + ); + } + + return undefined; +} + +export function isEventObjectLike(obj: any) { + if (!obj || !isObject(obj)) { + return false; + } + return Reflect.has(obj, 'target') && Reflect.has(obj, 'stopPropagation'); +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/index.ts new file mode 100644 index 00000000..7fc17065 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/form-render/index.ts @@ -0,0 +1,3 @@ +export { default as Form } from './form.vue'; +export { default as FormField } from './form-field.vue'; +export { default as FormLabel } from './form-label.vue'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/index.ts new file mode 100644 index 00000000..67ed4a50 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/index.ts @@ -0,0 +1,12 @@ +export { setupVbenForm } from './config'; + +export type { + BaseFormComponentType, + ExtendedFormApi, + FormSchema as VbenFormSchema, + VbenFormProps, +} from './types'; + +export * from './use-vben-form'; +// export { default as VbenForm } from './vben-form.vue'; +export * as z from 'zod'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/types.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/types.ts new file mode 100644 index 00000000..34312ae7 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/types.ts @@ -0,0 +1,442 @@ +import type { FieldOptions, FormContext, GenericObject } from 'vee-validate'; +import type { ZodTypeAny } from 'zod'; + +import type { Component, HtmlHTMLAttributes, Ref } from 'vue'; + +import type { VbenButtonProps } from '@vben-core/shadcn-ui'; +import type { ClassType, MaybeComputedRef } from '@vben-core/typings'; + +import type { FormApi } from './form-api'; + +export type FormLayout = 'horizontal' | 'vertical'; + +export type BaseFormComponentType = + | 'DefaultButton' + | 'PrimaryButton' + | 'VbenCheckbox' + | 'VbenInput' + | 'VbenInputPassword' + | 'VbenPinInput' + | 'VbenSelect' + | (Record & string); + +type Breakpoints = '2xl:' | '3xl:' | '' | 'lg:' | 'md:' | 'sm:' | 'xl:'; + +type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13; + +export type WrapperClassType = + | `${Breakpoints}grid-cols-${GridCols}` + | (Record & string); + +export type FormItemClassType = + | `${Breakpoints}cols-end-${'auto' | GridCols}` + | `${Breakpoints}cols-span-${'auto' | 'full' | GridCols}` + | `${Breakpoints}cols-start-${'auto' | GridCols}` + | (Record & string) + | WrapperClassType; + +export type FormFieldOptions = Partial< + FieldOptions & { + validateOnBlur?: boolean; + validateOnChange?: boolean; + validateOnInput?: boolean; + validateOnModelUpdate?: boolean; + } +>; + +export interface FormShape { + /** 默认值 */ + default?: any; + /** 字段名 */ + fieldName: string; + /** 是否必填 */ + required?: boolean; + rules?: ZodTypeAny; +} + +export type MaybeComponentPropKey = + | 'options' + | 'placeholder' + | 'title' + | keyof HtmlHTMLAttributes + | (Record & string); + +export type MaybeComponentProps = { [K in MaybeComponentPropKey]?: any }; + +export type FormActions = FormContext; + +export type CustomRenderType = (() => Component | string) | string; + +export type FormSchemaRuleType = + | 'required' + | 'selectRequired' + | null + | (Record & string) + | ZodTypeAny; + +type FormItemDependenciesCondition> = ( + value: Partial>, + actions: FormActions, +) => T; + +type FormItemDependenciesConditionWithRules = ( + value: Partial>, + actions: FormActions, +) => FormSchemaRuleType | PromiseLike; + +type FormItemDependenciesConditionWithProps = ( + value: Partial>, + actions: FormActions, +) => MaybeComponentProps | PromiseLike; + +export interface FormItemDependencies { + /** + * 组件参数 + * @returns 组件参数 + */ + componentProps?: FormItemDependenciesConditionWithProps; + /** + * 是否禁用 + * @returns 是否禁用 + */ + disabled?: boolean | FormItemDependenciesCondition; + /** + * 是否渲染(删除dom) + * @returns 是否渲染 + */ + if?: boolean | FormItemDependenciesCondition; + /** + * 是否必填 + * @returns 是否必填 + */ + required?: FormItemDependenciesCondition; + /** + * 字段规则 + */ + rules?: FormItemDependenciesConditionWithRules; + /** + * 是否隐藏(Css) + * @returns 是否隐藏 + */ + show?: boolean | FormItemDependenciesCondition; + /** + * 任意触发都会执行 + */ + trigger?: FormItemDependenciesCondition; + /** + * 触发字段 + */ + triggerFields: string[]; +} + +type ComponentProps = + | (( + value: Partial>, + actions: FormActions, + ) => MaybeComponentProps) + | MaybeComponentProps; + +export interface FormCommonConfig { + /** + * 在Label后显示一个冒号 + */ + colon?: boolean; + /** + * 所有表单项的props + */ + componentProps?: ComponentProps; + /** + * 所有表单项的控件样式 + */ + controlClass?: string; + /** + * 所有表单项的禁用状态 + * @default false + */ + disabled?: boolean; + /** + * 是否禁用所有表单项的change事件监听 + * @default true + */ + disabledOnChangeListener?: boolean; + /** + * 是否禁用所有表单项的input事件监听 + * @default true + */ + disabledOnInputListener?: boolean; + /** + * 所有表单项的空状态值,默认都是undefined,naive-ui的空状态值是null + */ + emptyStateValue?: null | undefined; + /** + * 所有表单项的控件样式 + * @default {} + */ + formFieldProps?: FormFieldOptions; + /** + * 所有表单项的栅格布局 + * @default "" + */ + formItemClass?: string; + /** + * 隐藏所有表单项label + * @default false + */ + hideLabel?: boolean; + /** + * 是否隐藏必填标记 + * @default false + */ + hideRequiredMark?: boolean; + /** + * 所有表单项的label样式 + * @default "" + */ + labelClass?: string; + /** + * 所有表单项的label宽度 + */ + labelWidth?: number; + /** + * 所有表单项的model属性名 + * @default "modelValue" + */ + modelPropName?: string; + /** + * 所有表单项的wrapper样式 + */ + wrapperClass?: string; +} + +type RenderComponentContentType = ( + value: Partial>, + api: FormActions, +) => Record; + +export type HandleSubmitFn = ( + values: Record, +) => Promise | void; + +export type HandleResetFn = ( + values: Record, +) => Promise | void; + +export type FieldMappingTime = [ + string, + [string, string], + ( + | ((value: any, fieldName: string) => any) + | [string, string] + | null + | string + )?, +][]; + +export type ArrayToStringFields = Array< + | [string[], string?] // 嵌套数组格式,可选分隔符 + | string // 单个字段,使用默认分隔符 + | string[] // 简单数组格式,最后一个元素可以是分隔符 +>; + +export interface FormSchema< + T extends BaseFormComponentType = BaseFormComponentType, +> extends FormCommonConfig { + /** 组件 */ + component: Component | T; + /** 组件参数 */ + componentProps?: ComponentProps; + /** 默认值 */ + defaultValue?: any; + /** 依赖 */ + dependencies?: FormItemDependencies; + /** 描述 */ + description?: CustomRenderType; + /** 字段名 */ + fieldName: string; + /** 帮助信息 */ + help?: CustomRenderType; + /** 表单项 */ + label?: CustomRenderType; + // 自定义组件内部渲染 + renderComponentContent?: RenderComponentContentType; + /** 字段规则 */ + rules?: FormSchemaRuleType; + /** 后缀 */ + suffix?: CustomRenderType; +} + +export interface FormFieldProps extends FormSchema { + required?: boolean; +} + +export interface FormRenderProps< + T extends BaseFormComponentType = BaseFormComponentType, +> { + /** + * 表单字段数组映射字符串配置 默认使用"," + */ + arrayToStringFields?: ArrayToStringFields; + /** + * 是否展开,在showCollapseButton=true下生效 + */ + collapsed?: boolean; + /** + * 折叠时保持行数 + * @default 1 + */ + collapsedRows?: number; + /** + * 是否触发resize事件 + * @default false + */ + collapseTriggerResize?: boolean; + /** + * 表单项通用后备配置,当子项目没配置时使用这里的配置,子项目配置优先级高于此配置 + */ + commonConfig?: FormCommonConfig; + /** + * 紧凑模式(移除表单每一项底部为校验信息预留的空间) + */ + compact?: boolean; + /** + * 组件v-model事件绑定 + */ + componentBindEventMap?: Partial>; + /** + * 组件集合 + */ + componentMap: Record; + /** + * 表单字段映射到时间格式 + */ + fieldMappingTime?: FieldMappingTime; + /** + * 表单实例 + */ + form?: FormContext; + /** + * 表单项布局 + */ + layout?: FormLayout; + /** + * 表单定义 + */ + schema?: FormSchema[]; + + /** + * 是否显示展开/折叠 + */ + showCollapseButton?: boolean; + /** + * 格式化日期 + */ + + /** + * 表单栅格布局 + * @default "grid-cols-1" + */ + wrapperClass?: WrapperClassType; +} + +export interface ActionButtonOptions extends VbenButtonProps { + [key: string]: any; + content?: MaybeComputedRef; + show?: boolean; +} + +export interface VbenFormProps< + T extends BaseFormComponentType = BaseFormComponentType, +> extends Omit< + FormRenderProps, + 'componentBindEventMap' | 'componentMap' | 'form' + > { + /** + * 操作按钮是否反转(提交按钮前置) + */ + actionButtonsReverse?: boolean; + /** + * 表单操作区域class + */ + actionWrapperClass?: ClassType; + /** + * 表单字段数组映射字符串配置 默认使用"," + */ + arrayToStringFields?: ArrayToStringFields; + + /** + * 表单字段映射 + */ + fieldMappingTime?: FieldMappingTime; + /** + * 表单重置回调 + */ + handleReset?: HandleResetFn; + /** + * 表单提交回调 + */ + handleSubmit?: HandleSubmitFn; + /** + * 表单值变化回调 + */ + handleValuesChange?: ( + values: Record, + fieldsChanged: string[], + ) => void; + /** + * 重置按钮参数 + */ + resetButtonOptions?: ActionButtonOptions; + + /** + * 是否显示默认操作按钮 + * @default true + */ + showDefaultActions?: boolean; + + /** + * 提交按钮参数 + */ + submitButtonOptions?: ActionButtonOptions; + + /** + * 是否在字段值改变时提交表单 + * @default false + */ + submitOnChange?: boolean; + + /** + * 是否在回车时提交表单 + * @default false + */ + submitOnEnter?: boolean; +} + +export type ExtendedFormApi = FormApi & { + useStore: >( + selector?: (state: NoInfer) => T, + ) => Readonly>; +}; + +export interface VbenFormAdapterOptions< + T extends BaseFormComponentType = BaseFormComponentType, +> { + config?: { + baseModelPropName?: string; + disabledOnChangeListener?: boolean; + disabledOnInputListener?: boolean; + emptyStateValue?: null | undefined; + modelPropNameMap?: Partial>; + }; + defineRules?: { + required?: ( + value: any, + params: any, + ctx: Record, + ) => boolean | string; + selectRequired?: ( + value: any, + params: any, + ctx: Record, + ) => boolean | string; + }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-form-context.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-form-context.ts new file mode 100644 index 00000000..4ef182ed --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-form-context.ts @@ -0,0 +1,109 @@ +import type { ZodRawShape } from 'zod'; + +import type { ComputedRef } from 'vue'; + +import type { ExtendedFormApi, FormActions, VbenFormProps } from './types'; + +import { computed, unref, useSlots } from 'vue'; + +import { createContext } from '@vben-core/shadcn-ui'; +import { isString, mergeWithArrayOverride, set } from '@vben-core/shared/utils'; + +import { useForm } from 'vee-validate'; +import { object, ZodIntersection, ZodNumber, ZodObject, ZodString } from 'zod'; +import { getDefaultsForSchema } from 'zod-defaults'; + +type ExtendFormProps = VbenFormProps & { formApi: ExtendedFormApi }; + +export const [injectFormProps, provideFormProps] = + createContext<[ComputedRef | ExtendFormProps, FormActions]>( + 'VbenFormProps', + ); + +export const [injectComponentRefMap, provideComponentRefMap] = + createContext>('ComponentRefMap'); + +export function useFormInitial( + props: ComputedRef | VbenFormProps, +) { + const slots = useSlots(); + const initialValues = generateInitialValues(); + + const form = useForm({ + ...(Object.keys(initialValues)?.length ? { initialValues } : {}), + }); + + const delegatedSlots = computed(() => { + const resultSlots: string[] = []; + + for (const key of Object.keys(slots)) { + if (key !== 'default') { + resultSlots.push(key); + } + } + return resultSlots; + }); + + function generateInitialValues() { + const initialValues: Record = {}; + + const zodObject: ZodRawShape = {}; + (unref(props).schema || []).forEach((item) => { + if (Reflect.has(item, 'defaultValue')) { + set(initialValues, item.fieldName, item.defaultValue); + } else if (item.rules && !isString(item.rules)) { + // 检查规则是否适合提取默认值 + const customDefaultValue = getCustomDefaultValue(item.rules); + zodObject[item.fieldName] = item.rules; + if (customDefaultValue !== undefined) { + initialValues[item.fieldName] = customDefaultValue; + } + } + }); + + const schemaInitialValues = getDefaultsForSchema(object(zodObject)); + + const zodDefaults: Record = {}; + for (const key in schemaInitialValues) { + set(zodDefaults, key, schemaInitialValues[key]); + } + return mergeWithArrayOverride(initialValues, zodDefaults); + } + // 自定义默认值提取逻辑 + function getCustomDefaultValue(rule: any): any { + if (rule instanceof ZodString) { + return ''; // 默认为空字符串 + } else if (rule instanceof ZodNumber) { + return null; // 默认为 null(避免显示 0) + } else if (rule instanceof ZodObject) { + // 递归提取嵌套对象的默认值 + const defaultValues: Record = {}; + for (const [key, valueSchema] of Object.entries(rule.shape)) { + defaultValues[key] = getCustomDefaultValue(valueSchema); + } + return defaultValues; + } else if (rule instanceof ZodIntersection) { + // 对于交集类型,从schema 提取默认值 + const leftDefaultValue = getCustomDefaultValue(rule._def.left); + const rightDefaultValue = getCustomDefaultValue(rule._def.right); + + // 如果左右两边都能提取默认值,合并它们 + if ( + typeof leftDefaultValue === 'object' && + typeof rightDefaultValue === 'object' + ) { + return { ...leftDefaultValue, ...rightDefaultValue }; + } + + // 否则优先使用左边的默认值 + return leftDefaultValue ?? rightDefaultValue; + } else { + return undefined; // 其他类型不提供默认值 + } + } + + return { + delegatedSlots, + form, + }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-vben-form.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-vben-form.ts new file mode 100644 index 00000000..73f1eb0b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/use-vben-form.ts @@ -0,0 +1,50 @@ +import type { + BaseFormComponentType, + ExtendedFormApi, + VbenFormProps, +} from './types'; + +import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue'; + +import { useStore } from '@vben-core/shared/store'; + +import { FormApi } from './form-api'; +import VbenUseForm from './vben-use-form.vue'; + +export function useVbenForm< + T extends BaseFormComponentType = BaseFormComponentType, +>(options: VbenFormProps) { + const IS_REACTIVE = isReactive(options); + const api = new FormApi(options); + const extendedApi: ExtendedFormApi = api as never; + extendedApi.useStore = (selector) => { + return useStore(api.store, selector); + }; + + const Form = defineComponent( + (props: VbenFormProps, { attrs, slots }) => { + onBeforeUnmount(() => { + api.unmount(); + }); + api.setState({ ...props, ...attrs }); + return () => + h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots); + }, + { + name: 'VbenUseForm', + inheritAttrs: false, + }, + ); + // Add reactivity support + if (IS_REACTIVE) { + watch( + () => options.schema, + () => { + api.setState({ schema: options.schema }); + }, + { immediate: true }, + ); + } + + return [Form, extendedApi] as const; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-form.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-form.vue new file mode 100644 index 00000000..260e75cd --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-form.vue @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-use-form.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-use-form.vue new file mode 100644 index 00000000..3e7b00b6 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/src/vben-use-form.vue @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tailwind.config.mjs b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tailwind.config.mjs new file mode 100644 index 00000000..f17f556f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tsconfig.json new file mode 100644 index 00000000..bbb80506 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/form-ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src", "__tests__"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/build.config.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/build.config.ts new file mode 100644 index 00000000..18eaa604 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/build.config.ts @@ -0,0 +1,21 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: [ + { + builder: 'mkdist', + input: './src', + loaders: ['vue'], + pattern: ['**/*.vue'], + }, + { + builder: 'mkdist', + format: 'esm', + input: './src', + loaders: ['js'], + pattern: ['**/*.ts'], + }, + ], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/package.json b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/package.json new file mode 100644 index 00000000..57a462fe --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/package.json @@ -0,0 +1,48 @@ +{ + "name": "@vben-core/layout-ui", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/uikit/layout-ui" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist" + ], + "sideEffects": [ + "**/*.css" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + } + }, + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@vben-core/composables": "workspace:*", + "@vben-core/icons": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", + "@vben-core/typings": "workspace:*", + "@vueuse/core": "catalog:", + "vue": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/postcss.config.mjs b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/postcss.config.mjs new file mode 100644 index 00000000..3d807045 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/index.ts new file mode 100644 index 00000000..f17826dd --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/index.ts @@ -0,0 +1,5 @@ +export { default as LayoutContent } from './layout-content.vue'; +export { default as LayoutFooter } from './layout-footer.vue'; +export { default as LayoutHeader } from './layout-header.vue'; +export { default as LayoutSidebar } from './layout-sidebar.vue'; +export { default as LayoutTabbar } from './layout-tabbar.vue'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue new file mode 100644 index 00000000..af066f84 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-content.vue @@ -0,0 +1,62 @@ + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue new file mode 100644 index 00000000..129209da --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-footer.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-header.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-header.vue new file mode 100644 index 00000000..534e4162 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-header.vue @@ -0,0 +1,77 @@ + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue new file mode 100644 index 00000000..214658d3 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-sidebar.vue @@ -0,0 +1,322 @@ + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-tabbar.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-tabbar.vue new file mode 100644 index 00000000..e588e615 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/layout-tabbar.vue @@ -0,0 +1,30 @@ + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/index.ts new file mode 100644 index 00000000..8dfbe6c3 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/index.ts @@ -0,0 +1,2 @@ +export { default as SidebarCollapseButton } from './sidebar-collapse-button.vue'; +export { default as SidebarFixedButton } from './sidebar-fixed-button.vue'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-collapse-button.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-collapse-button.vue new file mode 100644 index 00000000..8092c0d1 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-collapse-button.vue @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-fixed-button.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-fixed-button.vue new file mode 100644 index 00000000..ce839f5c --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/components/widgets/sidebar-fixed-button.vue @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts new file mode 100644 index 00000000..2f2b82bb --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/hooks/use-layout.ts @@ -0,0 +1,53 @@ +import type { LayoutType } from '@vben-core/typings'; + +import type { VbenLayoutProps } from '../vben-layout'; + +import { computed } from 'vue'; + +export function useLayout(props: VbenLayoutProps) { + const currentLayout = computed(() => + props.isMobile ? 'sidebar-nav' : (props.layout as LayoutType), + ); + + /** + * 是否全屏显示content,不需要侧边、底部、顶部、tab区域 + */ + const isFullContent = computed(() => currentLayout.value === 'full-content'); + + /** + * 是否侧边混合模式 + */ + const isSidebarMixedNav = computed( + () => currentLayout.value === 'sidebar-mixed-nav', + ); + + /** + * 是否为头部导航模式 + */ + const isHeaderNav = computed(() => currentLayout.value === 'header-nav'); + + /** + * 是否为混合导航模式 + */ + const isMixedNav = computed( + () => + currentLayout.value === 'mixed-nav' || + currentLayout.value === 'header-sidebar-nav', + ); + + /** + * 是否为头部混合模式 + */ + const isHeaderMixedNav = computed( + () => currentLayout.value === 'header-mixed-nav', + ); + + return { + currentLayout, + isFullContent, + isHeaderMixedNav, + isHeaderNav, + isMixedNav, + isSidebarMixedNav, + }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/index.ts new file mode 100644 index 00000000..ca8c8a7f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/index.ts @@ -0,0 +1,2 @@ +export type * from './vben-layout'; +export { default as VbenAdminLayout } from './vben-layout.vue'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.ts new file mode 100644 index 00000000..9b77ba96 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.ts @@ -0,0 +1,175 @@ +import type { + ContentCompactType, + LayoutHeaderModeType, + LayoutType, + ThemeModeType, +} from '@vben-core/typings'; + +interface VbenLayoutProps { + /** + * 内容区域定宽 + * @default 'wide' + */ + contentCompact?: ContentCompactType; + /** + * 定宽布局宽度 + * @default 1200 + */ + contentCompactWidth?: number; + /** + * padding + * @default 16 + */ + contentPadding?: number; + /** + * paddingBottom + * @default 16 + */ + contentPaddingBottom?: number; + /** + * paddingLeft + * @default 16 + */ + contentPaddingLeft?: number; + /** + * paddingRight + * @default 16 + */ + contentPaddingRight?: number; + /** + * paddingTop + * @default 16 + */ + contentPaddingTop?: number; + /** + * footer 是否可见 + * @default false + */ + footerEnable?: boolean; + /** + * footer 是否固定 + * @default true + */ + footerFixed?: boolean; + /** + * footer 高度 + * @default 32 + */ + footerHeight?: number; + + /** + * header高度 + * @default 48 + */ + headerHeight?: number; + /** + * 顶栏是否隐藏 + * @default false + */ + headerHidden?: boolean; + /** + * header 显示模式 + * @default 'fixed' + */ + headerMode?: LayoutHeaderModeType; + /** + * header 顶栏主题 + */ + headerTheme?: ThemeModeType; + /** + * 是否显示header切换侧边栏按钮 + * @default + */ + headerToggleSidebarButton?: boolean; + /** + * header是否显示 + * @default true + */ + headerVisible?: boolean; + /** + * 是否移动端显示 + * @default false + */ + isMobile?: boolean; + /** + * 布局方式 + * sidebar-nav 侧边菜单布局 + * header-nav 顶部菜单布局 + * mixed-nav 侧边&顶部菜单布局 + * sidebar-mixed-nav 侧边混合菜单布局 + * full-content 全屏内容布局 + * @default sidebar-nav + */ + layout?: LayoutType; + /** + * 侧边菜单折叠状态 + * @default false + */ + sidebarCollapse?: boolean; + /** + * 侧边菜单折叠按钮 + * @default true + */ + sidebarCollapsedButton?: boolean; + /** + * 侧边菜单是否折叠时,是否显示title + * @default true + */ + sidebarCollapseShowTitle?: boolean; + /** + * 侧边栏是否可见 + * @default true + */ + sidebarEnable?: boolean; + /** + * 侧边菜单折叠额外宽度 + * @default 48 + */ + sidebarExtraCollapsedWidth?: number; + /** + * 侧边菜单折叠按钮是否固定 + * @default true + */ + sidebarFixedButton?: boolean; + /** + * 侧边栏是否隐藏 + * @default false + */ + sidebarHidden?: boolean; + /** + * 混合侧边栏宽度 + * @default 80 + */ + sidebarMixedWidth?: number; + /** + * 侧边栏 + * @default dark + */ + sidebarTheme?: ThemeModeType; + /** + * 侧边栏宽度 + * @default 210 + */ + sidebarWidth?: number; + /** + * 侧边菜单折叠宽度 + * @default 48 + */ + sideCollapseWidth?: number; + /** + * tab是否可见 + * @default true + */ + tabbarEnable?: boolean; + /** + * tab高度 + * @default 30 + */ + tabbarHeight?: number; + /** + * zIndex + * @default 100 + */ + zIndex?: number; +} +export type { VbenLayoutProps }; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.vue new file mode 100644 index 00000000..e9e98dd4 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/src/vben-layout.vue @@ -0,0 +1,616 @@ + + + + + emit('sideMouseLeave')" + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tailwind.config.mjs b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tailwind.config.mjs new file mode 100644 index 00000000..f17f556f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tsconfig.json new file mode 100644 index 00000000..ce1a891f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/layout-ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/README.md b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/README.md new file mode 100644 index 00000000..dd77a950 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/README.md @@ -0,0 +1 @@ +# 菜单组件 diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/build.config.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/build.config.ts new file mode 100644 index 00000000..1ff78fab --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/build.config.ts @@ -0,0 +1,26 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: [ + { + builder: 'mkdist', + input: './src', + pattern: ['**/*'], + }, + { + builder: 'mkdist', + input: './src', + loaders: ['vue'], + pattern: ['**/*.vue'], + }, + { + builder: 'mkdist', + format: 'esm', + input: './src', + loaders: ['js'], + pattern: ['**/*.ts'], + }, + ], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/package.json b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/package.json new file mode 100644 index 00000000..760d7646 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/package.json @@ -0,0 +1,48 @@ +{ + "name": "@vben-core/menu-ui", + "version": "5.5.7", + "homepage": "https://github.com/vbenjs/vue-vben-admin", + "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/vbenjs/vue-vben-admin.git", + "directory": "packages/@vben-core/uikit/menu-ui" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist" + ], + "sideEffects": [ + "**/*.css" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + } + }, + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@vben-core/composables": "workspace:*", + "@vben-core/icons": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", + "@vben-core/typings": "workspace:*", + "@vueuse/core": "catalog:", + "vue": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/postcss.config.mjs b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/postcss.config.mjs new file mode 100644 index 00000000..3d807045 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/collapse-transition.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/collapse-transition.vue new file mode 100644 index 00000000..b89c4068 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/collapse-transition.vue @@ -0,0 +1,96 @@ + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/index.ts new file mode 100644 index 00000000..7c697dc1 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/index.ts @@ -0,0 +1,4 @@ +export { default as Menu } from './menu.vue'; +export { default as MenuBadge } from './menu-badge.vue'; +export { default as MenuItem } from './menu-item.vue'; +export { default as SubMenu } from './sub-menu.vue'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge-dot.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge-dot.vue new file mode 100644 index 00000000..823cbd74 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge-dot.vue @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge.vue new file mode 100644 index 00000000..b1dd94c2 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-badge.vue @@ -0,0 +1,57 @@ + + + + + + {{ badge }} + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-item.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-item.vue new file mode 100644 index 00000000..a72d3487 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu-item.vue @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu.vue new file mode 100644 index 00000000..0eee6f70 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/menu.vue @@ -0,0 +1,872 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/index.ts new file mode 100644 index 00000000..898ea162 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/index.ts @@ -0,0 +1,2 @@ +export type * from './normal-menu'; +export { default as NormalMenu } from './normal-menu.vue'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.ts new file mode 100644 index 00000000..82edb4fc --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.ts @@ -0,0 +1,27 @@ +import type { MenuRecordRaw } from '@vben-core/typings'; + +interface NormalMenuProps { + /** + * 菜单数据 + */ + activePath?: string; + /** + * 是否折叠 + */ + collapse?: boolean; + /** + * 菜单项 + */ + menus?: MenuRecordRaw[]; + /** + * @zh_CN 是否圆润风格 + * @default true + */ + rounded?: boolean; + /** + * 主题 + */ + theme?: 'dark' | 'light'; +} + +export type { NormalMenuProps }; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue new file mode 100644 index 00000000..7cb29e51 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/normal-menu/normal-menu.vue @@ -0,0 +1,161 @@ + + + + + + emit('select', menu)" + @mouseenter="() => emit('enter', menu)" + > + + + {{ menu.name }} + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu-content.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu-content.vue new file mode 100644 index 00000000..5622d413 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu-content.vue @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue new file mode 100644 index 00000000..7a82e724 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/components/sub-menu.vue @@ -0,0 +1,275 @@ + + + handleMouseleave()" + > + + + + + + + + + + handleMouseenter(e, 100)" + @mouseenter="(e) => handleMouseenter(e, 100)" + @mouseleave="() => handleMouseleave(true)" + > + + + + + + + + + + + + + + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/index.ts new file mode 100644 index 00000000..f9acf321 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './use-menu'; +export * from './use-menu-context'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-context.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-context.ts new file mode 100644 index 00000000..357b296b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-context.ts @@ -0,0 +1,55 @@ +import type { MenuProvider, SubMenuProvider } from '../types'; + +import { getCurrentInstance, inject, provide } from 'vue'; + +import { findComponentUpward } from '../utils'; + +const menuContextKey = Symbol('menuContext'); + +/** + * @zh_CN Provide menu context + */ +function createMenuContext(injectMenuData: MenuProvider) { + provide(menuContextKey, injectMenuData); +} + +/** + * @zh_CN Provide menu context + */ +function createSubMenuContext(injectSubMenuData: SubMenuProvider) { + const instance = getCurrentInstance(); + + provide(`subMenu:${instance?.uid}`, injectSubMenuData); +} + +/** + * @zh_CN Inject menu context + */ +function useMenuContext() { + const instance = getCurrentInstance(); + if (!instance) { + throw new Error('instance is required'); + } + const rootMenu = inject(menuContextKey) as MenuProvider; + return rootMenu; +} + +/** + * @zh_CN Inject menu context + */ +function useSubMenuContext() { + const instance = getCurrentInstance(); + if (!instance) { + throw new Error('instance is required'); + } + const parentMenu = findComponentUpward(instance, ['Menu', 'SubMenu']); + const subMenu = inject(`subMenu:${parentMenu?.uid}`) as SubMenuProvider; + return subMenu; +} + +export { + createMenuContext, + createSubMenuContext, + useMenuContext, + useSubMenuContext, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts new file mode 100644 index 00000000..270a0348 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu-scroll.ts @@ -0,0 +1,46 @@ +import type { Ref } from 'vue'; + +import { watch } from 'vue'; + +import { useDebounceFn } from '@vueuse/core'; + +interface UseMenuScrollOptions { + delay?: number; + enable?: boolean | Ref; +} + +export function useMenuScroll( + activePath: Ref, + options: UseMenuScrollOptions = {}, +) { + const { enable = true, delay = 320 } = options; + + function scrollToActiveItem() { + const isEnabled = typeof enable === 'boolean' ? enable : enable.value; + if (!isEnabled) return; + + const activeElement = document.querySelector( + `aside li[role=menuitem].is-active`, + ); + if (activeElement) { + activeElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center', + }); + } + } + + const debouncedScroll = useDebounceFn(scrollToActiveItem, delay); + + watch(activePath, () => { + const isEnabled = typeof enable === 'boolean' ? enable : enable.value; + if (!isEnabled) return; + + debouncedScroll(); + }); + + return { + scrollToActiveItem, + }; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu.ts new file mode 100644 index 00000000..9207445f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/hooks/use-menu.ts @@ -0,0 +1,48 @@ +import type { SubMenuProvider } from '../types'; + +import { computed, getCurrentInstance } from 'vue'; + +import { findComponentUpward } from '../utils'; + +function useMenu() { + const instance = getCurrentInstance(); + if (!instance) { + throw new Error('instance is required'); + } + + /** + * @zh_CN 获取所有父级菜单链路 + */ + const parentPaths = computed(() => { + let parent = instance.parent; + const paths: string[] = [instance.props.path as string]; + while (parent?.type.name !== 'Menu') { + if (parent?.props.path) { + paths.unshift(parent.props.path as string); + } + parent = parent?.parent ?? null; + } + + return paths; + }); + + const parentMenu = computed(() => { + return findComponentUpward(instance, ['Menu', 'SubMenu']); + }); + + return { + parentMenu, + parentPaths, + }; +} + +function useMenuStyle(menu?: SubMenuProvider) { + const subMenuStyle = computed(() => { + return { + '--menu-level': menu ? (menu?.level ?? 0 + 1) : 0, + }; + }); + return subMenuStyle; +} + +export { useMenu, useMenuStyle }; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/index.ts new file mode 100644 index 00000000..1e3bc143 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/index.ts @@ -0,0 +1,4 @@ +export { default as MenuBadge } from './components/menu-badge.vue'; +export * from './components/normal-menu'; +export { default as Menu } from './menu.vue'; +export type * from './types'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/menu.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/menu.vue new file mode 100644 index 00000000..e9ac6d92 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/menu.vue @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/sub-menu.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/sub-menu.vue new file mode 100644 index 00000000..e4d471fb --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/sub-menu.vue @@ -0,0 +1,71 @@ + + + + + + {{ menu.name }} + + + + + + + + {{ menu.name }} + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/types.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/types.ts new file mode 100644 index 00000000..b1f16f8b --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/types.ts @@ -0,0 +1,144 @@ +import type { MenuRecordBadgeRaw, ThemeModeType } from '@vben-core/typings'; +import type { Component, Ref } from 'vue'; + +interface MenuProps { + /** + * @zh_CN 是否开启手风琴模式 + * @default true + */ + accordion?: boolean; + /** + * @zh_CN 菜单是否折叠 + * @default false + */ + collapse?: boolean; + + /** + * @zh_CN 菜单折叠时是否显示菜单名称 + * @default false + */ + collapseShowTitle?: boolean; + + /** + * @zh_CN 默认激活的菜单 + */ + defaultActive?: string; + + /** + * @zh_CN 默认展开的菜单 + */ + defaultOpeneds?: string[]; + + /** + * @zh_CN 菜单模式 + * @default vertical + */ + mode?: 'horizontal' | 'vertical'; + + /** + * @zh_CN 是否圆润风格 + * @default true + */ + rounded?: boolean; + + /** + * @zh_CN 是否自动滚动到激活的菜单项 + * @default false + */ + scrollToActive?: boolean; + + /** + * @zh_CN 菜单主题 + * @default dark + */ + theme?: ThemeModeType; +} + +interface SubMenuProps extends MenuRecordBadgeRaw { + /** + * @zh_CN 激活图标 + */ + activeIcon?: string; + /** + * @zh_CN 是否禁用 + */ + disabled?: boolean; + /** + * @zh_CN 图标 + */ + icon?: Component | string; + /** + * @zh_CN submenu 名称 + */ + path: string; +} + +interface MenuItemProps extends MenuRecordBadgeRaw { + /** + * @zh_CN 图标 + */ + activeIcon?: string; + /** + * @zh_CN 是否禁用 + */ + disabled?: boolean; + /** + * @zh_CN 图标 + */ + icon?: Component | string; + /** + * @zh_CN menuitem 名称 + */ + path: string; +} + +interface MenuItemRegistered { + active: boolean; + parentPaths: string[]; + path: string; +} + +interface MenuItemClicked { + parentPaths: string[]; + path: string; +} + +interface MenuProvider { + activePath?: string; + addMenuItem: (item: MenuItemRegistered) => void; + + addSubMenu: (item: MenuItemRegistered) => void; + closeMenu: (path: string, parentLinks: string[]) => void; + handleMenuItemClick: (item: MenuItemClicked) => void; + handleSubMenuClick: (subMenu: MenuItemRegistered) => void; + isMenuPopup: boolean; + items: Record; + + openedMenus: string[]; + openMenu: (path: string, parentLinks: string[]) => void; + props: MenuProps; + removeMenuItem: (item: MenuItemRegistered) => void; + + removeSubMenu: (item: MenuItemRegistered) => void; + + subMenus: Record; + theme: string; +} + +interface SubMenuProvider { + addSubMenu: (item: MenuItemRegistered) => void; + handleMouseleave?: (deepDispatch: boolean) => void; + level: number; + mouseInChild: Ref; + removeSubMenu: (item: MenuItemRegistered) => void; +} + +export type { + MenuItemClicked, + MenuItemProps, + MenuItemRegistered, + MenuProps, + MenuProvider, + SubMenuProps, + SubMenuProvider, +}; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/utils/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/utils/index.ts new file mode 100644 index 00000000..23a1b154 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/src/utils/index.ts @@ -0,0 +1,52 @@ +import type { + ComponentInternalInstance, + VNode, + VNodeChild, + VNodeNormalizedChildren, +} from 'vue'; + +import { isVNode } from 'vue'; + +type VNodeChildAtom = Exclude>; +type RawSlots = Exclude | null | string>; + +type FlattenVNodes = Array; + +/** + * @zh_CN Find the parent component upward + * @param instance + * @param parentNames + */ +function findComponentUpward( + instance: ComponentInternalInstance, + parentNames: string[], +) { + let parent = instance.parent; + while (parent && !parentNames.includes(parent?.type?.name ?? '')) { + parent = parent.parent; + } + return parent; +} + +const flattedChildren = ( + children: FlattenVNodes | VNode | VNodeNormalizedChildren, +): FlattenVNodes => { + const vNodes = Array.isArray(children) ? children : [children]; + const result: FlattenVNodes = []; + + vNodes.forEach((child) => { + if (Array.isArray(child)) { + result.push(...flattedChildren(child)); + } else if (isVNode(child) && Array.isArray(child.children)) { + result.push(...flattedChildren(child.children)); + } else { + result.push(child); + if (isVNode(child) && child.component?.subTree) { + result.push(...flattedChildren(child.component.subTree)); + } + } + }); + return result; +}; + +export { findComponentUpward, flattedChildren }; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tailwind.config.mjs b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tailwind.config.mjs new file mode 100644 index 00000000..f17f556f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tailwind.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tsconfig.json b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tsconfig.json new file mode 100644 index 00000000..ce1a891f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/menu-ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@vben/tsconfig/web.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/build.config.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/build.config.ts new file mode 100644 index 00000000..18eaa604 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/build.config.ts @@ -0,0 +1,21 @@ +import { defineBuildConfig } from 'unbuild'; + +export default defineBuildConfig({ + clean: true, + declaration: true, + entries: [ + { + builder: 'mkdist', + input: './src', + loaders: ['vue'], + pattern: ['**/*.vue'], + }, + { + builder: 'mkdist', + format: 'esm', + input: './src', + loaders: ['js'], + pattern: ['**/*.ts'], + }, + ], +}); diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/package.json b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/package.json new file mode 100644 index 00000000..3ef78c72 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/package.json @@ -0,0 +1,48 @@ +{ + "name": "@vben-core/popup-ui", + "version": "5.2.1", + "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/uikit/popup-ui" + }, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm unbuild", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist" + ], + "sideEffects": [ + "**/*.css" + ], + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./src/index.ts", + "development": "./src/index.ts", + "default": "./dist/index.mjs" + } + }, + "publishConfig": { + "exports": { + ".": { + "default": "./dist/index.mjs" + } + } + }, + "dependencies": { + "@vben-core/composables": "workspace:*", + "@vben-core/icons": "workspace:*", + "@vben-core/shadcn-ui": "workspace:*", + "@vben-core/shared": "workspace:*", + "@vben-core/typings": "workspace:*", + "@vueuse/core": "catalog:", + "vue": "catalog:" + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/postcss.config.mjs b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/postcss.config.mjs new file mode 100644 index 00000000..3d807045 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/postcss.config.mjs @@ -0,0 +1 @@ +export { default } from '@vben/tailwind-config/postcss'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts new file mode 100644 index 00000000..521a9647 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/AlertBuilder.ts @@ -0,0 +1,244 @@ +import type { Component, VNode } from 'vue'; + +import type { Recordable } from '@vben-core/typings'; + +import type { AlertProps, BeforeCloseScope, PromptProps } from './alert'; + +import { h, nextTick, ref, render } from 'vue'; + +import { useSimpleLocale } from '@vben-core/composables'; +import { Input, VbenRenderContent } from '@vben-core/shadcn-ui'; +import { isFunction, isString } from '@vben-core/shared/utils'; + +import Alert from './alert.vue'; + +const alerts = ref>([]); + +const { $t } = useSimpleLocale(); + +export function vbenAlert(options: AlertProps): Promise; +export function vbenAlert( + message: string, + options?: Partial, +): Promise; +export function vbenAlert( + message: string, + title?: string, + options?: Partial, +): Promise; + +export function vbenAlert( + arg0: AlertProps | string, + arg1?: Partial | string, + arg2?: Partial, +): Promise { + return new Promise((resolve, reject) => { + const options: AlertProps = isString(arg0) + ? { + content: arg0, + } + : { ...arg0 }; + if (arg1) { + if (isString(arg1)) { + options.title = arg1; + } else if (!isString(arg1)) { + // 如果第二个参数是对象,则合并到选项中 + Object.assign(options, arg1); + } + } + + if (arg2 && !isString(arg2)) { + Object.assign(options, arg2); + } + // 创建容器元素 + const container = document.createElement('div'); + document.body.append(container); + + // 创建一个引用,用于在回调中访问实例 + const alertRef = { container, instance: null as any }; + + const props: AlertProps & Recordable = { + onClosed: (isConfirm: boolean) => { + // 移除组件实例以及创建的所有dom(恢复页面到打开前的状态) + // 从alerts数组中移除该实例 + alerts.value = alerts.value.filter((item) => item !== alertRef); + + // 从DOM中移除容器 + render(null, container); + if (container.parentNode) { + container.remove(); + } + + // 解析 Promise,传递用户操作结果 + if (isConfirm) { + resolve(); + } else { + reject(new Error('dialog cancelled')); + } + }, + ...options, + open: true, + title: options.title ?? $t.value('prompt'), + }; + + // 创建Alert组件的VNode + const vnode = h(Alert, props); + + // 渲染组件到容器 + render(vnode, container); + + // 保存组件实例引用 + alertRef.instance = vnode.component?.proxy as Component; + + // 将实例和容器添加到alerts数组中 + alerts.value.push(alertRef); + }); +} + +export function vbenConfirm(options: AlertProps): Promise; +export function vbenConfirm( + message: string, + options?: Partial, +): Promise; +export function vbenConfirm( + message: string, + title?: string, + options?: Partial, +): Promise; + +export function vbenConfirm( + arg0: AlertProps | string, + arg1?: Partial | string, + arg2?: Partial, +): Promise { + const defaultProps: Partial = { + showCancel: true, + }; + if (!arg1) { + return isString(arg0) + ? vbenAlert(arg0, defaultProps) + : vbenAlert({ ...defaultProps, ...arg0 }); + } else if (!arg2) { + return isString(arg1) + ? vbenAlert(arg0 as string, arg1, defaultProps) + : vbenAlert(arg0 as string, { ...defaultProps, ...arg1 }); + } + return vbenAlert(arg0 as string, arg1 as string, { + ...defaultProps, + ...arg2, + }); +} + +export async function vbenPrompt( + options: PromptProps, +): Promise { + const { + component: _component, + componentProps: _componentProps, + componentSlots, + content, + defaultValue, + modelPropName: _modelPropName, + ...delegated + } = options; + + const modelValue = ref(defaultValue); + const inputComponentRef = ref(null); + const staticContents: Component[] = []; + + staticContents.push(h(VbenRenderContent, { content, renderBr: true })); + + const modelPropName = _modelPropName || 'modelValue'; + const componentProps = { ..._componentProps }; + + // 每次渲染时都会重新计算的内容函数 + const contentRenderer = () => { + const currentProps = { ...componentProps }; + + // 设置当前值 + currentProps[modelPropName] = modelValue.value; + + // 设置更新处理函数 + currentProps[`onUpdate:${modelPropName}`] = (val: T) => { + modelValue.value = val; + }; + + // 创建输入组件 + inputComponentRef.value = h( + _component || Input, + currentProps, + componentSlots, + ); + + // 返回包含静态内容和输入组件的数组 + return h( + 'div', + { class: 'flex flex-col gap-2' }, + { default: () => [...staticContents, inputComponentRef.value] }, + ); + }; + + const props: AlertProps & Recordable = { + ...delegated, + async beforeClose(scope: BeforeCloseScope) { + if (delegated.beforeClose) { + return await delegated.beforeClose({ + ...scope, + value: modelValue.value, + }); + } + }, + // 使用函数形式,每次渲染都会重新计算内容 + content: contentRenderer, + contentMasking: true, + async onOpened() { + await nextTick(); + const componentRef: null | VNode = inputComponentRef.value; + if (componentRef) { + if ( + componentRef.component?.exposed && + isFunction(componentRef.component.exposed.focus) + ) { + componentRef.component.exposed.focus(); + } else { + if (componentRef.el) { + if ( + isFunction(componentRef.el.focus) && + ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'].includes( + componentRef.el.tagName, + ) + ) { + componentRef.el.focus(); + } else if (isFunction(componentRef.el.querySelector)) { + const focusableElement = componentRef.el.querySelector( + 'input, select, textarea, button', + ); + if (focusableElement && isFunction(focusableElement.focus)) { + focusableElement.focus(); + } + } else if ( + componentRef.el.nextElementSibling && + isFunction(componentRef.el.nextElementSibling.focus) + ) { + componentRef.el.nextElementSibling.focus(); + } + } + } + } + }, + }; + + await vbenConfirm(props); + return modelValue.value; +} + +export function clearAllAlerts() { + alerts.value.forEach((alert) => { + // 从DOM中移除容器 + render(null, alert.container); + if (alert.container.parentNode) { + alert.container.remove(); + } + }); + alerts.value = []; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.ts new file mode 100644 index 00000000..5a214fa2 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.ts @@ -0,0 +1,99 @@ +import type { Component, VNode, VNodeArrayChildren } from 'vue'; + +import type { Recordable } from '@vben-core/typings'; + +import { createContext } from '@vben-core/shadcn-ui'; + +export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning'; + +export type BeforeCloseScope = { + isConfirm: boolean; +}; + +export type AlertProps = { + /** 关闭前的回调,如果返回false,则终止关闭 */ + beforeClose?: ( + scope: BeforeCloseScope, + ) => boolean | Promise | undefined; + /** 边框 */ + bordered?: boolean; + /** + * 按钮对齐方式 + * @default 'end' + */ + buttonAlign?: 'center' | 'end' | 'start'; + /** 取消按钮的标题 */ + cancelText?: string; + /** 是否居中显示 */ + centered?: boolean; + /** 确认按钮的标题 */ + confirmText?: string; + /** 弹窗容器的额外样式 */ + containerClass?: string; + /** 弹窗提示内容 */ + content: Component | string; + /** 弹窗内容的额外样式 */ + contentClass?: string; + /** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/ + contentMasking?: boolean; + /** 弹窗底部内容(与按钮在同一个容器中) */ + footer?: Component | string; + /** 弹窗的图标(在标题的前面) */ + icon?: Component | IconType; + /** + * 弹窗遮罩模糊效果 + */ + overlayBlur?: number; + /** 是否显示取消按钮 */ + showCancel?: boolean; + /** 弹窗标题 */ + title?: string; +}; + +/** Prompt属性 */ +export type PromptProps = { + /** 关闭前的回调,如果返回false,则终止关闭 */ + beforeClose?: (scope: { + isConfirm: boolean; + value: T | undefined; + }) => boolean | Promise | undefined; + /** 用于接受用户输入的组件 */ + component?: Component; + /** 输入组件的属性 */ + componentProps?: Recordable; + /** 输入组件的插槽 */ + componentSlots?: + | (() => any) + | Recordable + | VNode + | VNodeArrayChildren; + /** 默认值 */ + defaultValue?: T; + /** 输入组件的值属性名 */ + modelPropName?: string; +} & Omit; + +/** + * Alert上下文 + */ +export type AlertContext = { + /** 执行取消操作 */ + doCancel: () => void; + /** 执行确认操作 */ + doConfirm: () => void; +}; + +export const [injectAlertContext, provideAlertContext] = + createContext('VbenAlertContext'); + +/** + * 获取Alert上下文 + * @returns AlertContext + */ +export function useAlertContext() { + const context = injectAlertContext(); + if (!context) { + throw new Error('useAlertContext must be used within an AlertProvider'); + } + return context; +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.vue b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.vue new file mode 100644 index 00000000..6997235a --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/alert.vue @@ -0,0 +1,210 @@ + + + + + + + + + {{ $t(title) }} + + + + + + + + + + + + + + + + + + {{ cancelText || $t('cancel') }} + + + + + {{ confirmText || $t('confirm') }} + + + + + + + diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/index.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/index.ts new file mode 100644 index 00000000..8419b5b8 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/alert/index.ts @@ -0,0 +1,14 @@ +export type { + AlertProps, + BeforeCloseScope, + IconType, + PromptProps, +} from './alert'; +export { useAlertContext } from './alert'; +export { default as Alert } from './alert.vue'; +export { + vbenAlert as alert, + clearAllAlerts, + vbenConfirm as confirm, + vbenPrompt as prompt, +} from './AlertBuilder'; diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts new file mode 100644 index 00000000..365a2e4a --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/__tests__/drawer-api.test.ts @@ -0,0 +1,116 @@ +import type { DrawerState } from '../drawer'; + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { DrawerApi } from '../drawer-api'; + +// 模拟 Store 类 +vi.mock('@vben-core/shared/store', () => { + return { + isFunction: (fn: any) => typeof fn === 'function', + Store: class { + get state() { + return this._state; + } + private _state: DrawerState; + + private options: any; + + constructor(initialState: DrawerState, options: any) { + this._state = initialState; + this.options = options; + } + + batch(cb: () => void) { + cb(); + } + + setState(fn: (prev: DrawerState) => DrawerState) { + this._state = fn(this._state); + this.options.onUpdate(); + } + }, + }; +}); + +describe('drawerApi', () => { + let drawerApi: DrawerApi; + let drawerState: DrawerState; + + beforeEach(() => { + drawerApi = new DrawerApi(); + drawerState = drawerApi.store.state; + }); + + it('should initialize with default state', () => { + expect(drawerState.isOpen).toBe(false); + expect(drawerState.cancelText).toBe(undefined); + expect(drawerState.confirmText).toBe(undefined); + }); + + it('should open the drawer', () => { + drawerApi.open(); + expect(drawerApi.store.state.isOpen).toBe(true); + }); + + it('should close the drawer if onBeforeClose allows it', () => { + drawerApi.close(); + expect(drawerApi.store.state.isOpen).toBe(false); + }); + + it('should not close the drawer if onBeforeClose returns false', () => { + const onBeforeClose = vi.fn(() => false); + const drawerApiWithHook = new DrawerApi({ onBeforeClose }); + drawerApiWithHook.open(); + drawerApiWithHook.close(); + expect(drawerApiWithHook.store.state.isOpen).toBe(true); + expect(onBeforeClose).toHaveBeenCalled(); + }); + + it('should trigger onCancel and keep drawer open if onCancel is provided', () => { + const onCancel = vi.fn(); + const drawerApiWithHook = new DrawerApi({ onCancel }); + drawerApiWithHook.open(); + drawerApiWithHook.onCancel(); + expect(onCancel).toHaveBeenCalled(); + expect(drawerApiWithHook.store.state.isOpen).toBe(true); // 关闭逻辑不在 onCancel 内 + }); + + it('should update shared data correctly', () => { + const testData = { key: 'value' }; + drawerApi.setData(testData); + expect(drawerApi.getData()).toEqual(testData); + }); + + it('should set state correctly using an object', () => { + drawerApi.setState({ title: 'New Title' }); + expect(drawerApi.store.state.title).toBe('New Title'); + }); + + it('should set state correctly using a function', () => { + drawerApi.setState((prev) => ({ ...prev, confirmText: 'Yes' })); + expect(drawerApi.store.state.confirmText).toBe('Yes'); + }); + + it('should call onOpenChange when state changes', () => { + const onOpenChange = vi.fn(); + const drawerApiWithHook = new DrawerApi({ onOpenChange }); + drawerApiWithHook.open(); + expect(onOpenChange).toHaveBeenCalledWith(true); + }); + + it('should call onClosed callback when provided', () => { + const onClosed = vi.fn(); + const drawerApiWithHook = new DrawerApi({ onClosed }); + drawerApiWithHook.onClosed(); + expect(onClosed).toHaveBeenCalled(); + }); + + it('should call onOpened callback when provided', () => { + const onOpened = vi.fn(); + const drawerApiWithHook = new DrawerApi({ onOpened }); + drawerApiWithHook.open(); + drawerApiWithHook.onOpened(); + expect(onOpened).toHaveBeenCalled(); + }); +}); diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts new file mode 100644 index 00000000..a19a1717 --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer-api.ts @@ -0,0 +1,193 @@ +import type { DrawerApiOptions, DrawerState } from './drawer'; + +import { Store } from '@vben-core/shared/store'; +import { bindMethods, isFunction } from '@vben-core/shared/utils'; + +export class DrawerApi { + // 共享数据 + public sharedData: Record<'payload', any> = { + payload: {}, + }; + public store: Store; + + private api: Pick< + DrawerApiOptions, + | 'onBeforeClose' + | 'onCancel' + | 'onClosed' + | 'onConfirm' + | 'onOpenChange' + | 'onOpened' + >; + + // private prevState!: DrawerState; + private state!: DrawerState; + + constructor(options: DrawerApiOptions = {}) { + const { + connectedComponent: _, + onBeforeClose, + onCancel, + onClosed, + onConfirm, + onOpenChange, + onOpened, + ...storeState + } = options; + + const defaultState: DrawerState = { + class: '', + closable: true, + closeIconPlacement: 'right', + closeOnClickModal: true, + closeOnPressEscape: true, + confirmLoading: false, + contentClass: '', + footer: true, + header: true, + isOpen: false, + loading: false, + modal: true, + openAutoFocus: false, + placement: 'right', + showCancelButton: true, + showConfirmButton: true, + submitting: false, + title: '', + }; + + this.store = new Store( + { + ...defaultState, + ...storeState, + }, + { + onUpdate: () => { + const state = this.store.state; + if (state?.isOpen === this.state?.isOpen) { + this.state = state; + } else { + this.state = state; + this.api.onOpenChange?.(!!state?.isOpen); + } + }, + }, + ); + this.state = this.store.state; + this.api = { + onBeforeClose, + onCancel, + onClosed, + onConfirm, + onOpenChange, + onOpened, + }; + bindMethods(this); + } + + /** + * 关闭抽屉 + * @description 关闭抽屉时会调用 onBeforeClose 钩子函数,如果 onBeforeClose 返回 false,则不关闭弹窗 + */ + async close() { + // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗 + // 如果 onBeforeClose 返回 false,则不关闭弹窗 + const allowClose = (await this.api.onBeforeClose?.()) ?? true; + if (allowClose) { + this.store.setState((prev) => ({ + ...prev, + isOpen: false, + submitting: false, + })); + } + } + + /** + * loading和lock的区别 + * loading允许关闭窗口 + * lock不允许关闭窗口 + * @param loading 是否loading + */ + drawerLoading(loading: boolean) { + this.setState({ confirmLoading: loading, loading }); + } + + getData>() { + return (this.sharedData?.payload ?? {}) as T; + } + + /** + * 锁定抽屉状态(用于提交过程中的等待状态) + * @description 锁定状态将禁用默认的取消按钮,使用spinner覆盖抽屉内容,隐藏关闭按钮,阻止手动关闭弹窗,将默认的提交按钮标记为loading状态 + * @param isLocked 是否锁定 + */ + lock(isLocked: boolean = true) { + return this.setState({ submitting: isLocked }); + } + + /** + * 取消操作 + */ + onCancel() { + if (this.api.onCancel) { + this.api.onCancel?.(); + } else { + this.close(); + } + } + + /** + * 弹窗关闭动画播放完毕后的回调 + */ + onClosed() { + if (!this.state.isOpen) { + this.api.onClosed?.(); + } + } + + /** + * 确认操作 + */ + onConfirm() { + this.api.onConfirm?.(); + } + + /** + * 弹窗打开动画播放完毕后的回调 + */ + onOpened() { + if (this.state.isOpen) { + this.api.onOpened?.(); + } + } + + open() { + this.store.setState((prev) => ({ ...prev, isOpen: true })); + } + + setData(payload: T) { + this.sharedData.payload = payload; + return this; + } + + setState( + stateOrFn: + | ((prev: DrawerState) => Partial) + | Partial, + ) { + if (isFunction(stateOrFn)) { + this.store.setState(stateOrFn); + } else { + this.store.setState((prev) => ({ ...prev, ...stateOrFn })); + } + return this; + } + + /** + * 解除抽屉的锁定状态 + * @description 解除由lock方法设置的锁定状态,是lock(false)的别名 + */ + unlock() { + return this.lock(false); + } +} diff --git a/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts new file mode 100644 index 00000000..89cd3b5f --- /dev/null +++ b/Yi.Vben5.Vue3/packages/@core/ui-kit/popup-ui/src/drawer/drawer.ts @@ -0,0 +1,179 @@ +import type { Component, Ref } from 'vue'; + +import type { ClassType, MaybePromise } from '@vben-core/typings'; + +import type { DrawerApi } from './drawer-api'; + +export type DrawerPlacement = 'bottom' | 'left' | 'right' | 'top'; + +export type CloseIconPlacement = 'left' | 'right'; + +export interface DrawerProps { + /** + * 是否挂载到内容区域 + * @default false + */ + appendToMain?: boolean; + /** + * 取消按钮文字 + */ + cancelText?: string; + class?: ClassType; + /** + * 是否显示关闭按钮 + * @default true + */ + closable?: boolean; + /** + * 关闭按钮的位置 + */ + closeIconPlacement?: CloseIconPlacement; + /** + * 点击弹窗遮罩是否关闭弹窗 + * @default true + */ + closeOnClickModal?: boolean; + /** + * 按下 ESC 键是否关闭弹窗 + * @default true + */ + closeOnPressEscape?: boolean; + /** + * 确定按钮 loading + * @default false + */ + confirmLoading?: boolean; + /** + * 确定按钮文字 + */ + confirmText?: string; + contentClass?: string; + /** + * 弹窗描述 + */ + description?: string; + /** + * 在关闭时销毁抽屉 + */ + destroyOnClose?: boolean; + /** + * 是否显示底部 + * @default true + */ + footer?: boolean; + /** + * 弹窗底部样式 + */ + footerClass?: ClassType; + /** + * 是否显示顶栏 + * @default true + */ + header?: boolean; + /** + * 弹窗头部样式 + */ + headerClass?: ClassType; + /** + * 弹窗是否显示 + * @default false + */ + loading?: boolean; + /** + * 是否显示遮罩 + * @default true + */ + modal?: boolean; + + /** + * 是否自动聚焦 + */ + openAutoFocus?: boolean; + /** + * 弹窗遮罩模糊效果 + */ + overlayBlur?: number; + /** + * 抽屉位置 + * @default right + */ + placement?: DrawerPlacement; + + /** + * 是否显示取消按钮 + * @default true + */ + showCancelButton?: boolean; + /** + * 是否显示确认按钮 + * @default true + */ + showConfirmButton?: boolean; + /** + * 提交中(锁定抽屉状态) + */ + submitting?: boolean; + /** + * 弹窗标题 + */ + title?: string; + /** + * 弹窗标题提示 + */ + titleTooltip?: string; + /** + * 抽屉层级 + */ + zIndex?: number; +} + +export interface DrawerState extends DrawerProps { + /** 弹窗打开状态 */ + isOpen?: boolean; + /** + * 共享数据 + */ + sharedData?: Record; +} + +export type ExtendedDrawerApi = DrawerApi & { + useStore: >( + selector?: (state: NoInfer) => T, + ) => Readonly