From 914dcc41668d7a5e49dc91ef71d865ff026263b8 Mon Sep 17 00:00:00 2001 From: msumshk Date: Thu, 29 Jan 2026 04:21:09 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E7=AE=A1=E7=90=86=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 25 + .env.development | 19 + .env.production | 16 + .gitattributes | 2 + .gitignore | 15 + .husky/commit-msg | 1 + .husky/pre-commit | 1 + .prettierignore | 3 + .prettierrc | 20 + .stylelintignore | 9 + .stylelintrc.cjs | 82 + .trae/rules/project_rules.md | 164 + .vscode/extensions.json | 10 + .vscode/settings.json | 12 + AGENTS.md | 157 + CLAUDE.md | 157 + GEMINI.md | 160 + README.md | 29 + commitlint.config.cjs | 97 + document/01项目结构.md | 190 + document/99页面ToDo.md | 138 + eslint.config.mjs | 83 + index.html | 47 + package.json | 133 + pnpm-lock.yaml | 8680 +++++++++++++++++ pnpm-workspace.yaml | 7 + public/favicon.ico | Bin 0 -> 4286 bytes scripts/clean-dev.ts | 838 ++ src/App.vue | 34 + src/api/announcement.ts | 254 + src/api/auth.ts | 71 + src/api/billing.ts | 222 + src/api/dictionary/group.ts | 70 + src/api/dictionary/item.ts | 33 + src/api/dictionary/labelOverride.ts | 70 + src/api/dictionary/metrics.ts | 35 + src/api/dictionary/override.ts | 49 + src/api/dictionary/query.ts | 27 + src/api/files.ts | 22 + src/api/merchant.ts | 126 + src/api/permission.ts | 21 + src/api/quotaPackage.ts | 100 + src/api/role-template.ts | 94 + src/api/statistics.ts | 47 + src/api/store.ts | 309 + src/api/storeAudit.ts | 103 + src/api/subscription.ts | 134 + src/api/system-manage.ts | 86 + src/api/tenant-onboarding.ts | 99 + src/api/tenant-package.ts | 103 + src/api/tenant-role.ts | 98 + src/api/tenant.ts | 263 + src/assets/images/avatar/avatar.webp | Bin 0 -> 954 bytes src/assets/images/avatar/avatar1.webp | Bin 0 -> 2296 bytes src/assets/images/avatar/avatar10.webp | Bin 0 -> 1410 bytes src/assets/images/avatar/avatar2.webp | Bin 0 -> 1214 bytes src/assets/images/avatar/avatar3.webp | Bin 0 -> 726 bytes src/assets/images/avatar/avatar4.webp | Bin 0 -> 944 bytes src/assets/images/avatar/avatar5.webp | Bin 0 -> 2272 bytes src/assets/images/avatar/avatar6.webp | Bin 0 -> 810 bytes src/assets/images/avatar/avatar7.webp | Bin 0 -> 2712 bytes src/assets/images/avatar/avatar8.webp | Bin 0 -> 3946 bytes src/assets/images/avatar/avatar9.webp | Bin 0 -> 1680 bytes src/assets/images/ceremony/hb.png | Bin 0 -> 2275 bytes src/assets/images/ceremony/sd.png | Bin 0 -> 4752 bytes src/assets/images/ceremony/xc.png | Bin 0 -> 4910 bytes src/assets/images/ceremony/yd.png | Bin 0 -> 4629 bytes src/assets/images/common/logo.webp | Bin 0 -> 2484 bytes src/assets/images/draw/draw1.png | Bin 0 -> 11315 bytes src/assets/images/favicon.ico | Bin 0 -> 4286 bytes src/assets/images/lock/bg_dark.webp | Bin 0 -> 70592 bytes src/assets/images/lock/bg_light.webp | Bin 0 -> 67246 bytes src/assets/images/login/lf_icon2.webp | Bin 0 -> 25016 bytes .../settings/menu_layouts/dual_column.png | Bin 0 -> 514 bytes .../settings/menu_layouts/horizontal.png | Bin 0 -> 409 bytes .../images/settings/menu_layouts/mixed.png | Bin 0 -> 431 bytes .../images/settings/menu_layouts/vertical.png | Bin 0 -> 439 bytes .../images/settings/menu_styles/dark.png | Bin 0 -> 292 bytes .../images/settings/menu_styles/design.png | Bin 0 -> 286 bytes .../images/settings/menu_styles/light.png | Bin 0 -> 293 bytes .../images/settings/theme_styles/dark.png | Bin 0 -> 448 bytes .../images/settings/theme_styles/light.png | Bin 0 -> 416 bytes .../images/settings/theme_styles/system.png | Bin 0 -> 509 bytes src/assets/images/svg/403.svg | 1 + src/assets/images/svg/404.svg | 1 + src/assets/images/svg/500.svg | 5 + src/assets/images/svg/login_icon.svg | 1 + src/assets/images/user/avatar.webp | Bin 0 -> 2130 bytes src/assets/images/user/bg.webp | Bin 0 -> 12352 bytes src/assets/styles/components/_action-btn.scss | 71 + src/assets/styles/core/app.scss | 292 + src/assets/styles/core/dark.scss | 93 + src/assets/styles/core/el-dark.scss | 2 + src/assets/styles/core/el-light.scss | 34 + src/assets/styles/core/el-ui.scss | 526 + src/assets/styles/core/md.scss | 1036 ++ src/assets/styles/core/mixin.scss | 157 + src/assets/styles/core/reset.scss | 41 + src/assets/styles/core/router-transition.scss | 104 + src/assets/styles/core/tailwind.css | 208 + src/assets/styles/core/theme-animation.scss | 63 + src/assets/styles/core/theme-change.scss | 11 + src/assets/styles/custom/one-dark-pro.scss | 98 + src/assets/styles/index.scss | 26 + src/assets/svg/loading.ts | 32 + .../announcement/AnnouncementPreview.vue | 413 + .../announcement/AudienceSelector.vue | 766 ++ .../billing/BillingAmountDisplay.vue | 51 + src/components/billing/BillingStatusTag.vue | 30 + src/components/billing/BillingTimeline.vue | 132 + src/components/billing/PaymentMethodIcon.vue | 42 + .../business/contact_modal/ContactModal.vue | 186 + src/components/common/CategorySelect.vue | 82 + src/components/common/ImageUpload.vue | 147 + src/components/common/MerchantSelect.vue | 118 + .../common/RichTextEditor.example.ts | 134 + src/components/common/RichTextEditor.vue | 182 + .../core/banners/art-basic-banner/index.vue | 343 + .../core/banners/art-card-banner/index.vue | 114 + .../core/base/art-back-to-top/index.vue | 40 + src/components/core/base/art-logo/index.vue | 21 + .../core/base/art-svg-icon/index.vue | 24 + .../core/cards/art-bar-chart-card/index.vue | 103 + .../core/cards/art-data-list-card/index.vue | 74 + .../core/cards/art-donut-chart-card/index.vue | 124 + .../core/cards/art-image-card/index.vue | 89 + .../core/cards/art-line-chart-card/index.vue | 126 + .../core/cards/art-progress-card/index.vue | 86 + .../core/cards/art-stats-card/index.vue | 67 + .../cards/art-timeline-list-card/index.vue | 69 + .../core/charts/art-bar-chart/index.vue | 203 + .../art-dual-bar-compare-chart/index.vue | 195 + .../core/charts/art-h-bar-chart/index.vue | 208 + .../core/charts/art-k-line-chart/index.vue | 152 + .../core/charts/art-line-chart/index.vue | 371 + .../core/charts/art-radar-chart/index.vue | 105 + .../core/charts/art-ring-chart/index.vue | 133 + .../core/charts/art-scatter-chart/index.vue | 115 + .../core/forms/art-button-more/index.vue | 71 + .../core/forms/art-button-table/index.vue | 59 + .../core/forms/art-drag-verify/index.vue | 430 + .../core/forms/art-excel-export/index.vue | 389 + .../core/forms/art-excel-import/index.vue | 62 + src/components/core/forms/art-form/index.vue | 311 + .../core/forms/art-search-bar/index.vue | 437 + .../core/forms/art-wang-editor/index.vue | 223 + .../core/forms/art-wang-editor/style.scss | 210 + .../core/layouts/art-breadcrumb/index.vue | 142 + .../core/layouts/art-chat-window/index.vue | 262 + .../core/layouts/art-fast-enter/index.vue | 113 + .../layouts/art-fireworks-effect/index.vue | 633 ++ .../layouts/art-global-component/index.vue | 14 + .../core/layouts/art-global-search/index.vue | 426 + .../core/layouts/art-header-bar/index.vue | 521 + .../art-header-bar/widget/ArtUserMenu.vue | 172 + .../art-menus/art-horizontal-menu/index.vue | 110 + .../widget/HorizontalSubmenu.vue | 95 + .../art-menus/art-mixed-menu/index.vue | 279 + .../art-menus/art-sidebar-menu/index.vue | 355 + .../art-menus/art-sidebar-menu/style.scss | 253 + .../art-menus/art-sidebar-menu/theme.scss | 258 + .../widget/SidebarSubmenu.vue | 188 + .../core/layouts/art-notification/index.vue | 456 + .../core/layouts/art-page-content/index.vue | 136 + .../core/layouts/art-screen-lock/index.vue | 522 + .../composables/useSettingsConfig.ts | 248 + .../composables/useSettingsHandlers.ts | 167 + .../composables/useSettingsPanel.ts | 207 + .../composables/useSettingsState.ts | 37 + .../core/layouts/art-settings-panel/index.vue | 72 + .../layouts/art-settings-panel/style.scss | 92 + .../widget/BasicSettings.vue | 77 + .../widget/BoxStyleSettings.vue | 38 + .../widget/ColorSettings.vue | 35 + .../widget/ContainerSettings.vue | 33 + .../widget/MenuLayoutSettings.vue | 31 + .../widget/MenuStyleSettings.vue | 44 + .../widget/SectionTitle.vue | 17 + .../widget/SettingActions.vue | 235 + .../widget/SettingDrawer.vue | 51 + .../widget/SettingHeader.vue | 18 + .../art-settings-panel/widget/SettingItem.vue | 101 + .../widget/ThemeSettings.vue | 28 + .../core/layouts/art-work-tab/index.vue | 584 ++ .../core/media/art-cutter-img/index.vue | 350 + .../core/media/art-video-player/index.vue | 111 + .../core/others/art-menu-right/index.vue | 415 + .../core/others/art-watermark/index.vue | 64 + .../core/tables/art-table-header/index.vue | 340 + .../core/tables/art-table/index.vue | 342 + .../core/tables/art-table/style.scss | 99 + .../core/text-effect/art-count-to/index.vue | 310 + .../art-festival-text-scroll/index.vue | 32 + .../text-effect/art-text-scroll/index.vue | 285 + src/components/core/theme/theme-svg/index.vue | 100 + .../core/views/exception/ArtException.vue | 43 + .../core/views/login/AuthTopBar.vue | 149 + .../core/views/login/LoginLeftView.vue | 120 + .../core/views/result/ArtResultPage.vue | 43 + .../core/widget/art-icon-button/index.vue | 23 + src/components/merchant/AuditTimeline.vue | 57 + src/config/assets/images.ts | 61 + src/config/fastEnter.ts | 79 + src/config/index.ts | 135 + src/config/modules/component.ts | 105 + src/config/modules/fastEnter.ts | 127 + src/config/modules/festival.ts | 51 + src/config/modules/headerBar.ts | 63 + src/config/setting.ts | 109 + src/directives/business/highlight.ts | 248 + src/directives/business/ripple.ts | 114 + src/directives/core/auth.ts | 92 + src/directives/core/roles.ts | 89 + src/directives/index.ts | 12 + src/enums/Billing.ts | 95 + src/enums/BusinessHourType.ts | 11 + src/enums/Dictionary.ts | 32 + src/enums/MerchantStatus.ts | 14 + src/enums/OperatingMode.ts | 8 + src/enums/OverrideType.ts | 9 + src/enums/PackagingFeeMode.ts | 7 + src/enums/ReviewAction.ts | 41 + src/enums/StoreAuditAction.ts | 17 + src/enums/StoreAuditStatus.ts | 11 + src/enums/StoreBusinessStatus.ts | 9 + src/enums/StoreClosureReason.ts | 17 + src/enums/StoreOwnershipType.ts | 7 + src/enums/StoreQualificationType.ts | 11 + src/enums/StoreStatus.ts | 14 + src/enums/SubscriptionStatus.ts | 17 + src/enums/TenantPackageType.ts | 7 + src/enums/TenantStatus.ts | 17 + src/enums/TenantVerificationStatus.ts | 14 + src/enums/appEnum.ts | 81 + src/enums/formEnum.ts | 24 + src/env.d.ts | 34 + src/hooks/core/useAppMode.ts | 45 + src/hooks/core/useAuth.ts | 78 + src/hooks/core/useCeremony.ts | 184 + src/hooks/core/useChart.ts | 745 ++ src/hooks/core/useCommon.ts | 87 + src/hooks/core/useFastEnter.ts | 55 + src/hooks/core/useHeaderBar.ts | 201 + src/hooks/core/useLayoutHeight.ts | 148 + src/hooks/core/useTable.ts | 737 ++ src/hooks/core/useTableColumns.ts | 312 + src/hooks/core/useTableHeight.ts | 105 + src/hooks/core/useTheme.ts | 174 + src/hooks/index.ts | 32 + src/locales/en-US/billing.json | 244 + src/locales/en/announcement.ts | 155 + src/locales/en/dictionary.json | 196 + src/locales/en/merchant.ts | 135 + src/locales/en/store.ts | 506 + src/locales/en/tenant.ts | 267 + src/locales/en/tenantPackage.ts | 137 + src/locales/index.ts | 193 + src/locales/lang/en/announcement.ts | 227 + src/locales/lang/zh-CN/announcement.ts | 227 + src/locales/langs/en.json | 2114 ++++ src/locales/langs/zh.json | 2211 +++++ src/locales/zh-CN/announcement.ts | 154 + src/locales/zh-CN/billing.json | 244 + src/locales/zh-CN/dictionary.json | 197 + src/locales/zh-CN/merchant.ts | 135 + src/locales/zh-CN/store.ts | 506 + src/locales/zh-CN/tenant.ts | 267 + src/locales/zh-CN/tenantPackage.ts | 137 + src/main.ts | 34 + src/mock/temp/formData.ts | 273 + src/mock/upgrade/changeLog.ts | 12 + src/plugins/echarts.ts | 76 + src/plugins/index.ts | 6 + src/router/core/ComponentLoader.ts | 85 + src/router/core/IframeRouteManager.ts | 78 + src/router/core/MenuProcessor.ts | 261 + src/router/core/RoutePermissionValidator.ts | 144 + src/router/core/RouteRegistry.ts | 90 + src/router/core/RouteTransformer.ts | 148 + src/router/core/RouteValidator.ts | 187 + src/router/core/index.ts | 14 + src/router/guards/afterEach.ts | 34 + src/router/guards/beforeEach.ts | 430 + src/router/index.ts | 23 + src/router/modules/announcement.ts | 144 + src/router/modules/dashboard.ts | 24 + src/router/modules/dictionary.ts | 76 + src/router/modules/exception.ts | 46 + src/router/modules/index.ts | 23 + src/router/modules/merchant.ts | 48 + src/router/modules/result.ts | 33 + src/router/modules/store.ts | 47 + src/router/modules/tenant.ts | 88 + src/router/routes/asyncRoutes.ts | 9 + src/router/routes/staticRoutes.ts | 108 + src/router/routesAlias.ts | 8 + src/store/index.ts | 52 + .../modules/__tests__/dictionaryGroup.spec.ts | 157 + .../__tests__/dictionaryOverride.spec.ts | 86 + src/store/modules/announcement.ts | 1004 ++ src/store/modules/billingStore.ts | 408 + src/store/modules/dictionaryCache.ts | 120 + src/store/modules/dictionaryGroup.ts | 227 + src/store/modules/dictionaryItem.ts | 171 + src/store/modules/dictionaryOverride.ts | 160 + src/store/modules/impersonation.ts | 119 + src/store/modules/labelOverride.ts | 301 + src/store/modules/menu.ts | 109 + src/store/modules/merchant.ts | 31 + src/store/modules/quota-alert.ts | 63 + src/store/modules/setting.ts | 450 + src/store/modules/table.ts | 97 + src/store/modules/user.ts | 257 + src/store/modules/worktab.ts | 568 ++ src/types/announcement.ts | 140 + src/types/api/auth.d.ts | 82 + src/types/api/billing.d.ts | 369 + src/types/api/common.d.ts | 43 + src/types/api/dictionary.d.ts | 215 + src/types/api/files.d.ts | 23 + src/types/api/merchant.d.ts | 187 + src/types/api/permission.d.ts | 25 + src/types/api/role-template.d.ts | 53 + src/types/api/statistics.d.ts | 144 + src/types/api/store.d.ts | 609 ++ src/types/api/subscription.d.ts | 194 + src/types/api/system-manage.d.ts | 112 + src/types/api/tenant-role.d.ts | 44 + src/types/api/tenant.d.ts | 412 + src/types/common/index.ts | 95 + src/types/common/response.ts | 54 + src/types/component/chart.ts | 324 + src/types/component/index.ts | 145 + src/types/config/index.ts | 211 + src/types/dictionary.ts | 25 + src/types/index.ts | 22 + src/types/json-bigint.d.ts | 1 + src/types/modules/quotaPackage.d.ts | 98 + src/types/router/index.ts | 80 + src/types/store/index.ts | 157 + src/types/tenant/index.ts | 105 + src/utils/announcementStatus.ts | 36 + src/utils/billing.ts | 308 + src/utils/constants/index.ts | 8 + src/utils/constants/links.ts | 35 + src/utils/errorHandler.ts | 70 + src/utils/form/index.ts | 12 + src/utils/form/responsive.ts | 122 + src/utils/form/validator.ts | 316 + src/utils/http/error.ts | 191 + src/utils/http/index.ts | 267 + src/utils/http/status.ts | 20 + src/utils/index.ts | 34 + src/utils/navigation/index.ts | 10 + src/utils/navigation/jump.ts | 74 + src/utils/navigation/route.ts | 78 + src/utils/navigation/worktab.ts | 67 + src/utils/router.ts | 61 + src/utils/socket/index.ts | 388 + src/utils/storage/index.ts | 7 + src/utils/storage/remember-login.ts | 77 + src/utils/storage/storage-config.ts | 125 + src/utils/storage/storage-key-manager.ts | 97 + src/utils/storage/storage.ts | 250 + src/utils/sys/console.ts | 13 + src/utils/sys/error-handle.ts | 102 + src/utils/sys/index.ts | 6 + src/utils/sys/mittBus.ts | 63 + src/utils/sys/upgrade.ts | 277 + src/utils/table/tableCache.ts | 266 + src/utils/table/tableConfig.ts | 55 + src/utils/table/tableUtils.ts | 297 + src/utils/tencent-map.ts | 42 + src/utils/ui/animation.ts | 80 + src/utils/ui/colors.ts | 273 + src/utils/ui/emojo.ts | 24 + src/utils/ui/iconify-loader.ts | 31 + src/utils/ui/index.ts | 11 + src/utils/ui/loading.ts | 84 + src/utils/ui/tabs.ts | 60 + src/views/announcement-drafts/index.vue | 717 ++ src/views/app/announcements/detail.vue | 260 + src/views/app/announcements/index.vue | 333 + src/views/auth/forget-password/index.vue | 62 + src/views/auth/login/index.vue | 355 + src/views/auth/login/style.css | 38 + src/views/auth/register/index.vue | 668 ++ src/views/auth/reset-password/index.vue | 153 + src/views/dashboard/console/index.vue | 41 + .../console/modules/about-project.vue | 44 + .../dashboard/console/modules/active-user.vue | 47 + .../dashboard/console/modules/card-list.vue | 74 + .../console/modules/dynamic-stats.vue | 79 + .../dashboard/console/modules/new-user.vue | 169 + .../console/modules/sales-overview.vue | 43 + .../dashboard/console/modules/todo-list.vue | 71 + src/views/exception/403/index.vue | 16 + src/views/exception/404/index.vue | 16 + src/views/exception/500/index.vue | 16 + src/views/index/index.vue | 29 + src/views/index/style.scss | 93 + .../detail/components/AuditHistoryTab.vue | 35 + .../merchant/detail/components/BasicInfo.vue | 95 + .../detail/components/ChangeHistoryTab.vue | 71 + .../merchant/detail/components/StoresTab.vue | 57 + .../detail/components/SubjectInfo.vue | 31 + src/views/merchant/detail/index.vue | 213 + .../merchant/detail/modules/EditDialog.vue | 163 + src/views/merchant/list/index.vue | 200 + .../merchant/list/modules/merchant-search.vue | 85 + src/views/merchant/list/types.ts | 9 + .../review/components/ReviewDialog.vue | 294 + src/views/merchant/review/index.vue | 208 + .../merchant/review/modules/review-search.vue | 65 + src/views/merchant/review/types/index.ts | 6 + src/views/onboarding/error/index.vue | 459 + src/views/onboarding/pricing/index.vue | 513 + src/views/onboarding/status/index.vue | 689 ++ .../onboarding/terms-of-service/index.vue | 227 + src/views/onboarding/waiting/index.vue | 376 + src/views/outside/Iframe.vue | 42 + src/views/platform/announcements/create.vue | 434 + src/views/platform/announcements/detail.vue | 504 + src/views/platform/announcements/edit.vue | 564 ++ src/views/platform/announcements/index.vue | 460 + .../platform/qualification-alerts/index.vue | 279 + .../components/StoreAuditDetailDrawer.vue | 545 ++ .../components/StoreRiskControlDialog.vue | 178 + src/views/platform/store-audits/index.vue | 314 + src/views/result/fail/index.vue | 28 + src/views/result/success/index.vue | 21 + .../components/BusinessHoursPanel.vue | 227 + .../components/DeliveryZoneMapEditor.vue | 485 + .../components/DeliveryZonePolygonDialog.vue | 460 + .../store-detail/components/StoreFeePanel.vue | 549 ++ .../components/StoreQualificationPanel.vue | 431 + .../components/TemporaryHoursPanel.vue | 348 + .../components/BusinessStatusDialog.vue | 222 + .../components/StoreDetailDrawer.vue | 232 + .../store-list/components/StoreFormDialog.vue | 662 ++ src/views/store/store-list/index.vue | 326 + .../store/store-list/modules/store-search.vue | 111 + .../PlatformLabelOverrideFormDialog.vue | 373 + .../dictionary-label-override/index.vue | 404 + src/views/system/dictionary-metrics/index.vue | 364 + .../dictionary/components/GroupFormDialog.vue | 222 + .../dictionary/components/GroupList.vue | 291 + .../dictionary/components/I18nValueEditor.vue | 97 + .../dictionary/components/ImportDialog.vue | 204 + .../dictionary/components/ItemFormDialog.vue | 217 + .../dictionary/components/ItemTable.vue | 424 + .../__tests__/I18nValueEditor.spec.ts | 116 + src/views/system/dictionary/index.vue | 44 + src/views/system/menu/index.vue | 479 + src/views/system/menu/modules/menu-dialog.vue | 384 + src/views/system/permission/index.vue | 274 + src/views/system/role-template/index.vue | 276 + .../modules/role-template-dialog.vue | 182 + .../role-template-permission-dialog.vue | 452 + .../modules/role-template-search.vue | 54 + src/views/system/tenant-role/index.vue | 305 + .../tenant-role/modules/role-edit-dialog.vue | 246 + .../modules/role-permission-dialog.vue | 433 + .../tenant-role/modules/role-search.vue | 66 + src/views/system/user-center/index.vue | 247 + src/views/system/user/index.vue | 1211 +++ src/views/system/user/modules/user-dialog.vue | 496 + src/views/system/user/modules/user-search.vue | 174 + .../components/AnnouncementDetailDrawer.vue | 404 + .../components/AnnouncementFormDialog.vue | 463 + src/views/tenant/announcements/create.vue | 746 ++ src/views/tenant/announcements/edit.vue | 621 ++ src/views/tenant/announcements/index.vue | 698 ++ .../billing/components/BatchActionToolbar.vue | 134 + .../components/BillingDetailDrawer.vue | 545 ++ .../components/CreateBillingDialog.vue | 373 + .../billing/components/ExportDialog.vue | 256 + .../components/RecordPaymentDialog.vue | 289 + src/views/tenant/billing/index.vue | 1233 +++ src/views/tenant/billing/statistics.vue | 443 + src/views/tenant/dashboard/index.vue | 538 + .../components/CustomItemsPanel.vue | 201 + .../components/DualPaneView.vue | 63 + .../components/LabelOverrideFormDialog.vue | 274 + .../components/LabelOverridePanel.vue | 269 + .../components/OverrideToggle.vue | 68 + .../components/SortableDragDrop.vue | 175 + .../components/SystemItemsPanel.vue | 107 + .../tenant/dictionary-override/index.vue | 261 + src/views/tenant/dictionary/index.vue | 44 + .../composables/useFeatureStrategyTable.ts | 115 + src/views/tenant/package/index.vue | 1301 +++ .../package/modules/package-detail-drawer.vue | 319 + .../modules/package-feature-policy-dialog.vue | 399 + .../package/modules/package-form-dialog.vue | 662 ++ .../package/modules/package-quota-dialog.vue | 362 + .../tenant/package/modules/package-search.vue | 72 + .../package/types/feature-policy-schema.ts | 355 + src/views/tenant/package/types/index.ts | 1 + .../components/PurchaseQuotaDialog.vue | 215 + .../components/QuotaAlertConfigPanel.vue | 137 + .../components/QuotaPackageFormDialog.vue | 239 + .../components/QuotaPackageListPanel.vue | 327 + .../components/TenantQuotaPurchaseList.vue | 270 + .../components/TenantQuotaUsageDashboard.vue | 276 + src/views/tenant/quota-package/index.vue | 38 + .../tenant/review/components/ReviewDialog.vue | 890 ++ src/views/tenant/review/index.vue | 245 + .../review/modules/tenant-review-search.vue | 103 + src/views/tenant/review/types/index.ts | 1 + src/views/tenant/review/types/search-form.ts | 10 + .../components/BatchExtendDialog.vue | 326 + .../components/BatchRemindDialog.vue | 286 + .../components/ChangePlanDialog.vue | 200 + .../components/ExtendSubscriptionDialog.vue | 180 + .../components/StatusChangeDialog.vue | 174 + .../components/SubscriptionDetailDrawer.vue | 525 + src/views/tenant/subscription/index.vue | 503 + .../components/ImageUploadField.vue | 139 + .../components/TenantDetailDrawer.vue | 529 + .../tenant-list/components/TenantEdit.vue | 300 + .../TenantExtendSubscriptionDialog.vue | 128 + .../components/TenantFreezeDialog.vue | 133 + .../components/TenantImpersonateDialog.vue | 143 + .../components/TenantManualCreateDialog.vue | 987 ++ .../components/TenantQuotaDrawer.vue | 477 + .../components/TenantResetAdminDialog.vue | 145 + src/views/tenant/tenant-list/index.vue | 352 + .../tenant-list/modules/tenant-search.vue | 111 + src/views/test/RichTextEditorTest.vue | 158 + tsconfig.json | 28 + vite.config.ts | 156 + vitest.config.ts | 30 + 533 files changed, 104838 insertions(+) create mode 100644 .env create mode 100644 .env.development create mode 100644 .env.production create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .stylelintignore create mode 100644 .stylelintrc.cjs create mode 100644 .trae/rules/project_rules.md create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md create mode 100644 README.md create mode 100644 commitlint.config.cjs create mode 100644 document/01项目结构.md create mode 100644 document/99页面ToDo.md create mode 100644 eslint.config.mjs create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 public/favicon.ico create mode 100644 scripts/clean-dev.ts create mode 100644 src/App.vue create mode 100644 src/api/announcement.ts create mode 100644 src/api/auth.ts create mode 100644 src/api/billing.ts create mode 100644 src/api/dictionary/group.ts create mode 100644 src/api/dictionary/item.ts create mode 100644 src/api/dictionary/labelOverride.ts create mode 100644 src/api/dictionary/metrics.ts create mode 100644 src/api/dictionary/override.ts create mode 100644 src/api/dictionary/query.ts create mode 100644 src/api/files.ts create mode 100644 src/api/merchant.ts create mode 100644 src/api/permission.ts create mode 100644 src/api/quotaPackage.ts create mode 100644 src/api/role-template.ts create mode 100644 src/api/statistics.ts create mode 100644 src/api/store.ts create mode 100644 src/api/storeAudit.ts create mode 100644 src/api/subscription.ts create mode 100644 src/api/system-manage.ts create mode 100644 src/api/tenant-onboarding.ts create mode 100644 src/api/tenant-package.ts create mode 100644 src/api/tenant-role.ts create mode 100644 src/api/tenant.ts create mode 100644 src/assets/images/avatar/avatar.webp create mode 100644 src/assets/images/avatar/avatar1.webp create mode 100644 src/assets/images/avatar/avatar10.webp create mode 100644 src/assets/images/avatar/avatar2.webp create mode 100644 src/assets/images/avatar/avatar3.webp create mode 100644 src/assets/images/avatar/avatar4.webp create mode 100644 src/assets/images/avatar/avatar5.webp create mode 100644 src/assets/images/avatar/avatar6.webp create mode 100644 src/assets/images/avatar/avatar7.webp create mode 100644 src/assets/images/avatar/avatar8.webp create mode 100644 src/assets/images/avatar/avatar9.webp create mode 100644 src/assets/images/ceremony/hb.png create mode 100644 src/assets/images/ceremony/sd.png create mode 100644 src/assets/images/ceremony/xc.png create mode 100644 src/assets/images/ceremony/yd.png create mode 100644 src/assets/images/common/logo.webp create mode 100644 src/assets/images/draw/draw1.png create mode 100644 src/assets/images/favicon.ico create mode 100644 src/assets/images/lock/bg_dark.webp create mode 100644 src/assets/images/lock/bg_light.webp create mode 100644 src/assets/images/login/lf_icon2.webp create mode 100644 src/assets/images/settings/menu_layouts/dual_column.png create mode 100644 src/assets/images/settings/menu_layouts/horizontal.png create mode 100644 src/assets/images/settings/menu_layouts/mixed.png create mode 100644 src/assets/images/settings/menu_layouts/vertical.png create mode 100644 src/assets/images/settings/menu_styles/dark.png create mode 100644 src/assets/images/settings/menu_styles/design.png create mode 100644 src/assets/images/settings/menu_styles/light.png create mode 100644 src/assets/images/settings/theme_styles/dark.png create mode 100644 src/assets/images/settings/theme_styles/light.png create mode 100644 src/assets/images/settings/theme_styles/system.png create mode 100644 src/assets/images/svg/403.svg create mode 100644 src/assets/images/svg/404.svg create mode 100644 src/assets/images/svg/500.svg create mode 100644 src/assets/images/svg/login_icon.svg create mode 100644 src/assets/images/user/avatar.webp create mode 100644 src/assets/images/user/bg.webp create mode 100644 src/assets/styles/components/_action-btn.scss create mode 100644 src/assets/styles/core/app.scss create mode 100644 src/assets/styles/core/dark.scss create mode 100644 src/assets/styles/core/el-dark.scss create mode 100644 src/assets/styles/core/el-light.scss create mode 100644 src/assets/styles/core/el-ui.scss create mode 100644 src/assets/styles/core/md.scss create mode 100644 src/assets/styles/core/mixin.scss create mode 100644 src/assets/styles/core/reset.scss create mode 100644 src/assets/styles/core/router-transition.scss create mode 100644 src/assets/styles/core/tailwind.css create mode 100644 src/assets/styles/core/theme-animation.scss create mode 100644 src/assets/styles/core/theme-change.scss create mode 100644 src/assets/styles/custom/one-dark-pro.scss create mode 100644 src/assets/styles/index.scss create mode 100644 src/assets/svg/loading.ts create mode 100644 src/components/announcement/AnnouncementPreview.vue create mode 100644 src/components/announcement/AudienceSelector.vue create mode 100644 src/components/billing/BillingAmountDisplay.vue create mode 100644 src/components/billing/BillingStatusTag.vue create mode 100644 src/components/billing/BillingTimeline.vue create mode 100644 src/components/billing/PaymentMethodIcon.vue create mode 100644 src/components/business/contact_modal/ContactModal.vue create mode 100644 src/components/common/CategorySelect.vue create mode 100644 src/components/common/ImageUpload.vue create mode 100644 src/components/common/MerchantSelect.vue create mode 100644 src/components/common/RichTextEditor.example.ts create mode 100644 src/components/common/RichTextEditor.vue create mode 100644 src/components/core/banners/art-basic-banner/index.vue create mode 100644 src/components/core/banners/art-card-banner/index.vue create mode 100644 src/components/core/base/art-back-to-top/index.vue create mode 100644 src/components/core/base/art-logo/index.vue create mode 100644 src/components/core/base/art-svg-icon/index.vue create mode 100644 src/components/core/cards/art-bar-chart-card/index.vue create mode 100644 src/components/core/cards/art-data-list-card/index.vue create mode 100644 src/components/core/cards/art-donut-chart-card/index.vue create mode 100644 src/components/core/cards/art-image-card/index.vue create mode 100644 src/components/core/cards/art-line-chart-card/index.vue create mode 100644 src/components/core/cards/art-progress-card/index.vue create mode 100644 src/components/core/cards/art-stats-card/index.vue create mode 100644 src/components/core/cards/art-timeline-list-card/index.vue create mode 100644 src/components/core/charts/art-bar-chart/index.vue create mode 100644 src/components/core/charts/art-dual-bar-compare-chart/index.vue create mode 100644 src/components/core/charts/art-h-bar-chart/index.vue create mode 100644 src/components/core/charts/art-k-line-chart/index.vue create mode 100644 src/components/core/charts/art-line-chart/index.vue create mode 100644 src/components/core/charts/art-radar-chart/index.vue create mode 100644 src/components/core/charts/art-ring-chart/index.vue create mode 100644 src/components/core/charts/art-scatter-chart/index.vue create mode 100644 src/components/core/forms/art-button-more/index.vue create mode 100644 src/components/core/forms/art-button-table/index.vue create mode 100644 src/components/core/forms/art-drag-verify/index.vue create mode 100644 src/components/core/forms/art-excel-export/index.vue create mode 100644 src/components/core/forms/art-excel-import/index.vue create mode 100644 src/components/core/forms/art-form/index.vue create mode 100644 src/components/core/forms/art-search-bar/index.vue create mode 100644 src/components/core/forms/art-wang-editor/index.vue create mode 100644 src/components/core/forms/art-wang-editor/style.scss create mode 100644 src/components/core/layouts/art-breadcrumb/index.vue create mode 100644 src/components/core/layouts/art-chat-window/index.vue create mode 100644 src/components/core/layouts/art-fast-enter/index.vue create mode 100644 src/components/core/layouts/art-fireworks-effect/index.vue create mode 100644 src/components/core/layouts/art-global-component/index.vue create mode 100644 src/components/core/layouts/art-global-search/index.vue create mode 100644 src/components/core/layouts/art-header-bar/index.vue create mode 100644 src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue create mode 100644 src/components/core/layouts/art-menus/art-horizontal-menu/index.vue create mode 100644 src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue create mode 100644 src/components/core/layouts/art-menus/art-mixed-menu/index.vue create mode 100644 src/components/core/layouts/art-menus/art-sidebar-menu/index.vue create mode 100644 src/components/core/layouts/art-menus/art-sidebar-menu/style.scss create mode 100644 src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss create mode 100644 src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue create mode 100644 src/components/core/layouts/art-notification/index.vue create mode 100644 src/components/core/layouts/art-page-content/index.vue create mode 100644 src/components/core/layouts/art-screen-lock/index.vue create mode 100644 src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts create mode 100644 src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts create mode 100644 src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts create mode 100644 src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts create mode 100644 src/components/core/layouts/art-settings-panel/index.vue create mode 100644 src/components/core/layouts/art-settings-panel/style.scss create mode 100644 src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/SettingActions.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/SettingItem.vue create mode 100644 src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue create mode 100644 src/components/core/layouts/art-work-tab/index.vue create mode 100644 src/components/core/media/art-cutter-img/index.vue create mode 100644 src/components/core/media/art-video-player/index.vue create mode 100644 src/components/core/others/art-menu-right/index.vue create mode 100644 src/components/core/others/art-watermark/index.vue create mode 100644 src/components/core/tables/art-table-header/index.vue create mode 100644 src/components/core/tables/art-table/index.vue create mode 100644 src/components/core/tables/art-table/style.scss create mode 100644 src/components/core/text-effect/art-count-to/index.vue create mode 100644 src/components/core/text-effect/art-festival-text-scroll/index.vue create mode 100644 src/components/core/text-effect/art-text-scroll/index.vue create mode 100644 src/components/core/theme/theme-svg/index.vue create mode 100644 src/components/core/views/exception/ArtException.vue create mode 100644 src/components/core/views/login/AuthTopBar.vue create mode 100644 src/components/core/views/login/LoginLeftView.vue create mode 100644 src/components/core/views/result/ArtResultPage.vue create mode 100644 src/components/core/widget/art-icon-button/index.vue create mode 100644 src/components/merchant/AuditTimeline.vue create mode 100644 src/config/assets/images.ts create mode 100644 src/config/fastEnter.ts create mode 100644 src/config/index.ts create mode 100644 src/config/modules/component.ts create mode 100644 src/config/modules/fastEnter.ts create mode 100644 src/config/modules/festival.ts create mode 100644 src/config/modules/headerBar.ts create mode 100644 src/config/setting.ts create mode 100644 src/directives/business/highlight.ts create mode 100644 src/directives/business/ripple.ts create mode 100644 src/directives/core/auth.ts create mode 100644 src/directives/core/roles.ts create mode 100644 src/directives/index.ts create mode 100644 src/enums/Billing.ts create mode 100644 src/enums/BusinessHourType.ts create mode 100644 src/enums/Dictionary.ts create mode 100644 src/enums/MerchantStatus.ts create mode 100644 src/enums/OperatingMode.ts create mode 100644 src/enums/OverrideType.ts create mode 100644 src/enums/PackagingFeeMode.ts create mode 100644 src/enums/ReviewAction.ts create mode 100644 src/enums/StoreAuditAction.ts create mode 100644 src/enums/StoreAuditStatus.ts create mode 100644 src/enums/StoreBusinessStatus.ts create mode 100644 src/enums/StoreClosureReason.ts create mode 100644 src/enums/StoreOwnershipType.ts create mode 100644 src/enums/StoreQualificationType.ts create mode 100644 src/enums/StoreStatus.ts create mode 100644 src/enums/SubscriptionStatus.ts create mode 100644 src/enums/TenantPackageType.ts create mode 100644 src/enums/TenantStatus.ts create mode 100644 src/enums/TenantVerificationStatus.ts create mode 100644 src/enums/appEnum.ts create mode 100644 src/enums/formEnum.ts create mode 100644 src/env.d.ts create mode 100644 src/hooks/core/useAppMode.ts create mode 100644 src/hooks/core/useAuth.ts create mode 100644 src/hooks/core/useCeremony.ts create mode 100644 src/hooks/core/useChart.ts create mode 100644 src/hooks/core/useCommon.ts create mode 100644 src/hooks/core/useFastEnter.ts create mode 100644 src/hooks/core/useHeaderBar.ts create mode 100644 src/hooks/core/useLayoutHeight.ts create mode 100644 src/hooks/core/useTable.ts create mode 100644 src/hooks/core/useTableColumns.ts create mode 100644 src/hooks/core/useTableHeight.ts create mode 100644 src/hooks/core/useTheme.ts create mode 100644 src/hooks/index.ts create mode 100644 src/locales/en-US/billing.json create mode 100644 src/locales/en/announcement.ts create mode 100644 src/locales/en/dictionary.json create mode 100644 src/locales/en/merchant.ts create mode 100644 src/locales/en/store.ts create mode 100644 src/locales/en/tenant.ts create mode 100644 src/locales/en/tenantPackage.ts create mode 100644 src/locales/index.ts create mode 100644 src/locales/lang/en/announcement.ts create mode 100644 src/locales/lang/zh-CN/announcement.ts create mode 100644 src/locales/langs/en.json create mode 100644 src/locales/langs/zh.json create mode 100644 src/locales/zh-CN/announcement.ts create mode 100644 src/locales/zh-CN/billing.json create mode 100644 src/locales/zh-CN/dictionary.json create mode 100644 src/locales/zh-CN/merchant.ts create mode 100644 src/locales/zh-CN/store.ts create mode 100644 src/locales/zh-CN/tenant.ts create mode 100644 src/locales/zh-CN/tenantPackage.ts create mode 100644 src/main.ts create mode 100644 src/mock/temp/formData.ts create mode 100644 src/mock/upgrade/changeLog.ts create mode 100644 src/plugins/echarts.ts create mode 100644 src/plugins/index.ts create mode 100644 src/router/core/ComponentLoader.ts create mode 100644 src/router/core/IframeRouteManager.ts create mode 100644 src/router/core/MenuProcessor.ts create mode 100644 src/router/core/RoutePermissionValidator.ts create mode 100644 src/router/core/RouteRegistry.ts create mode 100644 src/router/core/RouteTransformer.ts create mode 100644 src/router/core/RouteValidator.ts create mode 100644 src/router/core/index.ts create mode 100644 src/router/guards/afterEach.ts create mode 100644 src/router/guards/beforeEach.ts create mode 100644 src/router/index.ts create mode 100644 src/router/modules/announcement.ts create mode 100644 src/router/modules/dashboard.ts create mode 100644 src/router/modules/dictionary.ts create mode 100644 src/router/modules/exception.ts create mode 100644 src/router/modules/index.ts create mode 100644 src/router/modules/merchant.ts create mode 100644 src/router/modules/result.ts create mode 100644 src/router/modules/store.ts create mode 100644 src/router/modules/tenant.ts create mode 100644 src/router/routes/asyncRoutes.ts create mode 100644 src/router/routes/staticRoutes.ts create mode 100644 src/router/routesAlias.ts create mode 100644 src/store/index.ts create mode 100644 src/store/modules/__tests__/dictionaryGroup.spec.ts create mode 100644 src/store/modules/__tests__/dictionaryOverride.spec.ts create mode 100644 src/store/modules/announcement.ts create mode 100644 src/store/modules/billingStore.ts create mode 100644 src/store/modules/dictionaryCache.ts create mode 100644 src/store/modules/dictionaryGroup.ts create mode 100644 src/store/modules/dictionaryItem.ts create mode 100644 src/store/modules/dictionaryOverride.ts create mode 100644 src/store/modules/impersonation.ts create mode 100644 src/store/modules/labelOverride.ts create mode 100644 src/store/modules/menu.ts create mode 100644 src/store/modules/merchant.ts create mode 100644 src/store/modules/quota-alert.ts create mode 100644 src/store/modules/setting.ts create mode 100644 src/store/modules/table.ts create mode 100644 src/store/modules/user.ts create mode 100644 src/store/modules/worktab.ts create mode 100644 src/types/announcement.ts create mode 100644 src/types/api/auth.d.ts create mode 100644 src/types/api/billing.d.ts create mode 100644 src/types/api/common.d.ts create mode 100644 src/types/api/dictionary.d.ts create mode 100644 src/types/api/files.d.ts create mode 100644 src/types/api/merchant.d.ts create mode 100644 src/types/api/permission.d.ts create mode 100644 src/types/api/role-template.d.ts create mode 100644 src/types/api/statistics.d.ts create mode 100644 src/types/api/store.d.ts create mode 100644 src/types/api/subscription.d.ts create mode 100644 src/types/api/system-manage.d.ts create mode 100644 src/types/api/tenant-role.d.ts create mode 100644 src/types/api/tenant.d.ts create mode 100644 src/types/common/index.ts create mode 100644 src/types/common/response.ts create mode 100644 src/types/component/chart.ts create mode 100644 src/types/component/index.ts create mode 100644 src/types/config/index.ts create mode 100644 src/types/dictionary.ts create mode 100644 src/types/index.ts create mode 100644 src/types/json-bigint.d.ts create mode 100644 src/types/modules/quotaPackage.d.ts create mode 100644 src/types/router/index.ts create mode 100644 src/types/store/index.ts create mode 100644 src/types/tenant/index.ts create mode 100644 src/utils/announcementStatus.ts create mode 100644 src/utils/billing.ts create mode 100644 src/utils/constants/index.ts create mode 100644 src/utils/constants/links.ts create mode 100644 src/utils/errorHandler.ts create mode 100644 src/utils/form/index.ts create mode 100644 src/utils/form/responsive.ts create mode 100644 src/utils/form/validator.ts create mode 100644 src/utils/http/error.ts create mode 100644 src/utils/http/index.ts create mode 100644 src/utils/http/status.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/navigation/index.ts create mode 100644 src/utils/navigation/jump.ts create mode 100644 src/utils/navigation/route.ts create mode 100644 src/utils/navigation/worktab.ts create mode 100644 src/utils/router.ts create mode 100644 src/utils/socket/index.ts create mode 100644 src/utils/storage/index.ts create mode 100644 src/utils/storage/remember-login.ts create mode 100644 src/utils/storage/storage-config.ts create mode 100644 src/utils/storage/storage-key-manager.ts create mode 100644 src/utils/storage/storage.ts create mode 100644 src/utils/sys/console.ts create mode 100644 src/utils/sys/error-handle.ts create mode 100644 src/utils/sys/index.ts create mode 100644 src/utils/sys/mittBus.ts create mode 100644 src/utils/sys/upgrade.ts create mode 100644 src/utils/table/tableCache.ts create mode 100644 src/utils/table/tableConfig.ts create mode 100644 src/utils/table/tableUtils.ts create mode 100644 src/utils/tencent-map.ts create mode 100644 src/utils/ui/animation.ts create mode 100644 src/utils/ui/colors.ts create mode 100644 src/utils/ui/emojo.ts create mode 100644 src/utils/ui/iconify-loader.ts create mode 100644 src/utils/ui/index.ts create mode 100644 src/utils/ui/loading.ts create mode 100644 src/utils/ui/tabs.ts create mode 100644 src/views/announcement-drafts/index.vue create mode 100644 src/views/app/announcements/detail.vue create mode 100644 src/views/app/announcements/index.vue create mode 100644 src/views/auth/forget-password/index.vue create mode 100644 src/views/auth/login/index.vue create mode 100644 src/views/auth/login/style.css create mode 100644 src/views/auth/register/index.vue create mode 100644 src/views/auth/reset-password/index.vue create mode 100644 src/views/dashboard/console/index.vue create mode 100644 src/views/dashboard/console/modules/about-project.vue create mode 100644 src/views/dashboard/console/modules/active-user.vue create mode 100644 src/views/dashboard/console/modules/card-list.vue create mode 100644 src/views/dashboard/console/modules/dynamic-stats.vue create mode 100644 src/views/dashboard/console/modules/new-user.vue create mode 100644 src/views/dashboard/console/modules/sales-overview.vue create mode 100644 src/views/dashboard/console/modules/todo-list.vue create mode 100644 src/views/exception/403/index.vue create mode 100644 src/views/exception/404/index.vue create mode 100644 src/views/exception/500/index.vue create mode 100644 src/views/index/index.vue create mode 100644 src/views/index/style.scss create mode 100644 src/views/merchant/detail/components/AuditHistoryTab.vue create mode 100644 src/views/merchant/detail/components/BasicInfo.vue create mode 100644 src/views/merchant/detail/components/ChangeHistoryTab.vue create mode 100644 src/views/merchant/detail/components/StoresTab.vue create mode 100644 src/views/merchant/detail/components/SubjectInfo.vue create mode 100644 src/views/merchant/detail/index.vue create mode 100644 src/views/merchant/detail/modules/EditDialog.vue create mode 100644 src/views/merchant/list/index.vue create mode 100644 src/views/merchant/list/modules/merchant-search.vue create mode 100644 src/views/merchant/list/types.ts create mode 100644 src/views/merchant/review/components/ReviewDialog.vue create mode 100644 src/views/merchant/review/index.vue create mode 100644 src/views/merchant/review/modules/review-search.vue create mode 100644 src/views/merchant/review/types/index.ts create mode 100644 src/views/onboarding/error/index.vue create mode 100644 src/views/onboarding/pricing/index.vue create mode 100644 src/views/onboarding/status/index.vue create mode 100644 src/views/onboarding/terms-of-service/index.vue create mode 100644 src/views/onboarding/waiting/index.vue create mode 100644 src/views/outside/Iframe.vue create mode 100644 src/views/platform/announcements/create.vue create mode 100644 src/views/platform/announcements/detail.vue create mode 100644 src/views/platform/announcements/edit.vue create mode 100644 src/views/platform/announcements/index.vue create mode 100644 src/views/platform/qualification-alerts/index.vue create mode 100644 src/views/platform/store-audits/components/StoreAuditDetailDrawer.vue create mode 100644 src/views/platform/store-audits/components/StoreRiskControlDialog.vue create mode 100644 src/views/platform/store-audits/index.vue create mode 100644 src/views/result/fail/index.vue create mode 100644 src/views/result/success/index.vue create mode 100644 src/views/store/store-detail/components/BusinessHoursPanel.vue create mode 100644 src/views/store/store-detail/components/DeliveryZoneMapEditor.vue create mode 100644 src/views/store/store-detail/components/DeliveryZonePolygonDialog.vue create mode 100644 src/views/store/store-detail/components/StoreFeePanel.vue create mode 100644 src/views/store/store-detail/components/StoreQualificationPanel.vue create mode 100644 src/views/store/store-detail/components/TemporaryHoursPanel.vue create mode 100644 src/views/store/store-list/components/BusinessStatusDialog.vue create mode 100644 src/views/store/store-list/components/StoreDetailDrawer.vue create mode 100644 src/views/store/store-list/components/StoreFormDialog.vue create mode 100644 src/views/store/store-list/index.vue create mode 100644 src/views/store/store-list/modules/store-search.vue create mode 100644 src/views/system/dictionary-label-override/components/PlatformLabelOverrideFormDialog.vue create mode 100644 src/views/system/dictionary-label-override/index.vue create mode 100644 src/views/system/dictionary-metrics/index.vue create mode 100644 src/views/system/dictionary/components/GroupFormDialog.vue create mode 100644 src/views/system/dictionary/components/GroupList.vue create mode 100644 src/views/system/dictionary/components/I18nValueEditor.vue create mode 100644 src/views/system/dictionary/components/ImportDialog.vue create mode 100644 src/views/system/dictionary/components/ItemFormDialog.vue create mode 100644 src/views/system/dictionary/components/ItemTable.vue create mode 100644 src/views/system/dictionary/components/__tests__/I18nValueEditor.spec.ts create mode 100644 src/views/system/dictionary/index.vue create mode 100644 src/views/system/menu/index.vue create mode 100644 src/views/system/menu/modules/menu-dialog.vue create mode 100644 src/views/system/permission/index.vue create mode 100644 src/views/system/role-template/index.vue create mode 100644 src/views/system/role-template/modules/role-template-dialog.vue create mode 100644 src/views/system/role-template/modules/role-template-permission-dialog.vue create mode 100644 src/views/system/role-template/modules/role-template-search.vue create mode 100644 src/views/system/tenant-role/index.vue create mode 100644 src/views/system/tenant-role/modules/role-edit-dialog.vue create mode 100644 src/views/system/tenant-role/modules/role-permission-dialog.vue create mode 100644 src/views/system/tenant-role/modules/role-search.vue create mode 100644 src/views/system/user-center/index.vue create mode 100644 src/views/system/user/index.vue create mode 100644 src/views/system/user/modules/user-dialog.vue create mode 100644 src/views/system/user/modules/user-search.vue create mode 100644 src/views/tenant/announcements/components/AnnouncementDetailDrawer.vue create mode 100644 src/views/tenant/announcements/components/AnnouncementFormDialog.vue create mode 100644 src/views/tenant/announcements/create.vue create mode 100644 src/views/tenant/announcements/edit.vue create mode 100644 src/views/tenant/announcements/index.vue create mode 100644 src/views/tenant/billing/components/BatchActionToolbar.vue create mode 100644 src/views/tenant/billing/components/BillingDetailDrawer.vue create mode 100644 src/views/tenant/billing/components/CreateBillingDialog.vue create mode 100644 src/views/tenant/billing/components/ExportDialog.vue create mode 100644 src/views/tenant/billing/components/RecordPaymentDialog.vue create mode 100644 src/views/tenant/billing/index.vue create mode 100644 src/views/tenant/billing/statistics.vue create mode 100644 src/views/tenant/dashboard/index.vue create mode 100644 src/views/tenant/dictionary-override/components/CustomItemsPanel.vue create mode 100644 src/views/tenant/dictionary-override/components/DualPaneView.vue create mode 100644 src/views/tenant/dictionary-override/components/LabelOverrideFormDialog.vue create mode 100644 src/views/tenant/dictionary-override/components/LabelOverridePanel.vue create mode 100644 src/views/tenant/dictionary-override/components/OverrideToggle.vue create mode 100644 src/views/tenant/dictionary-override/components/SortableDragDrop.vue create mode 100644 src/views/tenant/dictionary-override/components/SystemItemsPanel.vue create mode 100644 src/views/tenant/dictionary-override/index.vue create mode 100644 src/views/tenant/dictionary/index.vue create mode 100644 src/views/tenant/package/composables/useFeatureStrategyTable.ts create mode 100644 src/views/tenant/package/index.vue create mode 100644 src/views/tenant/package/modules/package-detail-drawer.vue create mode 100644 src/views/tenant/package/modules/package-feature-policy-dialog.vue create mode 100644 src/views/tenant/package/modules/package-form-dialog.vue create mode 100644 src/views/tenant/package/modules/package-quota-dialog.vue create mode 100644 src/views/tenant/package/modules/package-search.vue create mode 100644 src/views/tenant/package/types/feature-policy-schema.ts create mode 100644 src/views/tenant/package/types/index.ts create mode 100644 src/views/tenant/quota-package/components/PurchaseQuotaDialog.vue create mode 100644 src/views/tenant/quota-package/components/QuotaAlertConfigPanel.vue create mode 100644 src/views/tenant/quota-package/components/QuotaPackageFormDialog.vue create mode 100644 src/views/tenant/quota-package/components/QuotaPackageListPanel.vue create mode 100644 src/views/tenant/quota-package/components/TenantQuotaPurchaseList.vue create mode 100644 src/views/tenant/quota-package/components/TenantQuotaUsageDashboard.vue create mode 100644 src/views/tenant/quota-package/index.vue create mode 100644 src/views/tenant/review/components/ReviewDialog.vue create mode 100644 src/views/tenant/review/index.vue create mode 100644 src/views/tenant/review/modules/tenant-review-search.vue create mode 100644 src/views/tenant/review/types/index.ts create mode 100644 src/views/tenant/review/types/search-form.ts create mode 100644 src/views/tenant/subscription/components/BatchExtendDialog.vue create mode 100644 src/views/tenant/subscription/components/BatchRemindDialog.vue create mode 100644 src/views/tenant/subscription/components/ChangePlanDialog.vue create mode 100644 src/views/tenant/subscription/components/ExtendSubscriptionDialog.vue create mode 100644 src/views/tenant/subscription/components/StatusChangeDialog.vue create mode 100644 src/views/tenant/subscription/components/SubscriptionDetailDrawer.vue create mode 100644 src/views/tenant/subscription/index.vue create mode 100644 src/views/tenant/tenant-list/components/ImageUploadField.vue create mode 100644 src/views/tenant/tenant-list/components/TenantDetailDrawer.vue create mode 100644 src/views/tenant/tenant-list/components/TenantEdit.vue create mode 100644 src/views/tenant/tenant-list/components/TenantExtendSubscriptionDialog.vue create mode 100644 src/views/tenant/tenant-list/components/TenantFreezeDialog.vue create mode 100644 src/views/tenant/tenant-list/components/TenantImpersonateDialog.vue create mode 100644 src/views/tenant/tenant-list/components/TenantManualCreateDialog.vue create mode 100644 src/views/tenant/tenant-list/components/TenantQuotaDrawer.vue create mode 100644 src/views/tenant/tenant-list/components/TenantResetAdminDialog.vue create mode 100644 src/views/tenant/tenant-list/index.vue create mode 100644 src/views/tenant/tenant-list/modules/tenant-search.vue create mode 100644 src/views/test/RichTextEditorTest.vue create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.env b/.env new file mode 100644 index 0000000..ce42b77 --- /dev/null +++ b/.env @@ -0,0 +1,25 @@ +# 【通用】环境变量 + +# 版本号 +VITE_VERSION = 3.0.1 + +# 端口号 +VITE_PORT = 3006 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# 权限模式【 frontend 前端模式 / backend 后端模式 】 +VITE_ACCESS_MODE = backend + +# 跨域请求时是否携带 Cookie(开启前需确保后端支持) +VITE_WITH_CREDENTIALS = false + +# 是否打开路由信息 +VITE_OPEN_ROUTE_INFO = false + +# 锁屏加密密钥 +VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro + +# 腾讯地图 Key +VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..771f171 --- /dev/null +++ b/.env.development @@ -0,0 +1,19 @@ +# 【开发】环境变量 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址) +VITE_API_URL = http://127.0.0.1:7801/ + +# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题) +VITE_API_PROXY_URL = http://127.0.0.1:7801/ + +# 腾讯地图 Key +VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ + +# 权限模式【 frontend 前端模式 / backend 后端模式 】 +VITE_ACCESS_MODE = backend + +# Delete console +VITE_DROP_CONSOLE = false diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..4a9effc --- /dev/null +++ b/.env.production @@ -0,0 +1,16 @@ +# 【生产】环境变量 + +# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) +VITE_BASE_URL = / + +# API 地址前缀 +VITE_API_URL = https://kjkj.qiyuesns.cn/ + +# 腾讯地图 Key +VITE_TENCENT_MAP_KEY = DGIBZ-YUM64-5ONUT-F4RIA-MPUP2-XFFXZ + +# 权限模式【 frontend 前端模式 / backend 后端模式 】 +VITE_ACCESS_MODE = backend + +# Delete console +VITE_DROP_CONSOLE = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..866e8ee --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.html linguist-detectable=false +*.vue linguist-detectable=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d1f72a --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.cursorrules +完整项目备份/ + +# Auto-generated files +src/types/import/auto-imports.d.ts +src/types/import/components.d.ts +.auto-import.json + +# Swagger exports +document/swagger/ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..09d2b14 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +pnpm dlx commitlint --edit $1 \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..22c0347 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm run lint:lint-staged \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..9e96efc --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +/node_modules/* +/dist/* +/src/main.ts \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f3d6ad5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,20 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "vueIndentScriptAndStyle": true, + "singleQuote": true, + "quoteProps": "as-needed", + "bracketSpacing": true, + "trailingComma": "none", + "bracketSameLine": false, + "jsxSingleQuote": false, + "arrowParens": "always", + "insertPragma": false, + "requirePragma": false, + "proseWrap": "never", + "htmlWhitespaceSensitivity": "strict", + "endOfLine": "auto", + "rangeStart": 0 +} diff --git a/.stylelintignore b/.stylelintignore new file mode 100644 index 0000000..476ea45 --- /dev/null +++ b/.stylelintignore @@ -0,0 +1,9 @@ +dist +node_modules +public +.husky +.vscode + +src/components/Layout/MenuLeft/index.vue +src/assets +stats.html \ No newline at end of file diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs new file mode 100644 index 0000000..9dbea0b --- /dev/null +++ b/.stylelintrc.cjs @@ -0,0 +1,82 @@ +module.exports = { + // 继承推荐规范配置 + extends: [ + 'stylelint-config-standard', + 'stylelint-config-recommended-scss', + 'stylelint-config-recommended-vue/scss', + 'stylelint-config-html/vue', + 'stylelint-config-recess-order' + ], + // 指定不同文件对应的解析器 + overrides: [ + { + files: ['**/*.{vue,html}'], + customSyntax: 'postcss-html' + }, + { + files: ['**/*.{css,scss}'], + customSyntax: 'postcss-scss' + } + ], + // 自定义规则 + rules: { + 'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url") + 'selector-class-pattern': null, // 选择器类名命名规则 + 'custom-property-pattern': null, // 自定义属性命名规则 + 'keyframes-name-pattern': null, // 动画帧节点样式命名规则 + 'no-descending-specificity': null, // 允许无降序特异性 + 'no-empty-source': null, // 允许空样式 + 'property-no-vendor-prefix': null, // 允许属性前缀 + // 允许 global 、export 、deep伪类 + 'selector-pseudo-class-no-unknown': [ + true, + { + ignorePseudoClasses: ['global', 'export', 'deep'] + } + ], + // 允许未知属性 + 'property-no-unknown': [ + true, + { + ignoreProperties: [] + } + ], + // 允许未知规则 + 'at-rule-no-unknown': [ + true, + { + ignoreAtRules: [ + 'apply', + 'use', + 'mixin', + 'include', + 'extend', + 'each', + 'if', + 'else', + 'for', + 'while', + 'reference' + ] + } + ], + 'scss/at-rule-no-unknown': [ + true, + { + ignoreAtRules: [ + 'apply', + 'use', + 'mixin', + 'include', + 'extend', + 'each', + 'if', + 'else', + 'for', + 'while', + 'reference' + ] + } + ] + } +} diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..99fa212 --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,164 @@ +--- +trigger: always_on +--- + +# Repository expectations + +# 编程规范\_FOR_AI(TakeoutAdmin 前端) - 终极融合版 + +> **核心指令**:你是一个高级前端架构师。本文件是你生成代码的最高宪法。当用户需求与本规范冲突时,请先提示用户,除非用户强制要求覆盖。 + +## 0. AI 交互核心约束 (元规则) + +1. **语言**:必须使用**中文**回复与注释。 +2. **文件完整性**:严禁随意删除现有逻辑(尤其是生命周期钩子);保持 UTF-8 无 BOM。 +3. **环境感知**: + - PowerShell 读取文件命令必须带 `-Encoding UTF8`。 + - 构建/本地请求依赖 `.env*`(`VITE_API_URL`、`VITE_WITH_CREDENTIALS`),找不到就询问,不要杜撰。 +4. **Git 原子性**:每个独立功能或修复完成后,必须提示用户进行 Git 提交。 +5. **不确定配置**:拿不准(如接口字段、鉴权流程)直接问用户。 + +## 1. 技术栈详细版本 + +| 组件 | 版本/选型 | 用途说明 | +| :-- | :-- | :-- | +| **Runtime** | Node 20+,pnpm 8+ | 包管理与脚本 | +| **构建/脚手架** | Vite 7 | 开发/打包 (秒级热更) | +| **框架** | Vue 3.5 + TypeScript 5.6 | 组合式 API (Script Setup) | +| **路由/状态** | Vue Router 4.5;Pinia 3 + 持久化插件 | 路由守卫 & 全局状态 | +| **UI/样式** | Element Plus 2.11;Tailwind CSS 4;SCSS | 组件与样式体系 | +| **网络** | Axios 1.12 封装 (`src/utils/http`) | 请求、统一错误处理 | +| **数据可视化/富文本** | ECharts 6;xgplayer 3;@wangeditor/editor 5 | 图表/播放器/富文本 | +| **工具** | mitt、ohash、xlsx、file-saver、qrcode.vue、vue-draggable-plus、highlight.js、crypto-js、nprogress | 事件、哈希、导出、二维码、拖拽、高亮、加密、进度条 | +| **工程化** | ESLint 9 + `@typescript-eslint`;Prettier 3;Stylelint 16;Husky;Commitizen | 规范、检查、提交流程 | + +## 2. 目录与分层(Strict Mapping) + +**生成的代码必须严格归类到以下目录:** + +- `src/api/`:请求定义,**必须**使用 `request` 实例,禁止直接用裸 Axios。 +- `src/components/`:**全局通用** UI 组件 (`PascalCase.vue`),禁止包含特定业务耦合。 +- `src/config/`:全局配置、常量(如主题、布局)。 +- `src/directives/`:自定义指令,按功能拆文件。 +- `src/enums/`:业务枚举常量(如 `OrderStatus.ts`)。 +- `src/hooks/`:可复用的组合式函数,命名 `useXxx.ts`。 +- `src/locales/`:多语言资源,新增文案必须**同步补齐**各语言。 +- `src/mock/`:本地 mock 数据/接口。 +- `src/plugins/`:Vue 插件注册。 +- `src/router/`:路由表与守卫,**鉴权逻辑放守卫中**,页面勿重复判断。 +- `src/store/`:Pinia store,模块化放 `modules/`。 +- `src/types/`:公共类型定义(如 `types/common/response.ts`)。 +- `src/utils/`:工具库(HTTP 封装、`StorageKeyManager`、加密等)。 +- `src/views/`:页面级组件,按 `views/业务模块/页面.vue` 组织。 + +## 3. 命名与代码风格 + +- **文件命名**:组件 `PascalCase.vue`;Hooks `useCamelCase.ts`;工具 `camelCase.ts`;样式 `kebab-case.scss`。 +- **变量/函数**:`camelCase`;布尔变量强制加 `is/has/should` 前缀。 +- **常量/枚举**:`PascalCase` 或 `UPPER_SNAKE_CASE`。 +- **路径别名**:**严禁**使用 `../../` 穿越多层。必须使用 `@/*`、`@views/*`、`@utils/*` 等别名。 +- **逻辑注释 (强制)**:代码逻辑块必须空行分隔,并加序号注释(1. 验证... 2. 请求...)。 +- **组件通信**:优先 `props/emit`,跨层用 `mitt` 或 store,**慎用** `provide/inject`。 + +## 4. 接口与 HTTP 规范 (含 .NET 兼容) + +1. **封装入口**:必须使用 `src/utils/http` 的 `api` 对象,禁止裸 `axios`。 +2. **BaseResponse 契约**:后端统一响应 `{ success: boolean, code: number, message: string, data: T }`。错误展示优先用 `message`。 +3. **Snowflake ID 精度处理 (关键)**: + - 后端 `.NET` 的 `long` 类型传到前端会有精度丢失。 + - **接收时**:确保后端 DTO 已序列化为 String。 + - **发送时**:前端保持 String 传输,由后端反序列化。 +4. **401 处理**:拦截器已自动登出。不要在业务内重复处理 401 跳转。 +5. **参数处理**:POST/PUT 若传 `params` 封装层会自动转为 `data`。 +6. **跨域/凭证**:严格跟随 `.env` 中 `VITE_WITH_CREDENTIALS` 配置。 + +## 5. 组件/页面开发规范 + +- **组织**:页面放 `views/`,复用组件抽到 `components/`。 +- **脚本语法**:全线使用 ` + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..e26f0cf --- /dev/null +++ b/package.json @@ -0,0 +1,133 @@ +{ + "name": "takeout-saas-adminui", + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=20.19.0", + "pnpm": ">=8.8.0" + }, + "scripts": { + "dev": "vite --open", + "build": "vue-tsc --noEmit && vite build", + "serve": "vite preview", + "lint": "eslint", + "fix": "eslint --fix", + "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,tsx,css,less,scss,vue,html,md}\"", + "lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix", + "lint:lint-staged": "lint-staged", + "prepare": "husky", + "commit": "git-cz", + "clean:dev": "tsx scripts/clean-dev.ts", + "test:unit": "vitest run" + }, + "config": { + "commitizen": { + "path": "node_modules/cz-git" + } + }, + "lint-staged": { + "*.{js,ts,mjs,mts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{cjs,json,jsonc}": [ + "prettier --write" + ], + "*.vue": [ + "eslint --fix", + "stylelint --fix --allow-empty-input", + "prettier --write" + ], + "*.{html,htm}": [ + "prettier --write" + ], + "*.{scss,css,less}": [ + "stylelint --fix --allow-empty-input", + "prettier --write" + ], + "*.{md,mdx}": [ + "prettier --write" + ], + "*.{yaml,yml}": [ + "prettier --write" + ] + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@iconify/vue": "^5.0.0", + "@tailwindcss/vite": "^4.1.14", + "@types/dompurify": "^3.2.0", + "@vue/reactivity": "^3.5.21", + "@vueuse/core": "^13.9.0", + "@wangeditor/editor": "^5.1.23", + "@wangeditor/editor-for-vue": "next", + "axios": "^1.12.2", + "crypto-js": "^4.2.0", + "dompurify": "^3.3.1", + "echarts": "^6.0.0", + "element-china-area-data": "^6.1.0", + "element-plus": "^2.11.2", + "file-saver": "^2.0.5", + "highlight.js": "^11.10.0", + "json-bigint": "^1.0.0", + "mitt": "^3.0.1", + "nprogress": "^0.2.0", + "ohash": "^2.0.11", + "pinia": "^3.0.3", + "pinia-plugin-persistedstate": "^4.3.0", + "qrcode.vue": "^3.6.0", + "tailwindcss": "^4.1.14", + "tlbs-map-vue": "^1.3.2", + "vue": "^3.5.21", + "vue-draggable-plus": "^0.6.0", + "vue-i18n": "^9.14.0", + "vue-router": "^4.5.1", + "xgplayer": "^3.0.20", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@commitlint/cli": "^19.4.1", + "@commitlint/config-conventional": "^19.4.1", + "@eslint/js": "^9.9.1", + "@pinia/testing": "^0.1.7", + "@types/node": "^24.0.5", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/compiler-sfc": "^3.0.5", + "@vue/test-utils": "^2.4.6", + "commitizen": "^4.3.0", + "cz-git": "^1.11.1", + "eslint": "^9.9.1", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-vue": "^9.27.0", + "globals": "^15.9.0", + "husky": "^9.1.5", + "jsdom": "^24.0.0", + "lint-staged": "^15.5.2", + "prettier": "^3.5.3", + "rollup-plugin-visualizer": "^5.12.0", + "sass": "^1.81.0", + "stylelint": "^16.20.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recess-order": "^4.6.0", + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-recommended-vue": "^1.5.0", + "stylelint-config-standard": "^36.0.1", + "terser": "^5.36.0", + "tsx": "^4.20.3", + "typescript": "~5.6.3", + "typescript-eslint": "^8.9.0", + "unplugin-auto-import": "^20.2.0", + "unplugin-element-plus": "^0.10.0", + "unplugin-vue-components": "^29.1.0", + "vite": "^7.1.5", + "vite-plugin-compression": "^0.5.1", + "vite-plugin-vue-devtools": "^7.7.6", + "vitest": "^2.1.5", + "vue-demi": "^0.14.9", + "vue-img-cutter": "^3.0.5", + "vue-tsc": "~2.1.6" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5b8d954 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,8680 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.22(typescript@5.6.3)) + '@iconify/vue': + specifier: ^5.0.0 + version: 5.0.0(vue@3.5.22(typescript@5.6.3)) + '@tailwindcss/vite': + specifier: ^4.1.14 + version: 4.1.14(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 + '@vue/reactivity': + specifier: ^3.5.21 + version: 3.5.22 + '@vueuse/core': + specifier: ^13.9.0 + version: 13.9.0(vue@3.5.22(typescript@5.6.3)) + '@wangeditor/editor': + specifier: ^5.1.23 + version: 5.1.23 + '@wangeditor/editor-for-vue': + specifier: next + version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.22(typescript@5.6.3)) + axios: + specifier: ^1.12.2 + version: 1.12.2 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + dompurify: + specifier: ^3.3.1 + version: 3.3.1 + echarts: + specifier: ^6.0.0 + version: 6.0.0 + element-china-area-data: + specifier: ^6.1.0 + version: 6.1.0 + element-plus: + specifier: ^2.11.2 + version: 2.11.4(vue@3.5.22(typescript@5.6.3)) + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + highlight.js: + specifier: ^11.10.0 + version: 11.11.1 + json-bigint: + specifier: ^1.0.0 + version: 1.0.0 + mitt: + specifier: ^3.0.1 + version: 3.0.1 + nprogress: + specifier: ^0.2.0 + version: 0.2.0 + ohash: + specifier: ^2.0.11 + version: 2.0.11 + pinia: + specifier: ^3.0.3 + version: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + pinia-plugin-persistedstate: + specifier: ^4.3.0 + version: 4.5.0(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3))) + qrcode.vue: + specifier: ^3.6.0 + version: 3.6.0(vue@3.5.22(typescript@5.6.3)) + tailwindcss: + specifier: ^4.1.14 + version: 4.1.14 + tlbs-map-vue: + specifier: ^1.3.2 + version: 1.3.2(vue@3.5.22(typescript@5.6.3)) + vue: + specifier: ^3.5.21 + version: 3.5.22(typescript@5.6.3) + vue-draggable-plus: + specifier: ^0.6.0 + version: 0.6.0(@types/sortablejs@1.15.8) + vue-i18n: + specifier: ^9.14.0 + version: 9.14.5(vue@3.5.22(typescript@5.6.3)) + vue-router: + specifier: ^4.5.1 + version: 4.5.1(vue@3.5.22(typescript@5.6.3)) + xgplayer: + specifier: ^3.0.20 + version: 3.0.23(core-js@3.45.1) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 + devDependencies: + '@commitlint/cli': + specifier: ^19.4.1 + version: 19.8.1(@types/node@24.8.1)(typescript@5.6.3) + '@commitlint/config-conventional': + specifier: ^19.4.1 + version: 19.8.1 + '@eslint/js': + specifier: ^9.9.1 + version: 9.36.0 + '@pinia/testing': + specifier: ^0.1.7 + version: 0.1.7(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)))(vue@3.5.22(typescript@5.6.3)) + '@types/node': + specifier: ^24.0.5 + version: 24.8.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.3.0 + version: 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.3.0 + version: 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + '@vue/compiler-sfc': + specifier: ^3.0.5 + version: 3.5.22 + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.6 + commitizen: + specifier: ^4.3.0 + version: 4.3.1(@types/node@24.8.1)(typescript@5.6.3) + cz-git: + specifier: ^1.11.1 + version: 1.12.0 + eslint: + specifier: ^9.9.1 + version: 9.36.0(jiti@2.6.0) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@9.36.0(jiti@2.6.0)) + eslint-plugin-prettier: + specifier: ^5.2.1 + version: 5.5.4(eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(prettier@3.6.2) + eslint-plugin-vue: + specifier: ^9.27.0 + version: 9.33.0(eslint@9.36.0(jiti@2.6.0)) + globals: + specifier: ^15.9.0 + version: 15.15.0 + husky: + specifier: ^9.1.5 + version: 9.1.7 + jsdom: + specifier: ^24.0.0 + version: 24.1.3 + lint-staged: + specifier: ^15.5.2 + version: 15.5.2 + prettier: + specifier: ^3.5.3 + version: 3.6.2 + rollup-plugin-visualizer: + specifier: ^5.12.0 + version: 5.14.0(rollup@4.52.3) + sass: + specifier: ^1.81.0 + version: 1.93.2 + stylelint: + specifier: ^16.20.0 + version: 16.24.0(typescript@5.6.3) + stylelint-config-html: + specifier: ^1.1.0 + version: 1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recess-order: + specifier: ^4.6.0 + version: 4.6.0(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended-scss: + specifier: ^14.1.0 + version: 14.1.0(postcss@8.5.6)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended-vue: + specifier: ^1.5.0 + version: 1.6.1(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-standard: + specifier: ^36.0.1 + version: 36.0.1(stylelint@16.24.0(typescript@5.6.3)) + terser: + specifier: ^5.36.0 + version: 5.44.0 + tsx: + specifier: ^4.20.3 + version: 4.20.6 + typescript: + specifier: ~5.6.3 + version: 5.6.3 + typescript-eslint: + specifier: ^8.9.0 + version: 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + unplugin-auto-import: + specifier: ^20.2.0 + version: 20.2.0(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))) + unplugin-element-plus: + specifier: ^0.10.0 + version: 0.10.0 + unplugin-vue-components: + specifier: ^29.1.0 + version: 29.1.0(@babel/parser@7.28.4)(vue@3.5.22(typescript@5.6.3)) + vite: + specifier: ^7.1.5 + version: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-compression: + specifier: ^0.5.1 + version: 0.5.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-devtools: + specifier: ^7.7.6 + version: 7.7.7(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + vitest: + specifier: ^2.1.5 + version: 2.1.9(@types/node@24.8.1)(jsdom@24.1.3)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + vue-demi: + specifier: ^0.14.9 + version: 0.14.10(vue@3.5.22(typescript@5.6.3)) + vue-img-cutter: + specifier: ^3.0.5 + version: 3.0.7(typescript@5.6.3) + vue-tsc: + specifier: ~2.1.6 + version: 2.1.10(typescript@5.6.3) + +packages: + + '@antfu/utils@0.7.10': + resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@cacheable/memoize@2.0.2': + resolution: {integrity: sha512-wPrr7FUiq3Qt4yQyda2/NcOLTJCFcQSU3Am2adP+WLy+sz93/fKTokVTHmtz+rjp4PD7ee0AEOeRVNN6IvIfsg==} + + '@cacheable/memory@2.0.2': + resolution: {integrity: sha512-sJTITLfeCI1rg7P3ssaGmQryq235EGT8dXGcx6oZwX5NRnKq9IE6lddlllcOl+oXW+yaeTRddCjo0xrfU6ZySA==} + + '@cacheable/utils@2.0.2': + resolution: {integrity: sha512-JTFM3raFhVv8LH95T7YnZbf2YoE9wEtkPPStuRF9a6ExZ103hFvs+QyCuYJ6r0hA9wRtbzgZtwUCoDWxssZd4Q==} + + '@commitlint/cli@19.8.1': + resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.8.1': + resolution: {integrity: sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.8.1': + resolution: {integrity: sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@20.0.0': + resolution: {integrity: sha512-BeyLMaRIJDdroJuYM2EGhDMGwVBMZna9UiIqV9hxj+J551Ctc6yoGuGSmghOy/qPhBSuhA6oMtbEiTmxECafsg==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.8.1': + resolution: {integrity: sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.8.1': + resolution: {integrity: sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@20.0.0': + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} + + '@commitlint/format@19.8.1': + resolution: {integrity: sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.8.1': + resolution: {integrity: sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.8.1': + resolution: {integrity: sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==} + engines: {node: '>=v18'} + + '@commitlint/load@19.8.1': + resolution: {integrity: sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==} + engines: {node: '>=v18'} + + '@commitlint/load@20.0.0': + resolution: {integrity: sha512-WiNKO9fDPlLY90Rruw2HqHKcghrmj5+kMDJ4GcTlX1weL8K07Q6b27C179DxnsrjGCRAKVwFKyzxV4x+xDY28Q==} + engines: {node: '>=v18'} + + '@commitlint/message@19.8.1': + resolution: {integrity: sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.8.1': + resolution: {integrity: sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==} + engines: {node: '>=v18'} + + '@commitlint/read@19.8.1': + resolution: {integrity: sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.8.1': + resolution: {integrity: sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@20.0.0': + resolution: {integrity: sha512-BA4vva1hY8y0/Hl80YDhe9TJZpRFMsUYzVxvwTLPTEBotbGx/gS49JlVvtF1tOCKODQp7pS7CbxCpiceBgp3Dg==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.8.1': + resolution: {integrity: sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.8.1': + resolution: {integrity: sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.8.1': + resolution: {integrity: sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==} + engines: {node: '>=v18'} + + '@commitlint/types@19.8.1': + resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} + engines: {node: '>=v18'} + + '@commitlint/types@20.0.0': + resolution: {integrity: sha512-bVUNBqG6aznYcYjTjnc3+Cat/iBgbgpflxbIBTnsHTX0YVpnmINPEkSRWymT2Q8aSH3Y7aKnEbunilkYe8TybA==} + engines: {node: '>=v18'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.36.0': + resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/vue@5.0.0': + resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==} + peerDependencies: + vue: '>=3' + + '@intlify/core-base@9.14.5': + resolution: {integrity: sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@9.14.5': + resolution: {integrity: sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==} + engines: {node: '>= 16'} + + '@intlify/shared@9.14.5': + resolution: {integrity: sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==} + engines: {node: '>= 16'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.0.2': + resolution: {integrity: sha512-KR03xkEZlAZNF4IxXgVXb+uNIVNvwdh8UwI0cnc7WI6a+aQcDp8GL80qVfeB4E5NpsKJzou5jU0r6yLSSbMOtA==} + engines: {node: '>= 18'} + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pinia/testing@0.1.7': + resolution: {integrity: sha512-xcDq6Ry/kNhZ5bsUMl7DeoFXwdume1NYzDggCiDUDKoPQ6Mo0eH9VU7bJvBtlurqe6byAntWoX5IhVFqWzRz/Q==} + peerDependencies: + pinia: '>=2.2.6' + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.52.3': + resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.3': + resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.3': + resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.3': + resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.3': + resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.3': + resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.3': + resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.3': + resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.3': + resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.3': + resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.3': + resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.3': + resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@tailwindcss/node@4.1.14': + resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==} + + '@tailwindcss/oxide-android-arm64@4.1.14': + resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.14': + resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.14': + resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.14': + resolution: {integrity: sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@transloadit/prettier-bytes@0.0.7': + resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==} + + '@types/conventional-commits-parser@5.0.1': + resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==} + + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/event-emitter@0.3.5': + resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/node@24.8.1': + resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} + + '@types/sortablejs@1.15.8': + resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@typescript-eslint/eslint-plugin@8.44.1': + resolution: {integrity: sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.44.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.44.1': + resolution: {integrity: sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.44.1': + resolution: {integrity: sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.44.1': + resolution: {integrity: sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.44.1': + resolution: {integrity: sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.44.1': + resolution: {integrity: sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.44.1': + resolution: {integrity: sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.44.1': + resolution: {integrity: sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.44.1': + resolution: {integrity: sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.44.1': + resolution: {integrity: sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@uppy/companion-client@2.2.2': + resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==} + + '@uppy/core@2.3.4': + resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==} + + '@uppy/store-default@2.1.1': + resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==} + + '@uppy/utils@4.1.3': + resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==} + + '@uppy/xhr-upload@2.1.3': + resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==} + peerDependencies: + '@uppy/core': ^2.3.3 + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.22': + resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + + '@vue/compiler-dom@3.5.22': + resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + + '@vue/compiler-sfc@3.5.22': + resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} + + '@vue/compiler-ssr@3.5.22': + resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-core@7.7.7': + resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/language-core@2.1.10': + resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.22': + resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} + + '@vue/runtime-core@3.5.22': + resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==} + + '@vue/runtime-dom@3.5.22': + resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==} + + '@vue/server-renderer@3.5.22': + resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==} + peerDependencies: + vue: 3.5.22 + + '@vue/shared@3.5.22': + resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + + '@vueuse/core@13.9.0': + resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@13.9.0': + resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@13.9.0': + resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + '@wangeditor/basic-modules@1.1.7': + resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/code-highlight@1.0.3': + resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/core@1.1.19': + resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==} + peerDependencies: + '@uppy/core': ^2.1.1 + '@uppy/xhr-upload': ^2.0.3 + dom7: ^3.0.0 + is-hotkey: ^0.2.0 + lodash.camelcase: ^4.3.0 + lodash.clonedeep: ^4.5.0 + lodash.debounce: ^4.0.8 + lodash.foreach: ^4.5.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + lodash.toarray: ^4.4.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/editor-for-vue@5.1.12': + resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==} + peerDependencies: + '@wangeditor/editor': '>=5.1.0' + vue: ^3.0.5 + + '@wangeditor/editor@5.1.23': + resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==} + + '@wangeditor/list-module@1.0.5': + resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/table-module@1.1.4': + resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==} + peerDependencies: + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.isequal: ^4.5.0 + lodash.throttle: ^4.1.1 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/upload-image-module@1.0.2': + resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==} + peerDependencies: + '@uppy/core': ^2.0.3 + '@uppy/xhr-upload': ^2.0.3 + '@wangeditor/basic-modules': 1.x + '@wangeditor/core': 1.x + dom7: ^3.0.0 + lodash.foreach: ^4.5.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + '@wangeditor/video-module@1.1.4': + resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==} + peerDependencies: + '@uppy/core': ^2.1.4 + '@uppy/xhr-upload': ^2.0.7 + '@wangeditor/core': 1.x + dom7: ^3.0.0 + nanoid: ^3.2.0 + slate: ^0.72.0 + snabbdom: ^3.1.0 + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + alien-signals@0.2.2: + resolution: {integrity: sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.1.1: + resolution: {integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@2.0.0: + resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.8.8: + resolution: {integrity: sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==} + hasBin: true + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + birpc@2.6.1: + resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.26.2: + resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacheable@2.0.2: + resolution: {integrity: sha512-dWjhLx8RWnPsAWVKwW/wI6OJpQ/hSVb1qS0NUif8TR9vRiSwci7Gey8x04kRU9iAF+Rnbtex5Kjjfg/aB5w8Pg==} + + cachedir@2.3.0: + resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001745: + resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + china-division@2.7.0: + resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commitizen@4.3.1: + resolution: {integrity: sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==} + engines: {node: '>= 12'} + hasBin: true + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commit-types@3.0.0: + resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + core-js@3.45.1: + resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} + + cosmiconfig-typescript-loader@6.1.0: + resolution: {integrity: sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + css-functions-list@3.2.3: + resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==} + engines: {node: '>=12 || >=16'} + + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + cz-conventional-changelog@3.3.0: + resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} + engines: {node: '>= 10'} + + cz-git@1.12.0: + resolution: {integrity: sha512-LaZ+8whPPUOo6Y0Zy4nIbf6JOleV3ejp41sT6N4RPKiKKA+ICWf4ueeIlxIO8b6JtdlDxRzHH/EcRji07nDxcg==} + engines: {node: '>=v12.20.0'} + + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + + danmu.js@1.1.13: + resolution: {integrity: sha512-knFd0/cB2HA4FFWiA7eB2suc5vCvoHdqio33FyyCSfP7C+1A+zQcTvnvwfxaZhrxsGj4qaQI2I8XiTqedRaVmg==} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deep-pick-omit@1.2.1: + resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegate@3.2.0: + resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom7@3.0.0: + resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + downloadjs@1.4.7: + resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + + editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + + electron-to-chromium@1.5.227: + resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==} + + element-china-area-data@6.1.0: + resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==} + + element-plus@2.11.4: + resolution: {integrity: sha512-sLq+Ypd0cIVilv8wGGMEGvzRVBBsRpJjnAS5PsI/1JU1COZXqzH3N1UYMUc/HCdvdjf6dfrBy80Sj7KcACsT7w==} + peerDependencies: + vue: ^3.2.0 + + emoji-regex@10.5.0: + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + error-stack-parser-es@0.1.5: + resolution: {integrity: sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@9.33.0: + resolution: {integrity: sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.36.0: + resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@10.1.4: + resolution: {integrity: sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-node-modules@2.1.3: + resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat-cache@6.1.14: + resolution: {integrity: sha512-ExZSCSV9e7v/Zt7RzCbX57lY2dnPdxzU/h3UE6WJ6NtEMfwBd8jmi1n4otDEUfz+T/R+zxrFDpICFdjhD3H/zw==} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.4.0: + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + + global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globjoin@0.1.4: + resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + hookified@1.12.1: + resolution: {integrity: sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + i18next@20.6.1: + resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + + immutable@5.1.3: + resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + + is-utf8@0.2.1: + resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.6.0: + resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==} + hasBin: true + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsdom@24.1.3: + resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + keyv@5.5.3: + resolution: {integrity: sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + known-css-properties@0.36.0: + resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==} + + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@15.5.2: + resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==} + engines: {node: '>=18.12.0'} + hasBin: true + + listr2@8.3.3: + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} + engines: {node: '>=18.0.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.foreach@4.5.0: + resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.map@4.6.0: + resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + lodash.toarray@4.4.0: + resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + longest@2.0.1: + resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} + engines: {node: '>=0.10.0'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mathml-tag-names@2.1.3: + resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} + + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + + mdn-data@2.24.0: + resolution: {integrity: sha512-i97fklrJl03tL1tdRVw0ZfLLvuDsdb6wxL+TrJ+PKkCbLrp2PCu2+OYdCKychIUm19nSM/35S6qz7pJpnXttoA==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + merge@2.1.1: + resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-match@1.0.2: + resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + namespace-emitter@2.0.1: + resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia-plugin-persistedstate@4.5.0: + resolution: {integrity: sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==} + peerDependencies: + '@nuxt/kit': '>=3.0.0' + '@pinia/nuxt': '>=0.10.0' + pinia: '>=3.0.0' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@pinia/nuxt': + optional: true + pinia: + optional: true + + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss-html@1.8.0: + resolution: {integrity: sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==} + engines: {node: ^12 || >=14} + + postcss-media-query-parser@0.2.3: + resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} + + postcss-resolve-nested-selector@0.1.6: + resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-sorting@8.0.2: + resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==} + peerDependencies: + postcss: ^8.4.20 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qrcode.vue@3.6.0: + resolution: {integrity: sha512-vQcl2fyHYHMjDO1GguCldJxepq2izQjBkDEEu9NENgfVKP6mv/e2SU62WbqYHGwTgWXLhxZ1NCD1dAZKHQq1fg==} + peerDependencies: + vue: ^3.0.0 + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup-plugin-visualizer@5.14.0: + resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + rolldown: 1.x + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + + rollup@4.52.3: + resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.93.2: + resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} + engines: {node: '>=14.0.0'} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slate-history@0.66.0: + resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==} + peerDependencies: + slate: '>=0.65.3' + + slate@0.72.8: + resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + snabbdom@3.6.2: + resolution: {integrity: sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==} + engines: {node: '>=12.17.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + ssr-window@3.0.0: + resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + stylelint-config-html@1.1.0: + resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recess-order@4.6.0: + resolution: {integrity: sha512-V76fhv3YtcNXh/hyAuAdSzi5FmcrG54Mp2AThJ3D/PTMTSYzUPd7GIhP6z9mTqnRhmkk6YTfcu/JWB8h+Yrcaw==} + peerDependencies: + stylelint: '>=15' + + stylelint-config-recommended-scss@14.1.0: + resolution: {integrity: sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==} + engines: {node: '>=18.12.0'} + peerDependencies: + postcss: ^8.3.3 + stylelint: ^16.6.1 + peerDependenciesMeta: + postcss: + optional: true + + stylelint-config-recommended-vue@1.6.1: + resolution: {integrity: sha512-lLW7hTIMBiTfjenGuDq2kyHA6fBWd/+Df7MO4/AWOxiFeXP9clbpKgg27kHfwA3H7UNMGC7aeP3mNlZB5LMmEQ==} + engines: {node: ^12 || >=14} + peerDependencies: + postcss-html: ^1.0.0 + stylelint: '>=14.0.0' + + stylelint-config-recommended@14.0.1: + resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-config-recommended@17.0.0: + resolution: {integrity: sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.23.0 + + stylelint-config-standard@36.0.1: + resolution: {integrity: sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.1.0 + + stylelint-order@6.0.4: + resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==} + peerDependencies: + stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1 + + stylelint-scss@6.12.1: + resolution: {integrity: sha512-UJUfBFIvXfly8WKIgmqfmkGKPilKB4L5j38JfsDd+OCg2GBdU0vGUV08Uw82tsRZzd4TbsUURVVNGeOhJVF7pA==} + engines: {node: '>=18.12.0'} + peerDependencies: + stylelint: ^16.0.2 + + stylelint@16.24.0: + resolution: {integrity: sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-hyperlinks@3.2.0: + resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==} + engines: {node: '>=14.18'} + + svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tailwindcss@4.1.14: + resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + engines: {node: '>=18'} + + terser@5.44.0: + resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} + engines: {node: '>=10'} + hasBin: true + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tlbs-map-vue@1.3.2: + resolution: {integrity: sha512-CIRhxWPpLz0A2yCylRkYRj6LOEhGU9mV6pWil2XTAs+Yo/7EN+aqn+QM/4Y5TJLpN7yjMjhDDx8cCkfje5iatA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@vue/composition-api': ^1.4.9 + vue: ^2.6.0 || >=3.0.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + + typescript-eslint@8.44.1: + resolution: {integrity: sha512-0ws8uWGrUVTjEeN2OM4K1pLKHK/4NiNP/vz6ns+LjT/6sqpaYzIVFajZb1fj/IDwpsrrHb3Jy0Qm5u9CPcKaeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + undici-types@7.14.0: + resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unimport@5.4.0: + resolution: {integrity: sha512-g/OLFZR2mEfqbC6NC9b2225eCJGvufxq34mj6kM3OmI5gdSL0qyqtnv+9qmsGpAmnzSl6x0IWZj4W+8j2hLkMA==} + engines: {node: '>=18.12.0'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-auto-import@20.2.0: + resolution: {integrity: sha512-vfBI/SvD9hJqYNinipVOAj5n8dS8DJXFlCKFR5iLDp2SaQwsfdnfLXgZ+34Kd3YY3YEY9omk8XQg0bwos3Q8ug==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^4.0.0 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-element-plus@0.10.0: + resolution: {integrity: sha512-oRSW0x6U58xBOWKy8TcoVZNA8ElIpfp3TUJRLQI6ey/E9PpjHl9/deeTAZNt8D57Li4OA4pCJtM6p2cb4Ff4ZA==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.3.0: + resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==} + engines: {node: '>=20.19.0'} + + unplugin-vue-components@29.1.0: + resolution: {integrity: sha512-z/9ACPXth199s9aCTCdKZAhe5QGOpvzJYP+Hkd0GN1/PpAmsu+W3UlRY3BJAewPqQxh5xi56+Og6mfiCV1Jzpg==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 || ^4.0.0 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin@2.3.10: + resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-plugin-compression@0.5.1: + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + + vite-plugin-inspect@0.8.9: + resolution: {integrity: sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@7.7.7: + resolution: {integrity: sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-vue-inspector@5.3.2: + resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@7.1.7: + resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-draggable-plus@0.6.0: + resolution: {integrity: sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==} + peerDependencies: + '@types/sortablejs': ^1.15.0 + '@vue/composition-api': '*' + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.4.3: + resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@9.14.5: + resolution: {integrity: sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-img-cutter@3.0.7: + resolution: {integrity: sha512-fNw3kimawg9XVXDZCw2bI74NI+Jq+H42wjymatZVVSY46wuBty6LbQsu4GeVfo/yzpS9AHY0tzckpYzX3D2fmA==} + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + + vue-tsc@2.1.10: + resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.22: + resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wildcard@1.1.2: + resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==} + + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xgplayer-subtitles@3.0.23: + resolution: {integrity: sha512-deGdV75giVzfTTdG9XATmji39NHwKTpEelWt2rRx/RyXGgU2bQFp0Ft7yWaK2Uu8A/WVrP5fpxEAj4MstREMkQ==} + peerDependencies: + core-js: '>=3.12.1' + + xgplayer@3.0.23: + resolution: {integrity: sha512-Bn3zQfMMAZimlVG9EeIDybMcklc+6FH8Sv47KpTq4K6ofCzyhPG/KenxailDedlHmxjb5B2o+240TpJtMQ3oJA==} + peerDependencies: + core-js: '>=3.12.1' + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + +snapshots: + + '@antfu/utils@0.7.10': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.28.4': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@cacheable/memoize@2.0.2': + dependencies: + '@cacheable/utils': 2.0.2 + + '@cacheable/memory@2.0.2': + dependencies: + '@cacheable/memoize': 2.0.2 + '@cacheable/utils': 2.0.2 + '@keyv/bigmap': 1.0.2 + hookified: 1.12.1 + keyv: 5.5.3 + + '@cacheable/utils@2.0.2': {} + + '@commitlint/cli@19.8.1(@types/node@24.8.1)(typescript@5.6.3)': + dependencies: + '@commitlint/format': 19.8.1 + '@commitlint/lint': 19.8.1 + '@commitlint/load': 19.8.1(@types/node@24.8.1)(typescript@5.6.3) + '@commitlint/read': 19.8.1 + '@commitlint/types': 19.8.1 + tinyexec: 1.0.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + ajv: 8.17.1 + + '@commitlint/config-validator@20.0.0': + dependencies: + '@commitlint/types': 20.0.0 + ajv: 8.17.1 + optional: true + + '@commitlint/ensure@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.8.1': {} + + '@commitlint/execute-rule@20.0.0': + optional: true + + '@commitlint/format@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + + '@commitlint/is-ignored@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + semver: 7.7.2 + + '@commitlint/lint@19.8.1': + dependencies: + '@commitlint/is-ignored': 19.8.1 + '@commitlint/parse': 19.8.1 + '@commitlint/rules': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/load@19.8.1(@types/node@24.8.1)(typescript@5.6.3)': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/execute-rule': 19.8.1 + '@commitlint/resolve-extends': 19.8.1 + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + cosmiconfig: 9.0.0(typescript@5.6.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@24.8.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/load@20.0.0(@types/node@24.8.1)(typescript@5.6.3)': + dependencies: + '@commitlint/config-validator': 20.0.0 + '@commitlint/execute-rule': 20.0.0 + '@commitlint/resolve-extends': 20.0.0 + '@commitlint/types': 20.0.0 + chalk: 5.6.2 + cosmiconfig: 9.0.0(typescript@5.6.3) + cosmiconfig-typescript-loader: 6.1.0(@types/node@24.8.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + optional: true + + '@commitlint/message@19.8.1': {} + + '@commitlint/parse@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.8.1': + dependencies: + '@commitlint/top-level': 19.8.1 + '@commitlint/types': 19.8.1 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 1.0.1 + + '@commitlint/resolve-extends@19.8.1': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/types': 19.8.1 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/resolve-extends@20.0.0': + dependencies: + '@commitlint/config-validator': 20.0.0 + '@commitlint/types': 20.0.0 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + optional: true + + '@commitlint/rules@19.8.1': + dependencies: + '@commitlint/ensure': 19.8.1 + '@commitlint/message': 19.8.1 + '@commitlint/to-lines': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/to-lines@19.8.1': {} + + '@commitlint/top-level@19.8.1': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.8.1': + dependencies: + '@types/conventional-commits-parser': 5.0.1 + chalk: 5.6.2 + + '@commitlint/types@20.0.0': + dependencies: + '@types/conventional-commits-parser': 5.0.1 + chalk: 5.6.2 + optional: true + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@ctrl/tinycolor@3.6.1': {} + + '@dual-bundle/import-meta-resolve@4.2.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue: 3.5.22(typescript@5.6.3) + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0(jiti@2.6.0))': + dependencies: + eslint: 9.36.0(jiti@2.6.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.36.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/vue@5.0.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.22(typescript@5.6.3) + + '@intlify/core-base@9.14.5': + dependencies: + '@intlify/message-compiler': 9.14.5 + '@intlify/shared': 9.14.5 + + '@intlify/message-compiler@9.14.5': + dependencies: + '@intlify/shared': 9.14.5 + source-map-js: 1.2.1 + + '@intlify/shared@9.14.5': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/bigmap@1.0.2': + dependencies: + hookified: 1.12.1 + + '@keyv/serialize@1.1.1': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@one-ini/wasm@0.1.1': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pinia/testing@0.1.7(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)))(vue@3.5.22(typescript@5.6.3))': + dependencies: + pinia: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/pluginutils@5.3.0(rollup@4.52.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.52.3 + + '@rollup/rollup-android-arm-eabi@4.52.3': + optional: true + + '@rollup/rollup-android-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-x64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.3': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@tailwindcss/node@4.1.14': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.0 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.14 + + '@tailwindcss/oxide-android-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide@4.1.14': + dependencies: + detect-libc: 2.1.2 + tar: 7.5.1 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-x64': 4.1.14 + '@tailwindcss/oxide-freebsd-x64': 4.1.14 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.14 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-x64-musl': 4.1.14 + '@tailwindcss/oxide-wasm32-wasi': 4.1.14 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.14 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.14 + + '@tailwindcss/vite@4.1.14(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@tailwindcss/node': 4.1.14 + '@tailwindcss/oxide': 4.1.14 + tailwindcss: 4.1.14 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + '@transloadit/prettier-bytes@0.0.7': {} + + '@types/conventional-commits-parser@5.0.1': + dependencies: + '@types/node': 24.8.1 + + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.1 + + '@types/estree@1.0.8': {} + + '@types/event-emitter@0.3.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/node@24.8.1': + dependencies: + undici-types: 7.14.0 + + '@types/sortablejs@1.15.8': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/web-bluetooth@0.0.16': {} + + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/type-utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.44.1 + eslint: 9.36.0(jiti@2.6.0) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.44.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.6.3) + '@typescript-eslint/types': 8.44.1 + debug: 4.4.3 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + + '@typescript-eslint/tsconfig-utils@8.44.1(typescript@5.6.3)': + dependencies: + typescript: 5.6.3 + + '@typescript-eslint/type-utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.44.1': {} + + '@typescript-eslint/typescript-estree@8.44.1(typescript@5.6.3)': + dependencies: + '@typescript-eslint/project-service': 8.44.1(typescript@5.6.3) + '@typescript-eslint/tsconfig-utils': 8.44.1(typescript@5.6.3) + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/visitor-keys': 8.44.1 + debug: 4.4.3 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.6.3) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + '@typescript-eslint/scope-manager': 8.44.1 + '@typescript-eslint/types': 8.44.1 + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.44.1': + dependencies: + '@typescript-eslint/types': 8.44.1 + eslint-visitor-keys: 4.2.1 + + '@uppy/companion-client@2.2.2': + dependencies: + '@uppy/utils': 4.1.3 + namespace-emitter: 2.0.1 + + '@uppy/core@2.3.4': + dependencies: + '@transloadit/prettier-bytes': 0.0.7 + '@uppy/store-default': 2.1.1 + '@uppy/utils': 4.1.3 + lodash.throttle: 4.1.1 + mime-match: 1.0.2 + namespace-emitter: 2.0.1 + nanoid: 3.3.11 + preact: 10.27.2 + + '@uppy/store-default@2.1.1': {} + + '@uppy/utils@4.1.3': + dependencies: + lodash.throttle: 4.1.1 + + '@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)': + dependencies: + '@uppy/companion-client': 2.2.2 + '@uppy/core': 2.3.4 + '@uppy/utils': 4.1.3 + nanoid: 3.3.11 + + '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vue: 3.5.22(typescript@5.6.3) + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.19 + optionalDependencies: + vite: 5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.19 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + + '@volar/source-map@2.4.23': {} + + '@volar/typescript@2.4.23': + dependencies: + '@volar/language-core': 2.4.23 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.4) + '@vue/shared': 3.5.22 + optionalDependencies: + '@babel/core': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.28.4 + '@vue/compiler-sfc': 3.5.22 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.22 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.22': + dependencies: + '@vue/compiler-core': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-sfc@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/compiler-core': 3.5.22 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + estree-walker: 2.0.2 + magic-string: 0.30.19 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.22': + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-core@7.7.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@vue/devtools-kit': 7.7.7 + '@vue/devtools-shared': 7.7.7 + mitt: 3.0.1 + nanoid: 5.1.6 + pathe: 2.0.3 + vite-hot-client: 2.1.0(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vue: 3.5.22(typescript@5.6.3) + transitivePeerDependencies: + - vite + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.6.1 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@2.1.10(typescript@5.6.3)': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.22 + alien-signals: 0.2.2 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.6.3 + + '@vue/reactivity@3.5.22': + dependencies: + '@vue/shared': 3.5.22 + + '@vue/runtime-core@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/runtime-dom@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/runtime-core': 3.5.22 + '@vue/shared': 3.5.22 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + vue: 3.5.22(typescript@5.6.3) + + '@vue/shared@3.5.22': {} + + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + + '@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.9.0 + '@vueuse/shared': 13.9.0(vue@3.5.22(typescript@5.6.3)) + vue: 3.5.22(typescript@5.6.3) + + '@vueuse/core@9.13.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.22(typescript@5.6.3)) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@13.9.0': {} + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@13.9.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue: 3.5.22(typescript@5.6.3) + + '@vueuse/shared@9.13.0(vue@3.5.22(typescript@5.6.3))': + dependencies: + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + is-url: 1.2.4 + lodash.throttle: 4.1.1 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + prismjs: 1.30.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@types/event-emitter': 0.3.5 + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + dom7: 3.0.0 + event-emitter: 0.3.5 + html-void-elements: 2.0.1 + i18next: 20.6.1 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.11 + scroll-into-view-if-needed: 2.2.31 + slate: 0.72.8 + slate-history: 0.66.0(slate@0.72.8) + snabbdom: 3.6.2 + + '@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.22(typescript@5.6.3))': + dependencies: + '@wangeditor/editor': 5.1.23 + vue: 3.5.22(typescript@5.6.3) + + '@wangeditor/editor@5.1.23': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + is-hotkey: 0.2.0 + lodash.camelcase: 4.3.0 + lodash.clonedeep: 4.5.0 + lodash.debounce: 4.0.8 + lodash.foreach: 4.5.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + lodash.toarray: 4.4.0 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + lodash.isequal: 4.5.0 + lodash.throttle: 4.1.1 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + lodash.foreach: 4.5.0 + slate: 0.72.8 + snabbdom: 3.6.2 + + '@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2)': + dependencies: + '@uppy/core': 2.3.4 + '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4) + '@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.2) + dom7: 3.0.0 + nanoid: 3.3.11 + slate: 0.72.8 + snabbdom: 3.6.2 + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + abbrev@2.0.0: {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + adler-32@1.3.1: {} + + agent-base@7.1.4: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + alien-signals@0.2.2: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.1.1: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + argparse@2.0.1: {} + + array-ify@1.0.0: {} + + array-union@2.1.0: {} + + assertion-error@2.0.1: {} + + astral-regex@2.0.0: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + balanced-match@2.0.0: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.8: {} + + bignumber.js@9.3.1: {} + + binary-extensions@2.3.0: {} + + birpc@2.6.1: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.26.2: + dependencies: + baseline-browser-mapping: 2.8.8 + caniuse-lite: 1.0.30001745 + electron-to-chromium: 1.5.227 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.2) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cac@6.7.14: {} + + cacheable@2.0.2: + dependencies: + '@cacheable/memoize': 2.0.2 + '@cacheable/memory': 2.0.2 + '@cacheable/utils': 2.0.2 + hookified: 1.12.1 + keyv: 5.5.3 + + cachedir@2.3.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001745: {} + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + chardet@0.7.0: {} + + check-error@2.1.1: {} + + china-division@2.7.0: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cli-width@3.0.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + codepage@1.15.0: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colord@2.9.3: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@13.1.0: {} + + commander@2.20.3: {} + + commitizen@4.3.1(@types/node@24.8.1)(typescript@5.6.3): + dependencies: + cachedir: 2.3.0 + cz-conventional-changelog: 3.3.0(@types/node@24.8.1)(typescript@5.6.3) + dedent: 0.7.0 + detect-indent: 6.1.0 + find-node-modules: 2.1.3 + find-root: 1.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + inquirer: 8.2.5 + is-utf8: 0.2.1 + lodash: 4.17.21 + minimist: 1.2.7 + strip-bom: 4.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + compute-scroll-into-view@1.0.20: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commit-types@3.0.0: {} + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + core-js@3.45.1: {} + + cosmiconfig-typescript-loader@6.1.0(@types/node@24.8.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): + dependencies: + '@types/node': 24.8.1 + cosmiconfig: 9.0.0(typescript@5.6.3) + jiti: 2.6.0 + typescript: 5.6.3 + + cosmiconfig@9.0.0(typescript@5.6.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.6.3 + + crc-32@1.2.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + css-functions-list@3.2.3: {} + + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.1.3: {} + + cz-conventional-changelog@3.3.0(@types/node@24.8.1)(typescript@5.6.3): + dependencies: + chalk: 2.4.2 + commitizen: 4.3.1(@types/node@24.8.1)(typescript@5.6.3) + conventional-commit-types: 3.0.0 + lodash.map: 4.6.0 + longest: 2.0.1 + word-wrap: 1.2.5 + optionalDependencies: + '@commitlint/load': 20.0.0(@types/node@24.8.1)(typescript@5.6.3) + transitivePeerDependencies: + - '@types/node' + - typescript + + cz-git@1.12.0: {} + + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + + danmu.js@1.1.13: + dependencies: + event-emitter: 0.3.5 + + dargs@8.1.0: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + dayjs@1.11.18: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dedent@0.7.0: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + deep-pick-omit@1.2.1: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@2.0.0: {} + + define-lazy-prop@3.0.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + delegate@3.2.0: {} + + destr@2.0.5: {} + + detect-file@1.0.0: {} + + detect-indent@6.1.0: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.1.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + dom7@3.0.0: + dependencies: + ssr-window: 3.0.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + downloadjs@1.4.7: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + + editorconfig@1.0.4: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.7.2 + + electron-to-chromium@1.5.227: {} + + element-china-area-data@6.1.0: + dependencies: + china-division: 2.7.0 + + element-plus@2.11.4(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.22(typescript@5.6.3)) + '@floating-ui/dom': 1.7.4 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.22(typescript@5.6.3)) + async-validator: 4.2.5 + dayjs: 1.11.18 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.22(typescript@5.6.3) + transitivePeerDependencies: + - '@vue/composition-api' + + emoji-regex@10.5.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + entities@6.0.1: {} + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser-es@0.1.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)): + dependencies: + eslint: 9.36.0(jiti@2.6.0) + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@9.1.2(eslint@9.36.0(jiti@2.6.0)))(eslint@9.36.0(jiti@2.6.0))(prettier@3.6.2): + dependencies: + eslint: 9.36.0(jiti@2.6.0) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 9.1.2(eslint@9.36.0(jiti@2.6.0)) + + eslint-plugin-vue@9.33.0(eslint@9.36.0(jiti@2.6.0)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + eslint: 9.36.0(jiti@2.6.0) + globals: 13.24.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.2 + vue-eslint-parser: 9.4.3(eslint@9.36.0(jiti@2.6.0)) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.36.0(jiti@2.6.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0(jiti@2.6.0)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.36.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.0 + transitivePeerDependencies: + - supports-color + + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.1: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + + expect-type@1.3.0: {} + + exsolve@1.0.7: {} + + ext@1.7.0: + dependencies: + type: 2.7.3 + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@10.1.4: + dependencies: + flat-cache: 6.1.14 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-saver@2.0.5: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-node-modules@2.1.3: + dependencies: + findup-sync: 4.0.0 + merge: 2.1.1 + + find-root@1.1.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + findup-sync@4.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flat-cache@6.1.14: + dependencies: + cacheable: 2.0.2 + flatted: 3.3.3 + hookified: 1.12.1 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + frac@1.1.2: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.4.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@8.0.1: {} + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-modules@2.0.0: + dependencies: + global-prefix: 3.0.0 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + + global-prefix@3.0.0: + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@14.0.0: {} + + globals@15.15.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globjoin@0.1.4: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + highlight.js@11.11.1: {} + + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + + hookable@5.5.3: {} + + hookified@1.12.1: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-tags@3.3.1: {} + + html-void-elements@2.0.1: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@5.0.0: {} + + human-signals@8.0.1: {} + + husky@9.1.7: {} + + i18next@20.6.1: + dependencies: + '@babel/runtime': 7.28.4 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immer@9.0.21: {} + + immutable@5.1.3: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.2.0: {} + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + inquirer@8.2.5: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-docker@2.2.1: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.4.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hotkey@0.2.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@5.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-stream@3.0.0: {} + + is-stream@4.0.1: {} + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@2.1.0: {} + + is-url@1.2.4: {} + + is-utf8@0.2.1: {} + + is-what@4.1.16: {} + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.6.0: {} + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.5.0 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsdom@24.1.3: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.4 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + keyv@5.5.3: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@6.0.3: {} + + known-css-properties@0.36.0: {} + + known-css-properties@0.37.0: {} + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lint-staged@15.5.2: + dependencies: + chalk: 5.6.2 + commander: 13.1.0 + debug: 4.4.3 + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.1 + transitivePeerDependencies: + - supports-color + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + + lodash.debounce@4.0.8: {} + + lodash.foreach@4.5.0: {} + + lodash.isequal@4.5.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.map@4.6.0: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.throttle@4.1.1: {} + + lodash.toarray@4.4.0: {} + + lodash.truncate@4.4.2: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.1.1 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + + longest@2.0.1: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mathml-tag-names@2.1.3: {} + + mdn-data@2.12.2: {} + + mdn-data@2.24.0: {} + + memoize-one@6.0.0: {} + + meow@12.1.1: {} + + meow@13.2.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + merge@2.1.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-match@1.0.2: + dependencies: + wildcard: 1.1.2 + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.1: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.7: {} + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mute-stream@0.0.8: {} + + namespace-emitter@2.0.1: {} + + nanoid@3.3.11: {} + + nanoid@5.1.6: {} + + natural-compare@1.4.0: {} + + next-tick@1.1.0: {} + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.21: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + normalize-path@3.0.0: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.23: {} + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-ms@4.0.0: {} + + parse-passwd@1.0.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + pinia-plugin-persistedstate@4.5.0(pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3))): + dependencies: + deep-pick-omit: 1.2.1 + defu: 6.1.4 + destr: 2.0.5 + optionalDependencies: + pinia: 3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)) + + pinia@3.0.3(typescript@5.6.3)(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 7.7.7 + vue: 3.5.22(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + postcss-html@1.8.0: + dependencies: + htmlparser2: 8.0.2 + js-tokens: 9.0.1 + postcss: 8.5.6 + postcss-safe-parser: 6.0.0(postcss@8.5.6) + + postcss-media-query-parser@0.2.3: {} + + postcss-resolve-nested-selector@0.1.6: {} + + postcss-safe-parser@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-safe-parser@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-scss@4.0.9(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sorting@8.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.27.2: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prismjs@1.30.0: {} + + proto-list@1.2.4: {} + + proxy-from-env@1.1.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + qrcode.vue@3.6.0(vue@3.5.22(typescript@5.6.3)): + dependencies: + vue: 3.5.22(typescript@5.6.3) + + quansync@0.2.11: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + requires-port@1.0.0: {} + + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rollup-plugin-visualizer@5.14.0(rollup@4.52.3): + dependencies: + open: 8.4.2 + picomatch: 4.0.3 + source-map: 0.7.6 + yargs: 17.7.2 + optionalDependencies: + rollup: 4.52.3 + + rollup@4.52.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.3 + '@rollup/rollup-android-arm64': 4.52.3 + '@rollup/rollup-darwin-arm64': 4.52.3 + '@rollup/rollup-darwin-x64': 4.52.3 + '@rollup/rollup-freebsd-arm64': 4.52.3 + '@rollup/rollup-freebsd-x64': 4.52.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 + '@rollup/rollup-linux-arm-musleabihf': 4.52.3 + '@rollup/rollup-linux-arm64-gnu': 4.52.3 + '@rollup/rollup-linux-arm64-musl': 4.52.3 + '@rollup/rollup-linux-loong64-gnu': 4.52.3 + '@rollup/rollup-linux-ppc64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-musl': 4.52.3 + '@rollup/rollup-linux-s390x-gnu': 4.52.3 + '@rollup/rollup-linux-x64-gnu': 4.52.3 + '@rollup/rollup-linux-x64-musl': 4.52.3 + '@rollup/rollup-openharmony-arm64': 4.52.3 + '@rollup/rollup-win32-arm64-msvc': 4.52.3 + '@rollup/rollup-win32-ia32-msvc': 4.52.3 + '@rollup/rollup-win32-x64-gnu': 4.52.3 + '@rollup/rollup-win32-x64-msvc': 4.52.3 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + + run-applescript@7.1.0: {} + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sass@1.93.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scroll-into-view-if-needed@2.2.31: + dependencies: + compute-scroll-into-view: 1.0.20 + + scule@1.3.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slash@3.0.0: {} + + slate-history@0.66.0(slate@0.72.8): + dependencies: + is-plain-object: 5.0.0 + slate: 0.72.8 + + slate@0.72.8: + dependencies: + immer: 9.0.21 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + snabbdom@3.6.2: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + speakingurl@14.0.1: {} + + split2@4.2.0: {} + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + ssr-window@3.0.0: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.5.0 + get-east-asian-width: 1.4.0 + strip-ansi: 7.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@4.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-html: 1.8.0 + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-recess-order@4.6.0(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + stylelint-order: 6.0.4(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-config-recommended-scss@14.1.0(postcss@8.5.6)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-scss: 4.0.9(postcss@8.5.6) + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-recommended: 14.0.1(stylelint@16.24.0(typescript@5.6.3)) + stylelint-scss: 6.12.1(stylelint@16.24.0(typescript@5.6.3)) + optionalDependencies: + postcss: 8.5.6 + + stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss-html: 1.8.0 + semver: 7.7.2 + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@16.24.0(typescript@5.6.3)) + stylelint-config-recommended: 17.0.0(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-config-recommended@14.0.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-recommended@17.0.0(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-config-standard@36.0.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + stylelint: 16.24.0(typescript@5.6.3) + stylelint-config-recommended: 14.0.1(stylelint@16.24.0(typescript@5.6.3)) + + stylelint-order@6.0.4(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + postcss: 8.5.6 + postcss-sorting: 8.0.2(postcss@8.5.6) + stylelint: 16.24.0(typescript@5.6.3) + + stylelint-scss@6.12.1(stylelint@16.24.0(typescript@5.6.3)): + dependencies: + css-tree: 3.1.0 + is-plain-object: 5.0.0 + known-css-properties: 0.36.0 + mdn-data: 2.24.0 + postcss-media-query-parser: 0.2.3 + postcss-resolve-nested-selector: 0.1.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + stylelint: 16.24.0(typescript@5.6.3) + + stylelint@16.24.0(typescript@5.6.3): + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + '@dual-bundle/import-meta-resolve': 4.2.1 + balanced-match: 2.0.0 + colord: 2.9.3 + cosmiconfig: 9.0.0(typescript@5.6.3) + css-functions-list: 3.2.3 + css-tree: 3.1.0 + debug: 4.4.3 + fast-glob: 3.3.3 + fastest-levenshtein: 1.0.16 + file-entry-cache: 10.1.4 + global-modules: 2.0.0 + globby: 11.1.0 + globjoin: 0.1.4 + html-tags: 3.3.1 + ignore: 7.0.5 + imurmurhash: 0.1.4 + is-plain-object: 5.0.0 + known-css-properties: 0.37.0 + mathml-tag-names: 2.1.3 + meow: 13.2.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-resolve-nested-selector: 0.1.6 + postcss-safe-parser: 7.0.1(postcss@8.5.6) + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + resolve-from: 5.0.0 + string-width: 4.2.3 + supports-hyperlinks: 3.2.0 + svg-tags: 1.0.0 + table: 6.9.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + - typescript + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@3.2.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + svg-tags@1.0.0: {} + + symbol-tree@3.2.4: {} + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + table@6.9.0: + dependencies: + ajv: 8.17.1 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tailwindcss@4.1.14: {} + + tapable@2.3.0: {} + + tar@7.5.1: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + terser@5.44.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + text-extensions@2.4.0: {} + + through@2.3.8: {} + + tiny-warning@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tlbs-map-vue@1.3.2(vue@3.5.22(typescript@5.6.3)): + dependencies: + fs-extra: 10.1.0 + vue: 3.5.22(typescript@5.6.3) + vue-demi: 0.14.10(vue@3.5.22(typescript@5.6.3)) + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + ts-api-utils@2.1.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + tslib@2.3.0: {} + + tslib@2.8.1: {} + + tsx@4.20.6: + dependencies: + esbuild: 0.25.10 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type@2.7.3: {} + + typescript-eslint@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.44.1(@typescript-eslint/parser@8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3))(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/parser': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + '@typescript-eslint/typescript-estree': 8.44.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.44.1(eslint@9.36.0(jiti@2.6.0))(typescript@5.6.3) + eslint: 9.36.0(jiti@2.6.0) + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + typescript@5.6.3: {} + + ufo@1.6.1: {} + + undici-types@7.14.0: {} + + unicorn-magic@0.1.0: {} + + unicorn-magic@0.3.0: {} + + unimport@5.4.0: + dependencies: + acorn: 8.15.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.19 + mlly: 1.8.0 + pathe: 2.0.3 + picomatch: 4.0.3 + pkg-types: 2.3.0 + scule: 1.3.0 + strip-literal: 3.1.0 + tinyglobby: 0.2.15 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unplugin-auto-import@20.2.0(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.6.3))): + dependencies: + local-pkg: 1.1.2 + magic-string: 0.30.19 + picomatch: 4.0.3 + unimport: 5.4.0 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + optionalDependencies: + '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.6.3)) + + unplugin-element-plus@0.10.0: + dependencies: + es-module-lexer: 1.7.0 + magic-string: 0.30.19 + unplugin: 2.3.10 + unplugin-utils: 0.2.5 + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-utils@0.3.0: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-vue-components@29.1.0(@babel/parser@7.28.4)(vue@3.5.22(typescript@5.6.3)): + dependencies: + chokidar: 3.6.0 + debug: 4.4.3 + local-pkg: 1.1.2 + magic-string: 0.30.19 + mlly: 1.8.0 + tinyglobby: 0.2.15 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + vue: 3.5.22(typescript@5.6.3) + optionalDependencies: + '@babel/parser': 7.28.4 + transitivePeerDependencies: + - supports-color + + unplugin@2.3.10: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.3(browserslist@4.26.2): + dependencies: + browserslist: 4.26.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + vite-hot-client@2.1.0(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + + vite-node@2.1.9(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-compression@0.5.1(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + vite-plugin-inspect@0.8.9(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@antfu/utils': 0.7.10 + '@rollup/pluginutils': 5.3.0(rollup@4.52.3) + debug: 4.4.3 + error-stack-parser-es: 0.1.5 + fs-extra: 11.3.2 + open: 10.2.0 + perfect-debounce: 1.0.0 + picocolors: 1.1.1 + sirv: 3.0.2 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - rollup + - supports-color + + vite-plugin-vue-devtools@7.7.7(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-core': 7.7.7(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.6.3)) + '@vue/devtools-kit': 7.7.7 + '@vue/devtools-shared': 7.7.7 + execa: 9.6.0 + sirv: 3.0.2 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite-plugin-inspect: 0.8.9(rollup@4.52.3)(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + transitivePeerDependencies: + - '@nuxt/kit' + - rollup + - supports-color + - vue + + vite-plugin-vue-inspector@5.3.2(vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.4) + '@vue/compiler-dom': 3.5.22 + kolorist: 1.8.0 + magic-string: 0.30.19 + vite: 7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + vite@5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.52.3 + optionalDependencies: + '@types/node': 24.8.1 + fsevents: 2.3.3 + lightningcss: 1.30.1 + sass: 1.93.2 + terser: 5.44.0 + + vite@7.1.7(@types/node@24.8.1)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.8.1 + fsevents: 2.3.3 + jiti: 2.6.0 + lightningcss: 1.30.1 + sass: 1.93.2 + terser: 5.44.0 + tsx: 4.20.6 + yaml: 2.8.1 + + vitest@2.1.9(@types/node@24.8.1)(jsdom@24.1.3)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.19 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + vite-node: 2.1.9(@types/node@24.8.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.8.1 + jsdom: 24.1.3 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@2.2.12: {} + + vue-demi@0.14.10(vue@3.5.22(typescript@5.6.3)): + dependencies: + vue: 3.5.22(typescript@5.6.3) + + vue-draggable-plus@0.6.0(@types/sortablejs@1.15.8): + dependencies: + '@types/sortablejs': 1.15.8 + + vue-eslint-parser@9.4.3(eslint@9.36.0(jiti@2.6.0)): + dependencies: + debug: 4.4.3 + eslint: 9.36.0(jiti@2.6.0) + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + lodash: 4.17.21 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + vue-i18n@9.14.5(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@intlify/core-base': 9.14.5 + '@intlify/shared': 9.14.5 + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@5.6.3) + + vue-img-cutter@3.0.7(typescript@5.6.3): + dependencies: + core-js: 3.45.1 + vue: 3.5.22(typescript@5.6.3) + vue-i18n: 9.14.5(vue@3.5.22(typescript@5.6.3)) + transitivePeerDependencies: + - typescript + + vue-router@4.5.1(vue@3.5.22(typescript@5.6.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.22(typescript@5.6.3) + + vue-tsc@2.1.10(typescript@5.6.3): + dependencies: + '@volar/typescript': 2.4.23 + '@vue/language-core': 2.1.10(typescript@5.6.3) + semver: 7.7.2 + typescript: 5.6.3 + + vue@3.5.22(typescript@5.6.3): + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-sfc': 3.5.22 + '@vue/runtime-dom': 3.5.22 + '@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@5.6.3)) + '@vue/shared': 3.5.22 + optionalDependencies: + typescript: 5.6.3 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@7.0.0: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wildcard@1.1.2: {} + + wmf@1.0.2: {} + + word-wrap@1.2.5: {} + + word@0.3.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + ws@8.18.3: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xgplayer-subtitles@3.0.23(core-js@3.45.1): + dependencies: + core-js: 3.45.1 + eventemitter3: 4.0.7 + + xgplayer@3.0.23(core-js@3.45.1): + dependencies: + core-js: 3.45.1 + danmu.js: 1.1.13 + delegate: 3.2.0 + downloadjs: 1.4.7 + eventemitter3: 4.0.7 + xgplayer-subtitles: 3.0.23(core-js@3.45.1) + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xml-name-validator@4.0.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yaml@2.8.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + yoctocolors@2.1.2: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..4226ea1 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +onlyBuiltDependencies: + - '@parcel/watcher' + - '@tailwindcss/oxide' + - core-js + - es5-ext + - esbuild + - vue-demi diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/scripts/clean-dev.ts b/scripts/clean-dev.ts new file mode 100644 index 0000000..cc0b9bc --- /dev/null +++ b/scripts/clean-dev.ts @@ -0,0 +1,838 @@ +// scripts/clean-dev.ts +import fs from 'fs/promises' +import path from 'path' + +// 现代化颜色主题 +const theme = { + // 基础颜色 + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + + // 前景色 + primary: '\x1b[38;5;75m', // 亮蓝色 + success: '\x1b[38;5;82m', // 亮绿色 + warning: '\x1b[38;5;220m', // 亮黄色 + error: '\x1b[38;5;196m', // 亮红色 + info: '\x1b[38;5;159m', // 青色 + purple: '\x1b[38;5;141m', // 紫色 + orange: '\x1b[38;5;208m', // 橙色 + gray: '\x1b[38;5;245m', // 灰色 + white: '\x1b[38;5;255m', // 白色 + + // 背景色 + bgDark: '\x1b[48;5;235m', // 深灰背景 + bgBlue: '\x1b[48;5;24m', // 蓝色背景 + bgGreen: '\x1b[48;5;22m', // 绿色背景 + bgRed: '\x1b[48;5;52m' // 红色背景 +} + +// 现代化图标集 +const icons = { + rocket: '🚀', + fire: '🔥', + star: '⭐', + gem: '💎', + crown: '👑', + magic: '✨', + warning: '⚠️', + success: '✅', + error: '❌', + info: 'ℹ️', + folder: '📁', + file: '📄', + image: '🖼️', + code: '💻', + data: '📊', + globe: '🌐', + map: '🗺️', + chat: '💬', + bolt: '⚡', + shield: '🛡️', + key: '🔑', + link: '🔗', + clean: '🧹', + trash: '🗑️', + check: '✓', + cross: '✗', + arrow: '→', + loading: '⏳' +} + +// 格式化工具 +const fmt = { + title: (text: string) => `${theme.bold}${theme.primary}${text}${theme.reset}`, + subtitle: (text: string) => `${theme.purple}${text}${theme.reset}`, + success: (text: string) => `${theme.success}${text}${theme.reset}`, + error: (text: string) => `${theme.error}${text}${theme.reset}`, + warning: (text: string) => `${theme.warning}${text}${theme.reset}`, + info: (text: string) => `${theme.info}${text}${theme.reset}`, + highlight: (text: string) => `${theme.bold}${theme.white}${text}${theme.reset}`, + dim: (text: string) => `${theme.dim}${theme.gray}${text}${theme.reset}`, + orange: (text: string) => `${theme.orange}${text}${theme.reset}`, + + // 带背景的文本 + badge: (text: string, bg: string = theme.bgBlue) => + `${bg}${theme.white}${theme.bold} ${text} ${theme.reset}`, + + // 渐变效果模拟 + gradient: (text: string) => { + const colors = ['\x1b[38;5;75m', '\x1b[38;5;81m', '\x1b[38;5;87m', '\x1b[38;5;159m'] + const chars = text.split('') + return chars.map((char, i) => `${colors[i % colors.length]}${char}`).join('') + theme.reset + } +} + +// 创建现代化标题横幅 +function createModernBanner() { + console.log() + console.log( + fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + ` ║ ${icons.rocket} ${fmt.title('ART DESIGN PRO')} ${fmt.subtitle('· 代码精简程序')} ${icons.magic} ║` + ) + console.log( + ` ║ ${fmt.dim('为项目移除演示数据,快速切换至开发模式')} ║` + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') + ) + console.log() +} + +// 创建分割线 +function createDivider(char = '─', color = theme.primary) { + console.log(`${color}${' ' + char.repeat(66)}${theme.reset}`) +} + +// 创建卡片样式容器 +function createCard(title: string, content: string[]) { + console.log(` ${fmt.badge('', theme.bgBlue)} ${fmt.title(title)}`) + console.log() + content.forEach((line) => { + console.log(` ${line}`) + }) + console.log() +} + +// 进度条动画 +function createProgressBar(current: number, total: number, text: string, width = 40) { + const percentage = Math.round((current / total) * 100) + const filled = Math.round((current / total) * width) + const empty = width - filled + + const filledBar = '█'.repeat(filled) + const emptyBar = '░'.repeat(empty) + + process.stdout.write( + `\r ${fmt.info('进度')} [${theme.success}${filledBar}${theme.gray}${emptyBar}${theme.reset}] ${fmt.highlight(percentage + '%')})}` + ) + + if (current === total) { + console.log() + } +} + +// 统计信息 +const stats = { + deletedFiles: 0, + deletedPaths: 0, + failedPaths: 0, + startTime: Date.now(), + totalFiles: 0 +} + +// 清理目标 +const targets = [ + 'README.md', + 'README.zh-CN.md', + 'CHANGELOG.md', + 'CHANGELOG.zh-CN.md', + 'src/views/change', + 'src/views/safeguard', + 'src/views/article', + 'src/views/examples', + 'src/views/system/nested', + 'src/views/widgets', + 'src/views/template', + 'src/views/dashboard/analysis', + 'src/views/dashboard/ecommerce', + 'src/mock/json', + 'src/mock/temp/articleList.ts', + 'src/mock/temp/commentDetail.ts', + 'src/mock/temp/commentList.ts', + 'src/assets/images/cover', + 'src/assets/images/safeguard', + 'src/assets/images/3d', + 'src/components/core/charts/art-map-chart', + 'src/components/business/comment-widget' +] + +// 递归统计文件数量 +async function countFiles(targetPath: string): Promise { + const fullPath = path.resolve(process.cwd(), targetPath) + + try { + const stat = await fs.stat(fullPath) + + if (stat.isFile()) { + return 1 + } else if (stat.isDirectory()) { + const entries = await fs.readdir(fullPath) + let count = 0 + + for (const entry of entries) { + const entryPath = path.join(targetPath, entry) + count += await countFiles(entryPath) + } + + return count + } + } catch { + return 0 + } + + return 0 +} + +// 统计所有目标的文件数量 +async function countAllFiles(): Promise { + let totalCount = 0 + + for (const target of targets) { + const count = await countFiles(target) + totalCount += count + } + + return totalCount +} + +// 删除文件和目录 +async function remove(targetPath: string, index: number) { + const fullPath = path.resolve(process.cwd(), targetPath) + + createProgressBar(index + 1, targets.length, targetPath) + + try { + const fileCount = await countFiles(targetPath) + await fs.rm(fullPath, { recursive: true, force: true }) + stats.deletedFiles += fileCount + stats.deletedPaths++ + await new Promise((resolve) => setTimeout(resolve, 50)) + } catch (err) { + stats.failedPaths++ + console.log() + console.log(` ${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(targetPath)}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理路由模块 +async function cleanRouteModules() { + const modulesPath = path.resolve(process.cwd(), 'src/router/modules') + + try { + // 删除演示相关的路由模块 + const modulesToRemove = [ + 'template.ts', + 'widgets.ts', + 'examples.ts', + 'article.ts', + 'safeguard.ts', + 'help.ts' + ] + + for (const module of modulesToRemove) { + const modulePath = path.join(modulesPath, module) + try { + await fs.rm(modulePath, { force: true }) + } catch { + // 文件不存在时忽略错误 + } + } + + // 重写 dashboard.ts - 只保留 console + const dashboardContent = `import { AppRouteRecord } from '@/types/router' + +export const dashboardRoutes: AppRouteRecord = { + name: 'Dashboard', + path: '/dashboard', + component: '/index/index', + meta: { + title: 'menus.dashboard.title', + icon: 'ri:pie-chart-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'console', + name: 'Console', + component: '/dashboard/console', + meta: { + title: 'menus.dashboard.console', + keepAlive: false, + fixedTab: true + } + } + ] +} +` + await fs.writeFile(path.join(modulesPath, 'dashboard.ts'), dashboardContent, 'utf-8') + + // 重写 system.ts - 移除 nested 嵌套菜单 + const systemContent = `import { AppRouteRecord } from '@/types/router' + +export const systemRoutes: AppRouteRecord = { + path: '/system', + name: 'System', + component: '/index/index', + meta: { + title: 'menus.system.title', + icon: 'ri:user-3-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'user', + name: 'User', + component: '/system/user', + meta: { + title: 'menus.system.user', + keepAlive: true, + roles: ['R_SUPER', 'R_ADMIN'] + } + }, + { + path: 'role', + name: 'Role', + component: '/system/role', + meta: { + title: 'menus.system.role', + keepAlive: true, + roles: ['R_SUPER'] + } + }, + { + path: 'user-center', + name: 'UserCenter', + component: '/system/user-center', + meta: { + title: 'menus.system.userCenter', + isHide: true, + keepAlive: true, + isHideTab: true + } + }, + { + path: 'menu', + name: 'Menus', + component: '/system/menu', + meta: { + title: 'menus.system.menu', + keepAlive: true, + roles: ['R_SUPER'], + authList: [ + { title: '新增', authMark: 'add' }, + { title: '编辑', authMark: 'edit' }, + { title: '删除', authMark: 'delete' } + ] + } + } + ] +} +` + await fs.writeFile(path.join(modulesPath, 'system.ts'), systemContent, 'utf-8') + + // 重写 index.ts - 只导入保留的模块 + const indexContent = `import { AppRouteRecord } from '@/types/router' +import { dashboardRoutes } from './dashboard' +import { systemRoutes } from './system' +import { resultRoutes } from './result' +import { exceptionRoutes } from './exception' + +/** + * 导出所有模块化路由 + */ +export const routeModules: AppRouteRecord[] = [ + dashboardRoutes, + systemRoutes, + resultRoutes, + exceptionRoutes +] +` + await fs.writeFile(path.join(modulesPath, 'index.ts'), indexContent, 'utf-8') + + console.log(` ${icons.success} ${fmt.success('清理路由模块完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理路由模块失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理路由别名 +async function cleanRoutesAlias() { + const routesAliasPath = path.resolve(process.cwd(), 'src/router/routesAlias.ts') + + try { + const cleanedAlias = `/** + * 公共路由别名 + # 存放系统级公共路由路径,如布局容器、登录页等 + */ +export enum RoutesAlias { + Layout = '/index/index', // 布局容器 + Login = '/auth/login' // 登录页 +} +` + + await fs.writeFile(routesAliasPath, cleanedAlias, 'utf-8') + console.log(` ${icons.success} ${fmt.success('重写路由别名配置完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理路由别名失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理变更日志 +async function cleanChangeLog() { + const changeLogPath = path.resolve(process.cwd(), 'src/mock/upgrade/changeLog.ts') + + try { + const cleanedChangeLog = `import { ref } from 'vue' + +interface UpgradeLog { + version: string // 版本号 + title: string // 更新标题 + date: string // 更新日期 + detail?: string[] // 更新内容 + requireReLogin?: boolean // 是否需要重新登录 + remark?: string // 备注 +} + +export const upgradeLogList = ref([]) +` + + await fs.writeFile(changeLogPath, cleanedChangeLog, 'utf-8') + console.log(` ${icons.success} ${fmt.success('清空变更日志数据完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理变更日志失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 清理语言文件 +async function cleanLanguageFiles() { + const languageFiles = [ + { path: 'src/locales/langs/zh.json', name: '中文语言文件' }, + { path: 'src/locales/langs/en.json', name: '英文语言文件' } + ] + + for (const { path: langPath, name } of languageFiles) { + try { + const fullPath = path.resolve(process.cwd(), langPath) + const content = await fs.readFile(fullPath, 'utf-8') + const langData = JSON.parse(content) + + const menusToRemove = [ + 'widgets', + 'template', + 'article', + 'examples', + 'safeguard', + 'plan', + 'help' + ] + + if (langData.menus) { + menusToRemove.forEach((menuKey) => { + if (langData.menus[menuKey]) { + delete langData.menus[menuKey] + } + }) + + if (langData.menus.dashboard) { + if (langData.menus.dashboard.analysis) { + delete langData.menus.dashboard.analysis + } + if (langData.menus.dashboard.ecommerce) { + delete langData.menus.dashboard.ecommerce + } + } + + if (langData.menus.system) { + const systemKeysToRemove = [ + 'nested', + 'menu1', + 'menu2', + 'menu21', + 'menu3', + 'menu31', + 'menu32', + 'menu321' + ] + systemKeysToRemove.forEach((key) => { + if (langData.menus.system[key]) { + delete langData.menus.system[key] + } + }) + } + } + + await fs.writeFile(fullPath, JSON.stringify(langData, null, 2), 'utf-8') + console.log(` ${icons.success} ${fmt.success(`清理${name}完成`)}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error(`清理${name}失败`)}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } + } +} + +// 清理快速入口组件 +async function cleanFastEnterComponent() { + const fastEnterPath = path.resolve(process.cwd(), 'src/config/fastEnter.ts') + + try { + const cleanedFastEnter = `/** + * 快速入口配置 + * 包含:应用列表、快速链接等配置 + */ +import { WEB_LINKS } from '@/utils/constants' +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '工作台', + description: '系统概览与数据统计', + icon: 'ri:pie-chart-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '官方文档', + description: '使用指南与开发文档', + icon: 'ri:bill-line', + iconColor: '#ffb100', + enabled: true, + order: 2, + link: WEB_LINKS.DOCS + }, + { + name: '技术支持', + description: '技术支持与问题反馈', + icon: 'ri:user-location-line', + iconColor: '#ff6b6b', + enabled: true, + order: 3, + link: WEB_LINKS.COMMUNITY + }, + { + name: '哔哩哔哩', + description: '技术分享与交流', + icon: 'ri:bilibili-line', + iconColor: '#FB7299', + enabled: true, + order: 4, + link: WEB_LINKS.BILIBILI + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '注册', + enabled: true, + order: 2, + routeName: 'Register' + }, + { + name: '忘记密码', + enabled: true, + order: 3, + routeName: 'ForgetPassword' + }, + { + name: '个人中心', + enabled: true, + order: 4, + routeName: 'UserCenter' + } + ] +} + +export default Object.freeze(fastEnterConfig) +` + + await fs.writeFile(fastEnterPath, cleanedFastEnter, 'utf-8') + console.log(` ${icons.success} ${fmt.success('清理快速入口配置完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('清理快速入口配置失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 更新菜单接口 +async function updateMenuApi() { + const apiPath = path.resolve(process.cwd(), 'src/api/system-manage.ts') + + try { + const content = await fs.readFile(apiPath, 'utf-8') + const updatedContent = content.replace( + "url: '/api/v3/system/menus'", + "url: '/api/v3/system/menus/simple'" + ) + + await fs.writeFile(apiPath, updatedContent, 'utf-8') + console.log(` ${icons.success} ${fmt.success('更新菜单接口完成')}`) + } catch (err) { + console.log(` ${icons.error} ${fmt.error('更新菜单接口失败')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + } +} + +// 用户确认函数 +async function getUserConfirmation(): Promise { + const { createInterface } = await import('readline') + + return new Promise((resolve) => { + const rl = createInterface({ + input: process.stdin, + output: process.stdout + }) + + console.log( + ` ${fmt.highlight('请输入')} ${fmt.success('yes')} ${fmt.highlight('确认执行清理操作,或按 Enter 取消')}` + ) + console.log() + process.stdout.write(` ${icons.arrow} `) + + rl.question('', (answer: string) => { + rl.close() + resolve(answer.toLowerCase().trim() === 'yes') + }) + }) +} + +// 显示清理警告 +async function showCleanupWarning() { + createCard('安全警告', [ + `${fmt.warning('此操作将永久删除以下演示内容,且无法恢复!')}`, + `${fmt.dim('请仔细阅读清理列表,确认后再继续操作')}` + ]) + + const cleanupItems = [ + { + icon: icons.image, + name: '图片资源', + desc: '演示用的封面图片、3D图片、运维图片等', + color: theme.orange + }, + { + icon: icons.file, + name: '演示页面', + desc: 'widgets、template、article、examples、safeguard等页面', + color: theme.purple + }, + { + icon: icons.code, + name: '路由模块文件', + desc: '删除演示路由模块,只保留核心模块(dashboard、system、result、exception)', + color: theme.primary + }, + { + icon: icons.link, + name: '路由别名', + desc: '重写routesAlias.ts,移除演示路由别名', + color: theme.info + }, + { + icon: icons.data, + name: 'Mock数据', + desc: '演示用的JSON数据、文章列表、评论数据等', + color: theme.success + }, + { + icon: icons.globe, + name: '多语言文件', + desc: '清理中英文语言包中的演示菜单项', + color: theme.warning + }, + { icon: icons.map, name: '地图组件', desc: '移除art-map-chart地图组件', color: theme.error }, + { icon: icons.chat, name: '评论组件', desc: '移除comment-widget评论组件', color: theme.orange }, + { + icon: icons.bolt, + name: '快速入口', + desc: '移除分析页、礼花效果、聊天、更新日志、定价、留言管理等无效项目', + color: theme.purple + } + ] + + console.log(` ${fmt.badge('', theme.bgRed)} ${fmt.title('将要清理的内容')}`) + console.log() + + cleanupItems.forEach((item, index) => { + console.log(` ${item.color}${theme.reset} ${fmt.highlight(`${index + 1}. ${item.name}`)}`) + console.log(` ${fmt.dim(item.desc)}`) + }) + + console.log() + console.log(` ${fmt.badge('', theme.bgGreen)} ${fmt.title('保留的功能模块')}`) + console.log() + + const preservedModules = [ + { name: 'Dashboard', desc: '工作台页面' }, + { name: 'System', desc: '系统管理模块' }, + { name: 'Result', desc: '结果页面' }, + { name: 'Exception', desc: '异常页面' }, + { name: 'Auth', desc: '登录注册功能' }, + { name: 'Core Components', desc: '核心组件库' } + ] + + preservedModules.forEach((module) => { + console.log(` ${icons.check} ${fmt.success(module.name)} ${fmt.dim(`- ${module.desc}`)}`) + }) + + console.log() + createDivider() + console.log() +} + +// 显示统计信息 +async function showStats() { + const duration = Date.now() - stats.startTime + const seconds = (duration / 1000).toFixed(2) + + console.log() + createCard('清理统计', [ + `${fmt.success('成功删除')}: ${fmt.highlight(stats.deletedFiles.toString())} 个文件`, + `${fmt.info('涉及路径')}: ${fmt.highlight(stats.deletedPaths.toString())} 个目录/文件`, + ...(stats.failedPaths > 0 + ? [ + `${icons.error} ${fmt.error('删除失败')}: ${fmt.highlight(stats.failedPaths.toString())} 个路径` + ] + : []), + `${fmt.info('耗时')}: ${fmt.highlight(seconds)} 秒` + ]) +} + +// 创建成功横幅 +function createSuccessBanner() { + console.log() + console.log( + fmt.gradient(' ╔══════════════════════════════════════════════════════════════════╗') + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + ` ║ ${icons.star} ${fmt.success('清理完成!项目已准备就绪')} ${icons.rocket} ║` + ) + console.log( + ` ║ ${fmt.dim('现在可以开始您的开发之旅了!')} ║` + ) + console.log( + fmt.gradient(' ║ ║') + ) + console.log( + fmt.gradient(' ╚══════════════════════════════════════════════════════════════════╝') + ) + console.log() +} + +// 主函数 +async function main() { + // 清屏并显示横幅 + console.clear() + createModernBanner() + + // 显示清理警告 + await showCleanupWarning() + + // 统计文件数量 + console.log(` ${fmt.info('正在统计文件数量...')}`) + stats.totalFiles = await countAllFiles() + + console.log(` ${fmt.info('即将清理')}: ${fmt.highlight(stats.totalFiles.toString())} 个文件`) + console.log(` ${fmt.dim(`涉及 ${targets.length} 个目录/文件路径`)}`) + console.log() + + // 用户确认 + const confirmed = await getUserConfirmation() + + if (!confirmed) { + console.log(` ${fmt.warning('操作已取消,清理中止')}`) + console.log() + return + } + + console.log() + console.log(` ${icons.check} ${fmt.success('确认成功,开始清理...')}`) + console.log() + + // 开始清理过程 + console.log(` ${fmt.badge('步骤 1/6', theme.bgBlue)} ${fmt.title('删除演示文件')}`) + console.log() + for (let i = 0; i < targets.length; i++) { + await remove(targets[i], i) + } + console.log() + + console.log(` ${fmt.badge('步骤 2/6', theme.bgBlue)} ${fmt.title('清理路由模块')}`) + console.log() + await cleanRouteModules() + console.log() + + console.log(` ${fmt.badge('步骤 3/6', theme.bgBlue)} ${fmt.title('重写路由别名')}`) + console.log() + await cleanRoutesAlias() + console.log() + + console.log(` ${fmt.badge('步骤 4/6', theme.bgBlue)} ${fmt.title('清空变更日志')}`) + console.log() + await cleanChangeLog() + console.log() + + console.log(` ${fmt.badge('步骤 5/6', theme.bgBlue)} ${fmt.title('清理语言文件')}`) + console.log() + await cleanLanguageFiles() + console.log() + + console.log(` ${fmt.badge('步骤 6/7', theme.bgBlue)} ${fmt.title('清理快速入口')}`) + console.log() + await cleanFastEnterComponent() + console.log() + + console.log(` ${fmt.badge('步骤 7/7', theme.bgBlue)} ${fmt.title('更新菜单接口')}`) + console.log() + await updateMenuApi() + + // 显示统计信息 + await showStats() + + // 显示成功横幅 + createSuccessBanner() +} + +main().catch((err) => { + console.log() + console.log(` ${icons.error} ${fmt.error('清理脚本执行出错')}`) + console.log(` ${fmt.dim('错误详情: ' + err)}`) + console.log() + process.exit(1) +}) diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..3433913 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/api/announcement.ts b/src/api/announcement.ts new file mode 100644 index 0000000..67de76c --- /dev/null +++ b/src/api/announcement.ts @@ -0,0 +1,254 @@ +/** + * 文件用途:公告模块 API 封装 + * 说明:统一对接平台/租户/应用端公告接口 + * 日期:2025-12-20 + */ + +import request from '@/utils/http' +import type { + AnnouncementCommand, + AnnouncementFormData, + AnnouncementQueryParams, + TenantAnnouncementDto +} from '@/types/announcement' +import type { PagedResult } from '@/types/common/response' + +/** 发布/撤销命令体(仅包含 RowVersion) */ +type RowVersionCommand = Pick + +const withTenantHeader = (tenantId: string) => ({ + headers: { + 'X-Tenant-Id': tenantId + } +}) + +const normalizeQueryParams = (params: AnnouncementQueryParams) => { + const { dateFrom, dateTo, ...rest } = params + return { + ...rest, + effectiveFrom: params.effectiveFrom ?? dateFrom, + effectiveTo: params.effectiveTo ?? dateTo + } +} + +/** + * 平台公告 API + * 路由前缀:/api/platform/announcements + */ +export const platformAnnouncementApi = { + /** + * 创建平台公告 + * @param data 创建参数 + */ + create: (data: AnnouncementFormData) => { + return request.post({ + url: '/api/platform/announcements', + data + }) + }, + + /** + * 查询平台公告列表 + * @param params 查询参数 + */ + list: (params: AnnouncementQueryParams) => { + return request.get>({ + url: '/api/platform/announcements', + params: normalizeQueryParams(params) + }) + }, + + /** + * 获取平台公告详情 + * @param announcementId 公告ID + */ + detail: (announcementId: string) => { + return request.get({ + url: `/api/platform/announcements/${announcementId}` + }) + }, + + /** + * 更新平台公告(仅草稿) + * @param announcementId 公告ID + * @param data 更新参数 + */ + update: (announcementId: string, data: AnnouncementFormData) => { + return request.put({ + url: `/api/platform/announcements/${announcementId}`, + data + }) + }, + + /** + * 发布平台公告 + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + publish: (announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/platform/announcements/${announcementId}/publish`, + data: command + }) + }, + + /** + * 撤销平台公告 + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + revoke: (announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/platform/announcements/${announcementId}/revoke`, + data: command + }) + } +} + +/** + * 租户公告 API + * 路由前缀:/api/admin/v1/tenants/{tenantId}/announcements + */ +export const tenantAnnouncementApi = { + /** + * 分页查询租户公告 + * @param tenantId 租户ID + * @param params 查询参数 + */ + list: (tenantId: string, params: AnnouncementQueryParams) => { + return request.get>({ + url: `/api/admin/v1/tenants/${tenantId}/announcements`, + params: normalizeQueryParams(params), + ...withTenantHeader(tenantId) + }) + }, + + /** + * 获取租户公告详情 + * @param tenantId 租户ID + * @param announcementId 公告ID + */ + detail: (tenantId: string, announcementId: string) => { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 创建租户公告 + * @param tenantId 租户ID + * @param data 创建参数 + */ + create: (tenantId: string, data: AnnouncementFormData) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements`, + data, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 更新租户公告(仅草稿) + * @param tenantId 租户ID + * @param announcementId 公告ID + * @param data 更新参数 + */ + update: (tenantId: string, announcementId: string, data: AnnouncementFormData) => { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`, + data, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 发布租户公告 + * @param tenantId 租户ID + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + publish: (tenantId: string, announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/publish`, + data: command, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 撤销租户公告 + * @param tenantId 租户ID + * @param announcementId 公告ID + * @param command 命令体(包含 rowVersion) + */ + revoke: (tenantId: string, announcementId: string, command: RowVersionCommand) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/revoke`, + data: command, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 删除租户公告 + * @param tenantId 租户ID + * @param announcementId 公告ID + */ + delete: (tenantId: string, announcementId: string) => { + return request.del({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}`, + ...withTenantHeader(tenantId) + }) + }, + + /** + * 标记公告已读(兼容旧路径) + * @param tenantId 租户ID + * @param announcementId 公告ID + */ + markRead: (tenantId: string, announcementId: string) => { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/announcements/${announcementId}/read`, + ...withTenantHeader(tenantId) + }) + } +} + +/** + * 应用端公告 API + * 路由前缀:/api/app/announcements + */ +export const appAnnouncementApi = { + /** + * 获取可见公告列表(已发布且有效期内) + * @param params 查询参数 + */ + list: (params: AnnouncementQueryParams) => { + return request.get>({ + url: '/api/app/announcements', + params: normalizeQueryParams(params) + }) + }, + + /** + * 获取未读公告列表 + * @param params 查询参数 + */ + unread: (params: AnnouncementQueryParams) => { + return request.get>({ + url: '/api/app/announcements/unread', + params: normalizeQueryParams(params) + }) + }, + + /** + * 标记公告已读 + * @param announcementId 公告ID + */ + markRead: (announcementId: string) => { + return request.post({ + url: `/api/app/announcements/${announcementId}/mark-read` + }) + } +} diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..e5f1fe4 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,71 @@ +import request from '@/utils/http' + +/** + * 登录 + * @param params 登录参数 + * @returns 登录响应 + */ +export function fetchLogin(params: Api.Auth.LoginParams) { + return request.post({ + url: '/api/admin/v1/auth/login', + params + // showSuccessMessage: true // 显示成功消息 + // showErrorMessage: false // 不显示错误消息 + }) +} + +/** + * 免租户号登录(仅账号+密码) + */ +export function fetchLoginSimple(params: Api.Auth.LoginParams) { + return request.post({ + url: '/api/admin/v1/auth/login/simple', + params, + skipTenantHeader: true + }) +} + +/** + * 获取用户信息 + * @returns 用户信息 + */ +export function fetchGetUserInfo() { + return request.get({ + url: '/api/admin/v1/auth/profile' + // 自定义请求头 + // headers: { + // 'X-Custom-Header': 'your-custom-value' + // } + }) +} + +/** + * 获取用户菜单 + * @returns 菜单列表 + */ +export function fetchGetMenu() { + return request.get({ + url: '/api/admin/v1/auth/menu' + }) +} + +/** + * 获取用户权限 + * @param userId 用户ID + * @returns 用户权限信息 + */ +export function fetchGetUserPermissions(userId: string) { + return request.get({ + url: `/api/admin/v1/auth/permissions/${userId}` + }) +} + +/** + * 通过重置链接令牌重置管理员密码 + */ +export function fetchResetAdminPassword(data: Api.Auth.ResetAdminPasswordRequest) { + return request.post({ + url: '/api/admin/v1/auth/reset-password', + data + }) +} diff --git a/src/api/billing.ts b/src/api/billing.ts new file mode 100644 index 0000000..0417334 --- /dev/null +++ b/src/api/billing.ts @@ -0,0 +1,222 @@ +import request from '@/utils/http' + +/** + * 账单接口基础路径 + * + * 说明:后端路由为 /api/admin/v1/billings。 + */ +const BILLING_BASE_URL = '/api/admin/v1/billings' + +// ==================== 查询类 API ==================== + +/** + * 获取账单列表 + * @param params 查询参数 + */ +export function fetchBillingList(params?: Partial) { + return request.get({ + url: BILLING_BASE_URL, + params + }) +} + +/** + * 获取账单列表(导出用:自动分页拉取) + * @param params 查询参数(不含分页也可) + * @param options 导出拉取选项 + */ +export async function fetchBillingListForExport( + params?: Partial, + options?: { + /** 单页拉取条数(越大越快,但后端可能有限制) */ + pageSize?: number + /** 最大导出条数(防止一次性导出过大) */ + maxRows?: number + } +) { + // 1. 兜底配置 + const pageSize = Math.min(Math.max(options?.pageSize ?? 200, 50), 2000) + const maxRows = Math.min(Math.max(options?.maxRows ?? 10000, 1), 100000) + + // 2. 循环分页拉取(直到 totalPages 或达到 maxRows) + const result: Api.Billing.BillingListDto[] = [] + let pageNumber = 1 + let totalPages = 1 + + while (pageNumber <= totalPages && result.length < maxRows) { + const res = await fetchBillingList({ + ...params, + PageNumber: pageNumber, + PageSize: pageSize + }) + + const items = res.items || [] + result.push(...items) + + totalPages = res.totalPages || 1 + if (items.length === 0) break + pageNumber += 1 + } + + // 3. 截断到 maxRows,避免极端情况内存爆炸 + return result.slice(0, maxRows) +} + +/** + * 获取账单详情 + * @param id 账单 ID + */ +export function fetchBillingDetail(id: string) { + return request.get({ + url: `${BILLING_BASE_URL}/${id}` + }) +} + +/** + * 获取账单支付记录 + * @param billingId 账单 ID + */ +export function fetchBillingPayments(billingId: string) { + return request.get({ + url: `${BILLING_BASE_URL}/${billingId}/payments` + }) +} + +/** + * 获取账单统计数据 + * @param params 统计查询参数 + */ +export function fetchBillingStatistics(params: Api.Billing.BillingStatisticsParams) { + return request.get({ + url: `${BILLING_BASE_URL}/statistics`, + params + }) +} + +/** + * 获取逾期账单列表 + * @param params 分页参数 + */ +export function fetchOverdueBillings(params?: Partial) { + return request.get({ + url: `${BILLING_BASE_URL}/overdue`, + params + }) +} + +// ==================== 命令类 API ==================== + +/** + * 创建账单 + * @param data 创建账单数据 + */ +export function createBilling(data: Api.Billing.CreateBillingCommand) { + return request.post({ + url: BILLING_BASE_URL, + data + }) +} + +/** + * 更新账单状态 + * @param id 账单 ID + * @param data 更新状态数据 + */ +export function updateBillingStatus(id: string, data: Api.Billing.UpdateStatusCommand) { + return request.put({ + url: `${BILLING_BASE_URL}/${id}/status`, + data + }) +} + +/** + * 取消账单 + * @param id 账单 ID + * @param data 取消原因 + */ +export function cancelBilling(id: string, data: Api.Billing.CancelBillingCommand) { + return request.del({ + url: `${BILLING_BASE_URL}/${id}`, + data + }) +} + +/** + * 记录支付(仅创建待审核支付记录,不会立即更新账单状态) + * @param billingId 账单 ID + * @param data 支付记录数据 + */ +export function recordPayment(billingId: string, data: Api.Billing.RecordPaymentCommand) { + return request.post({ + url: `${BILLING_BASE_URL}/${billingId}/payments`, + data + }) +} + +/** + * 一键确认收款(记录支付 + 立即审核通过 + 同步更新账单状态) + * @param billingId 账单 ID + * @param data 支付记录数据 + */ +export function confirmPayment(billingId: string, data: Api.Billing.RecordPaymentCommand) { + return request.post({ + url: `${BILLING_BASE_URL}/${billingId}/payments/confirm`, + data + }) +} + +/** + * 审核支付记录 + * @param paymentId 支付记录 ID + * @param data 审核数据 + */ +export function verifyPayment(paymentId: string, data: Api.Billing.VerifyPaymentCommand) { + return request.put({ + url: `${BILLING_BASE_URL}/payments/${paymentId}/verify`, + data + }) +} + +/** + * 批量更新账单状态 + * @param data 批量更新数据 + */ +export function batchUpdateStatus(data: Api.Billing.BatchUpdateStatusCommand) { + return request.post({ + url: `${BILLING_BASE_URL}/batch/status`, + data + }) +} + +/** + * 导出账单 + * @param data 导出参数 + * @returns Blob 对象(用于下载) + */ +export function exportBillings(data: Api.Billing.ExportParams) { + return request.post({ + url: `${BILLING_BASE_URL}/export`, + data, + responseType: 'blob' + }) +} + +// ==================== 兼容导出(适配现有页面命名) ==================== + +/** 获取账单列表(兼容旧命名) */ +export const fetchGetBillList = fetchBillingList + +/** 获取账单详情(兼容旧命名) */ +export const fetchGetBillDetail = fetchBillingDetail + +/** 获取账单列表(导出用,兼容旧命名) */ +export const fetchGetBillListForExport = fetchBillingListForExport + +/** 创建账单(兼容旧命名) */ +export const fetchCreateBill = createBilling + +/** 更新账单状态(兼容旧命名) */ +export const fetchUpdateBillStatus = updateBillingStatus + +/** 记录支付(兼容旧命名) */ +export const fetchRecordPayment = recordPayment diff --git a/src/api/dictionary/group.ts b/src/api/dictionary/group.ts new file mode 100644 index 0000000..74b4351 --- /dev/null +++ b/src/api/dictionary/group.ts @@ -0,0 +1,70 @@ +import request from '@/utils/http' + +const GROUP_BASE_URL = '/api/admin/v1/dictionary/groups' + +export function getGroups(params?: Api.Dictionary.DictionaryGroupQueryParams) { + return request.get>({ + url: GROUP_BASE_URL, + params + }) +} + +export function getGroupById(groupId: string) { + return request.get({ + url: `${GROUP_BASE_URL}/${groupId}` + }) +} + +export function createGroup(data: Api.Dictionary.CreateDictionaryGroupRequest) { + return request.post({ + url: GROUP_BASE_URL, + data + }) +} + +export function updateGroup(groupId: string, data: Api.Dictionary.UpdateDictionaryGroupRequest) { + return request.put({ + url: `${GROUP_BASE_URL}/${groupId}`, + data + }) +} + +export function deleteGroup(groupId: string) { + return request.del({ + url: `${GROUP_BASE_URL}/${groupId}` + }) +} + +export function exportGroup( + groupId: string, + format: Api.Dictionary.DictionaryExportRequest['format'] +) { + return request.post({ + url: `${GROUP_BASE_URL}/${groupId}/export`, + data: { format }, + responseType: 'blob' + }) +} + +export function importGroup( + groupId: string, + payload: { + file: File + conflictMode?: Api.Dictionary.ConflictResolutionMode | string + format?: 'csv' | 'json' + } +) { + const formData = new FormData() + formData.append('File', payload.file) + if (payload.conflictMode) { + formData.append('ConflictMode', String(payload.conflictMode)) + } + if (payload.format) { + formData.append('Format', payload.format) + } + + return request.post({ + url: `${GROUP_BASE_URL}/${groupId}/import`, + data: formData + }) +} diff --git a/src/api/dictionary/item.ts b/src/api/dictionary/item.ts new file mode 100644 index 0000000..01df563 --- /dev/null +++ b/src/api/dictionary/item.ts @@ -0,0 +1,33 @@ +import request from '@/utils/http' + +const GROUP_BASE_URL = '/api/admin/v1/dictionary/groups' + +export function getItems(groupId: string) { + return request.get({ + url: `${GROUP_BASE_URL}/${groupId}/items` + }) +} + +export function createItem(groupId: string, data: Api.Dictionary.CreateDictionaryItemRequest) { + return request.post({ + url: `${GROUP_BASE_URL}/${groupId}/items`, + data + }) +} + +export function updateItem( + groupId: string, + itemId: string, + data: Api.Dictionary.UpdateDictionaryItemRequest +) { + return request.put({ + url: `${GROUP_BASE_URL}/${groupId}/items/${itemId}`, + data + }) +} + +export function deleteItem(groupId: string, itemId: string) { + return request.del({ + url: `${GROUP_BASE_URL}/${groupId}/items/${itemId}` + }) +} diff --git a/src/api/dictionary/labelOverride.ts b/src/api/dictionary/labelOverride.ts new file mode 100644 index 0000000..3013623 --- /dev/null +++ b/src/api/dictionary/labelOverride.ts @@ -0,0 +1,70 @@ +import request from '@/utils/http' + +const LABEL_OVERRIDE_BASE_URL = '/api/admin/v1/dictionary/label-overrides' + +// ==================== 租户端 API ==================== + +/** + * 获取当前租户的标签覆盖列表 + */ +export function getTenantLabelOverrides() { + return request.get({ + url: `${LABEL_OVERRIDE_BASE_URL}/tenant` + }) +} + +/** + * 租户创建/更新标签覆盖 + */ +export function upsertTenantLabelOverride(data: Api.Dictionary.UpsertLabelOverrideRequest) { + return request.post({ + url: `${LABEL_OVERRIDE_BASE_URL}/tenant`, + data + }) +} + +/** + * 租户删除标签覆盖 + */ +export function deleteTenantLabelOverride(dictionaryItemId: string) { + return request.del({ + url: `${LABEL_OVERRIDE_BASE_URL}/tenant/${dictionaryItemId}` + }) +} + +// ==================== 平台端 API ==================== + +/** + * 获取指定租户的所有标签覆盖(平台管理员用) + */ +export function getPlatformLabelOverrides( + targetTenantId: string, + overrideType?: Api.Dictionary.OverrideType +) { + return request.get({ + url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}`, + params: { overrideType } + }) +} + +/** + * 平台强制覆盖租户字典项的标签 + */ +export function upsertPlatformLabelOverride( + targetTenantId: string, + data: Api.Dictionary.UpsertLabelOverrideRequest +) { + return request.post({ + url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}`, + data + }) +} + +/** + * 平台删除对租户的强制覆盖 + */ +export function deletePlatformLabelOverride(targetTenantId: string, dictionaryItemId: string) { + return request.del({ + url: `${LABEL_OVERRIDE_BASE_URL}/platform/${targetTenantId}/${dictionaryItemId}` + }) +} diff --git a/src/api/dictionary/metrics.ts b/src/api/dictionary/metrics.ts new file mode 100644 index 0000000..85b01c8 --- /dev/null +++ b/src/api/dictionary/metrics.ts @@ -0,0 +1,35 @@ +import request from '@/utils/http' + +const METRICS_BASE_URL = '/api/admin/v1/dictionary/metrics' + +export function getCacheStats(timeRange: '1h' | '24h' | '7d' = '1h') { + return request.get<{ + totalHits: number + totalMisses: number + hitRatio: number + hitsByLevel: { l1: number; l2: number } + missesByLevel: { l1: number; l2: number } + averageQueryDurationMs: number + topQueriedDictionaries: Array<{ code: string; queryCount: number }> + }>({ + url: `${METRICS_BASE_URL}/cache-stats`, + params: { timeRange } + }) +} + +export function getInvalidationEvents(params: { + page: number + pageSize: number + startDate?: string + endDate?: string +}) { + return request.get>({ + url: `${METRICS_BASE_URL}/invalidation-events`, + params: { + page: params.page, + pageSize: params.pageSize, + startDate: params.startDate, + endDate: params.endDate + } + }) +} diff --git a/src/api/dictionary/override.ts b/src/api/dictionary/override.ts new file mode 100644 index 0000000..65464c8 --- /dev/null +++ b/src/api/dictionary/override.ts @@ -0,0 +1,49 @@ +import request from '@/utils/http' + +const OVERRIDE_BASE_URL = '/api/admin/v1/dictionary/overrides' + +export function getOverrides() { + return request.get({ + url: OVERRIDE_BASE_URL + }) +} + +export function getOverride(groupCode: string) { + return request.get({ + url: `${OVERRIDE_BASE_URL}/${groupCode}` + }) +} + +export function enableOverride(groupCode: string) { + return request.post({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/enable`, + data: {} + }) +} + +export function disableOverride(groupCode: string) { + return request.post({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/disable`, + data: {} + }) +} + +export function updateHiddenItems( + groupCode: string, + hiddenItemIds: Api.Dictionary.DictionaryOverrideHiddenItemsRequest['hiddenItemIds'] +) { + return request.put({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/hidden-items`, + data: { hiddenItemIds } + }) +} + +export function updateSortOrder( + groupCode: string, + sortOrder: Api.Dictionary.DictionaryOverrideSortOrderRequest['sortOrder'] +) { + return request.put({ + url: `${OVERRIDE_BASE_URL}/${groupCode}/sort-order`, + data: { sortOrder } + }) +} diff --git a/src/api/dictionary/query.ts b/src/api/dictionary/query.ts new file mode 100644 index 0000000..01b7bff --- /dev/null +++ b/src/api/dictionary/query.ts @@ -0,0 +1,27 @@ +import request from '@/utils/http' + +const QUERY_BASE_URL = '/api/admin/v1/dictionaries' + +const normalizeCode = (code: string) => code.trim().toLowerCase() + +export async function getDictionary(code: string) { + const result = await batchGetDictionaries([code]) + return result[code] ?? [] +} + +export async function batchGetDictionaries(codes: string[]) { + if (!codes.length) return {} + + const result = await request.post>({ + url: `${QUERY_BASE_URL}/batch`, + data: { codes } + }) + + const mapped: Record = {} + codes.forEach((code) => { + const key = normalizeCode(code) + mapped[code] = result[key] ?? result[code] ?? [] + }) + + return mapped +} diff --git a/src/api/files.ts b/src/api/files.ts new file mode 100644 index 0000000..615c36f --- /dev/null +++ b/src/api/files.ts @@ -0,0 +1,22 @@ +import request from '@/utils/http' + +/** + * 文件上传 API + */ + +/** + * 上传图片或文件(Admin) + * @param file 上传的文件 + * @param type 上传类型(可选) + */ +export function fetchUploadFile(file: File, type: Api.Files.UploadFileType = 'other') { + const formData = new FormData() + formData.append('File', file) + formData.append('Type', type) + + return request.post({ + url: '/api/admin/v1/files/upload', + // 重要:不要手动设置 Content-Type,让浏览器自动带 boundary + data: formData + }) +} diff --git a/src/api/merchant.ts b/src/api/merchant.ts new file mode 100644 index 0000000..b4bf623 --- /dev/null +++ b/src/api/merchant.ts @@ -0,0 +1,126 @@ +import api from '@/utils/http' + +/** + * 获取商户列表 + */ +export function fetchMerchantList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/merchants', + params + }) +} + +/** + * 获取商户详情 + */ +export function fetchMerchantDetail(merchantId: string, options?: { showErrorMessage?: boolean }) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 更新商户信息 + */ +export function fetchUpdateMerchant( + merchantId: string, + data: Api.Merchant.UpdateMerchantRequest, + options?: { showErrorMessage?: boolean } +) { + return api.put({ + url: `/api/admin/v1/merchants/${merchantId}`, + data, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取商户审核历史 + */ +export function fetchMerchantAuditHistory(merchantId: string) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/audit-history` + }) +} + +/** + * 获取商户变更历史 + */ +export function fetchMerchantChangeLogs(merchantId: string, params?: { fieldName?: string }) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/change-history`, + params + }) +} + +/** + * 获取待审核商户列表 + */ +export function fetchPendingReviewList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/merchants/pending-review', + params + }) +} + +/** + * 获取审核领取信息 + */ +export function fetchMerchantReviewClaim(merchantId: string) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/review/claim` + }) +} + +/** + * 领取审核 + */ +export function fetchClaimMerchantReview(merchantId: string) { + return api.post({ + url: `/api/admin/v1/merchants/${merchantId}/review/claim`, + data: {} + }) +} + +/** + * 释放审核 + */ +export function fetchReleaseMerchantReview(merchantId: string) { + return api.del({ + url: `/api/admin/v1/merchants/${merchantId}/review/claim` + }) +} + +/** + * 执行审核 + */ +export function fetchReviewMerchant(merchantId: string, data: Api.Merchant.ReviewMerchantRequest) { + return api.post({ + url: `/api/admin/v1/merchants/${merchantId}/review`, + data + }) +} + +/** + * 撤销审核 + */ +export function fetchRevokeMerchantReview( + merchantId: string, + data: Api.Merchant.RevokeMerchantRequest +) { + return api.post({ + url: `/api/admin/v1/merchants/${merchantId}/review/revoke`, + data + }) +} + +/** + * 导出商户 PDF + */ +export function fetchExportMerchantPdf(merchantId: string) { + return api.get({ + url: `/api/admin/v1/merchants/${merchantId}/export-pdf`, + responseType: 'blob' + }) +} diff --git a/src/api/permission.ts b/src/api/permission.ts new file mode 100644 index 0000000..bad1571 --- /dev/null +++ b/src/api/permission.ts @@ -0,0 +1,21 @@ +import request from '@/utils/http' + +/** + * 获取权限列表(分页) + * @param params 查询参数 + */ +export function fetchGetPermissions(params?: Api.Permission.PermissionQueryParams) { + return request.get({ + url: '/api/admin/v1/permissions', + params + }) +} + +/** + * 获取权限树 + */ +export function fetchGetPermissionTree() { + return request.get({ + url: '/api/admin/v1/permissions/tree' + }) +} diff --git a/src/api/quotaPackage.ts b/src/api/quotaPackage.ts new file mode 100644 index 0000000..43212dc --- /dev/null +++ b/src/api/quotaPackage.ts @@ -0,0 +1,100 @@ +import request from '@/utils/http' + +/** + * 配额包管理 API + */ + +/** + * 获取配额包列表 + */ +export function fetchQuotaPackageList(params?: { + quotaType?: number + isActive?: boolean + page?: number + pageSize?: number +}) { + return request.get>({ + url: '/api/admin/v1/quota-packages', + params + }) +} + +/** + * 创建配额包 + */ +export function fetchCreateQuotaPackage(data: Api.QuotaPackage.CreateQuotaPackageCommand) { + return request.post({ + url: '/api/admin/v1/quota-packages', + data + }) +} + +/** + * 更新配额包 + */ +export function fetchUpdateQuotaPackage( + quotaPackageId: string, + data: Api.QuotaPackage.UpdateQuotaPackageCommand +) { + return request.put({ + url: `/api/admin/v1/quota-packages/${quotaPackageId}`, + data + }) +} + +/** + * 删除配额包(软删) + */ +export function fetchDeleteQuotaPackage(quotaPackageId: string) { + return request.del({ + url: `/api/admin/v1/quota-packages/${quotaPackageId}` + }) +} + +/** + * 更新配额包状态(上架/下架) + */ +export function fetchUpdateQuotaPackageStatus(quotaPackageId: string, isActive: boolean) { + return request.put({ + url: `/api/admin/v1/quota-packages/${quotaPackageId}/status`, + data: { + isActive + } + }) +} + +/** + * 为租户购买配额包 + */ +export function fetchPurchaseQuotaPackage( + tenantId: string, + data: Api.QuotaPackage.PurchaseQuotaPackageCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/quota-packages`, + data + }) +} + +/** + * 获取租户配额使用情况 + */ +export function fetchTenantQuotaUsage(tenantId: string, params?: { quotaType?: number }) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/quota-usage`, + params + }) +} + +/** + * 获取租户配额购买记录 + */ +export function fetchTenantQuotaPurchases( + tenantId: string, + params?: { page?: number; pageSize?: number } +) { + return request.get>({ + url: `/api/admin/v1/tenants/${tenantId}/quota-purchases`, + params + }) +} diff --git a/src/api/role-template.ts b/src/api/role-template.ts new file mode 100644 index 0000000..596793b --- /dev/null +++ b/src/api/role-template.ts @@ -0,0 +1,94 @@ +import request from '@/utils/http' + +/** + * 获取角色模板列表 + * @param params 查询参数 + */ +export function fetchGetRoleTemplates(params?: any) { + return request.get({ + url: '/api/admin/v1/role-templates', + params + }) +} + +/** + * 获取角色模板详情 + * @param templateCode 模板编码 + */ +export function fetchGetRoleTemplateDetail(templateCode: string) { + return request.get({ + url: `/api/admin/v1/role-templates/${templateCode}` + }) +} + +/** + * 创建角色模板 + * @param data 创建参数 + */ +export function fetchCreateRoleTemplate(data: Api.RoleTemplate.CreateRoleTemplateCommand) { + return request.post({ + url: '/api/admin/v1/role-templates', + data + }) +} + +/** + * 更新角色模板 + * @param templateCode 模板编码 + * @param data 更新参数 + */ +export function fetchUpdateRoleTemplate( + templateCode: string, + data: Api.RoleTemplate.UpdateRoleTemplateCommand +) { + return request.put({ + url: `/api/admin/v1/role-templates/${templateCode}`, + data + }) +} + +/** + * 删除角色模板 + * @param templateCode 模板编码 + */ +export function fetchDeleteRoleTemplate(templateCode: string) { + return request.del({ + url: `/api/admin/v1/role-templates/${templateCode}` + }) +} + +/** + * 克隆角色模板 + * @param templateCode 源模板编码 + * @param data 克隆参数 + */ +export function fetchCloneRoleTemplate( + templateCode: string, + data: Api.RoleTemplate.CloneRoleTemplateCommand +) { + return request.post({ + url: `/api/admin/v1/role-templates/${templateCode}/clone`, + data + }) +} + +/** + * 初始化角色模板 + * @param templateCodes 模板编码列表 + */ +export function fetchInitRoleTemplates(templateCodes: string[]) { + return request.post({ + url: '/api/admin/v1/role-templates/init', + data: { templateCodes } + }) +} + +/** + * 获取角色模板权限列表 + * @param templateCode 模板编码 + */ +export function fetchGetRoleTemplatePermissions(templateCode: string) { + return request.get({ + url: `/api/admin/v1/role-templates/${templateCode}/permissions` + }) +} diff --git a/src/api/statistics.ts b/src/api/statistics.ts new file mode 100644 index 0000000..d8c7d59 --- /dev/null +++ b/src/api/statistics.ts @@ -0,0 +1,47 @@ +import request from '@/utils/http' + +/** + * 统计分析 API + */ + +/** + * 获取订阅概览统计 + */ +export function fetchSubscriptionOverview() { + return request.get({ + url: '/api/admin/v1/statistics/subscriptions/overview' + }) +} + +/** + * 获取配额使用排行 + * @param params 查询参数 + */ +export function fetchQuotaUsageRanking(params?: Api.Statistics.QuotaUsageRankingParams) { + return request.get({ + url: '/api/admin/v1/statistics/quota/ranking', + params + }) +} + +/** + * 获取收入统计 + * @param params 查询参数 + */ +export function fetchRevenueStatistics(params?: Api.Statistics.RevenueStatisticsParams) { + return request.get({ + url: '/api/admin/v1/statistics/revenue', + params + }) +} + +/** + * 获取即将到期订阅列表 + * @param params 查询参数 + */ +export function fetchExpiringSubscriptions(params?: Api.Statistics.ExpiringSubscriptionsParams) { + return request.get({ + url: '/api/admin/v1/statistics/subscriptions/expiring', + params + }) +} diff --git a/src/api/store.ts b/src/api/store.ts new file mode 100644 index 0000000..2455f1f --- /dev/null +++ b/src/api/store.ts @@ -0,0 +1,309 @@ +import api from '@/utils/http' + +interface StoreRequestOptions { + tenantId?: string +} + +const buildTenantHeader = (options?: StoreRequestOptions) => { + // 1. 未提供租户ID时返回空配置 + if (!options?.tenantId) { + return {} + } + + // 2. 返回携带租户头的请求配置 + return { + headers: { + 'X-Tenant-Id': String(options.tenantId) + } + } +} + +/** + * 获取门店列表 + */ +export function fetchStoreList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/stores', + params + }) +} + +/** + * 获取门店详情 + */ +export function fetchStoreDetail(storeId: string, options?: { showErrorMessage?: boolean }) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 创建门店 + */ +export function fetchCreateStore(data: Api.Store.CreateStoreRequest) { + return api.post({ + url: '/api/admin/v1/stores', + data + }) +} + +/** + * 更新门店 + */ +export function fetchUpdateStore(storeId: string, data: Api.Store.UpdateStoreRequest) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}`, + data + }) +} + +/** + * 删除门店 + */ +export function fetchDeleteStore(storeId: string) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}` + }) +} + +/** + * 提交门店审核 + */ +export function fetchSubmitStoreAudit(storeId: string) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/submit`, + data: {} + }) +} + +/** + * 切换门店经营状态 + */ +export function fetchToggleBusinessStatus( + storeId: string, + data: Api.Store.ToggleBusinessStatusRequest +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/business-status`, + data + }) +} + +/** + * 获取门店资质列表 + */ +export function fetchStoreQualifications(storeId: string, options?: StoreRequestOptions) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/qualifications`, + ...buildTenantHeader(options) + }) +} + +/** + * 检查门店资质完整性 + */ +export function fetchCheckStoreQualifications(storeId: string, options?: StoreRequestOptions) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/qualifications/check`, + ...buildTenantHeader(options) + }) +} + +/** + * 创建门店资质 + */ +export function fetchCreateStoreQualification( + storeId: string, + data: Api.Store.CreateStoreQualificationRequest, + options?: StoreRequestOptions +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/qualifications`, + data, + ...buildTenantHeader(options) + }) +} + +/** + * 更新门店资质 + */ +export function fetchUpdateStoreQualification( + storeId: string, + qualificationId: string, + data: Api.Store.UpdateStoreQualificationRequest, + options?: StoreRequestOptions +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/qualifications/${qualificationId}`, + data, + ...buildTenantHeader(options) + }) +} + +/** + * 删除门店资质 + */ +export function fetchDeleteStoreQualification( + storeId: string, + qualificationId: string, + options?: StoreRequestOptions +) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}/qualifications/${qualificationId}`, + ...buildTenantHeader(options) + }) +} + +/** + * 获取门店营业时段 + */ +export function fetchStoreBusinessHours(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/business-hours` + }) +} + +/** + * 批量更新门店营业时段 + */ +export function fetchBatchUpdateBusinessHours( + storeId: string, + data: Api.Store.BatchUpdateBusinessHoursRequest +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/business-hours/batch`, + data + }) +} + +/** + * 获取配送区域 + */ +export function fetchStoreDeliveryZones(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones` + }) +} + +/** + * 创建配送区域 + */ +export function fetchCreateStoreDeliveryZone( + storeId: string, + data: Api.Store.CreateStoreDeliveryZoneRequest +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones`, + data + }) +} + +/** + * 更新配送区域 + */ +export function fetchUpdateStoreDeliveryZone( + storeId: string, + deliveryZoneId: string, + data: Api.Store.UpdateStoreDeliveryZoneRequest +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones/${deliveryZoneId}`, + data + }) +} + +/** + * 删除配送区域 + */ +export function fetchDeleteStoreDeliveryZone(storeId: string, deliveryZoneId: string) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}/delivery-zones/${deliveryZoneId}` + }) +} + +/** + * 配送范围检测 + */ +export function fetchDeliveryZoneCheck(storeId: string, data: Api.Store.StoreDeliveryCheckRequest) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/delivery-check`, + data + }) +} + +/** + * 获取门店费用配置 + */ +export function fetchStoreFee(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/fee` + }) +} + +/** + * 更新门店费用配置 + */ +export function fetchUpdateStoreFee(storeId: string, data: Api.Store.UpdateStoreFeeRequest) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/fee`, + data + }) +} + +/** + * 门店费用预览 + */ +export function fetchCalculateStoreFee(storeId: string, data: Api.Store.CalculateStoreFeeRequest) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/fee/calculate`, + data + }) +} + +// ==================== 临时时段 API ==================== + +/** + * 获取门店临时时段列表 + */ +export function fetchStoreHolidays(storeId: string) { + return api.get({ + url: `/api/admin/v1/stores/${storeId}/holidays` + }) +} + +/** + * 创建临时时段 + */ +export function fetchCreateStoreHoliday( + storeId: string, + data: Api.Store.CreateStoreHolidayRequest +) { + return api.post({ + url: `/api/admin/v1/stores/${storeId}/holidays`, + data + }) +} + +/** + * 更新临时时段 + */ +export function fetchUpdateStoreHoliday( + storeId: string, + holidayId: string, + data: Api.Store.UpdateStoreHolidayRequest +) { + return api.put({ + url: `/api/admin/v1/stores/${storeId}/holidays/${holidayId}`, + data + }) +} + +/** + * 删除临时时段 + */ +export function fetchDeleteStoreHoliday(storeId: string, holidayId: string) { + return api.del({ + url: `/api/admin/v1/stores/${storeId}/holidays/${holidayId}` + }) +} diff --git a/src/api/storeAudit.ts b/src/api/storeAudit.ts new file mode 100644 index 0000000..626462c --- /dev/null +++ b/src/api/storeAudit.ts @@ -0,0 +1,103 @@ +import api from '@/utils/http' + +/** + * 获取待审核门店列表 + */ +export function fetchPendingStoreAudits( + params?: Partial +) { + return api.get({ + url: '/api/admin/v1/platform/store-audits/pending', + params + }) +} + +/** + * 获取审核统计 + */ +export function fetchStoreAuditStatistics(params?: Api.StoreAudit.StoreAuditStatisticsParams) { + return api.get({ + url: '/api/admin/v1/platform/store-audits/statistics', + params + }) +} + +/** + * 获取门店审核详情 + */ +export function fetchStoreAuditDetail(storeId: string) { + return api.get({ + url: `/api/admin/v1/platform/store-audits/${storeId}` + }) +} + +/** + * 获取审核记录 + */ +export function fetchStoreAuditRecords( + storeId: string, + params?: Api.StoreAudit.ListStoreAuditRecordsParams +) { + return api.get>({ + url: `/api/admin/v1/platform/store-audits/${storeId}/records`, + params + }) +} + +/** + * 审核通过 + */ +export function fetchApproveStoreAudit( + storeId: string, + data: Api.StoreAudit.ApproveStoreAuditRequest +) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/approve`, + data + }) +} + +/** + * 审核驳回 + */ +export function fetchRejectStoreAudit( + storeId: string, + data: Api.StoreAudit.RejectStoreAuditRequest +) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/reject`, + data + }) +} + +/** + * 强制关闭门店 + */ +export function fetchForceCloseStore(storeId: string, data: Api.StoreAudit.ForceCloseStoreRequest) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/force-close`, + data + }) +} + +/** + * 解除强制关闭 + */ +export function fetchReopenStore(storeId: string, data: Api.StoreAudit.ReopenStoreRequest) { + return api.post({ + url: `/api/admin/v1/platform/store-audits/${storeId}/reopen`, + data + }) +} + +/** + * 获取资质预警列表(平台) + */ +export function fetchQualificationAlerts( + params?: Partial +) { + return api.get({ + url: '/api/admin/v1/platform/store-qualifications/expiring', + params + }) +} diff --git a/src/api/subscription.ts b/src/api/subscription.ts new file mode 100644 index 0000000..ad9e914 --- /dev/null +++ b/src/api/subscription.ts @@ -0,0 +1,134 @@ +import request from '@/utils/http' + +/** + * 订阅管理 API + */ + +/** + * 获取订阅列表 + * @param params 查询参数 + */ +export function getSubscriptionList(params?: Api.Subscription.SubscriptionListParams) { + return request.get({ + url: '/api/admin/v1/subscriptions', + params + }) +} + +/** + * 获取订阅详情 + * @param id 订阅ID + */ +export function getSubscriptionDetail(id: string) { + return request.get({ + url: `/api/admin/v1/subscriptions/${id}` + }) +} + +/** + * 更新订阅 + * @param id 订阅ID + * @param data 更新数据 + */ +export function updateSubscription( + id: string, + data: Omit +) { + return request.put({ + url: `/api/admin/v1/subscriptions/${id}`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 延期订阅 + * @param id 订阅ID + * @param data 延期数据 + */ +export function extendSubscription( + id: string, + data: Omit +) { + return request.post({ + url: `/api/admin/v1/subscriptions/${id}/extend`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 变更套餐 + * @param id 订阅ID + * @param data 变更数据 + */ +export function changeSubscriptionPlan( + id: string, + data: Omit +) { + return request.post({ + url: `/api/admin/v1/subscriptions/${id}/change-plan`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 变更订阅状态 + * @param id 订阅ID + * @param data 状态数据 + */ +export function updateSubscriptionStatus( + id: string, + data: Omit +) { + return request.post({ + url: `/api/admin/v1/subscriptions/${id}/status`, + data: { + ...data, + subscriptionId: id + } + }) +} + +/** + * 批量延期订阅 + * @param data 批量延期数据 + */ +export function batchExtendSubscriptions(data: { + subscriptionIds: string[] + durationDays: number + notes?: string +}) { + return request.post<{ + successCount: number + failureCount: number + failures: Array<{ subscriptionId: string; reason: string }> + }>({ + url: '/api/admin/v1/subscriptions/batch-extend', + data, + showSuccessMessage: true + }) +} + +/** + * 批量发送提醒 + * @param data 批量提醒数据 + */ +export function batchSendReminders(data: { subscriptionIds: string[]; reminderContent: string }) { + return request.post<{ + successCount: number + failureCount: number + failures: Array<{ subscriptionId: string; reason: string }> + }>({ + url: '/api/admin/v1/subscriptions/batch-remind', + data, + showSuccessMessage: true + }) +} diff --git a/src/api/system-manage.ts b/src/api/system-manage.ts new file mode 100644 index 0000000..3bd83c5 --- /dev/null +++ b/src/api/system-manage.ts @@ -0,0 +1,86 @@ +import api from '@/utils/http' +import type { AppRouteRecord } from '@/types/router' + +// 1. 获取用户列表 +export function fetchGetUserList(params: Api.SystemManage.UserSearchParams) { + return api.get({ + url: '/api/admin/v1/users', + params + }) +} + +// 2. 获取用户详情 +export function fetchGetUserDetail(userId: string, includeDeleted?: boolean) { + return api.get({ + url: `/api/admin/v1/users/${userId}`, + params: includeDeleted ? { includeDeleted } : undefined + }) +} + +// 3. 创建用户 +export function fetchCreateUser(command: Api.SystemManage.CreateIdentityUserCommand) { + return api.post({ + url: '/api/admin/v1/users', + data: command + }) +} + +// 4. 更新用户 +export function fetchUpdateUser( + userId: string, + command: Api.SystemManage.UpdateIdentityUserCommand +) { + return api.put({ + url: `/api/admin/v1/users/${userId}`, + data: command + }) +} + +// 5. 删除用户 +export function fetchDeleteUser(userId: string) { + return api.del({ + url: `/api/admin/v1/users/${userId}` + }) +} + +// 6. 恢复用户 +export function fetchRestoreUser(userId: string) { + return api.post({ + url: `/api/admin/v1/users/${userId}/restore` + }) +} + +// 7. 更新用户状态 +export function fetchChangeUserStatus( + userId: string, + command: Api.SystemManage.ChangeIdentityUserStatusCommand +) { + return api.put({ + url: `/api/admin/v1/users/${userId}/status`, + data: command + }) +} + +// 8. 重置用户密码 +export function fetchResetUserPassword(userId: string) { + return api.post({ + url: `/api/admin/v1/users/${userId}/password-reset` + }) +} + +// 9. 批量用户操作 +export function fetchBatchUserOperation( + command: Api.SystemManage.BatchIdentityUserOperationCommand +) { + return api.post({ + url: '/api/admin/v1/users/batch', + data: command + }) +} + +// 10. 获取菜单列表 +export function fetchGetMenuList() { + return api.get({ + url: '/api/v3/system/menus/simple' + }) +} diff --git a/src/api/tenant-onboarding.ts b/src/api/tenant-onboarding.ts new file mode 100644 index 0000000..9ab68f7 --- /dev/null +++ b/src/api/tenant-onboarding.ts @@ -0,0 +1,99 @@ +import request from '@/utils/http' + +/** + * 自助注册租户 + * @param data 自助注册参数 + */ +export function fetchSelfRegisterTenant(data: Api.Tenant.SelfRegisterTenantCommand) { + return request.post({ + url: '/api/public/v1/tenants/self-register', + data + }) +} + +/** + * 查询租户入驻进度 + * @param tenantId 租户ID + */ +export function fetchTenantProgress(tenantId: string) { + return request.get({ + url: `/api/public/v1/tenants/${tenantId}/status` + }) +} + +/** + * 提交或更新租户实名信息 + * @param tenantId 租户ID + * @param data 实名资料 + */ +export function submitTenantVerification( + tenantId: string, + data: Api.Tenant.SubmitTenantVerificationCommand +) { + return request.post({ + url: `/api/public/v1/tenants/${tenantId}/verification`, + data: { ...data, tenantId } + }) +} + +/** + * 提交或更新租户实名信息(管理端) + * @param tenantId 租户ID + * @param data 实名资料 + */ +export function submitTenantVerificationAdmin( + tenantId: string, + data: Api.Tenant.SubmitTenantVerificationCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/verification`, + data: { ...data, tenantId } + }) +} + +/** + * 创建租户订阅 + * @param tenantId 租户ID + * @param data 订阅参数 + */ +export function createTenantSubscription( + tenantId: string, + data: Api.Tenant.CreateTenantSubscriptionCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions`, + data: { ...data, tenantId } + }) +} + +/** + * 初次绑定租户订阅(自助入驻) + * @param tenantId 租户ID + * @param data 初次绑定参数 + */ +export function bindInitialTenantSubscription( + tenantId: string, + data: Api.Tenant.BindInitialTenantSubscriptionCommand +) { + return request.post({ + url: `/api/public/v1/tenants/${tenantId}/subscriptions/initial`, + data: { ...data, tenantId } + }) +} + +/** + * 升降配租户套餐 + * @param tenantId 租户ID + * @param subscriptionId 订阅ID + * @param data 升降配参数 + */ +export function changeTenantSubscriptionPlan( + tenantId: string, + subscriptionId: string, + data: Api.Tenant.ChangeTenantSubscriptionPlanCommand +) { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions/${subscriptionId}/plan`, + data: { ...data, tenantId, tenantSubscriptionId: subscriptionId } + }) +} diff --git a/src/api/tenant-package.ts b/src/api/tenant-package.ts new file mode 100644 index 0000000..0d2600b --- /dev/null +++ b/src/api/tenant-package.ts @@ -0,0 +1,103 @@ +import request from '@/utils/http' + +/** + * 租户套餐管理 API + */ + +/** + * 获取租户套餐分页列表 + */ +export function fetchTenantPackageList(params?: Api.Tenant.TenantPackageQueryParams) { + return request.get({ + url: '/api/admin/v1/tenant-packages', + params + }) +} + +/** + * 公共租户套餐列表(匿名可访问) + */ +export function fetchPublicTenantPackageList(params?: Api.Tenant.TenantPackageQueryParams) { + return request.get({ + url: '/api/public/v1/tenant-packages', + params + }) +} + +/** + * 获取租户套餐详情 + */ +export function fetchTenantPackageDetail(tenantPackageId: string) { + return request.get({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}` + }) +} + +/** + * 创建租户套餐 + */ +export function fetchCreateTenantPackage(data: Api.Tenant.CreateTenantPackageCommand) { + return request.post({ + url: '/api/admin/v1/tenant-packages', + data + }) +} + +/** + * 更新租户套餐 + */ +export function fetchUpdateTenantPackage( + tenantPackageId: string, + data: Api.Tenant.UpdateTenantPackageCommand +) { + return request.put({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}`, + data: { + ...data, + tenantPackageId + } + }) +} + +/** + * 删除租户套餐(软删) + */ +export function fetchDeleteTenantPackage(tenantPackageId: string) { + return request.del({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}` + }) +} + +/** + * 查询套餐使用统计(订阅关联数量、使用租户数量) + */ +export function fetchTenantPackageUsages(tenantPackageIds?: string[]) { + const params = new URLSearchParams() + if (tenantPackageIds?.length) { + tenantPackageIds.forEach((id) => params.append('tenantPackageIds', id)) + } + + return request.get({ + url: '/api/admin/v1/tenant-packages/usages', + params, + showErrorMessage: false + }) +} + +/** + * 查询套餐当前使用租户列表(按有效订阅口径) + */ +export function fetchTenantPackageTenants( + tenantPackageId: string, + params: { keyword?: string; page?: number; pageSize?: number; expiringWithinDays?: number } = {} +) { + return request.get>({ + url: `/api/admin/v1/tenant-packages/${tenantPackageId}/tenants`, + params: { + keyword: params.keyword || undefined, + expiringWithinDays: params.expiringWithinDays ?? undefined, + page: params.page ?? 1, + pageSize: params.pageSize ?? 20 + } + }) +} diff --git a/src/api/tenant-role.ts b/src/api/tenant-role.ts new file mode 100644 index 0000000..d5ec2fe --- /dev/null +++ b/src/api/tenant-role.ts @@ -0,0 +1,98 @@ +import request from '@/utils/http' + +/** + * 获取租户角色列表 + * @param tenantId 租户ID + * @param params 查询参数 + */ +export function fetchGetTenantRoles( + tenantId: number | string, + params?: Api.TenantRole.RoleQueryParams +) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/roles`, + params + }) +} + +/** + * 获取租户角色详情 + * @param tenantId 租户ID + * @param roleId 角色ID + */ +export function fetchGetTenantRoleDetail(tenantId: number | string, roleId: number | string) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}` + }) +} + +/** + * 创建租户角色 + * @param tenantId 租户ID + * @param data 创建参数 + */ +export function fetchCreateTenantRole( + tenantId: number | string, + data: Api.TenantRole.CreateRoleCommand +) { + return request.post({ + url: `/api/admin/v1/tenants/${tenantId}/roles`, + data + }) +} + +/** + * 更新租户角色 + * @param tenantId 租户ID + * @param roleId 角色ID + * @param data 更新参数 + */ +export function fetchUpdateTenantRole( + tenantId: number | string, + roleId: number | string, + data: Api.TenantRole.UpdateRoleCommand +) { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}`, + data + }) +} + +/** + * 删除租户角色 + * @param tenantId 租户ID + * @param roleId 角色ID + */ +export function fetchDeleteTenantRole(tenantId: number | string, roleId: number | string) { + return request.del({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}` + }) +} + +/** + * 获取租户角色权限 + * @param tenantId 租户ID + * @param roleId 角色ID + */ +export function fetchGetTenantRolePermissions(tenantId: number | string, roleId: number | string) { + return request.get({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}/permissions` + }) +} + +/** + * 更新租户角色权限 + * @param tenantId 租户ID + * @param roleId 角色ID + * @param permissions 权限代码列表 + */ +export function fetchUpdateTenantRolePermissions( + tenantId: number | string, + roleId: number | string, + permissionIds: string[] +) { + return request.put({ + url: `/api/admin/v1/tenants/${tenantId}/roles/${roleId}/permissions`, + data: { permissionIds } + }) +} diff --git a/src/api/tenant.ts b/src/api/tenant.ts new file mode 100644 index 0000000..06a5742 --- /dev/null +++ b/src/api/tenant.ts @@ -0,0 +1,263 @@ +import api from '@/utils/http' +import { fetchTenantPackageList } from './tenant-package' +import type { QuotaUsageHistoryDto, UpdateTenantCommand } from '@/types/tenant' + +/** + * 获取租户列表 + * @param params 搜索参数 + */ +export function fetchGetTenantList(params?: Partial) { + return api.get({ + url: '/api/admin/v1/tenants', + params + }) +} + +/** + * 注册租户 + */ +export function fetchRegisterTenant(data: Api.Tenant.RegisterTenantCommand) { + return api.post({ + url: '/api/admin/v1/tenants', + data + }) +} + +/** + * 后台手动新增租户并直接入驻(创建租户 + 认证 + 订阅 + 管理员账号) + */ +export function fetchCreateTenantManually(data: Api.Tenant.CreateTenantManuallyCommand) { + return api.post({ + url: '/api/admin/v1/tenants/manual', + data + }) +} + +/** + * 获取租户详情(包含认证信息、订阅信息) + */ +export function fetchGetTenantDetail(tenantId: string, options?: { showErrorMessage?: boolean }) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取租户配额使用情况 + */ +export function fetchGetTenantQuotaUsage( + tenantId: string, + options?: { showErrorMessage?: boolean } +) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}/quota-usage`, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取租户配额使用历史 + * @param tenantId 租户ID + * @param params 分页参数 + */ +export function fetchGetTenantQuotaUsageHistory( + tenantId: string, + params?: { Page?: number; PageSize?: number } +) { + return api.get>({ + url: `/api/admin/v1/tenants/${tenantId}/quota-usage-history`, + params + }) +} + +/** + * 获取订阅信息(注意:该接口为 POST) + */ +export function fetchGetTenantSubscriptions( + tenantId: string, + options?: { showErrorMessage?: boolean } +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions`, + data: {}, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 获取账单信息(分页,最近10条可传 Page=1 PageSize=10) + */ +export function fetchGetTenantBillings( + tenantId: string, + params: { Page?: number; PageSize?: number } = {}, + options?: { showErrorMessage?: boolean } +) { + return api.get>({ + url: `/api/admin/v1/tenants/${tenantId}/billings`, + params, + showErrorMessage: options?.showErrorMessage + }) +} + +/** + * 更新租户信息(TD-001:待后端接口补充) + * @param tenantId 租户ID(Snowflake long → string) + * @param data 更新数据(tenantId 会以入参为准覆盖) + * @returns void(后端 BaseResponse 的 data 字段) + * + * 后端接口:PUT /api/admin/v1/tenants/{tenantId} + * 预期响应:BaseResponse + * 预期错误码:400(参数错误)、404(租户不存在)、409(code 冲突) + * TODO(TD-001): Swagger 中缺失该端点(当前可能返回 404/405),待后端补充后再联调验证 + */ +export function fetchUpdateTenant(tenantId: string, data: UpdateTenantCommand) { + return api.put({ + url: `/api/admin/v1/tenants/${tenantId}`, + data: { ...data, tenantId }, + showErrorMessage: false + }) +} + +/** + * 获取租户套餐列表(兼容旧调用) + */ +export const fetchGetTenantPackages = fetchTenantPackageList + +/** + * 审核租户 + */ +export function fetchReviewTenant( + tenantId: string, + data: Omit +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review`, + data: { ...data, tenantId } + }) +} + +/** + * 获取租户审核领取信息(未领取返回 null) + */ +export function fetchGetTenantReviewClaim(tenantId: string) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}/review/claim` + }) +} + +/** + * 领取租户审核 + */ +export function fetchClaimTenantReview(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review/claim` + }) +} + +/** + * 强制接管租户审核(仅超级管理员) + */ +export function fetchForceClaimTenantReview(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review/force-claim` + }) +} + +/** + * 释放租户审核领取(仅领取人) + */ +export function fetchReleaseTenantReviewClaim(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/review/release` + }) +} + +/** + * 查询租户审核日志 + */ +export function fetchGetTenantAuditLogs( + tenantId: string, + params: { page?: number; pageSize?: number } = {} +) { + return api.get({ + url: `/api/admin/v1/tenants/${tenantId}/audits`, + params: { + page: params.page ?? 1, + pageSize: params.pageSize ?? 20 + } + }) +} + +/** + * 冻结租户(暂停服务) + */ +export function fetchFreezeTenant( + tenantId: string, + data: Omit +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/freeze`, + data: { ...data, tenantId } + }) +} + +/** + * 解冻租户(恢复服务) + */ +export function fetchUnfreezeTenant( + tenantId: string, + data: Omit = {} +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/unfreeze`, + data: { ...data, tenantId } + }) +} + +/** + * 延期/赠送租户订阅时长(按当前订阅套餐续费) + */ +export function fetchExtendTenantSubscription( + tenantId: string, + data: Omit +) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/subscriptions/extend`, + data: { ...data, tenantId } + }) +} + +/** + * 伪装登录租户(返回租户主管理员的 Token) + */ +export function fetchImpersonateTenant(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/impersonate` + }) +} + +/** + * 生成租户主管理员重置密码链接(仅展示一次) + */ +export function fetchCreateTenantAdminResetLink(tenantId: string) { + return api.post({ + url: `/api/admin/v1/tenants/${tenantId}/admin/reset-link` + }) +} + +/** + * 获取租户配额购买记录(分页) + */ +export function fetchGetTenantQuotaPurchases( + tenantId: string, + params: { Page?: number; PageSize?: number } = {} +) { + return api.get>({ + url: `/api/admin/v1/tenants/${tenantId}/quota-purchases`, + params: { + Page: params.Page ?? 1, + PageSize: params.PageSize ?? 10 + } + }) +} diff --git a/src/assets/images/avatar/avatar.webp b/src/assets/images/avatar/avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..bea307b7d2e5e1a9d952d834eb57f73605dc55b8 GIT binary patch literal 954 zcmV;r14aB&Nk&Gp0{{S5MM6+kP&go_0{{Rp6abw8Do_AW06w)$ok}MpBB89-ZP?%z ziD>{53Yw2F8^s*TeR(t1;sau?)6x+IJ1ULup4dPqlKRaw%L00&PyRBV@TRirkLMoh znecI$mizfs;B~7~IjdMHs2{A`^5xH2WwlFsv{w~GUrD-c6mAR;p8@&L1l{|oRAzySXHiNX^g1uHD- zk>h_3$=>CvsNexHz6NhKDop!@&$-)@k^G zpxC;k(_I+TKnb=kfcV<3(d@OeA$m8)Ux;2hwn++&N9ggUrswPzKFBA>H1|z9H9&|78oYJ6 zqmYWjF_7HcNYd8We?h}xgx?|h{o>j~Y6Ort0-Tr&f;blhrY6~brlb8`aT?w+$`-AaIO=h`ieN z$~Gg-mXxYMelEHs`4HK%%m9=g4(z;v{W(jv8Yy0+*!MmE7Wb&l>IMYrKWC`)P-M2m zu$RqPHPng}*nD$a!U%{{NAQRJtAxj3Skt$rNr5Sz(*ys48Sm7CQ28#6@u$SQp8jq1 z)0|6ELT|Zgn}T|MFJ^9l$ZpXeQ$e+^QI50?#DWtekW>TGc)Xar_&#%;r9pb{&G`?s zaP-tYMb^D)AT=a2z3RNn!)}O2cKg(?2+hD+pV;B^mgx83f_)BBT#K1H) cmY(20IDjVb)DMN`D2T(p^ZM$jri0Q@7_2mk;8 literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar/avatar1.webp b/src/assets/images/avatar/avatar1.webp new file mode 100644 index 0000000000000000000000000000000000000000..68e256c297cea6017efb892509f005290f32c95f GIT binary patch literal 2296 zcmVLHNCsINV&i=yrkQTpUuSn>Acb z2FE|`v?5W%`1-J1k%&b^{1D+E*v&*cgPXpOkC1j6c3zl~THPT33XotPrLtyQ6T5WG zK)!TAT}RO#s23>vzA4R_R4Z`A z)!tkNowc|9f8G#J-=@;b?8g;}XfMv3vXpQ%%8H}JEd>Q-E=Is*dOG0dKP#mpzK6Zu z!~hOswhdJQ>~$JV9YE{yxG0G7eyp!4WjSqX4`vFrIzpIQ0C7!XfcamHM$sc`M2fb8 zp?r)l(JQ^6s8qm)T%}1_SBx8Ei&#F=-z9}B6N}fuspI`o)nIez>LrpO#ZCv{{2?o7 zizMB-j`ht%neuFw9`x-5QLn<^-`6#2Q&Jjy+o`+KTIQK8(?v)43qCH3lBIY}J^8V+ z!&1a_htv&I@_M)c0092^2zg+Z2FA6yE){J(rlOXGqB`_f57m^A&4PKhbc!KFs7OB8 zdlaLr7TvuJI%#4#-P7{r$I-Vawb#sYrY1ZHul|y?8{3F>u|DwD9~|=ZB=TKNp#;aH z6p{8M)-)<+c{^=m6B0X{RudGJjVD&&L_)4Q_O-UM8aHDF436jm=6;o62nG$yl1N%V z^US=K*lNl}uT*xK!uV;kLRBU>FcxuN!aJ-6^T-zU<4BjZHoo!!CG`NKe=YJ;2G_dxc7RRuC!*ki8f7LL%n`-+KYn3a!J?P@;tfvk2Nst5@wK3y zRdIUSYB8(CZTDhk8nxBMCfFOiau)hb=Rx7ub2hR3WL{Hyrpsf+J+hGLMDG-~woLz+c}Z@!7eEkAC9e9%w>(!zlJ@E3xS^#||H1lMw>!0H5qz`? zDTk_akMd^!5{RFXFk>aW@yCUb(ne!t0QU(B9Kw(BX97mmD2irsOwo)JKe)-C9>6-W z8>zQiA&goy3?**OV)g6ha!+LWYa$kEDvBX5cFt_J}Y0=#0<)9Gxp~2QlLcB8}FqW>Rt=Zky2M5|a z;Hc#Pb0Ak4j|G(TYytIeThz9Q7q9nW1+bIw%qZ{hkGJdCN^(2I&!>>*jiM;zsNsay zfD%f*gMDH+xlIIpl2^zsEveXG-)Itxrc5f{VG z2+%x%J-=#LxVgp3INtBtb9Dls=&UII?9k3BspvyU-yDWy#ZZ~p{*3H`GxnG%ZA&CL z&*2_n6+ig<_r zr$au4SfLz_%DUXgc@3zY%{O#}L*SV!h@$ z0&J2lMSWRS?_2t+t%IovykIn^Gf0E(4)1S?`Q&xoPeYMB3I7AQVb*GGTG&qx)z35` zm-*_NYHX|F!A=SFfAm+@A_k_kST8zZ^As*W84Ywga!W-8E;52QU#|P1>A>0AG0ZKh z1{j-VQFdICVO6OER;VL$V>bR5N9PP03GmD;8Qdn}J)%-_?J)}HeZciZ5q@(VMY4g( z+?!k6V8t}Nw0vuxbc5?JCw4MK7peL5#PnJHvA}FX`~lUGz3J zY|qT!%$9q#ltNvl4zfC+1!n>208_(%8;BBBv?g=;kmh|_nCPRCC&3y^hj$-@-t&>} z-b^A#l{w#iwS?_Z7@c;6zG?p5>zH=`r^z&UF!0@+o>6G)n+?TjkXzz577X8g2C<5o SVD2lzkuUx<_ZG<9l>h)e2Ygfj literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar/avatar10.webp b/src/assets/images/avatar/avatar10.webp new file mode 100644 index 0000000000000000000000000000000000000000..a813d4c23ca9d2945fa089f0e4281a7e6e122e42 GIT binary patch literal 1410 zcmV-|1%3KbNk&F`1pok7MM6+kP&goN1pok0BmkWODxd(M06w);rA;R!qoSu4ia_8N ziDxRj@G(#c8w@MiZ2Pm1xtXg&SV}tO*upn_$e3T{3IKsG)2P7zl)<|FlG0beLZ66? zP8MKY$g2W!|C9%Jz7n>$67R(*k0AH;%P<`vmTFO)=V##pXZCZ)gtN(Z%p*SC`lfMv zD>&oYT&*hXU^Rn_m=qyJP9*z79qaCF(f*3BmcvomuMR=u8kHme^Vh+Y#SverH3t|FRmiPRS&TRUoCKe-bsYMg$7@O?MPgGwcid>XA$|n6Z=L) zZ2uB6qou3e=(axuwzL2Vp-2eWFAM$_IkRILs9hSH#S0Z+MXb;wb8oKdBte3;R)bA} z*+BGDjYM<{i3fa8{Zlse2Nb(xOK+^ybEndK`Zlp`kDA%hMHUtv;J0j=R{~n*D$iXB z&!JSJ=<`@=^kw-&i^Bsa0`G0K_b+M}`aMe#zB&dJWAP(c47c>965b2)B(4!Vx4$; zw1H=hX#>fy7P?nk_rl#aH{h1yQ$2BN$smP1u-VbsMnWv11? zLp058?xk1an|Sw(?NjDb;uebjVkWHHiQ7W(f`P#D#fli5R=&I_)-XU?fc6SBhQ?kN zJ$AQ42J^$gWWE%oe-N;M|ApbrPsxz_(cK-~ZAFgxPE2^B`Q75YhbMy^7Pw~YD-ChS zA=>F9_HN9N3FH?)0f>*-9{l#atdGpurvFT^8H)aFa0%PFn~F3`ubg8gY$SI3_!~2` zS_l(^H=T)7S7kzz(;dvwpf17sUZ}3lKcvMGGLfTwlHX5cAC;>SC~wT_CZ)TPLJ|+^ zO1^m4YAnp%qoe6yc=?;Ew9>7%+BplwY*&G+-T%ccSxkn1Qw7Y2OFWn*`da)d=D&$8 zPbb~qkF!m1wX=~@J>R*@ZYZ}fr6gbA|E>MzHvxRQN8k;s6giiV7hzBm=J2h|Fq!!k zd3*yobrI{Cf)}+vu8w>&2vWX5W68fofgrK9qFsRY-2?*b~wc7TUc73C>=31jaEDl4iGgxUWZ zB4rTdKzTY{)gGK&8Am&B2>R;B9wKiG*l5R=A=#t?3mmUkxUGS7kn QTF;h&;#lF?m{x!Q0CCo}*Z=?k literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar/avatar2.webp b/src/assets/images/avatar/avatar2.webp new file mode 100644 index 0000000000000000000000000000000000000000..6716e3ff9a7c07198562500a8b7b15d86eddd92a GIT binary patch literal 1214 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 SOT){6x=sa6x ziDv-LJda-J@i#;hzW8~^&ycs+x+Z)S_h@HPs{&NWIsvF}Yf|wew7S9Za;U>aQ$~y5id98~wmJMzcHjDBq)*uuBf~SLu z)Gp7IuZx!uI9P=5&_Dqy#yuEZD#{4@MMZH0ciM^|XF*;3r z9;A{NP`bRGi#ZJEeNWg`G5RMK9xvPpo%yTs_LyO5m9BZ6M8d=6DrzKBk#26MucKg% z@Jmlisk&6TglV1uE@+Mi6dU?WTlj^OWWD+VusOAR0QO@7&Is!osAf6}pPg{NW71Lz z;rRW1v6D6$EyDKb`U3Y#iU6JgDBoZ>M~GrxedkT`+jJu#XwKj42Uj3@#eB16DhPMe z?b(<`( zs`~_!4&_XB-I(Go!Ti!(B zzX28-dwQ8RJL`7&KAUMlIpDVwTj)zHMR}xDz-D@3~Cc z7|ZkGAVEdXA(ao{SKLu?NRz`qi;$@P>N24l+deOf>s))>An)p$#2X!_5;0xOako)X zjIOe+FA+F+H}$q69Y(8V+=}dvt+qbRSF%REpd_z<{gNTS|1wDRih&;=->ZRCM90dv%N9SSzr(f;PM-%fXrH3Pfe2f(DQ2NV&X2&-DORYw}T--4;V);q5 z_0DY^&XL%`xt(>cWVu58mGS#f*E!W%9W)`^Ht({sb?}UPP-X{41Jd+OelWn1DB=!? z?MC>k2#5dG-RGm{KTf5t%~`eqSg6fR1{uXI1M|uCGp8g4^^df%E{456Kx`7P(>#EL zmhGFzWA7={)deTE9PtU4F*C@=vOzZKQHFT0AgxVwj1%kfZW*%UC{wbm;GRO6K_yc< zw+EhfXW12v2HeMBD0!Hq=)pPKt|b&p`+N5s*oO##&@~ZAhA7o3S52Pz{bcveBBC5r zCvhxa8Nh;#y@aO<5eJyW?$Hl}R1WvJQx&b&?*|4N4T}!E?@yxeE3b2sV1`a@^R+7J zp27xGD+@x*xmx7d&{M%Yfd#$BB*qzx1LzqTkQa0)>ycf>%P|WAHa3;I)bi6;o@^&8 zlF!9&kvZXgXme;5J9SggsLI9x!bbkq`v&rTj4TS4=%It-3h6O>)z;jL+26!mg@0s7 z{4(NDv3IR6vYgZOFlTl!c!b?aLBP6L#|@u(xpJ83IMLF?7);-IWQly}m|w*of%ad$ zs4&NHOL!Vvik=DMMaQ-=WnunMXR#%% zIGbDlMeFoEBXYga#NI7n8!Wj(pa!rZe>u0q3$8d;_hEE_F!k`bcamCdsV5*a6aolrOAz(+B zd@P)%Sve@b1T5Gzk>0D?uqoxzL9g;Hq#>W|iLtr^A-5BQTpT9ebo8522y|Yi&}N}+ z!nlr^T4DNF?&R$Cvv9rm^Me9(!ik`_w@ulxS$XEX<#KKx{wypTfIc~YJvA0GHEjBa z{tEX`KyB>SnrEk(@oucu?SqXbBaEcn5w^tTc2WGK(%4D)aa56stfLL0kuMEM7Rqi; zo`xQal&o-#zU;IGUcZdFg?D_jdE*(s*c%p)6)*r98xXMy&ILWlQaYSn60FD<1I}dkS@TL!3$rhUX&agD&Hyk%~IJImH>Onl3MQU|Ay%bPf&#ECZX4~J>IhG$r&FUr~Xpd&X)r{G|={i?b_HnA@GVU z)1>)kALc5Yj)mf+#4Bd2-y+^^2?%cJf4F uD!N*}&DvQ+z%4JRZt z**wNaQ)XNFP~Hyc-5LfbIC&@l{_)m6_A6qduzFFQLe7hG z+`Dg7`MP=8PMs#W z>)Zvn?0Q`J3a~$cdz%M6qCYPRal2tH)ADr7pFDSr0hhEecfKJAnD?cGv8&)tngt<% zBEa;{SzSMNkCB(s_+f1EmeKCik^88@dGSs1>7b6hrH2O!%;F^AILl`PB#npS3=mrv ztu!z!I`*4f;;#|N@y$x9puJq@e%MLELvFmQ$7sQp+jYi8zXn z7!yB{(N+rF_;=ECz2YtbVQ{P8&hmx-SM^Px`+Fx?{VVD?*kb%yULK5fou&W;zx@57 ztjN5fd9K(%4A*ppn$C~0uwAicQHGUJqaXtTq-SEO3i*S5UtzkmT-;2S-*yKx^S4)p zu(xtY`uuGFdnEcF#&D-^5r5^8ea?Ikb(enTfiNd*6A`*r_w3nuP4|nOjv4Ar7>Bn* oum=sD9}=@(JYWHzN=bN_eTt*WCui4r!^|MQ?*(KtUpN2&0LE>RZvX%Q literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar/avatar7.webp b/src/assets/images/avatar/avatar7.webp new file mode 100644 index 0000000000000000000000000000000000000000..e5ef6fea28c4dea1f9e10e78c8183bbb544b75d6 GIT binary patch literal 2712 zcmV;J3TO3FNk&GH3IG6CMM6+kP&goj3IG6bGyt6eDxd(M06w);p-ZPFBO<9-OL*WG zi9i_aoN0I-4^TUVIodrc|E|84pR(K^4Ye}2him)-9_C)Uf2WJ@`4U!&jCB@!jQSWf zkWi1W!e|7=uqM|}&jWCK0bJPHdljLAMtSOBhK2FL9vIJwZ*jR0^qz{%o|;eu@jIID z>1Dho=M;OiIBQ6k90N7)U-LUB4fNbnB zcVd!#l@ge#a^5^>ovyb^=oxMdg}d1U^l`2Fm~d{`GrP+hqwRdlsSp zZ-P@AID|3okzavUmSMCE8gScpMng0Q`5{KtM0ZUi0ncfw=fKTHEdntzAfZJM&W&Uv z67K-k4KmgBg(%$Njk|Jg-*xprnCV_zKfhnobWW_!Q|jolDX+OMrq@H(J)qOVrR-qv zRIBNovB7=pht`u{?#HO!58IuZqG!54`tQ%x7=FyAh}uj_ZRsSbgQ2Z1#W=b?n({iD zm$Y-O(G)Itc^B80u=Sua!8J2(sb2BUsv8#G)g?fpX(OpesA<(_`VsuMZ6dfL41iYJ z?*M;4b?t@DbYfq${(Al({l4n!l089uXnn-u$MA2MX|98@2|Cx#>)`8d($l@QFME%7 z_6}QMOoeB?j@rwb*TH=HoDV(FiLvgaLY2@MYNJ=k3TkS{3csOEw_&C-HxHy;ajX(Z z@#lxXph1y}+iOME!-6i8f&P2>gA0==<@-?sKQ{II&j$D`7MMhkLpi2;N%;HPy+G{o z000uB!pDPiCpwuSQQiLY##czfeQr3R&d!)&9!I35n8R!_q{}a)ZBY|b^-u<_=bdjLM zsH;7oYDXr^TFEo<|H}DS%GT1C0L2i5=EqD1g9i2J^W;u>#;j@z=K^hzFU= zYY2QMrIAKg`G1?4OlL+SauH1y`A{Uz#84Uv44XbR0-O9-ol{Wgj|tx$nKLP-P2du% z-Q0jz6G7SVI+1s!A4*xZp5N|eZYSJHTa=>)Ajvyi?EeB#w59bKeTsO@(I;I(WI77R zTk~JMwUgwE@w2~AD^;ya94Kp&WMaW0WwLS@H`8wZvU0zjb@itK&I(FIx@VOp7hB0H zotLS1e03mJ6E`f%jAQB%^&LSPkvnigLijZK-J)3ej9wE3&g(7ejXm+7C@$Tjez_I- z&tVa7hX+KoAg}8LEcb@&UGwPMMlWWtOM7&0LePzZIRAMRmZmAs1`e%T@SSGa=*OGY zhQzw}Ca-M_vl&mQDqK~wClFH;H49m0Y^Ym{L4&A ztXLwKYl_@y2ljr2kkQYC=V5w0{+F69RiFdTm)%T&!mfzP-kw#`aCmmRG$H1>hu-6$F77UAgTvGa;v!U9uan0`(ci0>}JF8n)U zn-=c1mA3i;uD8W=(^j(nz4X`EN%~rB&EFjc;cV(?)IMy<#JIAZJKCn2H!jd#+FP3d zh|Te*k9)A5x_gw)Y-n@x$ zX==z!&?XI>ePW83uOwKEkDLxe*zeJA4Pq9#;q%y~fS0-v=()A!rQFSY%-}3u_+s~H zW9zimEqBTB=n_B`hS84Ispf(XDkch~Aqqj;#aFJNd-kp*#*uNX>22y$d5{l&6GXjo zj>~j?H5qFF3f<<()y8WFAO-OjT6Bgy#(Rr=E4LU_h_>6k79MC*;=Qq*6sN;;TC+an#IgsR6KrAO z$c@_{eBZ9#Y{_X^v3H=kdKtigpF(Gy(*r=wmktt#QMs28C30ForRlH#j}jMaWuk@E zA_pE51~<1C0n?zwLyZVm9FlRO5^l`sqbf$ri^uN7MVKB^fThgOshG+$Pouyye&KMJ zbG2%FWThe&(<80fo5owGc^_cOzvBH@`F?*~*BqZnd>}Yoo_nqw(`pk)V z*Z@dqLMOC}XJjR@tX1aZzBlMg5_@O^19uBchc*Buw}AJhXJgG{jpTle SlnZDgHxy*uU+B%U0002~4@gr0 literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar/avatar8.webp b/src/assets/images/avatar/avatar8.webp new file mode 100644 index 0000000000000000000000000000000000000000..b66e48f7b056cc570d368c4e0f5d962216d29971 GIT binary patch literal 3946 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 E044g>D*ylh literal 0 HcmV?d00001 diff --git a/src/assets/images/avatar/avatar9.webp b/src/assets/images/avatar/avatar9.webp new file mode 100644 index 0000000000000000000000000000000000000000..7974139777dcf5db9848b8f0b97b58708d60d369 GIT binary patch literal 1680 zcmV;B25Cq`s!Rw6r*sJG2T- zxVvRExAV}fjI%u($cwje4h=Qh8M^Vhm$-}w&Q^=5w2y(KN)o9n&3k|tlSOs7*31#L zZd*A}kBRj5Ao@<*ftTKbJz|QgG6yI1)!FB5ojbeD5SIT??x5_85>vF3(iY~_s}B6z zJIL03h+X)vz5D?ncg&2BI7M|1dFC`>e0JBim5qthgnd;jgMITvIsJRf`(5IKpu&FX z-rCs3S~Eqe$321H8YQh+4o=)+a^L}065GaGUCEK0%hpy+*Qx>$m=Rv}gtBWa=77lX z(VYa&S4AY~L9+k={`NoGgSxyFZfl)Pi^vA#KNDL6dX8Q>us5c5t;6L0o&x6W@m%lA zDM)$!t-!khhGc^`eu!>cuJy#x%f0VI5!yaa=td}Xunnnhr8ev;uMHIhcr0J$XyqK9 zuX57K{X^gJE0uk0OB+EWP-c7ns_RMON}bt%QVj{Q$m;#|WErNiIZFFI{kvAmC0JDA z6S~aC#;gaT!rVstFalV1u>i%0BG3(ck`%1_k7)7_k4dLbflBWF-$=y zYXuFm?Pkkw>>-p*ze|ub5=lw7qz?%ivp7%e~o)zuOB^QE*8~1b{~M=$X$;g4OB&<`_pQ(fAcH!qE9%DO$8BLT*BZq0o#qHcMiMyX~<| z-=+m)v%h3{wy>hD`#}La=L=Ej{j!TN*UL@gmGY6yNO_SKe0~V$KMCY{uAJ4ke=00S z#(Ss89O&bj<#0={Y6sz^ccLBU{iacl0AlStaKAP5^H&%N#arv<9wubKsR@^3#Bh3j zH^cK&-QW8*&El;j|62<;pvqeA-&I-=$SY3PHw%JH{W2HBMLgw~VZOlbad0jQB0lI? z&A2OQ_kK9>21w6V#o%fNugN!cbI}(?Mnr>8{3YLGv|JBl>tsulVv@2-!G>Bp(?L%z z_4KMfeT23VcUa&**Qq9gpKCv+thWy@(5FZ;SP-pYt;n#N8hgyGcT~yp0o7wWy%$XW zaP2yYd?+xSzH1i5u=5^{bo z<@blJ^<1?6)cNf7eacQ)dqC94Z}gOf7S(g?b+Z=2PdS8(tvK#JzU@=G-<7lJE^qf| zB`U!>-htj`Lio0dQ+Ic}ohghs09r;1A1!tJ0XI!YVxlj%Az=?yczeG5GPnkT{O zS^iGH_&v0;c>ZPbqB~?md!#IKb(FKUmVRUw=bZ%(nx>OTGVw(Tc|O;5oLA5MX`wQgQMaiXBB zpcHtwlRdTvXZ+CKlb4)N9T#5@t0>l_Y-OHSL)^qXurPW*__rJjBN4{NpNd!kblcd- a+aN;YfSUTyaGIA8bqP%u?tI%K&_Do!2TN1{ literal 0 HcmV?d00001 diff --git a/src/assets/images/ceremony/hb.png b/src/assets/images/ceremony/hb.png new file mode 100644 index 0000000000000000000000000000000000000000..41033245879f1051f34c33766bb6fdb4fffb38a3 GIT binary patch literal 2275 zcmcImS5#B!8a)ttO(dY8U=WaA1Y@Yu5|SVSLls*nQRyN9At)NUNHK_l2oV%TMHq%6 zW~HUV&dd2T-CeP=QYuma z0LbETXb%7o0!tx40s`K`s*ZSpjlH{z7Y1zkPUy`(6yFK+V`I>pEOS2QpB60u2atRa z*&=|CHo&?cCD<6u+7|PZwe=5v|Ia}Tf6!8(^HbId%`rgn(T)P-2M!tue5VWrG=c1Y zkT+Nd_-qS47W*&AGXD__%+_x6kNBj)76=aZn@?`(2$HZY1yh1oaM~J2Z-T)_>suDMOUNnq>hN9D^-_6J|>Uy(NO0(zje=p;q63A<>}zMlWb#0TvU62S8ac z07zD!Aq9aSF*wG<)f1pZWe1pPJ%2*G6%G6F_GRUjI6r$kBgt=^G5T*;Bzg0ljC=>^ zTA$bQySC>KX({n%kGSrXof++Ks6K%aUFVLzA`*S^mMFw_IJ=zSVXphv=Z`g|v}=@v zkW=o5F-T**U1gO1`|gn_LJ|OoE#T4iUh%`+ku$z3_HZ#yp`3kNGF)E^iVL*YYTQv? z8+LCYGgG%>Y?9BXh&0l}l}5J}sGqsxre+&;o>0IpvWi1UB>&w09{yeJikG(;SxTLS z#Wn6tiR!tOM!#_~RsUW`wJIH9zsm|cIN*~Rvmf1v}8ir>tu{H8@G7<-_?S9f4WqpX>ZAj-RS!{3iHgYJB zUwD;AvY_vo`WkkWk(^XsHys=n)m2qKB41Wbsh5zOEWl?xo1jVXChu6YO<~L&VFjYw zw#cTr@2bd+kbW!cXN?C?T-A;JvN^iHmR^4TNyPNjBe~X$BU#ALLS7$a{CvEOYDRRV zG6%F*{<3bL?KU@yIzv>b<7oQjr)z$E;WZSrh^J8kV~U^F+JusB<=9WDqlDcRbn?lo zDw^VpRVgbFTqxPpSSGWpx|7=Vg`GJH({vk>Wl(p0=&mBKY>r9U9Y&BHDmvuUl><-- zke+~Sm&f6DQ|Cw)7o~azQXhx8IGA?04Egj`Qze(paIRz3;vJsB&Y1brzu-=qc%C*W zd2@Bwt3g;?GotQhY6qXmmG&yXwFUU)Yf$?(kc(W`pdCN5-v1v zToE&Uf$CjP!!rM9eto^UnAX)6u2_d=JoFBbCuX!>Up9};7BjBz&PorOhBeMurH0=f zJhhx@)-9aJV>Kk5tAI5H(RWDg%UMq1Vdil_oj?8Cn($3MxV$#rSQXuVp<QP8WMB>}Vqe`-kf-H`C_x$6V+`uqTu6i7O9 zesm99cek;XW0C5kb#mWzqqrL(Id-*-nH3{LNVOzgqb`tZiz|gS zah?EmIB}!yIU4$WTNq*NILvI~9UMQ^rq$Ss{Djx zCuivMP<2n!v=%s=P&EFXkiv{8Pww@7q^4wA($G!y(@*=7XW2erCPmTxN}3%W@7J@n zaUixi-xr&6qZq&N&WHO&%gqHbB?%`2iTK}Fk8vLLzZD8MFBRvK-fQCu49rUh$Mx+uW!0jUCcU-x}#WRz?c^S|49F*g^)J36r3gg%c2}Kte-E~7qe#2lqfT1 zofWC(7}s;Jxq}l?&*3doa{VHgsRu_M2W_J`=m3<@h2QhDWJ^j!7Kdz;Bt^7g4C!;^ rtK9?{oKY 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

yjkcOw&(>Y8mz^;5!^gv~u``#^d4Gx|bp z2-7XGTS)^qhf7!w*}02Gj=X#OF@kb6Jt3zqO+zHUi|*y6rT)zw2fWpG5HATLaEHBA?3FMMUbKN! z#PTx@LRbQWSrGV>{xDg;Y&mz%O`b0->jK)iZ$3px^C{9_{ufhP&J*iK#sD|4mEY65 zo_)#pd^kuy=pjs7EPZ($mXiA4=KVU~KK%qdoT$y{+sF>raM|L3$68%+)I;OfUJTDx z1U>Y4LD0a6u&j$I!_&)rdJZMen2S(UBFCq5k=S`7fSS^^=A=x(J*#i zKz~~J*K*g#S=`a;a@2hrk*YK539RJvg8R}@o;E{PoRRvhvd?og-ce(w(-^A772(B( zwL2heplT-AQ4b4Hp0}RR@qK_+3oDXt)Qd>>d`CW+gt~#ZDk|uueQ--tz-xBiqRf(p zSffC>me&9Q_wx4x z#)Qrvl1BIk!nP|C;SW46WI9zBNIC596gbh(Um)?0RRV@0Qr)AD2Mixv&BN8B^nL0xEYRQ+MWL)?U}O`I{8z}8Q? zqG61P(MtfE8g1vOCeR!PQzyS$q+)y+-wzSkduvQVFUmM|rHAQycjo0CzZDu)rXQXWDzDqA%HPG7qYby8MxfS z>2kznxxt&D$SV6{@UgaOVu)ace0YO7ty}dTMy^_LW@vip+jR!lQOLYfcUR}c^Y@!Jnct35&-L@u9|L%xY>y6!J@{c3l!bY^F&>hg%@dSqG{+5` zE4-aaWE0hb+H_tHF}e*kiqSi+VPyDuDOBh%0Q9PS8x*jW{fsR42v9aN&nPoT-pD;Mch2^&mj&z{}% z?ao99Xsn)nJGGiN&%)>Y*vrk5Qd)>~eENEgD#>kYk?|f?-Dc2*)GlQs5fWw2%`c@1 zquNsRdGT*p7sf+aGhl!xV{-_s@?%#S8;Q64tRn2_{uzADxkbqN!l&w_!_$_;R zdnsX>lMrYouxu71`|g>Q3b*oO4VgPmk9>qyZ|=MVra^f>#y@Cjo3Le`o&ytIK5lE8sW9IS|@f;Q>AtCT=tq~<&h4j>sRb|(%9~?-avmzJm z(1;a^3(S5?{Nf<=?U3i=pJL|ad65!sGoKsxu4*RAB1&0F-Mi^-asgy-m6dR=Z{s3z zavmTR&xu&hAESu(?TAP1rOXTC!xu7C0j{|8YD=rQ4Ws+FS?yX}djleb%}G-Eu3=yv z)mED!6H2>+u;Zxto<8<9Xt45@(?|y8300?P{epb5UOjZ0LTRvgAN0Gwa^aTvL}hcM z)9UMLmZ(pjz3255xlAiZd9kwXYEhk{s~v3B#SbqriecXENeu4c8U6V)#W*Wz7YpRb zLVC&9vT1wI*EHG3Ct7ZF#!Rq;MAkc;sIb$ya+2Ek5XwAq3L;suewr*%>z-P4&q}Ax zsEye_4g<-Mn9J*lN*ADRlVX_0btd~eEGu;uXcR_)`tC!^^j(VFwKJ-BR~mm~ZhU>= zTNr;_cxu*T!7VxB&C=LX(l&Z7ZKcou4OcsR9#@%L%ez_E-QhOB!gpU@uEl@cTj}=6 zb4utCT-AslKou@|scXX_;#vr13|;2cdxC|i810%0mPcpmHEW+^gai|EgUzEXEtq-2 zG@*;-2-P_$0p0UO1rm9p^wUi_;7Y?p7k{1K!t4@hxiGazC{z9&y8B&~{Soo0+aYe{ z$lVfa8?;-5&uRUxp?d4ZgPt(ISdo%bOB#jOF+-ujYKA}jn`5q`)J-P4USjFJZw%MhK;@VL4a^|*giXyGFs{+74$5gu-fsXz^`$=Lh literal 0 HcmV?d00001 diff --git a/src/assets/images/ceremony/yd.png b/src/assets/images/ceremony/yd.png new file mode 100644 index 0000000000000000000000000000000000000000..426912d581d699afa00e95ad5e9c54d9cdda03f7 GIT binary patch literal 4629 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/draw/draw1.png b/src/assets/images/draw/draw1.png new file mode 100644 index 0000000000000000000000000000000000000000..da5d87a3528ce62da611614d3cf3ed57c8b0288f GIT binary patch literal 11315 zcmZX4WmFtZu=XOsA&Wa<7lOM>&|QMN24`^yuEBL#TtaYnCs>dKUkEP2AwYm6XpkfX zXYaoE{`h`;bI#Q1ex91@ny#*%o}P)<(NZQRpd$bP0K}>)P(1(u1BpIu@UYOxnhuvI z0DuM1(J)X#SG#sl=mU^}qT~u`AD%>w@myVB^Yba720xR}IPmhwY8u)6yF>lFM4jI} zl2g;CSN!^Vj`9p`zP-Jrn0BD%2@QcKQ*Z{4?4vfnoqRb#JxU8C&YnYd8GIR9}|Doe+U|{ii#tjcA%i9f8RZ{TGb@qh;r&f_4oIa=)OfOU}QW}(9oyl7Ntj} z@d&&mr=la}dr3h>N5&>pQQv0NiJB}Zqs{uq&nwTzD}UQJ+&Xr#yj;PBe$OzCuUp@$(ebg#GT$*WH29hIlq^ZsCV9L6O}3 zi(YO)0mWiziQ5I6`@NkmX`q+P{5j4B=o=WeB5SS(bbmt7FB2HVXp9_oh#3n z4*-aWK%sQ$>3;@~_n{L5;H#&lkG`q@{|H5)JpUhRivpq4(A7b8)c*gWwJ%UN)+k>h z)a(E1PcHtGkN*i)QdBrf>ak$~72fwiV2*m3jf(q^qGL+UJQo~CJuZP!`PKhvK0&pv z{+EM#sLTJo60#A9LN%cz9#O=o!BR9a@JNi7MCfz$9)(&LLp|Q(qWaOgk5c+5lwpD3 z_2VYSpL7fql;8*lI{-udIjjfuuz*92`_Q|11VV{CvS8Lf;e5ZIghM0~KBYePz6Jwj zSc-AhnobghYVF1&bQe-8k#@2Wx-3<3*&72<~&EcJd?{sL#Uqb)}Wre(QB zyilx(6vBs#8Kl!>qzR+}Kj5*EYG69!Q6$-HTMd-(xSI^d?kZamnmh?egVk?Bf5GZM zoEjB}TD-oqyxWqVw3z8W?iRnA>3u8NKdW3g)f@10=H_;LM)IlvkZtAXF;e?0*3i%} zc0=3n>+$8Z=i1!Xkn|81UTlCbKi7*(;bxD-M2l)FU2OYI<+=|(>(N#S+gY%jE(%MW@nP`-4gEi zU)Fk>^%#kgiQ{MF)tE$omVj?@a{hHXb0(3QHF4Bm^k;~I*q-}s#3v?xdX(|1?Pte| z6#?OB8NO;34m3#Wqx%Cwy6}eQVf##X)*W0}>tDbGZ`bi@*~Fa`DFQ!vpP6kC+GI22 z6zVRyIplvWera_FMB4e4;xW5a=Z%&lu~MK{kKGaCp%{>pxu_aP131>mj?Gm zPQll#%tfrnQfB{S3&SzcttvYdF}`F$;Ir5Xg(NV57p0H$qj8QJ+`P{x?GUSUv@meS zV>>Yud8tc&DjlPHotyvpBx?Y{9@v6cZLK!zbm3@!_V?`{9g>%f_v838_Qi{w zLRS7|KU{!l!4^CuD(g0jvHoiO-8+6@0|6k7G~bB)ilw+?MTiv7U(3~i-GjS#ANn*o z1WSI8w|g8|Orz(6v;QThx->>1*Zn1|8BTN?Hl6HQo9605yQp)I(HdWHj1}{?)y3 z`RX9LGRvs`&=@7*czrI}1B_)l`A&j8pQ7$&po3j(NX+!!x+m*2B607~eFaOyHk<{e zDPJHQ@@=4{ZJ>qPanM+?4+41+mxxVTMx4Fg^^{myb=_3v8yf*=GYkKWnq88rqQbjmhTfh2Al*)QPs1(c^qJ@WjszUkY%xu zx6y>>wc1ng`--^V+T`et()_TFR1C`uDO-8}SLDlGRSG6_u#^i#qMa+v{^UkK6cHH^ zpa&!;UorKrefKt2?Rob+hWUCF@L~7yt9P{z90fp`JV#Q;VJRo3(8v>sa59^Eq7}2* z*|yjWsKZ<5tp?ZtUIk{wzq&pBrHtrubsVdGVw?0>5Ro?oi1_%o#kSY~CT?8k$DiV{ z&@PZ=ZyX3%lb%a?XM0$YN2t8JOHr`%sx8mL-`ARSjlNWxbf0X>dBx(jyUZ@?$;ZbG zB?UgYVo_Y=`KRP8FXCT2yD1XRd0rkx@{EpdJbyXL$6Pw}{RMA%pMFxnX~E9<2+xkD zKca`P!+Y^dM;S3012sfC#`Gu7NZ;#twRGTqNr>NRmq(P;1w9P6juPRrVj}7C+jI?Z z+5YuSD#1(D{=@6ITSEifYv^ii!q-EOKF&Tzd>B~J$Xr!Y$MzMUh~|^l1<91~8Y`t# zm+9&&>CbU6M7IxV%r%n%!=^W$j)Ab_7mXc?K56Q za~$QUka}c^IExYO(rJ;@wFRCeI;tVT8Cp{#YZhScAosq zIF_tTWJa~WtMXq?7k~2oWvW>;nU$fNGU4jq=~U3ac=N^2%#QO*Le(nI{hU^3y%b z8Y^hx0)N&%OI$mAGfOpXsV2JIK5`2AlyK^l;Dv~E#6{7p?M|KrUm38yTVW-UshE!< zx%zn7c;fpoTB^N;+Pgao|0R4z)oh%3%gZ7GRmc1jy^cDY%FW*TjN{61b^vu~tJ&M^+QD`=vlfm=_4!)~p`zvMNuG^RG+3K>?+UQsy@bRDM zlTWqJsUv<3`k(cgQ&}9jX2;LW29#5L1m;K4KUgCYKE3>?;mZKCYC)QY)0BamRiUX_ zbw5rbz^W#?Ld$S;{9nV4+F7a&SPA3LhC^WxQ`p}2jBGXkA zwztMU(lS!LmAb1}nvhmw?B|0pbuo%TidQJq4n|ug><&$$VSP5K7jjtgtl9GMhKMul z=)Za~9%;g&4H{M_g%p*thHR&7WQLBQDCG?tvhYzZB%8e@MB!e*1^AA9Z%pVy7Z2GQ47Aj@qOIg+wLg@8fMdC**dGd3y$ihpP?ex@Nn_-XlaeQa-51&8( zNOl5St;9(r*U?Bq^;kZalH4Z)!PXr4HksJe_}O9_V|O29k)?|b;w0%5<|J6ym)7Ug z1Fr@IjvxuUnA=XT*2Szy(FOHfAPb`ui%NjN6{ojA%I25xq%7;hCUbgVjFe8Rf?V@jV*&pU9w<;BCM8Y$t+Kco%kc4Oj34|b z+38;=;jYFtI*R?ZSe2X&!6W%Wf!?(1UmmGBY`OeYaVq$ZVw@{DaunIc&t)cq@C0o> zjQVLj8Z;{7iCxXdr8u8|{JoOOxKZ^;(MW$}K`+4j{nQM>(jwRU;gxyvL%yA5TOa-A zs_LcRzkBDV6Akk4T8V!-nBR!|L!gnlkj2FX?H3o%GAAa(uLQk99_kN9n~bCv-xzO{ zaRe{WJ9*@#$;Tk++lkX4i_6-$x^?QBnh%U#T~rW_oU*pF?73U@xvYcBLV(@7V5Yg?-ss3>5f>k9V>q{9S6dZ^kFxvEeu)O z-ZbDE^7z8Hmp_aVs9N|n;qH4pK?Mt5)2nJ%Pr}#I|HfF*OTeA*D~2ESVCT8awKGhd4k|Ml#z@p8C}`eVjKK=_Cn&-P!T-X)y8=jwsTsu|4>- ziY&3>FZ%u44fqFmSYo0a2gTeq5=F)}irnW2QmN98?FgmQmW3-l8Ih7=#gX#(Je@^; zqyuT+)m9gi+}B{#*0OyDrldGNNaIlWN;SR`;WSw6&f%9*x&UfF)@@g;tD5bVA}uO zLE!u&MW(Sqjlz#uA=T3TkuW(nJ%yGOrp7d8+2U&AdA;5K?7GlrTdgPzw_9(=xVx|As5Ac2Mw%Q*aZfyK&t-3fYXeY{jCyh{qsTSE?L|2kA zi3|?D{M{djby2T7x?rYB$sLVF&-YQoMOHEU9XN94PSoU^$d6nS#VOuO*&Ai-VuU(TFa!3H-%C?yOkJk-p#zk(uKm zNjJI`6~rWM{2rZMY;7XO+Q%#;sV^u0iJ;j4fXw)cq=bq&9^oQdIfZCo<_dPc=$1M1 z!20iO@Giiv6BO$cyY{rdssD9WsmBNRF5dsmZA=0Ruk~#$ofw#i#hCQPL68%sZtdPt z&CF4n8{g_c8vCQv$j1OSxsdvsq?dB?NJ1e&T3CTX2v95a<9^qg&vjn}9(9+ksUFu1 zGZ>E86NbBsZaf8+a<;Y)EN<&z0ecn;MDs>cU8 zfGDVyMB)#1PDpLxGE-r&Oikec7BCbe^qo_rPBSB7Dq=kFT4=?BtGqV|3bC~f)BaDbh3gHb1CMEV|H+OyQCQ8AH)_6 zdz+PB^xWJD)Dk&E0V#;IGL4leMP)pG?4xJH+6ygHcZiZG4a6Tyu8s~bqa&BY4OnR2 z-f!w!NIKE@u6%bnj8F1bno^d?PSLq!gH4(@ni1T@3qV>hx5-Pn6YrAckW8;KfIhHy zk0nSK>b%$qylA2wQ4C|?MHv4(zLMWvd4`Sh9x1G{AL7D#AgmJS0k#3J+FyGPDjj|c z%3*97h1wAV%hHOqI&nyE2A)48vd8iv`25=0uow{l=IdKny3Hc9Y(&!ehp^7>9W7l0 zY)eYmIG~omER|TG|8J77rwA5ca_qGFdJkJInO!c1Anv4qceJA+gSYef*L$!)CbMyE>ck-?f4g#!j@Qo*lU5i~p zBcjk#(r)DgE%I?;M4l(1NpCaydLRHwsxV*cE}lfvrB6XhS*p2GNKmUob+E4hBo6ZZ z%lyv@oZzDTN5N*ETnm5`qI49S8$|S1w#+1Hq;3mL4GJr~HeaJ+JeKaoOPf0moY#JR zF6#V>*7-ciSAEkR4x?R*qe&$!Y|8heJy6VS5J&(%W0o~eNtjW9GS;l)5#z+hGUbEM zn>F4&^vHg8=m{c9q%A9jQG-Qzf9*iGHD})sFh4RM#Y4UWC7~qqyB)IdYqy)X{Jj~F zBuzxT@fMq!!x(6k2CC_Wc#P|6iH9V;LHz0%1uC~Q`~*IbwA6O{)aX?Qu!P0!98*~% z7|LcrIG8Ag(#7_y5#a7Ab0D!5&mLOw;@PC3tnbwvm37ibvlzK-7fa*$Y0xbWti-#O zZm%@UIqnH!a^F=pQJ1Ev&h#_r_DLDMC_nt#cM=BB-N3_&>`lzb7*fTRgg%2{|LpdK zs_RhiZk@8$_#JGU9OKMW4B8QJlQBD(d`gN4tW`?K=20hZp?>ylcW)DrByb~SB zn`oEC{**}rciA>8$yZNz{WZ}uIq!(2xa!IHW-PV4X+7};I+-de=ED32z6oVc);qok zc)R>Dn3x8$^4~8}L}50&dP$DFhT8g>UmG&()2q*HuV1!I9{b00zQMX65k{!y_qykp zWQYUnH9+vdj0rvuW+Oz+@1VN+V8v44)5P9%W!H~`FgXLb(UWn0?I#oZiW-+a_AnJD zg$6u!L&C{Fz6x+UinD(`&M;-m#@4_-b#t3q5{QBB_s7gvmJ0Xsrbk3?^obMvEX6fX z##j*2&*6+<2r~h!XhoA;`Zo@2kMMU{G;sze7e~;hNDGSx^8_4kiA|r?m3Zsv43{>u zm@JfkWP@3s71P$s)Bq~-(zkDp^v%-?uaUe}SZ*YT+NhIPS**plTdDcp=xiBB5N7(n zDuV)=|Rx?LOB;AhBJXizkHE_p-hHT5uB1>Vdh*d^?&pib}saWYVHsd?ym$r zA5X-GzfE*7<0B&}bxjM}#-HDSZifuIN9ys!CnVcy@;oUDl$$3>AhBSWXYc)=yaWaU@dUQ6O_;QS7 zjPgz50Z}#K0IJ5H2r}rsZ}&{X$K@Xa$jia^*Gc}GG!c@6ft%fh&o~5r2KX+GKP8a9^qHo3 zjtKMud~SVKz3J>$p>f~dTJ-&UoQa#D=WcXLNW5)WSQuoD7r9rt9JV->dkJ}&$|Pf_ z@}QB34wPc=8UyK&N&h40y~TccxLxn>`eJ{KBoniZ9x;94tF`dCw59r(U(+q7ok>lO z{do|m%Q{WB;&qoQskLdUFywxSn(5`s)%p4K@c2f**A6L`N+&uT$9cRB43f}>%fS~Q z9$7q@J;MD}P*Wy&x3N)}7t)|Cz2I6Ws_#heSt$Wk=tx(%-V;a-Ef%oofT;~$2eTrq zwHua$cDJU&$I^^sqCBHT3N>{Mc7abK-y^uAanGRg8p4Zjf~%(bXhjrLkmpP%PwbN% zw2jat6jqw~VHGs#) z=@_0%M<6{tQ@J2@S1rYQ=qM3O90z`k(gn+8{oAXM0Z&lW&&jtm+sKg1!<^^@mf$HI!~S*tg!~_M@}CQVIhP^ zoO0SmN?Q39gD-RGS>jYw2om4#{RAbG|E#4|+`MwOWc9ZGw7cp0_hOOoT`x$H)wx-A zDJwTd0;;#qGL%4_y^OsfqL)v(4!m5f+v8Az2Yu!oKh8Z8D=uaZJG@h|!sPEyvN|d} zTrW9yO*i8!Z13lzAlmPUT&!~5p*T8cas>{`PMBsH@mGkLCFMi(K3w};(yeI&`;%6- z;K2SUON-ioDp%k(ainhTCl?W~U$Ir|#^mnRB-$+mf;)Mjx_oNd6fswORi!e)I)A~D zU7+)?6Cm#M-y34Q9I9P;QbFbR2xe_Vn(}YzK8wD@tPMPy{E)ZcI2HAvu`*r{1&sS- z?y>UDjfkAIw!BZ5xtI{bXbPRKtC!VbzVp0cAh)H5tgb zh>n5JZIQ%|yi5sXoOQ=aB)X+l>obHEA3x=V;3M@$>V;C~GzeP9|WxM>tz@u0& z7*7rcwh5)5KWo8ksr}GR|3kR-=eTk#!7vpPd+dp{{)@wPmHG}%QFV5n9$eCWedp`c z)rjk#w?F@FbJ2&VhJ=WPRr^LVBEEfaEF+jNk=D}bE$XM_BCdq|0NOWtc%T}4;_fO! zq6Dz*pMvoc%;pIHLoCoLAWciR%h~)s#miLNj)6k3D0TK5N0S{~5FDOI5DEq^`~N=F zy(lkotKHN5K1Go3|IutNCdsI+sxjNl$c#>N9E8ccsOH2P&*-6@_~;)w*XTkDR&&)j z4*A;Y;)yAyRBF*8fgB+C74tG<^@5G~cq1Bd@I9xaVXo7~m%v@!o6hztq(EGWy)>QG z{}V68c23>Ff=%>T=JGc#Bn{Zzq;*gx?)m|@qz}_72*WG2lzoIuYOBH~G#HpZMG$HO z-V=qh?^tP9h!sKZrG_p=uZ3=berk}|m=y8Duf$1v@h%95EdwWGazxjkO5szOrhdYJiKLHhO=#^>+o)pClzp z=$;qH8%CTL%1iPLWm&7Y+d;U3pOLBwsYOB>K;>BQtk%2Mw66lQCI<2%g%-j-qQ7ez zfBMkvU?n0U_w#YrmCc~|&_mhCu|?ERA?WJ+@*hjDiQXw`F{j8vF)1^xm?v!e#Nhb3 zS|Wv(wq^`vtjmtw|E<~VG<9>U3%Qu{lK3wb8OnG@A3FsZ%2Gi%Mgo;J4GUU^=!J}| z#x77*@Up*U#>C)>wsc4i5~K$^3~wWzKe|1cLeDaStl~pSBNw-OcinsH1Zr`~YZVbr zwjEEzyb$d)Y=Q*9cR2WC??}vq9R?SP^P}>`G%N%hrUgbCBuLUT5()WpuKL|L);_qI zkvPO_l0+uHUIz@mqsly(E^qB{z)6&VRubK9d%K64n0LzHuhR-iVyD>rrnrN-Q;mS? z+`P6DWP+)R3WR=)(mT%fJ^akaui5zi?&|Gu5;--#Iy9d`O~|UG(2xPlvNV}rtyS1l zyh_CdV+6lhA!&Ma!D&(XTEkrg0*f~C^KztJ398OeuWUI|f88GF;iRt=i6vH zGk}dk8Ya`AjNjPcq-0Mkj)oKr2H1%LRGsIi3PtC{N9*@4gmEepgoq)1-C2ZLE<-z! z7v=u~yDWDb?*%`+id#*{=AoeMAd7}%vdYQ6^Enn49e2a!p)WI*5kto$h(v1!!__r9 z*TeP;p%nGW9cRu_M58XaU}O!{kz)%6E%+!z<3wkvb8Rhx62Q=^## zG+e&@-wVO>a@+5bp(OL{K;9QxL=QFX(gcexB!Z*NigI-(4{joH2eioIUyU^qelB4 zL@D_NPYy^Da^N(#9~C&h>eM;EZ_+eE?gG=bas@@=a_UK98DIt=|9nLe+IE^|N`mLz zt2e|WAVtF*ArUxUgJ;tG_fAWY5H&Zl;Wg{duiGV+1JO{ZEK~uxU$Q^d|MyH~SNP6K zAw2Yz*Z0W-N>c+!8OE_*kHDbe$DLc* zvE~d0GsAuqJZ4l>c#8{qBO(VZd%*1g!Op)WeT|rPQCNJjXCW&xNepF8pe#);@d&&Z z2iKyQN481(GDpo?N^)pFg^gpVW0xx0pIUzhV#W3(TQ2uod0lMgPqxdO z3M=C8J$KJcV%X)Df+GaCWNPc{t56jWexS*Lb~lMLviryeU|EjtGggU zI+u}szN>H7zT_4I!!D^XlfCgGAf&YCA%MD>A#)-IDBpbeMP}*ZCn6ZJF`XWRLkUTm z`(#8#?HZ?|_$JzDtG9pV8}{dXmQzq(s#O}z+YCM2rN8~b{g1T+)|dLGBkL5zoPX@q0GDE zPwp(+_0-x*f^kaUG-81@BuQ0OU8f~0n%~1OF?VNahQZ!dn5|5P)+d5}*Wc{5CJX(2 z@5LGqetR4qhih^ez1OsO&74+?xhI2DZeU&D@`t)`*}<5`a`!q^s&WN&%WIpUVWaC^ zWwP{!QD~6vocESRw|0gFJ80_(9M`DFtD5A{joi!*FEK`VWIeAqX-g41QtbF&PtV`- zg6acTxYd0bJ~CL7{j1K5K?w~P|1l6x(K!#c#e5aEI5Ca-S!3=+Lo$I~_Qu3OLjSu? zu%5m?zVL#;h)!-lYCDBDS3!g@k;{>Qq810JkFTz;l|iuN6{9slQ8r$l;NnPdyUh&w zLM6UXKKvsrK)Z#_2N9e+R0bE;JXL_J=(1|g7iNky*^|t3@W4fg1fK=#Gq3F$sZ|}A zs?p8DBAf)8FMsWVlTzsB;xr+CNB$3935#cTR`={Gd42UdtRE;`F zwflY82#&5a4`$SlPQ0JbZIUu|f#YSxlazvV;Ix+}M3p4qMnc=yW8BrC-(_?5y)!Nd zQy-e8OC}@-W&F{1_$2Ww>l%{8TGL{ax6Oa6?7WA6eA@ajq(e(JOxb^XywO}QtDl?} zr?m3-b|bt44nz8cGklwlj0pm}twhG_wyop9@OmmI{3n9_*N&KGX*LO@#3EAD}SaB&{#Lv)n@e Q{0ji6DrrHR6s@EF2ReyiTmS$7 literal 0 HcmV?d00001 diff --git a/src/assets/images/favicon.ico b/src/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..21e7063503b8c6eaaad646f6592ff0372aa0cf88 GIT binary patch literal 4286 zcmeH}S!`5Q7{^ZmX^4%bgOEf`9AQ5&B8oFA?$zA!I!YZ;>n%y{{bj?D_FL0{^85fX7IpmzXuNGUKjz zsqSE+ROd>RXb=$jMCCKi_Y1J^o>{Yy4P-D{>;kHF0vQD^a=K zC3Trya<9cDQT3sGR+kj8y2POE06$o;DK@zOg=nei?L=vA2-lZUFs&RMqsnuD(}d42 zC&ek}97tT+4$(7N5IMFPBF8tw#E+Zd$;mAcJDUl7LoPs{8RUMH&jND53Ubg2Fvx&3 zWPivHz!u&0FStoJG<7Wz)l+9O7X^JU$ z^a_tMg&IAGwT7g;Mx37u(Njt9lem{e#Ws*J{(y0Z0aS zw!$z(J!)PRE_$j;`cjxGw+4AslM zEK$F>zTAjvqQX5iqpl#k1=X6A&}fWG-DXb7yXt)^_Hx+qNEpe_5=Zj1*HFC5NMdfw zMy^Ufs?`?eTlFr~VxFhilES7;RErUy71fTqifoj0)y(sqj?YBDty;#KSL$v6<4$WH z^Dcy)-;IS1ElPxoHU7AF7lCtY2y9?8`()N);2Ps1N*xZ z)nx{;i)oE@7Ovp^J2px|=1(Q>*zd`HbNs@N6sB_{`5Idt#N0OW#N&LKa~pb+Y#r8F zvc9D{WKuSbH}&0$bwxdQAiERQjWvGF3{sDIjO%f36?$ysK~o8lf{rib0Y;{EFAv+| ze0azEaNSHE4o-Vl`Qh^)5#D1U@oGko=f@)Um&TvUxkPh~Y*}F=AK5bPZ~BhuMelvK zN8U2|wLS7SM@_89dp zp?9U$sP{HhN0JSc-Gh5Tw$yLE&iV4wdEX9Yxk%3*f?OgD=DjC|?A2np;7wt8y5>~o z+%xNCNWyD>jjuKRmaIA2V>Iu`#d@RO>IZSTcIifvVXz-vipMW=;uwLsvvChcSK9^{((VC;)X`OeeF~>8$-=uz~hV>V} z$oqF{%SJBf$XJPC+2mz?j^H&M7Mjh@L@TqBZ!u|e78*oron)eR$8w?G+@N@;{3-9W z=9HXc+@TNq&1bpC`SRWp{B~`*`=w6u$N4>rqZf5wa}_z0Vx122W)OrfOI575C@tPo zsLfqyz&Dx*+sI^yO~${Ibw|A?W3Sd5{;tZR-~QEKzn)Fu2X|{rO_W&)>Q(%IVN~$s zV*K=2tN7i1OPmilbwXfzuiBU93?>>2Ntfqz3g2uwDygx@y3@SV-*i6{5C#fYi379y zIa+Qa(Q@-U`1>4@>d|{U_G>T3EocET!ZtIj#TL_WvhI*idky0* z`^-`FVwHS)^4Ra1wLr|n;CtP+lJ^>Y@)cxvW95rh zk=t?!*T&>YS4~l+?r6WEchP6PFcH|5IvIF6m-icX6KLN`;*Iu9vE9;%{drdk+J@PI zyj^#FA1)dX?wLV(_At>|NTMCZOT`XLi4uF->ptu6@qWj8*0*pR$6EIinAkuP-9^jA zK1VtqELu6{Kb$hrxBuC|d7et$_#WwUtQNbipNqY=W4zC~1%QypzE9_w{ruMR2>gE{ z0P`G2=ihz`-ltmJiy9>)b(Abgi;$(Lw6!c*yH+IWX(Gu;l@%RH1tO-xQ8^X9gf#HY I1HxPX0yQi+Qvd(} literal 0 HcmV?d00001 diff --git a/src/assets/images/lock/bg_dark.webp b/src/assets/images/lock/bg_dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..1c334354f0145e7ae1d91c8d51022fb8067d0b83 GIT binary patch literal 70592 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

TD-eWE$*|X9h61tDog6 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 ZSO66fIy%JW+RF@^AI^XP00000008fkeAui8$xP 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/login/lf_icon2.webp b/src/assets/images/login/lf_icon2.webp new file mode 100644 index 0000000000000000000000000000000000000000..5e4f3fd0dd461d4e92ab3222aa9f546e2c4ca2bd GIT binary patch literal 25016 zcmeFXV~}Lg_a&NTTV1xaT$z1jc*>wMY-W<8s+!7cJ=z@gRSxP^bQfI z9W+oWj=>OEDa??GZvhNLM3{;Hv%ZMI&jMHr8p8sXH4rtJW%D%ij1jcMgd4;FTzMI^ z3SSFc_}UeC&yed2sM-u^#l6e>n3RVY9t8Hi&dK(JcwNFC$I%i3DQo>ej z0JP`#m(LU5*WLfq^0of#?W$REpGCC$M|G!S=gcuKxv*Z0L|kt;nf zzF*Vi0#YSQWifbcd&Z}nOT|>BqFkVK4$jpCF`nU+DUgut~#yYP; z?U{bGWLo!DuRzDi3#T4m(=%Aqs8qBs2VLDjLautOaB4&Yt8$?ReG4)RZP1< zPrauh(E63o^2TOVfl1vOuF3`#mCF1BD(V%bSyl2Y3kswcs7bt)xfcbyVhi;Tme41`B_RH@z2NgUcs!f2nFwHhrlYu);QB zXD>p-omVGv1~o$m{YGDehCQE}*TC)7;t@W-#K7>KOT+4)!|FH?f`x$voa0Muh3snK z+pE(v7U}!-XjGnt)N3vvge397Qv0Y^ygUEr8kE#+aA2qM(*H5GrEqZ7aEsyng#UGe z{}q;6`i^G*xySdv6ee(Vm46R1q)?Lhtd6?7Rq}7m=H4N!zCWg_$f$fC|5|?E&U{-N zS48>qJmc#*=Bs#YvKcwwf2l9hUsieHRb4w{07UgIlk}99N)JNa8hZdp^<7i_#wxEb zKYmfr1Z)f)tr8u5dvkg?Tn~SOs$9*BW+*Bz2CbV{_5D@-=FDiY`v35SgMvzimS(py zHwdj88K@dT{&o{O@}t3-A10`u3_NJu&^jstDD5&YYrmC( zf~CPDy0P@%I1PqCr(Wd`Yub@E)?|@QU_pEZU>@UBAyteh3fIsuCZbhx&7k`0 zXcO5M@`*HF{)@ram4EH@aZnG&fO`9}lX*xsm=MDi!SL#Kde_podW0z?lh6gIY2AtI z&B#zq!i8%7!rhQ{(*z|KJwEbGcJ1z$bR|I`cdj-@S z{$UVM6d*2L?6Tyw{i@cPW}OF!SjZ>*inSQf+60wJbqy!QT=e68YUNQr7#Mb0>b-NrYomr7A4@zLkK*Ny=4zKRqT^cQ4Lr8>7oD;~#s^XU^ zQBVyu)$a~v~TJSF; zVgm|F!Bxlx1ek38Ml-S8>!roZZq{QubwgiE=mUn>U1FQ_Q;4^IL2?aa4QGLeWI}6> zKLD95SLAh;2~Gep9$Z9vZ1M|c;+7FL{ZI5EWHNDVKT4~T{uOJL2GH`acT;_TaQs*i zaqRXXdAwSP#jPI&$>qJ>>WAvRR?P0g`@{}_mxU%?vuO9t@F^Oddg(S~fBAj5=4F2M7RyciY_ zRcm=r)Eyylxq2ju0bvlaTjZhoUp6+`XaVT;o^_y+j4f9A1LL)D{Dg zTKyKC$N|EASA-*x9*xsq6OYw#=tsAS=s!~l?l$Jw!5bD3WSaZS-~zg=#nCK772&gKfW3SH-(5Y48XV_U zP08S{Ke%8Iic+mb8yMr{4Fp}KWgF4%lqn8nl@wtu>gYqd* zxu9Lgequ>4_d5WY=RV>qW6MJ#XH4ZKNa2B6B>~G{>-w7oIuQOi3y~Q#49`y!VuAn_ z03cE^1HgC+yTOSkYt;bB;I1X=`Ee9$ryTqo0AKO60i~VqX2GOQ7u9}ldj>ZXK)h+9 z6i6Td;qt)T+>rYxebE7wH>;!QDN5WqidSfGOKJW=E2wNtoPpqvs0Acs+XZNjqBIcy z9xpHyG-Nq2gp;e@NI2o)*UTbL#tci5OkfWFlM?{xvE--&ww0H6QwI={7=jZTv%|89@1y!i{!k{fE){%Ek{k4JB&*WCc7o zEmuZQmagfIfS*UpO>KBsx0v(rX$N+0sJ1}7I-ru-#2>93$*zXZQc9lk);%;2IEX9Y zYyA9Z?UtT-Z@`Db*6O(Fx2m;J^*!WwNMu?9;;6pyN8&!V(V`O9vbBL)oE(8`0>X>{ zmhNdUpaOOiekI!96SNcDw@m?10=wl7(f3E-9KEFKAB-uU`{*M;akL}bLure=rEYRh zV6dx0ts7A?3QE-bDRPIzk&M16#!&ndj)mQ<5(vXCORQj}xHnR%LJsKHOo6$_ouCXS z%a2&a=H)EgDQy~oSZC6R16m|V6sLqAjS7d$^<2ZK&6b1jz;Z7Ewq(8x5)woh%AG0O zm?mL1?E$TX*;bOIuuP2M<-9;G3f!2&bQEWVA1pu6ZU%1MgL|}+xQ$a8LIj}?u1H-) z9T2gTmskfKWe9VOPC1yTr-WXu7%V|%YCBMrwkrh*hkKMhNnUlNuK)h>hfLmL7=j2Q zNZN%?fG2PYyCM1??X5lF z96OYTbn0O}$8!8kmyo&VB5Tb&Y#i6A_6Vn(Zpjy6ze&{)YO|I>O@0rPTAapb)mcUA z3eH*E!J57oz=$-*|Cw8|fYIi`Am4v`cq-wkYN`)Vn2cgZ;soQ&`nIkE*5Yq}vdS{X zpT4*d$d+}Lv80ehS^pRjhSPqASyU$3$Krf}s34w~Y3}rGElv)f*N3&{t$H7!cb>Cm zyYA1?+R|EuP0C%OrJe*JWjW?o!6z{0&$+9uP^4~!&(r0(S&HWHXHO=^AbAKKo5{FO zZQ`Sh-EOUf01ng<M2>rv}PAm^7cc{w@F3_ABdbA&01z(G8tfxK_0R=gvWvy3w48c8)1KVzkzj} z0OhTAs>2eumSMaogy+SsEBLMV5e?;NV3DlFB52zCYpW~W5|+vupMOGP5Hf02nM6TQ zIURHZLf)xT_~#!*N4Vo0t8iEKpQmYiP2l0+Dh0umyvzbCHI|@8D4s1g{(S*saZIX~ zo3@{;?H|`2gLfD0WE6NAg34lmRVk6fJr@vw_a};Zc4}WR3>y0d$I34b{1}f;m$&@^ zKc@@r{p6_?47aJ8f`_q`ie-W>Xr@fS~CyzjX@UR^v&^v_sK@LwmAgHWttkzZ`O zb_u_XzqBmbvU9eK2o!MNj5mmSbO=AB?70a2%n9nl3G-^E;liX>*gAL?E{Y90ikt5j7;U;H7pMO)!+K47Ai1cl%%9jhTOr6>KC(xt`O zIFkq`f{bU0C4Mp3;B(SWI=lSi3RUAyp66&5WA3x)Dp^oQDO87rd!O*x806&N5VRiN z3}95q_6oyziWqZJTLr*S>!NoovFI7>qw4Bku(PnhVCcrk#RK& z=0B@3c(qzaM4iV4?pyozh+XTlDAhnCnd@-x(})ZUr`_q&412_#&rX;`{)%6Zx)e*- z9u`h!z+!NeS#ZH*Uu%@va*d$mSqQg6zMb9h`UDS?(9`(nmfJ- ziujA#)p^}g_W9U%MCMwT4kiVv{BtXEQ#()@X)92b=^!!I?o@ToEG(328~gcw(UAYZ ziX>QX*p!ZAFp2!wnl0~8D*_5_r2a7wC*uZ_bO-18eu#)HT!XyfM~VX3pSgw~`eA|Q zJ)GCZv@o(^u4cxxO4;ftK^-VvU{J0lwzOW^{DwKdw@+w;qz)LTMpWrIz*3Lwns)00 z#S-o1HdM7m!&)JDHK1z(<(Lg}qO)k&O?r4A$f?tAh51oy5uE-MPEitm@ehmAxe<{- z|A5S_nLBY}jpR(sq2T@9l(MwavGd!4x8xZo(Y<8{k)oP{v!;=nHh6+qMLRn*RNG~h zur=;b0^54VfI`2(sM6D72i+k39_)I+mu#mX0*>FEoHq85lIOJz zXKVuhP2l)k7UM3nz2Rswquao-p!26Y5`ttraVZBiZ*YXRC|G2T=K|G{XCjF zKn;|+$B4p))&%ZUMuSvB!AWKh7@zk)D-$YnDAoacwML2`pc<6^RTM-`J_ZV{T{k9N z1Qj(5NzVvhE+=0b@g@`CjIXuUTu3`^Ph+MBV*M{VoU@s@&&kx)N|#KXJ6Ku~2Dw|y}2tHMD>B3xe;Eh~vGm~v>b6GMlzhsHsUJTFg)5=9I>d#-a{TJ(&SvY=- z!AW@W*NZkJS_vFY*3Y)J7MZ%boWm6ak;>3+hKyF>;EM4R+!(zVdK+n^zvPz%8wHzI zR_w(O$A8567J@Bfc2QgUX#&}G6zW(YjXaCrk(gO(O<`weh?vXllpWlG7wg61*e(;o zO#HI23TgQv0;5r-J&eW0jB%<(6>BY%VM$KCL5E)E zZ~U%pXD@DBNUC^Bf=ijg$}$`|)!4VEAY2Yd0nFO63@H3<@!QGjK#;PFhl6Kbv7;@B z2)z{DVMt{)A`zQ2ah2pH19d4~HaYb}$GbNsxCLm1H>g^31e47ohB-#_BdU+2V;=%Fw`~gZdq8G-3k>6I zcW2C$MNo1Q^>HS^GvWJn2$`OpW_vD?NPgxp0UL9wxAfuoN$KGl%3q1igY`4~z8(B< zuTF~#|8msYUzh1W@l4LeVfG`%`Gc+G+Ku9mOg}rT@Uwz9cC1M+&L(6k8eynHKtT=& z3?fq+s3}8?^CZyqWhQ@wWvJGe*nBacyR>iYoUL3tKRKTrn>T}FDGHwszB>r~YB30* ziNA|G46Cw}BNzX$#u$o%6nr4_tPNm{E`r!dN93-LdwVB3x{ivRX2tTWrh zS#~m$@#jtYZl@xkgc#peO1u%oKq0jdXUcc98BHU0tHnh0eI@ickJT>XnI#p@VLPW_ z0n^eX`e-*yGDo}~tWCtW5cllNlbt0z!|*lDOEZpOxqjI-1Q}^jvi=~aBwQ(CnIN4H zU*HFcq5pPY=9ja!Bii{AUP4NgTj@i{>4Q|WJvxrV#1jQHF%QP66}ym1KFpT)DNf68cqtb)eo2u&l}8@SR_8er;t$sBelX-UeI9$*COM@Z5Z zbN2!Z1k-`6PfMLFR9omgwZxzYwF@gw0*)P^2-pJO!qWo_gT?oPUnzzQUV?5` z_jnOC3=m&tE^Q*~~?B<=8RzJ02TICx430Kg}Kkx0XrC(2}9a0LiT3FmA- zKCn6+`+G0Mmx*K{7W%Z8HS=t1hP}wm#z}+1Oc6}U;eag zPU5G@OTu9j`LjO^TpS5@OBFu9W74$#Yb`kboG4f!ol$cv^Co@lEJbL<6C7R((YMno zg_hlFrvl`U_|7pkv1bs~8X!8jtDbh2))c@JMNcYz`{+Tq5GvcEaW)pxb0~-LVd5lnD%-tB;E}=1D^Ya0!1XdwN=P7<4GboaDs{7 zp_i*IL`b)p;vPbRVL^XtQ--x`IIr(_32f5-+7ge5v4MO!Z7t3e@Ao}B+m+u<{4d}_ z+L=kV!`Lq3RkL`c%=-&rg)vwrx<=ttSa1%j6f1PB>?CX!zd2>5C^&Bs#iC}bA}q1s zYqci*@(?ku>Vv4h5t8Ki9UjhlZ$aU&)GXZ||0~6%0AqKz6FoGd9@#dq$+v*Qomx^zqB$TV}(KW(x_S4)DE-tG4=KS|7NLz z{XQWh@D~LDViAC|fT#eV08m~V))Wa6zV6cec!_{(xG*!@cPdQV8J{se$ah@RDU$p4 zci>VUUvH{giqH0EsW<9RwFZ!92qCr|UGU`Ymw>IRPvlJr@vYJKzqh2(-uQ2NcY3Wk zyYGdp99v&_o=QFe!`04Ttw%5u`1gDRUvVGxFBP9>3s;YPk9toZeLiJAjt}j}XYXRU zIV&+6^uOhvecE&{`C5IN-pgMApN*epUvf=ypK@k$H$Kli0o_SoeP4SQlwb8vRyzf6 zdLKTU2$o-8UvHf~e9!K_K3`{QclhT%dwgBqy`RfoxHnVxJ{uh;=lD&B&}UXp<1br# zJl$U$Usip3tM7sz0UdOJt|qthkLr&vG4B}vO0mhk~ zfzL7DuKT?A)%W@@V8(9~IO%7Q>mct;Ku!Lu$+=2V-#036L}?~Lrt6UavB0u;9B8yDH~N9rHn;7|M~Tz5Y^F( zB$9g`5k&S2Yv_MkV;^!C!J<9}NTn+Ppc35}tw;59v3C4G6n^`F$TXkpjlGnMXh$Lo z<2bbCzxN-{GDKHIpzSW4w1T33(Dmu`r?s)GW!gZ#1aY15t({hLj!X29Q%hEIjHlPP zyZ@jvq+~I9jn15kYz=vq*G5n>@K_ zHf~2XJ@O>$|LZ~A=6I}9=pHo;V|WJszz65lBSsJF5Ana5t*!Pvt(@&$1;y;7Y^+!= zmu>uvpSa9Ys8~rQ@IFcg_*(XBEbRSH5Ta{Pt=J^2nh)c?yRF9X>p6FukYXr##EmpV zgD}?e&>$52kX097Q|qKCW>;qsPX8~6I2SoQOMB1z zI3uS*$Fjan6WM3knt9trNgSjCj^F|4rbma-v5IY-l5?V zgd~Vto1JI>vGDiw?UX1EvDwt7ypM#S@rozSIjRhHn0Ro4((Jn};f*nlabwSmgbe*q zoAkSQL%*5hq?i9qhQvbpm2gY4gxk^8^kMp>A7|R0**x+!Oi#kmt7Go24DWOyL1_lq7)@7?6{vU4t}^ z1U+c~jkvfMM29^P3}C8bFUu1UUBtKm;IuF8RT32i@!n@Xi0w~?h5Z{ykvgHDpxT0@ zP;P(!JBKMSO53zG;YV6HMHZLigR}3SN;-K?#)l}fdVKZx9!6$uD7bJLqF!Vn%bP=@ z%!TxsYUX6=Yg$tGcsN5y%@lr5Rn&~8+!b+p`Yi7L?msD)5(EAmP%!5rikC1WO63JM zlgYSH21TQ_+p^3hD6{f@Qvm{#(t27gg`}p1IzMg}eK8oEGrw$zz>q!3{JaV*cz*b# zxEPZ*Z%nD++YjwtJwe}8Vc@#dCb+M{#Qn%1@}&5;&4|?z`=uY(Z^{1c!2i&zXXRBE zuPDN+Z*c{UQ^l6_cYyOav-u5c*6w}awth!dKPz+yS50cy52`Qi2bwwLNkRp%Sq~=2 z1-CK~ZRp&Oo*!Etlfw1F;zd$hD}w?{7*P->JpJ!ci1AonKn?vEFP`Q)rBL8__k#_PQ$HKAK$vyf#$rkX<2LJTt95xl73Q z$o?-04sI?tQDU4kLdBog5imQ1(GYHhDLJx zM0&8>%yc7n^h9+rBey zS5_Qy0Um#f_bz)K*BK%NB{z}nbqr`w&M&?L%bNqLLX`WV21|EBnYf`!mjxElQF44v z7P)#t<1Qh!wUUBc>qtD&4Yhr_KJX->)Pc`+C2;e`X}((+NYBaHWOz<{cf3HZk?!BM zDlQNCgTuJWyD-px5Y(JKzvPC#ixB?I@>h_B0=5>lLh&35?Hm#GfI8Z$)?GG5IrYtV zbK`Sl8e-jw#~xWaFYTjP%|V~zEC@n-?G!!X4aWM>?kb<{aMD2`xdu%Q=mp^#KMefl zZtE++C(OdFH_>ooHbOWb93=h(WZ1k7U5cRb>u~}56%lzfI(;)rhGqEHS8Xmaq0@9# zyiU{Z*mm&`^rTy@ii_p~;fhdB`T^O$XOz+Zwx`kz?opuHFCSO_@TE1ltE-=1qqD8I zqp3bq&BG&vm^;_3Y3x_g;*G5U0y=u$W!a%Og*X|b61j85FiY@KXA$zIf=Z<72NC>$ z5csC2r3!UP<#wO0KYU%<0U24-Gq9r=)PgyoapP24S6A{#-`*X4?rMQHNL&`Fyweh> zr0pz+0o&ZmJMQBo1iLg!jn|BzsRK6O!8?&eXplyeFq(aUfu#n8()43ef6q=f<-fep zx4%*{49jzEo8mZ~pt9J?;T>N2!8U*v5}J|E;yb%IcWse8;B$PwD)qS*v}DY48xIRC z2*MuOCR+~%jX{C0@Gm-LjYhfyc$WD;@?|W-lb@O;((Y>fm-%dg1Y3(3jk#8=fit;4J<{ItKOz35!LRY3EIV!@y znkuX3V=83oR+E?eTi50n(UL8kz@Hhc?;s+*t8~x~m&Hwy^2&?~zU?6#2HtL=!jGZe zVLKaG(Oo!s`6L}$v5pZN;zt)=o$d@iz01==-oF)^7u`8ZoLYm;^ECs9g(X1rfz?T5 zbkD)ck5-+jGv{tPV#zpqBV~k_Sg2vPdkiFihiCndbL8n>_fCD(y#7%}rAV@Q$vW)F z#uzA+W|zjb)A`(CV2*(T5iy8>iu&+?DtfH^C5Tgvg)rqog?_=uHDK3EPHyl&Y7(!x z)=mux53SZ?CLf8aQjYlFwH=7?TJi-k?_m}cz-o4-tGp!Qb+SUTYhgZruvw$bJ)dZx zxu+0QqvA#{_FnDb{Z@H_(V!zOIMZM7143+ z)Kl1}A)-_?Pf>Sb2T@>8>NX2CWnGzx_U#y5IKn`{V$xN03@Jhe6EI5F{m;q#Dgnb* zNA@6zpsI}S*;8L59laM-_x%oy;gb5kTehM=&&9d-mnbZv<8|~z`>ll{eLWTf|8(!x z?dm#}*suELo}Mm*O@T5v7ggioQuSKe&(N4Ff$tD4WXPuG6h&v7&deklj(hmFU>*D> zDSa|e@AFTW$X3bE_GVu!4He9J#EsGowc5%nnK`|kS#^}@y(TqIz*4r9ilE2Xc701M zt~QDNkzGFmT=L=+wK5_KMn3Y@>gS2^YF7kV69MOi- z2!X7(!U*g&(Fam80mR63=C|WSO$3fq_Uhbyxt-JBCp~ISpkVtvi!=U@s}X|WbeO$T8#>}1 zRGvPv9Vk*pRsJ2-|08K=+aO#N85X>bpu|NvvsYKyKt(WU4QVmw^YtVBTLiDZtVMbk z(n-dAAr&cxf72C6bXnzfhyIs@7Md^%_8%lcE~#@>68Ab(72LK8kEk(ASW&G#qJo`i zSk!%-13B(}r%kLs5NY#kA}uk=xBQQ|wy!Tot@a9xG)bPD4d80ee7h_Sq?GH$?+d8= zr@gW_SGp{wLGX%Wss1Lr#lDpY&7uC5Lk#Ffg2cRV#G%!TY_2A;dh?85l@u zm!yu&|B(?*!B4$v+!TWF?`aGs*AjAHoMWkf!~37SNd%AYA07THlTwoYmk*4Y&{`n< zZ_SKWjt9W2`-K>gSDh?6k@y)L>+Qc0{2KWZ^g9Wl9x@n2n>=Vxlzv)9azqDc?)qTrGYEMcan;^X`{2k*~$7>e!f&Pl*X(_gr1<*2#*v z-TiqX+pCKr_6HXw+-FvnY_4vtU*Dg(B}C$6mY8wM_t@`IGCQ2}EG7#%SuA9Orszo# zv6s2n6^=sE$+ME70rzg{S@wHz@BG&;r8|SHH}``5t_v%(1cE@amz2#h`RLd~l>{iH zdd{DmciL*lvGV zB~oF;!+KAvAuTFltR`?UY&7aqkGj(EdL!%c;n*Zbg#+bZ+Ddq7W%cR9=MTTkJ}y+e zPdOqQM%UIBeV}`F9~lwo-O_$$o7ogp5y$gZ;pj&@pQU*bwPFT3Zaux zS#RB1Y*@%AIFFxRW2lrQJ(4~n46)gKyd~TEfLsGh{V&_r}(epxB8U}3a~w_bz9C)i}55ff8_v;i)GF$Z;jx9iI>CZ>1zT= zEzw_b)tdu4?Wq~I<)Vm7))SNi!~2u<4S8PT*XEmf-o2d#1E|gPxd*jlw}p{UG0M8F z66-lfLzFNugTue#HmiU$=s&Ilp0cz98s2Rb^tjD`ct0N1PFN?DDMBfL!e$<+MD&yz z^Ot(%J}3H$H@IIzI^Th0H?>+CrM(M6JB{1DM`x8;ITKJY^-o%$kGc4Ubge4o%tOO#a2p-;H@4nF?1|7EX-{^2| z>_i$SDF;p=hVJ(8jr&VogaOT&z~d2}N;Z4@&#P$x-;DHHF9V~18MJVq{_GPH2M6aQ z5DN3oq|ny;OtFP%G!;gUC1}h5caI56U*w7gd_^Rm-rVflTs5%PY70{FWL zNL22W9fx<#mMOWXu?~4?3Q%7qP>b3Jj3OawtxeRm)t@=yHnw4n!2}D9TI(6`YVw3? zzD`ljUr?mmj>8{*xhsu%@)8PmI7e3<@RKq%Zjc=Mw{e6w z9zSO4F+tcFkJdtWJ{2D{A3yObQIUVmMb_8$_|qIhpVWxiWT&78i4_E2?_@fK^o0!e z*#_w5hJ@)tAMtr~xYT`S?rp*r5=oumDP#(ArraZMQ36rkkZE|Gq=q5RcPyM~4Hu;CEP)(lc4T*$0BL_p z@NQ7;S8?ccuUF4XC>T+Tp43rhY(ScY?4|OFFgB`w>);7VE+APgmxO)l(mQEU4hcqL z$=Jsv9x@{#)mq_lgq891W#18h$m`|!6V$(L^!nq4+j7-U(XsEz>y8=2fSl9=n(ARS zI7=HLRrnwneu+JA1eSW&lZZh%mp?u^=-fdftRp=Jl`4%kk$p&57`H4PgidmZj)X5m zn4fj@rHV=-MK)gmUVs@-5KJjDAAh$XBdM8C!pyhZNtx+9ROHg_XzQ|}M1Gs{C~~;a z)qI;SK<_QTku9FSi=9-k>wfHSOecnpQI%_vyJu*L?MUu#!zK)N2cUOYVXj8V4IntA zqb%9+6xl({42A+#Ks*4Ga)8U|;*q}G&5V(f;c_%|m1+yob8AkcyTICZd82k>G(~;( zMc0}^#7I%+8jJimHaL(f65|3md&v*=7IO9F;8vE@yK$Wzuf4Dwuw@+<)8c=tPGbV@ zqi$NbPmWe7LZ#8FOC<^COW%*>#;PPIC`BR`5l*Ozd+CzoGkKTeOwe2=T1?!{OOQVM ztAj!R1fQ`FR|_oT(l1VOZ~2WQL5=$2;!-!t%^)oBVqWI2?k$S$?&-E>1$FS*A4ie` zjD^fF%3UWy;(Ai{6tq=ODCgd0LU*1>mT${EqeJR&GpOXL`q~UZ{43lZ2SaJ}}4Bg5EAC z&4aPohT=zkt~)KB`+INF(I zhD0Enid@4NK#9@);cc?zOL zEOoM~uk`mBeu}v{m?3XAN~2ol2_l&?Yj)Zs3NZla=vQRZ?ezo5ur6nlwfD!*8#jk) zv0Q7Vk*>hBkBeJbnzyq+y-`xwJk`I#f+_*~bS~>S^08~4^a$)9$Z;IF?y5g1BhnN^ zLLr(Hmy*elyB&UiVk#sK$16hjW9qwM{n`@W_G~>oykW;*nen$)*b&q7eh$@Y-vM|` zt%+k(0&nE6G*e!|4k#2aehus@{UCSEDuXCNaM7>#vz?C>r z5pD57kWxU`<3XY|VhMjboY^Z6MrTlYME61(XD~lZF8y;#ev!bo_pVu)hY^U;%Tqji zSDAjYa6(Z2<_?D-Flu6r7eN-qG^APe`4)`p`Io?ne2lxp&BD3k`F=EpIU`=vRA)Nat!j5>V?K6qW?eVO}RZ?*e%s zd>5YvF0$@rqX|QxFee|a1Lz9jp_0!s{MHxtnlkzrja*(53d20KQ!V;#oL<#bo3hmd z=nT@p3k!RFMoqrnXY0;_`7x)6;q_?Lw96SO_eP@8X>q1Ji-8YzO;_!(6IjS>n1C-; z(OvSuZ{`jKxPl;ff8}K>n4w;bh<@swY#*`#6}Q+ET@MI*oPUGj%n{s_?mbl(D%LI|*S6 zmLaTG+y3HO4!i#_|G{yr7U@p`51lz@HCSwT?G+LCx-FAe&I;!I=GsAkem8R3J349C{~$M4MzJQ%cIlVw6>I&{v6^||6Ow*4&ll-o{VdQCru4e_c<5@n(i z8Qu;fG#Ssf`>WG<)X;$Ifx-fj4yq1AU_46@Aqbu^VgFQxva4<0D4L;w#{+b70w z007uL(~(lcIeR9rkkVL2+9oJ19%kZcL@mxd?azMZ{>!7mS)*tdd1*2gQ(gJ!9&6Dm z?$l>>h)c|lR=jZau;wbhs3IZRbATB|%*&n{tZ2|474Q;X_mXKkq`cBLaV~$=hu4lk zbVJ4ItcXgv#wK)`eRTKND_%$F@kGE+Nvvv3*a;0E-!yyE0dJ5A+^eoj~!s!o#FZ->rv)K$LR)vs^gMPN%+!5_AWa zpzVf^e$MYF<1^{=8hQyuCu0Y4^#yF1g_`dLZbLWn7yycGK9grdSAmetMui5Xz2Y7?V2B6g2ilszzTw^xf`vuwGKgY(r*p z1*avz@3o4?1le0ZfJ3sou^eSgn0UCuj8_w0gWscYgvRhqI}byLUE7ho*%QgJ9gbNx zUBU;VpNeez7c2mFM>)2^mmm!cBHbF|NpdGIT5Kas#zs^;T9c#{$`_DnQy+#UqkAB1(uIkelt7}yMG|1x5 zio-tc?&9oTry^hi);(ks$r8K7x z6v(UbsJ}Q0F3YM3$r_^O z)j39PP6Rw}{kGdB?QrxK%Gor8{~Y67dqKPh1%5QIe$a=&EAQ(pRhdO zMz5YLk+0At&bH`GXE7&{sA{Be4$&kL*)a<@p9wIyqHjysym%^oRftyXSgE|%0QH_yUH14Dmuu356`k>rl0g~ z6pnjg+dOmTl2|Y!o#rZHI-Dn8kEi-=eC$?82uN0t=ETp=!{3OBQ*9dh%yw>T7EBq3 zo_*7Z%V>yBGG9f~0)7@^JgzaaR4za_3OCR7dPE6btE&DJ0X3um^x6R&aN=eUa%~fa z?Dr8AOx)1w2eQ>0%NOC2K~b^+JmDnR&!##zI*-#+*aB}RVe^c1O+8XOjbN_%q4*BR zdc1ASEV9r)&&0N60RG$Fl!XhCP;oWHQqaegQw?oWz)~lN=TLxaeJ{sv_Q{R~q!UjO z@Vg;Fa$Ctd8Pb=@%Gf-;)hx$XR|H@0&F zv3!M6)^-l;<}*>4=iOe3a)qjmb?9&HKJm4(=oKIcMZ9XR3A4dIBUs3?M?1GvaEB%Z z=W?JaVJt&ShAbIcaSj2hEX0DNjSSc4wm)`R5n3<~wt5S&%+?3OuEu1qGhZa5lq@fZ z@I>%|&4z7qsh|6+3#lvU)5qLY_cQ`Kr(97u+fbMrARp%PbRA6JG{K2jo$KB#O$2=;_~vVH!t9lO)^_ZSrWuTe_XoCO#i`@^X)8p8W*bep%5*n=_7GSB#Xoz%xj5Xc=EpxN`1VT$Fsn6WpN=I| zx0AAddxh1rY3e#xpKnXQI7bw3LiF6CvfZEkmJ3N5K3OL04aaZx#M8hffxc6{p?USU zHq8B!f(C;1X+%+3^6MJm`*5q^W2KOmM~74vHMBhCfAZ#(&P6;WBbIN-n19h0xA`4+ z2|rPbpsQ)_+ztPS|H_;z3YoUe!SR@zjc~_9c5%Y2ndOLwgW6d0w%|p4%y#N5N z#X2R!u$O@{?4+`95>JYhnP^hRg-*Oz=CGJilht}U2{`@0voG^RkU7JUxgbNwY2mQV z8sU2Js?KK(CUA932a>q& z+JoqQ;-IkCOZ_zr4iQb{fG4z{gie1=c?wau{6`7G!pb{t&I z5PXz9t*%`+eky-QHv$skJ~7jcR#Heuc-JOA!{$puIRzJaP(&D59D$*#{BTfP= zh@?-v6SRF{PFFo%VD~2gjhHegOe(B0iFbZSVu!1n)$Naj?Cv9yteeI)nriMp0zawEDVwr= z`Ooo!##HfZLZA_QXW%SV1ndYvxRXicu}{_QFxG0g4#k;IdN~!Cr$qiH8p+x%O3$(w zZM&KcG@s>guZ>G#ORwl(XxBxhLhe>rPVJ%vhKkxHW!p)QcLj&PuL~~ ziS{0(3M{dae`R;2o{T~oX})~Me^ig70wO6>2OkW!##*j{0(*m+F)eNLR^TWg0=Vcs zmZ!qQ)9ZG5s#=?QONSDeGr6qpe<)Z|dyO3>AuAFjLbj0IdoT#jtyum%Z_5c=#+OrK z%y;wwG%-`N7QpGr5Be`aRXo?gNRQJwWuI5Lao{37viVMd-d|-JcuN2y8FMN#kf&z5 zEB78AXy@&ru}lj6#18=)$!a-qxX$ZaO1^J&tio623RG*~zY}Bv`Q1wPd%C+?s?#c; zd)2}vR~QDxSv}2sua!Gk=D{U%)w29A5R#T!KN^Xc=n298FsY6mYu%pjHXX=L5Qzw< zR&f5{S%#=r5tpELy(!=4r-+=zRj5H^I1>A(X(u)239ByYCSG@ZjLD(Q$!5+7y}E@h z5NO=MV2_f^Vxrxe{>Fv>O-=V+*<;Z@M~k00XQ{AJ5(u=Pu>;A2G@5-adYzYirtTE% ze8U$Bq+CXu^S+Vt{Y5HhO`lOQJ7ziC?mMA~MnwU4J|lFs9<%{JSLEG37fnmw79#;4 z#_k_CT`q4P%F!E`b5qwQ!Vi@AQn6qUxef3lQAs7akQ!wK>;j6>wgx&JJAFyu@qCsh z7qf2Hc{IpXjm8pv5y)xfSB5TxMC=y*xZ>qL|5xkg+!qPMM&WFmJ7KHM zcAIV6Ha6Rw+HBjly-lvoTU(oFvW@ro5byoh`3U#PIE$5K5tm zgO~r_1YJCuYKyXw|K>WQ2f;S}^Q6%2vPe$Uwc+kb*JG`>TYQ@Gb)4qU0#lsWLjvIH z<0~hf$yO?dQ*qA$aT5}}wMx%BVwNP%U}MCVg^9vx`JlmN*x!{+s5J7gB@%8T!7tkb zYnKgJFk}yB7J|x7F5Lqw7yH|$u*8E~=3*6jH>GcMQ#`HX0xpWhw&l#hl+nu(Pc?t^ zc+$z1wV;in`BP&yv%^bl5q>3TNG+h4lDp(^AbcG;C8Xp@PPnkOT%Dy4yoi$j*HR)E z{`xS!Sj7wO{IRRVx=t9kll?q0h`Jxji!*2x*>Ux;0{e!>9Nf9p6>E}>Zjxer^S$yG zFyDzOXx7uC=yOwr*P(jTN3gipA+#S*T3;$;O;@o_DS?H1^+BuPu}(Etw74pequ;~m z_c!o;P8Wh1%ZgGIW*%7o5(WgOxv|(jB{jvNXG|+}1BmC$g9*tnXyHtolD~dL;QL>{ z5~%nmDz+G&j2YXfskB8i70rr2i}q@*Z7#)V*%OL#Upf-pbA$u!B7}bqYmHVxsH}4| z6RfFXW(HMP7wqimFSx%$X^cX1ECj4`|CW7{n&p!k47mG^9%~gguo!J6#hC}Npwo&i zK{R?uS{&}8{_#TI;jzr{Qr<{H&=Xv{Oy>)HbQrr+R$%c^3H-D$i{Biu{JY&mRFu-yHc&}!x(=(b`HJg-ftMQ<&G>s8Zne&;Zw zul{Tp7>h+kZ(BM{{g40tp1W`+m?*=4a2E#$m`&6Q|b@zg~w;VT%JWB{3e$$=P%TAPxu{)>Q_Dzb%MZ*~h?N0CIx zb^5pJ!&`Sc1VE4Ytwsz(6CPZLyn~IN%yuwi@^f~V!lhK4qX~+tFzW|nwDEuLkcDY< zHA_Fw8(o-Ghuy2IR;^iYQ2u8SFiiGN!nk8OH~`caI%05G zG+ntw`3S@Y-zPlgaR;gQ@c_4NNBE_PAb<{(CZ{Gyvh~i~wGWLII*pre`K%4Gt6=+5 zYf??hROtK!*x;wKQuBm%~|fqBW*mV4>-npoT4avhY%K8y?#z^9zFM5zNvYLG7U$VAA{M^>(V+s&yZR| z%M${|4$anKCiISxI*@3E&SsT+ic6v_oMhiP4caZ&>ZvnFo&+hSGv*eFGw%W% zj(tQ3k!LY@)#CBhd}i?OO&p3pc!zW*OeD9*L`Rv*&OWXYw!%iCTr{G9@gCoF_@jI% zLHe)Rf32L&bN}6*IS^UW9T4{X&m(?i#Szg~Jrl1(OyZk$Ss$Od`sox>Ig-BO^*8nC zf3)<3S>6e6%rW&<;#@$n}7AabY_B)6ZXJIevmUuC>0 z)`?odzATkb2^-1C9S2*KBz-$ziA;#_TCFrZlocqtJk$n5Z7>Et+#`kBFEVcQ9dkQkX!N!fO)Gy3bRnKonmnO;5 zY$j(~7Ac3_{JT8lI-|ea6Ri3i^~+}w@KFR~XT==(i;nR}rE6maE=g4lg6EubBP&kT z`#IgU!3f!c8ZE=8W*6SygU6lPTq{H1%3n@4urBsVF!UmvLToI=L%MOw$Y#p>dJ~`G^g87kE=aSJ+8ocQ(O+~1Yt^Y9Qh4c_!(x7r6TTa{BS8;G+QG1 z1!NgFxYxmN<0_Y&?I5k)OEli9K2D$;Q>hTer5CjJEO+R}C%}i`V-$qX+Z!m8s+j?m z*}6Z&3iH>i4j*cFP_ctPYjOT#*xmT@t8IUh_r|+RKY^j?VhL{iFVYQs*>eL*dv<)q$d3LT1mgO zDk~+|UK6l61f;iQ<=lB&kAmng2{Egl_LfaQNo$j{xbMXyAy=OUmEzbeoLq%s#A3mV z#I7g6`Dg6a!hc~&QRRAD!SXCn($Sp8FSdW>FEr_~z8-LtE%q@uS8R_{@TQn=Bz|Yr*U3{@Q3E@l z0Dl~p%p)5P#&}@-Epkv7ZCmN!ulA5QIQGd0hWj)Gd;gr#I$)Hrcjb$Z;~wi6D6<7m zY+QtL8kr4*ZFXNZjeF*gXpM&O?qv^@quIqQRmJCXjhFz);Ma9^Zq29W4^nTu6Hpq5SCoK zhLS+pL6n(7XHG@bwja7VLr+y^1?s$n3Cu^p`%YG_KOchHhcq{`$5?-wQ3LG`Y3XMF z?nHQo5R+=$pb+Lv4S2wO&-s(hB1CO%MV{^Pea{cIOT(+xnkbo0ug9ngwzj|(qf-8~ zsL-AN4(ilXEsAWDE2-mW-fsVpcjmINSt6z#LO{_r{l6k^PBsS>UJY-qV8M|p5`*)k z+q)ZWSeIi2-?1foEsP@Bw9o|(=tF97p2^5|*1bH|4_=WQp{*oPP``o_%%3mU-Tk3g zZwV*11P?9+L+4cxV~ia05V@1q_!_rOb!TEj*uI0*rOY}5W-FNc`x0}@i_;app-u&i zO`b!f?Yqr03>uz-Dc~>{LKX#NZXl$amon z?v_>y?ZWt-U8DR3n9rcaOU80BDClPdX0xS}+6A69tD6Ry!cLgr?6EX7WwiXSI*rYt z=Cyndk2r;436w-F*?RSG#zyS6WYSh;6oJJK=_? zz87Jd0bmoDxkhaa5npciM;117oEHUCV9a6Fb00;1vv5R&cTaHk=G-K43mzwr!|XH*uZbFcw1YMReU=ww|o#{A$uR-yFOddWu&1rD90- zXqUmxN`^r58O5LP?6(tqt-GVwsxSbaB@=G&gU4bm>C9h9fNlgiHhjySeno5Ceqs>| zYJye!_6Sx`|JQ+F@|dZR8;1>K7QE`8g3saQ*Yw~{*`TpWIP>2)4b z{mX6bD?Qx6Sf%X_npfZ&XgOE>s1>7A;U&Lryjbst8{Q>lV>C8=P%h*B&e_-u`j#Bh z)mq+NM%|jWw2=e)>Yb)-9K3n1`cNEqt;f49dMIJUE@Iq4PWn2FbKn;1TCoAEi_{(V zbbMvnkC&#Z_s=hgRd)xTd9*?%sl~Pegrl4A zM*7W(Qb(zs)UO7kUgDdi zhswmBylVtagl3m$MlyX`Q2ZYMFnL!69z5f%Xl^=87Ftz&q<+J~@6R_*Uue$_HGkZ3 zdI~vs+(6UEG%IxTMZ)UBr z;WL0FwQ=Nv4Dr=29zeHWPzv1zUR#xKMl$3kWv=|@mG@+f3*BzQNE z*c^4;tbfDVrL6j=25BzHqNTzLn>3M{5~Hh68~%P{p@QhjC&Qt`ZM`me^bw_fFgWCs zG8z5LGrJ!-S6h!R3%SC`$V70GPDZK!9lwo5U@|olqF}<0B?<4?9}-^-K9ll=FnL>1 zB~;3Ua>C2LF|WkGoik&5QCOn#+U7Dcg`1NpL=NFyuWL+vZh2m#tjcNlUzzgKF$36x zfu{Bp>;h!c5n?_8@l-T;K2=cO1m4M4P+GGQNA?#A< zH3>wn;&zBqag+Y8)@w)}irL>8}q8Y%O2m&nw%|wN-pJGdpUO}eHJv$+_?~N zTf=o+f~gkkFD{nU7VU)YTZ9fYY13njw%#H^f%s|m>Bi9^O4|Sqb`yeM!q0SW_~X9A z=0s4XrXr3W4OZw~vqciE$XfTlOuI~f9G#cTT>kw@Zl5GCn6uYicxChVg9egJI?A7w zQ-_)m?v-kVO#(jrBXjoxd-Xej(Sso!pe)9Hj^wN~0FL#?mqkKukquS1fbWo}ty7Zj zk9UN_hplK1!dc!^dm?P1p2z!8lob<|%(4E>N9{cF$+YQT<2%3W1sz$giW>jDDxb*! z3283dOPd=vN+A6!gQidCo@oUh{=qy6R-D|#3_HgZ6{_tjHSM`!j*Ei zeaji)8Gr9Xu(_%fN_U8xg9adpeg(=A9i)7mpt;`YgMPL-sZ(pFvWLJ03T)to66TsR z{Gqcfzg#4BOBQK@6dr|iGH{eh%*OOTBv%oPA|*jFv;MUCqjAX=KscPj{b4l z#?o^;iyG+i@wni<-(VCS&qOUbfy^b{Pp1FhI(uZ9*yE`kKQ)3+9h)fB$!>wJs=AB` zJg1rTa03YqyZ)8l`OYn_AuOfS7()9KCZA24&{cA1tB~I;AUsCmS~h52aL4b`40mfQ z)14~DS*D|2P=W~;{$(8+j`TmFw>AT@B#R;)?yL*lR>Xh{t(PxvtYpJati48(j96_| z#7-%GwV09IHVzjiYVlPUFWI1{_mgV!-nzb{B=|-v9!gS{_no7EL6-J<%v~`ZtUu4< zBkr1u!o%eHP%^s`G$x--nh;3!O(x`Kzyl$a^|qj?hZ&T21!eiVdW(aq@!X%+uOikj z`LNt`(f20%a$f%0K@vUQl+^a$BP=E1F$^PB;K)xyMQAB^qYI1mMG$`>QUY@fLFy?o zOt@=Lg^(f@!TfqYGJDZHK7kPc%iAn49^sXJok-_ zNoHo?t+|iSrmW6q5te`#_S1CT*&LnR3JVL+9a71r)EL8r$e9u#bG(EMudZFs76fj6 zq)8Y~nmz$dEQ|w}uD(%E@?0^#L-`2i4iXeA!W1h)JHK-d5oI!zEZlnPJdW$Gd0c?E zD-Mgh%C|$JZqzxoz2;|~jbC|1|M59+5FA))gzFwgykb~I39#o}kX~JGKotkLjEGyk zTsAUcT`ba&XKEpOMjNjt@tE)8$`x#z+ydD#B>m>5Av0SL@jH2`GgRO9B-YTL9u5k9 z6}qyEKKDE6dqoPGFsI&W@u#XDMkM<>ew{I(4h@cl&!B-^4}#vl#f{uJL{6EGDvhu; zF2ulj(BQ0t+3DnWs~r?V?-oNQW4_wjO^c(QzZAT2{qgVBg2UHpDrgDI{@_NX*Cn1| zwCcOD{lM-zC>VF&aEM$Cmwh2m{M?xuh*3+8VCWYpAw#G!!-tY1g()15ew&u3O&XSZhSsoklc zrLX;A7T!@vL;K>S+NiF(QB+5Z4S|~+{=SG@C~0;8b%tYeV>hg_!$rXY1$D&?Grnnx zog9S@tvr;}tA4FD1+u(AXMpBCIGovdta)?STigo(Zz z-fej2dkf&lJ_)(ux}Gf)r2J`jLkLwP2|H<`RVC;Ut|*|frI&T z2Vn7K%iu&0`3aeWCkvw3>4C({Q&8P^xSKN4}eoY;?zlO*{D4M6olp%XF+dTa$-CYENJ|GFmVSh6g$H;)F$Ns ze$PRUmB4mgIk{gsN@6NyQY( z`abqbEmz)+Qf_9q1u$OP2l#)i7{CK#2*iS1x_4HV^`OF z5+(IQ`t;mlmfp@iQa$+Ak%j>ir|X0^YpekXcYEMzY^gv|J7rR)e!^7<}E_boyFGVAEKaR za_N1vR6)k=#`erM&qucL+2Tme^TLSQ)?GAu%RxcWBC;8_p-2NMW((gjTg|^1qBg(N z%G2}udhoU6a_bsqVw*ILFk;D1aYZQH=d1ewv+Cv7g6&DA>)O5R+oZnRuXPFeRX5sE z7OtbXO>vHgG94bJlq;Wef?i_F$@(yrZEUgnlM1(_*LZm9L3b4q6CvUKRd2gB9p*<( zcmygqlq1W$({P%)J>F=*b3NW(1UG6!tF9bQZD^IVz&Xn+ml*2+lyfgempPLjQ(f>l z6AC#%X41;yuQ_gCwG%^;dXD;?2&0V_#qOj2DcdSTK}8|XU|eyiNExvfAOYTB{!3P zCFp>=!PU|kHQ;xv9Nx>}@JHR-IH58HtF6LGTa9j^;_u^ohhI2EW#7um`b*f(-<7_d z=0FfcWQ#uIN}tnykoY*RgDKmLCkU1%hO?ehm2|0 z+vl7=b83$44pNn%9)tur7SKwcO)!$OO|qvE$&oO%VI+8}pa`dTv9UXJ)!K|e{tLBi z7nD?FqTK}{$2umJud*0Nd8ickOgg7KDzhD8OEG#sCfDoZwHx?O>Y~X&meo{Pv_~Xj z1gt+DB8=5ATyF@(dzyFlDMh00;~ASE*P&2<1;~yWi6No7ybF}`&+ZXIEHW@oUFwVe^pjTP zhK0UEpn#?#zuP54eM#WY%nJ53x_?C6E&vQnr0TI<=s9CSQ^KL~x#YEYu#-F;M>b}s zD-Y|KBE%=xb44#3qURyAP_15q%UX{03VpF$X)5Yku%whmZ(M~PC@j-X6HH)~j#GsJ zkLwHtVrjMe@i85Je=Ii5eK}1ceMZ<7K7oJ*v8OVW(z6FW>*~E#9b4tEw!BvzOf^K& zwo31&*DWC`1c9lLcrjl0&-eAYP{u{lSIj}uGAMEe)v>6_=MgutzUY#HabIhjPp%lQ zgn8c(sP>p;7y(JvgLQS%57A8luRJ}AIunlEpa#B^7Mu;=kNl>&rvHp4Y%f0Ok(S^u+pLnXS z1G2mrm-5={`kzSGKnEH|cY%?h`=$GsNxIjWq=SKE0Heb|VvNq%I)hPl#sSZ|>mGRS z9pG7K5y!nlg!r4Ybwh;Ev3uuRXWvkD#sISJf`Q$-7w!VYZru?suhL1kbVN%hoph0N z{LS~L!!nGHf4)OkR|mrqhRV7^U1gmW>P&U2BkI^AnyTYiuxbZk;ix#?ROoDvTLN|q(DUvOk=*HBOFHWGF($AMKDj(5FhxM>J?hO|@ zUI)Wk3>$Pr-3FZzb;jvN-@4JaF0ggv5zW?d7a-5NxTfnUo$3m89&fm;<8^_kJHU;f zbUS~XlukP7PJgQ0y$=6kmlKQy-Np$1?FS@Vd6?;N^O8@`>07*qoM6N<$ Ef{6$HX8-^I literal 0 HcmV?d00001 diff --git a/src/assets/images/settings/menu_layouts/horizontal.png b/src/assets/images/settings/menu_layouts/horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..ca779bc78ad5bf45e4b26e4e75e6481295383369 GIT binary patch literal 409 zcmV;K0cQS*P) zvbn(=z`lWa1n(axjo^q~;4t&bS@^%KH`vFc7`p2Q0I=EJ`>gf&x{iCB)gIPy+H$?# zI&Rh@j<_p{ds>?h{E0Z?i2HPk&-@4Ch&w}E!NafkSzG;R2LRxg$z)yzam_;Ieh_DF zDlv-FwX;E-%i6gQ+qsC>eh{Sfe@zd^TDtroDA!A5hii^ozT^kui2GyQ`9Fk7vFhZSNmiXsv;oD2I1{Wo zId{pb(`jB}R@*LWXIIUn~YTl0Pqhc#kP}ccEGly z+kj$coC&s_oYRhWw4>dQcKbJ=%iasfw_w`=03i1PMtw+J%=}$J00000NkvXXu0mjf DJf+bZ literal 0 HcmV?d00001 diff --git a/src/assets/images/settings/menu_layouts/mixed.png b/src/assets/images/settings/menu_layouts/mixed.png new file mode 100644 index 0000000000000000000000000000000000000000..c82b58038fcbdab260ef4df85cd0bf53fe232ecf GIT binary patch literal 431 zcmV;g0Z{&lP)Yy$(XK&WqF=)bm5 z&;o+?zlB7C)K}fi`)JgA{>Jxlc!KXfgb6|jAx2Z&sveg*c(ShsoP#IVp>AiIo`88@ zr(tz#nx24pU$**en#2z4P8E+W)>TaYs*4?@TZnE@z)$QW={$9Pq+5t?5s4t(vUL8d zk>BD6MzrHvYjymk9q8Cd*CO3GT_5SB%UpMi1j=u9n@Hh+b*4E&dq@{atWMW0ODCOl z4eTM7=ZGc+P_FT6@YfY7)!;zX6{(>H2c9mqfCEieq?Y>qVa~er;Qc|m&m&qoqK^6@ zVBN$3Du2{%NO?+K9&mf!tI@d6TsrBbTaC_bh#7oGv|&;J<@f7bI_ad#=vP1p(n&`P zAWTV(SImLAJYY(!i%iy}>sF+bPPzt{A(q{UW(H7(@yY~TT`{;`%(2Y&+R=+kRYC}n ZKX+7z8kqwQRWJYm002ovPDHLkV1jqY&3ym> literal 0 HcmV?d00001 diff --git a/src/assets/images/settings/menu_layouts/vertical.png b/src/assets/images/settings/menu_layouts/vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..16e942b0e9f3919eb199134692e32004544b280c GIT binary patch literal 439 zcmV;o0Z9IdP)(# zD2kaKe_n4u2iGeYpx=`+HyKd(2SbJL9}%}U{@8`CedQuyi89i?#7jp@$(zBsZj{qeqluEicf z*6Rj#SRgdoBZzCSqT|}D=pbE-bUD@21=xhdeC5x5a>aJJ;D*%S|aFn{?|8j z&_So{5lo1xtA1;g<9us`n+K#rx>Kg(1@+SDdjvb0&iU3z)8W<#y8VnK9qURScO3WW h%8Q3N0002Cig literal 0 HcmV?d00001 diff --git a/src/assets/images/settings/menu_styles/dark.png b/src/assets/images/settings/menu_styles/dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e1653b7a657edd6e2e83b8b479bb1b4af4620e15 GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^cYt^j3p0qNwsRej;t%i%asBk^tGc?@&!0c-?cF|q z{-!dsl@Ta)!PCVtB;(%On+JK17;v}*%3nHAmJs>!0B^+Y`oPQsN=8b26YP2SNGmV+ z(<1Wuz(Ft&-I2P^pJ{r#^(-^z!+)>dS^035-TET1?9HMwt^CXP@rda`@oSCvV)pRD9`Ic+V>RN|cZ9-Q}41PonSK>R4CZA96Tw@1diy fo(~{Er;M1%fByXW@#Dw4 zckfEI`&$F0PIK_}Tq&4}k`{J&n3>JYDATM6ZXdH1D1|rwka( a+00#Z%j6q-R9HLER}7x6elF{r5}E*&a(=A< literal 0 HcmV?d00001 diff --git a/src/assets/images/settings/menu_styles/light.png b/src/assets/images/settings/menu_styles/light.png new file mode 100644 index 0000000000000000000000000000000000000000..3007b99ea227f5f203ad309d28358d95aa40aad2 GIT binary patch literal 293 zcmeAS@N?(olHy`uVBq!ia0vp^cYt^j3p0qNwsRej5(@AMasBk^>;M1%fByXW@#Dw4 zckhOVhNk`~V+6{c^K@|x$+-9SW+HE^0gvlLR`&CZGg~7o#6IM+GMaerh)K|XF7e~v z*2wU>xm~svV6cO!eAexM#~%n4hWb7dn;9*-?$zE?M-Ku)rRC!eF^lI+S&sX@aO&C)xGbBzUl*1H|~cH`T*7S|L8+xoqd^+pNbuX$T2rdEOtd33n=<}rH(r4Bw?X0 z19j9Zq$jx5yQq&=Rv*u@&uV zZv5wD2VJi=A#|57GsIK59E1=t&(Kn59-)r9I&_O8$G{R>i|A%Y2O}3yS41Z&E$H;i qY~iWsL1-c78CvShBh*n>SoaHFuY$So!<9_{0000$3b*O46wRy83EweL3E-p zTDPTC^?=rG?@;xC)#cYaJXSqGb<2^89-z9fBYo%rs(T&jp$Dk$Kk9+HS#|cPcrJon zbI(i6T+zM{&>wNHId#;{s7qL=Yd{@!)cq1mSEY~blI|(J>{oO}dReaMR_P^Q(MdlO z>i~~A@u&cbFmcTrMB15WsH1KUUF~df!d~+osw?&ewtya>4AqH=XPy0+o=3$lg6Eny zh_o}$P)FSyx+-_rpi9hVN3Lj_1$lsMGW5LKxX|5xOc$eay9h37-XPM>JVPCIbLeVE zkAX{UAEGNAI~aL@@(`VV>$4IoA-;gkJ&1+Cgm8pmlOdPn{^_RrAUFTa z?Nwrlv;qeX95`^`z<~n?4ji~TxhJFJb(>akZP2?200000NkvXXu0mjfdG+rO literal 0 HcmV?d00001 diff --git a/src/assets/images/svg/403.svg b/src/assets/images/svg/403.svg new file mode 100644 index 0000000..68790ad --- /dev/null +++ b/src/assets/images/svg/403.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/svg/404.svg b/src/assets/images/svg/404.svg new file mode 100644 index 0000000..48e1ca3 --- /dev/null +++ b/src/assets/images/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/svg/500.svg b/src/assets/images/svg/500.svg new file mode 100644 index 0000000..512429f --- /dev/null +++ b/src/assets/images/svg/500.svg @@ -0,0 +1,5 @@ + diff --git a/src/assets/images/svg/login_icon.svg b/src/assets/images/svg/login_icon.svg new file mode 100644 index 0000000..4beb3ab --- /dev/null +++ b/src/assets/images/svg/login_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/user/avatar.webp b/src/assets/images/user/avatar.webp new file mode 100644 index 0000000000000000000000000000000000000000..6d7234b9eb1d4962efb9d01a4703fe4540d3ec34 GIT binary patch literal 2130 zcmV-Y2(9;0Nk&FW2mkB(|iQh=J6I zyxsF})jWZrx1zAfq`;31uOrtG^aslZ++-*oHbA=Ws=r@BH$nEcT>f}v8c<1MaH|2|TsFVAnjLEj8 z)v3~&1Tg$gPbv>1OlgM(i_=4U(n13#54;Dp%@xYc7c42<_;HI{&blw#e zNxIkz>nHOh3a&_eJ&|2SelyaGrVT5H-&xr3qchs0k*P(yrJZP27PwoGyrzP(HQNq) zdnp%e(8+0Kvd=#P@!j@e@d-;H6ynU*1u2;INSHcM)CHM>4FQx&W>lUOt}HM!x%Fka z%X5GL{_uBtT#(I|h-+J>-VdN=0k`87mNLbrC!e0hz)eum&xzb_(qj;)6C_`RprQp+gHMV1x!s=;2Rb%2PsYfg* zh??Ve&~jKa^pwSDNm#|Es3PLmb7sTuZ(x2P^RA`Y&UPQ||EZaGfbNJ@(YlYt9%HYh zZd6LrWC~-IbvBzuEVN?AAJR!ig~P0-MlXvR7Z7bx7?0-n<*4=SW=S5*A~>8h3`T5! z`I{Rw5T_p0VkFU17Z&8dG9Tw3V-0ioX90$ba-jE=Z$T+Ka@XHj^&Ah5&3^#B4xzV6Zt7h5hW%IMfcreWr(GyA?@Q(v8ZjmLibkv4Aj2n8A)< z=`*~B`m=?dsA{+ox>qZp7zsL(cLHfKRXL9|zDz?~SSeGwu!UZyhdt^PsU~45yW9Oj z|7T$>K9wSeG)6;}llOZ5%}(}z0=hks13|B$4L31VdOF*sd@YU?jiH_PWPK9UZtNa*gq3V&9l+*yS!e&SOs`1NHt-i^1q)x3)% z>It^qg|*?RqvUXDdRScQn|t^Lu)KXY5VsWF%#?Kcx(iSN8x!^6ce+4JmX87y6y?wZZ4&b%iwh!QfdXkYoDFzcx@r36&& zo2APee^J><5DkZ2e=>DFvmAxNih)1iwKOrXu;mfGI%PS5k!rJn1B3wHU(p$^yBEHx zq~i|&7X}r0s20A5acrQR9#hvK2Xx=6c*tFM@J9UuD8ohc`RLDsu}deGD`*`c;m8QS zgCUifRD^>%<4S8#^2TMADqw+gjHZ>sulGW1(}gDKwF2hjvQB>n-jtmz#9@~4F*^x@%R}e{Y2&KCz?duu^aa>y%iPm%1zqnPR{G?Y4 z48C*_3ipwVb2oo`j1WM8AYDT&ioo^DN+XA-8Nq=KolVDJ>^(q9Nykz^AO8@jU-TDL8=@Yp# z`0V^~J;sb-xt;21tD-A@Q!b4`@x|E>D83vGJA5%XgyFaLGshP4j^0BP@nJi;8QPq} zZasCOfyHw%T^l_D156Rxq9l?k^_qF>+c(_`j%kBrqB>?FPa!(O&K%uAjw|3vex zg~6iGGvYe?pO(JYfzZ{%9l2^x{GmywWC-Ereh^T;KWV6K)&<>!={pX|l+g~?h}`RK z0g*I#Xa`GBWma{trr5d?fk)++ZmoO}fx6-Yk^Ncr>2Umkn-MFU(O?_Y9peO6vtnRF z$QVFE%8MY|Xohq7+4+t3Y-1=b`+7f0p6}a*tiScRyO=V+;Q3*I3IM{IKduC`j8lKO I28@6J0QOxWo&W#< literal 0 HcmV?d00001 diff --git a/src/assets/images/user/bg.webp b/src/assets/images/user/bg.webp new file mode 100644 index 0000000000000000000000000000000000000000..762b22dbbe34305886a78b1d103dddcc61d106ec GIT binary patch literal 12352 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 literal 0 HcmV?d00001 diff --git a/src/assets/styles/components/_action-btn.scss b/src/assets/styles/components/_action-btn.scss new file mode 100644 index 0000000..cf0841d --- /dev/null +++ b/src/assets/styles/components/_action-btn.scss @@ -0,0 +1,71 @@ +@charset "UTF-8"; + +/** + * 统一的操作按钮样式 + * 用于表格操作列等场景 + */ + +.art-action-btn { + display: inline-flex; + gap: 6px; + align-items: center; + justify-content: center; /* 增加居中 */ + min-width: 64px; + padding: 6px 12px; + font-size: 13px; + line-height: 1; + color: var(--el-text-color-primary); + cursor: pointer; + background: var(--el-fill-color-light); + border: 1px solid var(--el-border-color-lighter); + border-radius: 10px; + transition: all 0.15s ease; /* 移动 transition 到这里 */ + + .art-action-icon { + display: flex; + font-size: 16px; + } + + &:hover { + filter: brightness(0.97); + } + + /* 样式变体 */ + &.primary { + color: var(--el-color-primary); + background: var(--el-color-primary-light-9); + border-color: var(--el-color-primary-light-8); + } + + &.success { + color: var(--el-color-success); + background: var(--el-color-success-light-9); + border-color: var(--el-color-success-light-8); + } + + &.warning { + color: var(--el-color-warning); + background: var(--el-color-warning-light-9); + border-color: var(--el-color-warning-light-8); + } + + &.danger { + color: var(--el-color-danger); + background: var(--el-color-danger-light-9); + border-color: var(--el-color-danger-light-8); + } + + &.info { + color: var(--el-text-color-secondary); + background: var(--el-fill-color-lighter); + border-color: var(--el-border-color-lighter); + } +} + +/* 操作按钮容器 */ +.action-wrap { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} diff --git a/src/assets/styles/core/app.scss b/src/assets/styles/core/app.scss new file mode 100644 index 0000000..c0efeed --- /dev/null +++ b/src/assets/styles/core/app.scss @@ -0,0 +1,292 @@ +// 全局样式 +// 顶部进度条颜色 +#nprogress .bar { + z-index: 2400; + background-color: color-mix(in srgb, var(--theme-color) 70%, white); +} + +#nprogress .peg { + box-shadow: + 0 0 10px var(--theme-color), + 0 0 5px var(--theme-color) !important; +} + +#nprogress .spinner-icon { + border-top-color: var(--theme-color) !important; + border-left-color: var(--theme-color) !important; +} + +// 处理移动端组件兼容性 +@media screen and (max-width: 640px) { + * { + cursor: default !important; + } +} + +// 背景滤镜 +*, +::before, +::after { + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +// 色弱模式 +.color-weak { + filter: invert(80%); + -webkit-filter: invert(80%); +} + +#noop { + display: none; +} + +// 语言切换选中样式 +.langDropDownStyle { + // 选中项背景颜色 + .is-selected { + background-color: var(--art-el-active-color) !important; + } + + // 语言切换按钮菜单样式优化 + .lang-btn-item { + .el-dropdown-menu__item { + padding-left: 13px !important; + padding-right: 6px !important; + margin-bottom: 3px !important; + } + + &:last-child { + .el-dropdown-menu__item { + margin-bottom: 0 !important; + } + } + + .menu-txt { + min-width: 60px; + display: block; + } + + i { + font-size: 10px; + margin-left: 10px; + } + } +} + +// 盒子默认边框 +.page-content { + border: 1px solid var(--art-card-border) !important; +} + +@mixin art-card-base($border-color, $shadow: none, $radius-diff: 4px) { + background: var(--default-box-color); + border: 1px solid #{$border-color} !important; + border-radius: calc(var(--custom-radius) + #{$radius-diff}) !important; + box-shadow: #{$shadow} !important; + + --el-card-border-color: var(--default-border) !important; +} + +.art-card, +.art-card-sm, +.art-card-xs { + border: 1px solid var(--art-card-border); +} + +// 盒子边框 +[data-box-mode='border-mode'] { + .page-content, + .art-table-card { + border: 1px solid var(--art-card-border) !important; + } + + .art-card { + @include art-card-base(var(--art-card-border), none, 4px); + } + + .art-card-sm { + @include art-card-base(var(--art-card-border), none, 0px); + } + + .art-card-xs { + @include art-card-base(var(--art-card-border), none, -4px); + } +} + +// 盒子阴影 +[data-box-mode='shadow-mode'] { + .page-content, + .art-table-card { + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.04) !important; + border: 1px solid var(--art-gray-200) !important; + } + + .layout-sidebar { + border-right: 1px solid var(--art-card-border) !important; + } + + .art-card { + @include art-card-base( + var(--art-gray-200), + (0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)), + 4px + ); + } + + .art-card-sm { + @include art-card-base( + var(--art-gray-200), + (0 1px 3px 0 rgba(0, 0, 0, 0.03), 0 1px 2px -1px rgba(0, 0, 0, 0.08)), + 2px + ); + } + + .art-card-xs { + @include art-card-base( + var(--art-gray-200), + (0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 1px -1px rgba(0, 0, 0, 0.08)), + -4px + ); + } +} + +// 元素全屏 +.el-full-screen { + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100vw !important; + height: 100% !important; + z-index: 2300; + margin-top: 0; + padding: 15px; + box-sizing: border-box; + background-color: var(--default-box-color); + display: flex; + flex-direction: column; +} + +// 表格卡片 +.art-table-card { + flex: 1; + display: flex; + flex-direction: column; + margin-top: 12px; + border-radius: calc(var(--custom-radius) / 2 + 2px) !important; + + .el-card__body { + height: 100%; + overflow: hidden; + } +} + +// 容器全高 +.art-full-height { + height: var(--art-full-height); + display: flex; + flex-direction: column; + + @media (max-width: 640px) { + height: auto; + } +} + +// 徽章样式 +.art-badge { + position: absolute; + top: 0; + right: 20px; + bottom: 0; + width: 6px; + height: 6px; + margin: auto; + background: #ff3860; + border-radius: 50%; + animation: breathe 1.5s ease-in-out infinite; + + &.art-badge-horizontal { + right: 0; + } + + &.art-badge-mixed { + right: 0; + } + + &.art-badge-dual { + right: 5px; + top: 5px; + bottom: auto; + } +} + +// 文字徽章样式 +.art-text-badge { + position: absolute; + top: 0; + right: 12px; + bottom: 0; + min-width: 20px; + height: 18px; + line-height: 17px; + padding: 0 5px; + margin: auto; + font-size: 10px; + color: #fff; + text-align: center; + background: #fd4e4e; + border-radius: 4px; +} + +@keyframes breathe { + 0% { + opacity: 0.7; + transform: scale(1); + } + + 50% { + opacity: 1; + transform: scale(1.1); + } + + 100% { + opacity: 0.7; + transform: scale(1); + } +} + +// 修复老机型 loading 定位问题 +.art-loading-fix { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.art-loading-fix .el-loading-spinner { + position: static !important; + top: auto !important; + left: auto !important; + transform: none !important; +} + +// 去除移动端点击背景色 +@media screen and (max-width: 1180px) { + * { + -webkit-tap-highlight-color: transparent; + } +} diff --git a/src/assets/styles/core/dark.scss b/src/assets/styles/core/dark.scss new file mode 100644 index 0000000..c52abc3 --- /dev/null +++ b/src/assets/styles/core/dark.scss @@ -0,0 +1,93 @@ +/* +* 深色主题 +* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class') +*/ + +$font-color: rgba(#ffffff, 0.85); + +/* 覆盖element-plus默认深色背景色 */ +html.dark { + // element-plus + --el-bg-color: var(--default-box-color); + --el-text-color-regular: #{$font-color}; + + // 富文本编辑器 + // 工具栏背景颜色 + --w-e-toolbar-bg-color: #18191c; + // 输入区域背景颜色 + --w-e-textarea-bg-color: #090909; + // 工具栏文字颜色 + --w-e-toolbar-color: var(--art-gray-600); + // 选中菜单颜色 + --w-e-toolbar-active-bg-color: #25262b; + // 弹窗边框颜色 + --w-e-toolbar-border-color: var(--default-border-dashed); + // 分割线颜色 + --w-e-textarea-border-color: var(--default-border-dashed); + // 链接输入框边框颜色 + --w-e-modal-button-border-color: var(--default-border-dashed); + // 表格头颜色 + --w-e-textarea-slight-bg-color: #090909; + // 按钮背景颜色 + --w-e-modal-button-bg-color: #090909; + // hover toolbar 背景颜色 + --w-e-toolbar-active-color: var(--art-gray-800); +} + +.dark { + .page-content .article-list .item .left .outer > div { + border-right-color: var(--dark-border-color) !important; + } + + // 富文本编辑器 + .editor-wrapper { + *:not(pre code *) { + color: inherit !important; + } + } + // 分隔线 + .w-e-bar-divider { + background-color: var(--art-gray-300) !important; + } + + .w-e-select-list, + .w-e-drop-panel, + .w-e-bar-item-group .w-e-bar-item-menus-container, + .w-e-text-container [data-slate-editor] pre > code { + border: 1px solid var(--default-border) !important; + } + + // 下拉选择框 + .w-e-select-list { + background-color: var(--default-box-color) !important; + } + + /* 下拉选择框 hover 样式调整 */ + .w-e-select-list ul li:hover, + /* 工具栏 hover 按钮背景颜色 */ + .w-e-bar-item button:hover { + background-color: #090909 !important; + } + + /* 代码块 */ + .w-e-text-container [data-slate-editor] pre > code { + background-color: #25262b !important; + text-shadow: none !important; + } + + /* 引用 */ + .w-e-text-container [data-slate-editor] blockquote { + border-left: 4px solid var(--default-border-dashed) !important; + background-color: var(--art-color); + } + + .editor-wrapper { + .w-e-text-container [data-slate-editor] .table-container th:last-of-type { + border-right: 1px solid var(--default-border-dashed) !important; + } + + .w-e-modal { + background-color: var(--art-color); + } + } +} diff --git a/src/assets/styles/core/el-dark.scss b/src/assets/styles/core/el-dark.scss new file mode 100644 index 0000000..8f81cdf --- /dev/null +++ b/src/assets/styles/core/el-dark.scss @@ -0,0 +1,2 @@ +// 导入暗黑主题 +@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *; diff --git a/src/assets/styles/core/el-light.scss b/src/assets/styles/core/el-light.scss new file mode 100644 index 0000000..ddf2bc5 --- /dev/null +++ b/src/assets/styles/core/el-light.scss @@ -0,0 +1,34 @@ +// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss +// 自定义Element 亮色主题 + +@forward 'element-plus/theme-chalk/src/common/var.scss' with ( + $colors: ( + 'white': #ffffff, + 'black': #000000, + 'success': ( + 'base': #13deb9 + ), + 'warning': ( + 'base': #ffae1f + ), + 'danger': ( + 'base': #ff4d4f + ), + 'error': ( + 'base': #fa896b + ) + ), + $button: ( + 'hover-bg-color': var(--el-color-primary-light-9), + 'hover-border-color': var(--el-color-primary), + 'border-color': var(--el-color-primary), + 'text-color': var(--el-color-primary) + ), + $messagebox: ( + 'border-radius': '12px' + ), + $popover: ( + 'padding': '14px', + 'border-radius': '10px' + ) +); diff --git a/src/assets/styles/core/el-ui.scss b/src/assets/styles/core/el-ui.scss new file mode 100644 index 0000000..57c0786 --- /dev/null +++ b/src/assets/styles/core/el-ui.scss @@ -0,0 +1,526 @@ +// 优化 Element Plus 组件库默认样式 + +:root { + // 系统主色 + --main-color: var(--el-color-primary); + --el-color-white: white !important; + --el-color-black: white !important; + // 输入框边框颜色 + // --el-border-color: #E4E4E7 !important; // DCDFE6 + // 按钮粗度 + --el-font-weight-primary: 400 !important; + + --el-component-custom-height: 36px !important; + + --el-component-size: var(--el-component-custom-height) !important; + + // 边框、按钮圆角... + --el-border-radius-base: calc(var(--custom-radius) / 3 + 2px) !important; + + --el-border-radius-small: calc(var(--custom-radius) / 3 + 4px) !important; + --el-messagebox-border-radius: calc(var(--custom-radius) / 3 + 4px) !important; + --el-popover-border-radius: calc(var(--custom-radius) / 3 + 4px) !important; + + .region .el-radio-button__original-radio:checked + .el-radio-button__inner { + color: var(--theme-color); + } +} + +// 优化 el-form-item 标签高度 +.el-form-item__label { + height: var(--el-component-custom-height) !important; + line-height: var(--el-component-custom-height) !important; +} + +// 日期选择器 +.el-date-range-picker { + --el-datepicker-inrange-bg-color: var(--art-gray-200) !important; +} + +// el-card 背景色跟系统背景色保持一致 +html.dark .el-card { + --el-card-bg-color: var(--default-box-color) !important; +} + +// 修改 el-pagination 大小 +.el-pagination--default { + & { + --el-pagination-button-width: 32px !important; + --el-pagination-button-height: var(--el-pagination-button-width) !important; + } + + @media (max-width: 1180px) { + & { + --el-pagination-button-width: 28px !important; + } + } + + .el-select--default .el-select__wrapper { + min-height: var(--el-pagination-button-width) !important; + } + + .el-pagination__jump .el-input { + height: var(--el-pagination-button-width) !important; + } +} + +.el-pager li { + padding: 0 10px !important; + // border: 1px solid red !important; +} + +// 优化菜单折叠展开动画(提升动画流畅度) +.el-menu.el-menu--inline { + transition: max-height 0.26s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +// 优化菜单 item hover 动画(提升鼠标跟手感) +.el-sub-menu__title, +.el-menu-item { + transition: background-color 0s !important; +} + +// -------------------------------- 修改 el-size=default 组件默认高度 start -------------------------------- +// 修改 el-button 高度 +.el-button--default { + height: var(--el-component-custom-height) !important; +} + +// circle 按钮宽度优化 +.el-button--default.is-circle { + width: var(--el-component-custom-height) !important; +} + +// 修改 el-select 高度 +.el-select--default { + .el-select__wrapper { + min-height: var(--el-component-custom-height) !important; + } +} + +// 修改 el-checkbox-button 高度 +.el-checkbox-button--default .el-checkbox-button__inner, +// 修改 el-radio-button 高度 +.el-radio-button--default .el-radio-button__inner { + padding: 10px 15px !important; +} +// -------------------------------- 修改 el-size=default 组件默认高度 end -------------------------------- + +.el-pagination.is-background .btn-next, +.el-pagination.is-background .btn-prev, +.el-pagination.is-background .el-pager li { + border-radius: 6px; +} + +.el-popover { + min-width: 80px; + border-radius: var(--el-border-radius-small) !important; +} + +.el-dialog { + border-radius: 100px !important; + border-radius: calc(var(--custom-radius) / 1.2 + 2px) !important; + overflow: hidden; +} + +.el-dialog__header { + .el-dialog__title { + font-size: 16px; + } +} + +.el-dialog__body { + padding: 25px 0 !important; + position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275; +} + +.el-dialog.el-dialog-border { + .el-dialog__body { + // 上边框 + &::before, + // 下边框 + &::after { + content: ''; + position: absolute; + left: -16px; + width: calc(100% + 32px); + height: 1px; + background-color: var(--art-gray-300); + } + + &::before { + top: 0; + } + + &::after { + bottom: 0; + } + } +} + +// el-message 样式优化 +.el-message { + background-color: var(--default-box-color) !important; + border: 0 !important; + box-shadow: + 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important; + + p { + font-size: 13px; + } +} + +// 修改 el-dropdown 样式 +.el-dropdown-menu { + padding: 6px !important; + border-radius: 10px !important; + border: none !important; + + .el-dropdown-menu__item { + padding: 6px 16px !important; + border-radius: 6px !important; + + &:hover:not(.is-disabled) { + color: var(--art-gray-900) !important; + background-color: var(--art-el-active-color) !important; + } + + &:focus:not(.is-disabled) { + color: var(--art-gray-900) !important; + background-color: var(--art-gray-200) !important; + } + } +} + +// 隐藏 select、dropdown 的三角 +.el-select__popper, +.el-dropdown__popper { + margin-top: -6px !important; + + .el-popper__arrow { + display: none; + } +} + +.el-dropdown-selfdefine:focus { + outline: none !important; +} + +// 处理移动端组件兼容性 +@media screen and (max-width: 640px) { + .el-message-box, + .el-dialog { + width: calc(100% - 24px) !important; + } + + .el-date-picker.has-sidebar.has-time { + width: calc(100% - 24px); + left: 12px !important; + } + + .el-picker-panel *[slot='sidebar'], + .el-picker-panel__sidebar { + display: none; + } + + .el-picker-panel *[slot='sidebar'] + .el-picker-panel__body, + .el-picker-panel__sidebar + .el-picker-panel__body { + margin-left: 0; + } +} + +// 修改el-button样式 +.el-button { + &.el-button--text { + background-color: transparent !important; + padding: 0 !important; + + span { + margin-left: 0 !important; + } + } +} + +// 修改el-tag样式 +.el-tag { + font-weight: 500; + transition: all 0s !important; + + &.el-tag--default { + height: 26px !important; + } +} + +.el-checkbox-group { + &.el-table-filter__checkbox-group label.el-checkbox { + height: 17px !important; + + .el-checkbox__label { + font-weight: 400 !important; + } + } +} + +.el-radio--default { + // 优化单选按钮大小 + .el-radio__input { + .el-radio__inner { + width: 16px; + height: 16px; + + &::after { + width: 6px; + height: 6px; + } + } + } +} + +.el-checkbox { + .el-checkbox__inner { + border-radius: 2px !important; + } +} + +// 优化复选框样式 +.el-checkbox--default { + .el-checkbox__inner { + width: 16px !important; + height: 16px !important; + border-radius: 4px !important; + + &::before { + content: ''; + height: 4px !important; + top: 5px !important; + background-color: #fff !important; + transform: scale(0.6) !important; + } + } + + .is-checked { + .el-checkbox__inner { + &::after { + width: 3px; + height: 8px; + margin: auto; + border: 2px solid var(--el-checkbox-checked-icon-color); + border-left: 0; + border-top: 0; + transform: translate(-45%, -60%) rotate(45deg) scale(0.86) !important; + transform-origin: center; + } + } + } +} + +.el-notification .el-notification__icon { + font-size: 22px !important; +} + +// 修改 el-message-box 样式 +.el-message-box__headerbtn .el-message-box__close, +.el-dialog__headerbtn .el-dialog__close { + top: 7px; + right: 7px; + width: 30px; + height: 30px; + border-radius: 5px; + transition: all 0.3s; + + &:hover { + background-color: var(--art-hover-color) !important; + color: var(--art-gray-900) !important; + } +} + +.el-message-box { + padding: 25px 20px !important; +} + +.el-message-box__title { + font-weight: 500 !important; +} + +.el-table__column-filter-trigger i { + color: var(--theme-color) !important; + margin: -3px 0 0 2px; +} + +// 去除 el-dropdown 鼠标放上去出现的边框 +.el-tooltip__trigger:focus-visible { + outline: unset; +} + +// ipad 表单右侧按钮优化 +@media screen and (max-width: 1180px) { + .el-table-fixed-column--right { + padding-right: 0 !important; + } +} + +.login-out-dialog { + padding: 30px 20px !important; + border-radius: 10px !important; +} + +// 修改 dialog 动画 +.dialog-fade-enter-active { + .el-dialog:not(.is-draggable) { + animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86); + + // 修复 el-dialog 动画后宽度不自适应问题 + .el-select__selected-item { + display: inline-block; + } + } +} + +.dialog-fade-leave-active { + animation: fade-out 0.2s linear; + + .el-dialog:not(.is-draggable) { + animation: dialog-close 0.5s; + } +} + +@keyframes dialog-open { + 0% { + opacity: 0; + transform: scale(0.2); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes dialog-close { + 0% { + opacity: 1; + transform: scale(1); + } + + 100% { + opacity: 0; + transform: scale(0.2); + } +} + +// 遮罩层动画 +@keyframes fade-out { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +// 修改 el-select 样式 +.el-select__popper:not(.el-tree-select__popper) { + .el-select-dropdown__list { + padding: 5px !important; + + .el-select-dropdown__item { + height: 34px !important; + line-height: 34px !important; + border-radius: 6px !important; + + &.is-selected { + color: var(--art-gray-900) !important; + font-weight: 400 !important; + background-color: var(--art-el-active-color) !important; + margin-bottom: 4px !important; + } + + &:hover { + background-color: var(--art-hover-color) !important; + } + } + + .el-select-dropdown__item:hover ~ .is-selected, + .el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) { + background-color: transparent !important; + } + } +} + +// 修改 el-tree-select 样式 +.el-tree-select__popper { + .el-select-dropdown__list { + padding: 5px !important; + + .el-tree-node { + .el-tree-node__content { + height: 36px !important; + border-radius: 6px !important; + + &:hover { + background-color: var(--art-gray-200) !important; + } + } + } + } +} + +// 实现水波纹在文字下面效果 +.el-button > span { + position: relative; + z-index: 10; +} + +// 优化颜色选择器圆角 +.el-color-picker__color { + border-radius: 2px !important; +} + +// 优化日期时间选择器底部圆角 +.el-picker-panel { + .el-picker-panel__footer { + border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base); + } +} + +// 优化树型菜单样式 +.el-tree-node__content { + border-radius: 4px; + margin-bottom: 4px; + padding: 1px 0; + + &:hover { + background-color: var(--art-hover-color) !important; + } +} + +.dark { + .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content { + background-color: var(--art-gray-300) !important; + } +} + +// 隐藏折叠菜单弹窗 hover 出现的边框 +.menu-left-popper:focus-within, +.horizontal-menu-popper:focus-within { + box-shadow: none !important; + outline: none !important; +} + +// 数字输入组件右侧按钮高度跟随自定义组件高度 +.el-input-number--default.is-controls-right { + .el-input-number__decrease, + .el-input-number__increase { + height: calc((var(--el-component-size) / 2)) !important; + } +} + +// 全局输入框文本左对齐(统一表单输入体验) +.el-input__inner, +.el-textarea__inner, +.el-input-number .el-input__inner { + text-align: left !important; +} diff --git a/src/assets/styles/core/md.scss b/src/assets/styles/core/md.scss new file mode 100644 index 0000000..b22fdc2 --- /dev/null +++ b/src/assets/styles/core/md.scss @@ -0,0 +1,1036 @@ +/* 文章标题设置(h1-h6)*/ +/* ------------------------------------------------ */ +$font-color: #24292e; + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + color: var(--art-gray-800) !important; + margin: 30px 0 10px 0; + font-weight: 600; +} + +.markdown-body h1 { + font-size: 30px; +} + +@media only screen and (max-width: 550px) { + .markdown-body h1 { + font-size: 26px; + } + + .markdown-body h2 { + font-size: 22px; + } + + .markdown-body h3 { + font-size: 18px; + } +} + +/* 块引用 */ +/* ------------------------------------------------ */ +.markdown-body blockquote { + color: rgba(60, 60, 67, 0.7); + font-size: 15px !important; + border-left: 0.18em solid #e7e7e8; + background: #f8f8f8; + padding: 15px 1em; + font-weight: 400 !important; +} + +/* 详情页文章字体颜色 */ +/* ------------------------------------------------ */ +.markdown-body p { + line-height: 28px; + margin-bottom: 10px; +} + +.markdown-body li, +.markdown-body p { + color: var(--art-gray-800) !important; + font-size: 16px !important; +} + +.dark .markdown-body li span { + color: var(--art-gray-800) !important; + background-color: transparent !important; +} + +.dark .markdown-body p span { + color: var(--art-gray-800) !important; + background-color: transparent !important; +} + +.line-numbers-mode { + background-color: var(--art-code-bg); + border-radius: 8px; + position: relative; + padding-left: 32px; + box-sizing: border-box; +} + +.line-numbers-mode pre { + flex: 1; + border-radius: 0 8px 8px 0; + background-color: var(--art-code-bg); +} + +.line-numbers-mode .line-numbers-wrapper { + width: 32px; + height: 100%; + text-align: center; + padding: 16px 0; + box-sizing: border-box; + border-right: 1px solid #000000; + position: absolute; + left: 0; + top: 0; +} + +.line-numbers-mode .line-numbers-wrapper span { + height: 23.6px; + line-height: 23.6px; + display: block; + color: #72747b; + font-size: 13px; + box-sizing: border-box; +} + +.line-numbers-mode .copy-btn { + display: inline-block; + display: flex; + position: absolute; + right: 10px; + top: 10px; + cursor: pointer; + opacity: 0; + background-color: #000; + border-radius: 5px; + text-align: center; + color: rgba(255, 255, 255, 0.6); + transition: opacity 0.3s; +} + +.line-numbers-mode .copy-btn div { + width: 34px; + height: 34px; + line-height: 34px; + cursor: pointer; + text-align: center; + font-size: 20px; +} + +.line-numbers-mode:hover .copy-btn { + opacity: 1; +} + +.line-numbers-mode .copy-btn span { + height: 34px; + line-height: 34px; + font-size: 13px; + padding-left: 10px; + display: none; +} + +.line-numbers-mode .copy-btn .show-copy { + opacity: 1; + display: block; +} + +.line-numbers-mode ::-webkit-scrollbar-track { + background-color: #292b30 !important; +} + +.markdown-body .anchor { + float: left; + line-height: 1; + margin-left: -20px; + padding-right: 4px; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + line-height: 1.5; + color: $font-color; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body details { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body a { + background-color: initial; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body strong { + font-weight: inherit; + font-weight: bolder; +} + +.markdown-body p br { + display: inline; + line-height: 11px; +} + +.markdown-body img { + border-style: none; +} + +.markdown-body hr { + box-sizing: initial; + height: 0; + overflow: visible; +} + +.markdown-body input { + font: inherit; + margin: 0; +} + +.markdown-body input { + overflow: visible; +} + +.markdown-body [type='checkbox'] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font-size: inherit; + line-height: inherit; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 600; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; +} + +.markdown-body hr:after, +.markdown-body hr:before { + display: table; + content: ''; +} + +.markdown-body hr:after { + clear: both; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: + 11px SFMono-Regular, + Consolas, + Liberation Mono, + Menlo, + monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ol ol ol, +.markdown-body ol ul ol, +.markdown-body ul ol ol, +.markdown-body ul ul ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code, +.markdown-body pre, +.markdown-body .line-number { + font-size: 14px !important; + border-radius: 8px; + background-color: #282c34; +} + +.dark { + .markdown-body code, + .markdown-body pre, + .markdown-body .line-number { + background-color: #252525; + } +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body input::-webkit-inner-spin-button, +.markdown-body input::-webkit-outer-spin-button { + margin: 0; + -webkit-appearance: none; + appearance: none; +} + +.markdown-body :checked + .radio-label { + position: relative; + z-index: 1; + border-color: #0366d6; +} + +.markdown-body .border { + border: 1px solid #e1e4e8 !important; +} + +.markdown-body .border-0 { + border: 0 !important; +} + +.markdown-body .border-bottom { + border-bottom: 1px solid #e1e4e8 !important; +} + +.markdown-body .rounded-1 { + border-radius: 3px !important; +} + +.markdown-body .bg-white { + background-color: #fff !important; +} + +.markdown-body .bg-gray-light { + background-color: #fafbfc !important; +} + +.markdown-body .text-gray-light { + color: #6a737d !important; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; + margin-bottom: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3, +.markdown-body .px-3 { + padding-left: 16px !important; +} + +.markdown-body .px-3 { + padding-right: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .f6 { + font-size: 12px !important; +} + +.markdown-body .lh-condensed { + line-height: 1.25 !important; +} + +.markdown-body .text-bold { + font-weight: 600 !important; +} + +.markdown-body .pl-c { + color: #6a737d; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #005cc5; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6f42c1; +} + +.markdown-body .pl-s .pl-s1, +.markdown-body .pl-smi { + color: $font-color; +} + +.markdown-body .pl-ent { + color: #22863a; +} + +.markdown-body .pl-k { + color: #d73a49; +} + +.markdown-body .pl-pds, +.markdown-body .pl-s, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sra, +.markdown-body .pl-sr .pl-sre { + color: #032f62; +} + +.markdown-body .pl-smw, +.markdown-body .pl-v { + color: #e36209; +} + +.markdown-body .pl-bu { + color: #b31d28; +} + +.markdown-body .pl-ii { + color: #fafbfc; + background-color: #b31d28; +} + +.markdown-body .pl-c2 { + color: #fafbfc; + background-color: #d73a49; +} + +.markdown-body .pl-c2:before { + content: '^M'; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: 700; + color: #22863a; +} + +.markdown-body .pl-ml { + color: #735c0f; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: 700; + color: #005cc5; +} + +.markdown-body .pl-mi { + font-style: italic; + color: $font-color; +} + +.markdown-body .pl-mb { + font-weight: 700; + color: $font-color; +} + +.markdown-body .pl-md { + color: #b31d28; + background-color: #ffeef0; +} + +.markdown-body .pl-mi1 { + color: #22863a; + background-color: #f0fff4; +} + +.markdown-body .pl-mc { + color: #e36209; + background-color: #ffebda; +} + +.markdown-body .pl-mi2 { + color: #f6f8fa; + background-color: #005cc5; +} + +.markdown-body .pl-mdr { + font-weight: 700; + color: #6f42c1; +} + +.markdown-body .pl-ba { + color: #586069; +} + +.markdown-body .pl-sg { + color: #959da5; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #032f62; +} + +.markdown-body .mb-0 { + margin-bottom: 0 !important; +} + +.markdown-body .my-2 { + margin-bottom: 8px !important; +} + +.markdown-body .my-2 { + margin-top: 8px !important; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .py-2 { + padding-top: 8px !important; + padding-bottom: 8px !important; +} + +.markdown-body .pl-3 { + padding-left: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body .pl-7 { + padding-left: 48px !important; +} + +.markdown-body .pl-8 { + padding-left: 64px !important; +} + +.markdown-body .pl-9 { + padding-left: 80px !important; +} + +.markdown-body .pl-10 { + padding-left: 96px !important; +} + +.markdown-body .pl-11 { + padding-left: 112px !important; +} + +.markdown-body .pl-12 { + padding-left: 128px !important; +} + +.markdown-body hr { + border-bottom-color: #eee; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: + 11px SFMono-Regular, + Consolas, + Liberation Mono, + Menlo, + monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: 1px solid #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; +} + +.markdown-body:after, +.markdown-body:before { + display: table; + content: ''; +} + +.markdown-body:after { + clear: both; +} + +.markdown-body > :first-child { + margin-top: 0 !important; +} + +.markdown-body > :last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body blockquote, +.markdown-body details, +.markdown-body dl, +.markdown-body ol, +.markdown-body pre, +.markdown-body table, +.markdown-body ul { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body blockquote > :first-child { + margin-top: 0; +} + +.markdown-body blockquote > :last-child { + margin-bottom: 0; +} + +.markdown-body ol, +.markdown-body ul { + padding-left: 1em; +} + +.markdown-body ol ol, +.markdown-body ol ul, +.markdown-body ul ol, +.markdown-body ul ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li { + line-height: 28px; + font-size: 14px; + word-wrap: break-all; + list-style: disc; + margin-left: 10px; +} + +.markdown-body li > p { + margin-top: 16px; +} + +.markdown-body li + li { + margin-top: 0.25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table td, +.markdown-body table th { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: initial; + background-color: #fff; + border: 1px solid #eee; + border: 1px solid var(--art-c-border-2); + cursor: zoom-in; +} + +.markdown-body img[align='right'] { + padding-left: 20px; +} + +.markdown-body img[align='left'] { + padding-right: 20px; +} + +.markdown-body code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27, 31, 35, 0.05); + border-radius: 3px; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre > code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 15px 20px 15px 0; + overflow: auto; + font-size: 92%; + line-height: 1.6; +} + +.markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: initial; + border: 0; +} + +.markdown-body .commit-tease-sha { + display: inline-block; + font-size: 90%; + color: #444d56; +} + +.markdown-body .full-commit .btn-outline:not(:disabled):hover { + color: #005cc5; + border-color: #005cc5; +} + +.markdown-body .blob-wrapper { + overflow-x: auto; + overflow-y: hidden; +} + +.markdown-body .blob-wrapper-embedded { + max-height: 240px; + overflow-y: auto; +} + +.markdown-body .blob-num { + width: 1%; + min-width: 50px; + padding-right: 10px; + padding-left: 10px; + font-size: 12px; + line-height: 20px; + color: rgba(27, 31, 35, 0.3); + text-align: right; + white-space: nowrap; + vertical-align: top; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.markdown-body .blob-num:hover { + color: rgba(27, 31, 35, 0.6); +} + +.markdown-body .blob-num:before { + content: attr(data-line-number); +} + +.markdown-body .blob-code { + position: relative; + padding-right: 10px; + padding-left: 10px; + line-height: 20px; + vertical-align: top; +} + +.markdown-body .blob-code-inner { + overflow: visible; + font-size: 12px; + color: $font-color; + word-wrap: normal; + white-space: pre; +} + +.markdown-body .pl-token.active, +.markdown-body .pl-token:hover { + cursor: pointer; + background: #ffea7f; +} + +.markdown-body .tab-size[data-tab-size='1'] { + -moz-tab-size: 1; + tab-size: 1; +} + +.markdown-body .tab-size[data-tab-size='2'] { + -moz-tab-size: 2; + tab-size: 2; +} + +.markdown-body .tab-size[data-tab-size='3'] { + -moz-tab-size: 3; + tab-size: 3; +} + +.markdown-body .tab-size[data-tab-size='4'] { + -moz-tab-size: 4; + tab-size: 4; +} + +.markdown-body .tab-size[data-tab-size='5'] { + -moz-tab-size: 5; + tab-size: 5; +} + +.markdown-body .tab-size[data-tab-size='6'] { + -moz-tab-size: 6; + tab-size: 6; +} + +.markdown-body .tab-size[data-tab-size='7'] { + -moz-tab-size: 7; + tab-size: 7; +} + +.markdown-body .tab-size[data-tab-size='8'] { + -moz-tab-size: 8; + tab-size: 8; +} + +.markdown-body .tab-size[data-tab-size='9'] { + -moz-tab-size: 9; + tab-size: 9; +} + +.markdown-body .tab-size[data-tab-size='10'] { + -moz-tab-size: 10; + tab-size: 10; +} + +.markdown-body .tab-size[data-tab-size='11'] { + -moz-tab-size: 11; + tab-size: 11; +} + +.markdown-body .tab-size[data-tab-size='12'] { + -moz-tab-size: 12; + tab-size: 12; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item + .task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; +} diff --git a/src/assets/styles/core/mixin.scss b/src/assets/styles/core/mixin.scss new file mode 100644 index 0000000..db36888 --- /dev/null +++ b/src/assets/styles/core/mixin.scss @@ -0,0 +1,157 @@ +// sass 混合宏(函数) + +/** +* 溢出省略号 +* @param {Number} 行数 +*/ +@mixin ellipsis($rowCount: 1) { + @if $rowCount <=1 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } @else { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: $rowCount; + -webkit-box-orient: vertical; + } +} + +/** +* 控制用户能否选中文本 +* @param {String} 类型 +*/ +@mixin userSelect($value: none) { + user-select: $value; + -moz-user-select: $value; + -ms-user-select: $value; + -webkit-user-select: $value; +} + +// 绝对定位居中 +@mixin absoluteCenter() { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; +} + +/** +* css3动画 +* +*/ +@mixin animation( + $from: ( + width: 0px + ), + $to: ( + width: 100px + ), + $name: mymove, + $animate: mymove 2s 1 linear infinite +) { + -webkit-animation: $animate; + -o-animation: $animate; + animation: $animate; + + @keyframes #{$name} { + from { + @each $key, $value in $from { + #{$key}: #{$value}; + } + } + + to { + @each $key, $value in $to { + #{$key}: #{$value}; + } + } + } + + @-webkit-keyframes #{$name} { + from { + @each $key, $value in $from { + $key: $value; + } + } + + to { + @each $key, $value in $to { + $key: $value; + } + } + } +} + +// 圆形盒子 +@mixin circle($size: 11px, $bg: #fff) { + border-radius: 50%; + width: $size; + height: $size; + line-height: $size; + text-align: center; + background: $bg; +} + +// placeholder +@mixin placeholder($color: #bbb) { + // Firefox + &::-moz-placeholder { + color: $color; + opacity: 1; + } + + // Internet Explorer 10+ + &:-ms-input-placeholder { + color: $color; + } + + // Safari and Chrome + &::-webkit-input-placeholder { + color: $color; + } + + &:placeholder-shown { + text-overflow: ellipsis; + } +} + +//背景透明,文字不透明。兼容IE8 +@mixin betterTransparentize($color, $alpha) { + $c: rgba($color, $alpha); + $ie_c: ie_hex_str($c); + background: rgba($color, 1); + background: $c; + background: transparent \9; + zoom: 1; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c}); + -ms-filter: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})'; +} + +//添加浏览器前缀 +@mixin browserPrefix($propertyName, $value) { + @each $prefix in -webkit-, -moz-, -ms-, -o-, '' { + #{$prefix}#{$propertyName}: $value; + } +} + +// 边框 +@mixin border($color: red) { + border: 1px solid $color; +} + +// 背景滤镜 +@mixin backdropBlur() { + --tw-backdrop-blur: blur(30px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) + var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) + var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) + var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) + var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) + var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} diff --git a/src/assets/styles/core/reset.scss b/src/assets/styles/core/reset.scss new file mode 100644 index 0000000..17a3bcf --- /dev/null +++ b/src/assets/styles/core/reset.scss @@ -0,0 +1,41 @@ +@charset "UTF-8"; + +/*滚动条*/ +/*滚动条整体部分,必须要设置*/ +::-webkit-scrollbar { + width: 8px !important; + height: 0 !important; +} + +/*滚动条的轨道*/ +::-webkit-scrollbar-track { + background-color: var(--art-gray-200); +} + +/*滚动条的滑块按钮*/ +::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: #cccccc !important; + transition: all 0.2s; + -webkit-transition: all 0.2s; +} + +::-webkit-scrollbar-thumb:hover { + background-color: #b0abab !important; +} + +/*滚动条的上下两端的按钮*/ +::-webkit-scrollbar-button { + height: 0px; + width: 0; +} + +.dark { + ::-webkit-scrollbar-track { + background-color: var(--default-bg-color); + } + + ::-webkit-scrollbar-thumb { + background-color: var(--art-gray-300) !important; + } +} diff --git a/src/assets/styles/core/router-transition.scss b/src/assets/styles/core/router-transition.scss new file mode 100644 index 0000000..f47c741 --- /dev/null +++ b/src/assets/styles/core/router-transition.scss @@ -0,0 +1,104 @@ +@use 'sass:map'; + +// === 变量区域 === +$transition: ( + // 动画持续时间 + duration: 0.25s, + // 滑动动画的移动距离 + distance: 15px, + // 默认缓动函数 + easing: cubic-bezier(0.25, 0.1, 0.25, 1), + // 淡入淡出专用的缓动函数 + fade-easing: cubic-bezier(0.4, 0, 0.6, 1) +); + +// 抽取配置值函数,提高可复用性 +@function transition-config($key) { + @return map.get($transition, $key); +} + +// 变量简写 +$duration: transition-config('duration'); +$distance: transition-config('distance'); +$easing: transition-config('easing'); +$fade-easing: transition-config('fade-easing'); + +// === 动画类 === + +// 淡入淡出动画 +.fade { + &-enter-active, + &-leave-active { + transition: opacity $duration $fade-easing; + will-change: opacity; + } + + &-enter-from, + &-leave-to { + opacity: 0; + } + + &-enter-to, + &-leave-from { + opacity: 1; + } +} + +// 滑动动画通用样式 +@mixin slide-transition($direction) { + $distance-x: 0; + $distance-y: 0; + + @if $direction == 'left' { + $distance-x: -$distance; + } @else if $direction == 'right' { + $distance-x: $distance; + } @else if $direction == 'top' { + $distance-y: -$distance; + } @else if $direction == 'bottom' { + $distance-y: $distance; + } + + &-enter-active { + transition: + opacity $duration $easing, + transform $duration $easing; + will-change: opacity, transform; + } + + &-leave-active { + transition: + opacity calc($duration * 0.7) $easing, + transform calc($duration * 0.7) $easing; + will-change: opacity, transform; + } + + &-enter-from { + opacity: 0; + transform: translate3d($distance-x, $distance-y, 0); + } + + &-enter-to { + opacity: 1; + transform: translate3d(0, 0, 0); + } + + &-leave-to { + opacity: 0; + transform: translate3d(-$distance-x, -$distance-y, 0); + } +} + +// 滑动动画方向类 +.slide-left { + @include slide-transition('left'); +} +.slide-right { + @include slide-transition('right'); +} +.slide-top { + @include slide-transition('top'); +} +.slide-bottom { + @include slide-transition('bottom'); +} diff --git a/src/assets/styles/core/tailwind.css b/src/assets/styles/core/tailwind.css new file mode 100644 index 0000000..1a9e22c --- /dev/null +++ b/src/assets/styles/core/tailwind.css @@ -0,0 +1,208 @@ +@import 'tailwindcss'; +@custom-variant dark (&:where(.dark, .dark *)); + +/* ==================== Light Mode Variables ==================== */ +:root { + /* Base Colors */ + --art-color: #ffffff; + --theme-color: var(--main-color); + + /* Theme Colors - OKLCH Format */ + --art-primary: oklch(0.7 0.23 260); + --art-secondary: oklch(0.72 0.19 231.6); + --art-error: oklch(0.73 0.15 25.3); + --art-info: oklch(0.58 0.03 254.1); + --art-success: oklch(0.78 0.17 166.1); + --art-warning: oklch(0.78 0.14 75.5); + --art-danger: oklch(0.68 0.22 25.3); + + /* Gray Scale - Light Mode */ + --art-gray-100: #f9fafb; + --art-gray-200: #f2f4f5; + --art-gray-300: #e6eaeb; + --art-gray-400: #dbdfe1; + --art-gray-500: #949eb7; + --art-gray-600: #7987a1; + --art-gray-700: #4d5875; + --art-gray-800: #383853; + --art-gray-900: #323251; + + /* Border Colors */ + --art-card-border: rgba(0, 0, 0, 0.08); + + --default-border: #e2e8ee; + --default-border-dashed: #dbdfe9; + + /* Background Colors */ + --default-bg-color: #fafbfc; + --default-box-color: #ffffff; + + /* Hover Color */ + --art-hover-color: #edeff0; + + /* Active Color */ + --art-active-color: #f2f4f5; + + /* Element Component Active Color */ + --art-el-active-color: #f2f4f5; +} + +/* ==================== Dark Mode Variables ==================== */ +.dark { + /* Base Colors */ + --art-color: #000000; + + /* Gray Scale - Dark Mode */ + --art-gray-100: #110f0f; + --art-gray-200: #17171c; + --art-gray-300: #393946; + --art-gray-400: #505062; + --art-gray-500: #73738c; + --art-gray-600: #8f8fa3; + --art-gray-700: #ababba; + --art-gray-800: #c7c7d1; + --art-gray-900: #e3e3e8; + + /* Border Colors */ + --art-card-border: rgba(255, 255, 255, 0.08); + + --default-border: rgba(255, 255, 255, 0.1); + --default-border-dashed: #363843; + + /* Background Colors */ + --default-bg-color: #070707; + --default-box-color: #161618; + + /* Hover Color */ + --art-hover-color: #252530; + + /* Active Color */ + --art-active-color: #202226; + + /* Element Component Active Color */ + --art-el-active-color: #2e2e38; +} + +/* ==================== Tailwind Theme Configuration ==================== */ +@theme { + /* Box Color (Light: white / Dark: black) */ + --color-box: var(--default-box-color); + + /* System Theme Color */ + --color-theme: var(--theme-color); + + /* Hover Color */ + --color-hover-color: var(--art-hover-color); + + /* Active Color */ + --color-active-color: var(--art-active-color); + + /* Active Color */ + --color-el-active-color: var(--art-active-color); + + /* ElementPlus Theme Colors */ + --color-primary: var(--art-primary); + --color-secondary: var(--art-secondary); + --color-error: var(--art-error); + --color-info: var(--art-info); + --color-success: var(--art-success); + --color-warning: var(--art-warning); + --color-danger: var(--art-danger); + + /* Gray Scale Colors (Auto-adapts to dark mode) */ + --color-g-100: var(--art-gray-100); + --color-g-200: var(--art-gray-200); + --color-g-300: var(--art-gray-300); + --color-g-400: var(--art-gray-400); + --color-g-500: var(--art-gray-500); + --color-g-600: var(--art-gray-600); + --color-g-700: var(--art-gray-700); + --color-g-800: var(--art-gray-800); + --color-g-900: var(--art-gray-900); +} + +/* ==================== Custom Border Radius Utilities ==================== */ +@utility rounded-custom-xs { + border-radius: calc(var(--custom-radius) / 2); +} + +@utility rounded-custom-sm { + border-radius: calc(var(--custom-radius) / 2 + 2px); +} + +/* ==================== Custom Utility Classes ==================== */ +@layer utilities { + /* Flexbox Layout Utilities */ + .flex-c { + @apply flex items-center; + } + + .flex-b { + @apply flex justify-between; + } + + .flex-cc { + @apply flex items-center justify-center; + } + + .flex-cb { + @apply flex items-center justify-between; + } + + /* Transition Utilities */ + .tad-200 { + @apply transition-all duration-200; + } + + .tad-300 { + @apply transition-all duration-300; + } + + /* Border Utilities */ + .border-full-d { + @apply border border-[var(--default-border)]; + } + + .border-b-d { + @apply border-b border-[var(--default-border)]; + } + + .border-t-d { + @apply border-t border-[var(--default-border)]; + } + + .border-l-d { + @apply border-l border-[var(--default-border)]; + } + + .border-r-d { + @apply border-r border-[var(--default-border)]; + } + + /* Cursor Utilities */ + .c-p { + @apply cursor-pointer; + } +} + +/* ==================== Custom Component Classes ==================== */ +@layer components { + /* Art Card Header Component */ + .art-card-header { + @apply flex justify-between pr-6 pb-1; + + .title { + h4 { + @apply text-lg font-medium text-g-900; + } + + p { + @apply mt-1 text-sm text-g-600; + + span { + @apply ml-2 font-medium; + } + } + } + } +} diff --git a/src/assets/styles/core/theme-animation.scss b/src/assets/styles/core/theme-animation.scss new file mode 100644 index 0000000..377b945 --- /dev/null +++ b/src/assets/styles/core/theme-animation.scss @@ -0,0 +1,63 @@ +// 定义基础变量 +$bg-animation-color-light: #000; +$bg-animation-color-dark: #fff; +$bg-animation-duration: 0.5s; + +html { + --bg-animation-color: $bg-animation-color-light; + + &.dark { + --bg-animation-color: $bg-animation-color-dark; + } + + // View transition styles + &::view-transition-old(*) { + animation: none; + } + + &::view-transition-new(*) { + animation: clip $bg-animation-duration ease-in both; + } + + &::view-transition-old(root) { + z-index: 1; + } + + &::view-transition-new(root) { + z-index: 9999; + } + + &.dark { + &::view-transition-old(*) { + animation: clip $bg-animation-duration ease-in reverse both; + } + + &::view-transition-new(*) { + animation: none; + } + + &::view-transition-old(root) { + z-index: 9999; + } + + &::view-transition-new(root) { + z-index: 1; + } + } +} + +// 定义动画 +@keyframes clip { + from { + clip-path: circle(0% at var(--x) var(--y)); + } + + to { + clip-path: circle(var(--r) at var(--x) var(--y)); + } +} + +// body 相关样式 +body { + background-color: var(--bg-animation-color); +} diff --git a/src/assets/styles/core/theme-change.scss b/src/assets/styles/core/theme-change.scss new file mode 100644 index 0000000..5b640d2 --- /dev/null +++ b/src/assets/styles/core/theme-change.scss @@ -0,0 +1,11 @@ +// 主题切换过渡优化,优化除视觉上的不适感 +.theme-change { + * { + transition: 0s !important; + } + + .el-switch__core, + .el-switch__action { + transition: all 0.3s !important; + } +} diff --git a/src/assets/styles/custom/one-dark-pro.scss b/src/assets/styles/custom/one-dark-pro.scss new file mode 100644 index 0000000..36bdf63 --- /dev/null +++ b/src/assets/styles/custom/one-dark-pro.scss @@ -0,0 +1,98 @@ +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + + color: #a6accd; +} + +.hljs-string, +.hljs-section, +.hljs-selector-class, +.hljs-template-variable, +.hljs-deletion { + color: #aed07e !important; +} + +.hljs-comment, +.hljs-quote { + color: #6f747d; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-formula { + color: #c792ea; +} + +.hljs-section, +.hljs-name, +.hljs-selector-tag, +.hljs-deletion, +.hljs-subst { + color: #c86068; +} + +.hljs-literal { + color: #56b6c2; +} + +.hljs-string, +.hljs-regexp, +.hljs-addition, +.hljs-attribute, +.hljs-meta-string { + color: #abb2bf; +} + +.hljs-attribute { + color: #c792ea; +} + +.hljs-function { + color: #c792ea; +} + +.hljs-type { + color: #f07178; +} + +.hljs-title { + color: #82aaff !important; +} + +.hljs-built_in, +.hljs-class { + color: #82aaff; +} + +// 括号 +.hljs-params { + color: #a6accd; +} + +.hljs-attr, +.hljs-variable, +.hljs-template-variable, +.hljs-selector-class, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-number { + color: #de7e61; +} + +.hljs-symbol, +.hljs-bullet, +.hljs-link, +.hljs-meta, +.hljs-selector-id { + color: #61aeee; +} + +.hljs-strong { + font-weight: bold; +} + +.hljs-link { + text-decoration: underline; +} diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss new file mode 100644 index 0000000..0267276 --- /dev/null +++ b/src/assets/styles/index.scss @@ -0,0 +1,26 @@ +// 重置默认样式 +@use './core/reset.scss'; + +// 应用全局样式 +@use './core/app.scss'; + +// Element Plus 样式优化 +@use './core/el-ui.scss'; + +// Element Plus 暗黑主题 +@use './core/el-dark.scss'; + +// 暗黑主题样式优化 +@use './core/dark.scss'; + +// 路由切换动画 +@use './core/router-transition'; + +// 主题切换过渡优化 +@use './core/theme-change.scss'; + +// 主题切换圆形扩散动画 +@use './core/theme-animation.scss'; + +// 统一操作按钮组件 +@use './components/action-btn.scss'; diff --git a/src/assets/svg/loading.ts b/src/assets/svg/loading.ts new file mode 100644 index 0000000..fdfb078 --- /dev/null +++ b/src/assets/svg/loading.ts @@ -0,0 +1,32 @@ +// 自定义四点旋转SVG +export const fourDotsSpinnerSvg = ` + + + + + + + + + +` diff --git a/src/components/announcement/AnnouncementPreview.vue b/src/components/announcement/AnnouncementPreview.vue new file mode 100644 index 0000000..4626f89 --- /dev/null +++ b/src/components/announcement/AnnouncementPreview.vue @@ -0,0 +1,413 @@ + + + + + diff --git a/src/components/announcement/AudienceSelector.vue b/src/components/announcement/AudienceSelector.vue new file mode 100644 index 0000000..8bcdd4c --- /dev/null +++ b/src/components/announcement/AudienceSelector.vue @@ -0,0 +1,766 @@ + + + diff --git a/src/components/billing/BillingAmountDisplay.vue b/src/components/billing/BillingAmountDisplay.vue new file mode 100644 index 0000000..4c417cf --- /dev/null +++ b/src/components/billing/BillingAmountDisplay.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/billing/BillingStatusTag.vue b/src/components/billing/BillingStatusTag.vue new file mode 100644 index 0000000..5cd85af --- /dev/null +++ b/src/components/billing/BillingStatusTag.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/billing/BillingTimeline.vue b/src/components/billing/BillingTimeline.vue new file mode 100644 index 0000000..f344f20 --- /dev/null +++ b/src/components/billing/BillingTimeline.vue @@ -0,0 +1,132 @@ + + + diff --git a/src/components/billing/PaymentMethodIcon.vue b/src/components/billing/PaymentMethodIcon.vue new file mode 100644 index 0000000..60de7b8 --- /dev/null +++ b/src/components/billing/PaymentMethodIcon.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/business/contact_modal/ContactModal.vue b/src/components/business/contact_modal/ContactModal.vue new file mode 100644 index 0000000..03b2370 --- /dev/null +++ b/src/components/business/contact_modal/ContactModal.vue @@ -0,0 +1,186 @@ + + + diff --git a/src/components/common/CategorySelect.vue b/src/components/common/CategorySelect.vue new file mode 100644 index 0000000..5e2cc25 --- /dev/null +++ b/src/components/common/CategorySelect.vue @@ -0,0 +1,82 @@ + + + diff --git a/src/components/common/ImageUpload.vue b/src/components/common/ImageUpload.vue new file mode 100644 index 0000000..6034e00 --- /dev/null +++ b/src/components/common/ImageUpload.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/src/components/common/MerchantSelect.vue b/src/components/common/MerchantSelect.vue new file mode 100644 index 0000000..fd2424d --- /dev/null +++ b/src/components/common/MerchantSelect.vue @@ -0,0 +1,118 @@ + + + diff --git a/src/components/common/RichTextEditor.example.ts b/src/components/common/RichTextEditor.example.ts new file mode 100644 index 0000000..ac03c19 --- /dev/null +++ b/src/components/common/RichTextEditor.example.ts @@ -0,0 +1,134 @@ +/** + * RichTextEditor 组件使用示例和验收标准验证 + * + * 任务:FE-ANNOUNCE-07 创建 RichTextEditor 组件 + * + * ✅ 验收标准检查清单: + * + * 1. ✅ 组件可通过 v-model 绑定 HTML 内容 + * - 实现:使用 defineModel 和 v-model 实现双向绑定 + * - 代码:line 59, 85-92, 100-105 + * + * 2. ✅ 工具栏功能正常(加粗、斜体等) + * - 实现:配置简化工具栏包含 bold、italic、underline、headerSelect、list、link + * - 代码:line 63-76 toolbarConfig + * + * 3. ✅ disabled 状态下编辑器只读 + * - 实现:通过 readOnly 配置和动态监听 disabled 变化调用 enable/disable + * - 代码:line 80 (readOnly), line 94-103 (watch disabled) + * + * 4. ✅ 组件销毁时正确清理编辑器实例 + * - 实现:onBeforeUnmount 钩子中调用 editor.destroy() + * - 代码:line 116-121 + * + * 5. ✅ TypeScript 类型完整无 any + * - 实现:所有 props、emits、变量都有明确类型定义 + * - 代码:Props interface (line 17-30), emits (line 34-37), + * IDomEditor (line 40), computed types (line 43-48, 63-76, 79-83) + * + * 📋 实现的额外功能: + * - 支持自定义高度(height prop) + * - 支持自定义占位符(placeholder prop) + * - 支持编辑器模式切换(mode prop: default/simple) + * - 暴露编辑器实例方法(getEditor, setHtml, getHtml, clear, focus, disable, enable) + * - 正确处理外部 modelValue 变化(避免循环更新) + * + * 🔧 技术栈: + * - @wangeditor/editor 5.1.23 + * - @wangeditor/editor-for-vue (next) + * - Vue 3.5 Composition API (script setup) + * - TypeScript 5.6 + * + * 📝 使用示例: + */ + +// 示例 1: 基础使用 +/* + + + +*/ + +// 示例 2: 禁用状态 +/* + + + +*/ + +// 示例 3: 自定义高度和监听变化 +/* + + + +*/ + +// 示例 4: 使用 ref 访问编辑器实例方法 +/* + + + +*/ + +export default {} diff --git a/src/components/common/RichTextEditor.vue b/src/components/common/RichTextEditor.vue new file mode 100644 index 0000000..110fe09 --- /dev/null +++ b/src/components/common/RichTextEditor.vue @@ -0,0 +1,182 @@ + + + + + + diff --git a/src/components/core/banners/art-basic-banner/index.vue b/src/components/core/banners/art-basic-banner/index.vue new file mode 100644 index 0000000..65b47e4 --- /dev/null +++ b/src/components/core/banners/art-basic-banner/index.vue @@ -0,0 +1,343 @@ + + + + + + diff --git a/src/components/core/banners/art-card-banner/index.vue b/src/components/core/banners/art-card-banner/index.vue new file mode 100644 index 0000000..8a5f9d4 --- /dev/null +++ b/src/components/core/banners/art-card-banner/index.vue @@ -0,0 +1,114 @@ + + + + diff --git a/src/components/core/base/art-back-to-top/index.vue b/src/components/core/base/art-back-to-top/index.vue new file mode 100644 index 0000000..6f8da61 --- /dev/null +++ b/src/components/core/base/art-back-to-top/index.vue @@ -0,0 +1,40 @@ + + + + diff --git a/src/components/core/base/art-logo/index.vue b/src/components/core/base/art-logo/index.vue new file mode 100644 index 0000000..8bc8309 --- /dev/null +++ b/src/components/core/base/art-logo/index.vue @@ -0,0 +1,21 @@ + + + + diff --git a/src/components/core/base/art-svg-icon/index.vue b/src/components/core/base/art-svg-icon/index.vue new file mode 100644 index 0000000..0bfcd0c --- /dev/null +++ b/src/components/core/base/art-svg-icon/index.vue @@ -0,0 +1,24 @@ + + + + diff --git a/src/components/core/cards/art-bar-chart-card/index.vue b/src/components/core/cards/art-bar-chart-card/index.vue new file mode 100644 index 0000000..6815c2b --- /dev/null +++ b/src/components/core/cards/art-bar-chart-card/index.vue @@ -0,0 +1,103 @@ + + + + diff --git a/src/components/core/cards/art-data-list-card/index.vue b/src/components/core/cards/art-data-list-card/index.vue new file mode 100644 index 0000000..fc43323 --- /dev/null +++ b/src/components/core/cards/art-data-list-card/index.vue @@ -0,0 +1,74 @@ + + + + diff --git a/src/components/core/cards/art-donut-chart-card/index.vue b/src/components/core/cards/art-donut-chart-card/index.vue new file mode 100644 index 0000000..df2dcbb --- /dev/null +++ b/src/components/core/cards/art-donut-chart-card/index.vue @@ -0,0 +1,124 @@ + + + + diff --git a/src/components/core/cards/art-image-card/index.vue b/src/components/core/cards/art-image-card/index.vue new file mode 100644 index 0000000..d27fe00 --- /dev/null +++ b/src/components/core/cards/art-image-card/index.vue @@ -0,0 +1,89 @@ + + + + diff --git a/src/components/core/cards/art-line-chart-card/index.vue b/src/components/core/cards/art-line-chart-card/index.vue new file mode 100644 index 0000000..e58c9b2 --- /dev/null +++ b/src/components/core/cards/art-line-chart-card/index.vue @@ -0,0 +1,126 @@ + + + + diff --git a/src/components/core/cards/art-progress-card/index.vue b/src/components/core/cards/art-progress-card/index.vue new file mode 100644 index 0000000..048a836 --- /dev/null +++ b/src/components/core/cards/art-progress-card/index.vue @@ -0,0 +1,86 @@ + + + + diff --git a/src/components/core/cards/art-stats-card/index.vue b/src/components/core/cards/art-stats-card/index.vue new file mode 100644 index 0000000..8e0341b --- /dev/null +++ b/src/components/core/cards/art-stats-card/index.vue @@ -0,0 +1,67 @@ + + + + diff --git a/src/components/core/cards/art-timeline-list-card/index.vue b/src/components/core/cards/art-timeline-list-card/index.vue new file mode 100644 index 0000000..fbb2c78 --- /dev/null +++ b/src/components/core/cards/art-timeline-list-card/index.vue @@ -0,0 +1,69 @@ + + + + diff --git a/src/components/core/charts/art-bar-chart/index.vue b/src/components/core/charts/art-bar-chart/index.vue new file mode 100644 index 0000000..d677196 --- /dev/null +++ b/src/components/core/charts/art-bar-chart/index.vue @@ -0,0 +1,203 @@ + + + + diff --git a/src/components/core/charts/art-dual-bar-compare-chart/index.vue b/src/components/core/charts/art-dual-bar-compare-chart/index.vue new file mode 100644 index 0000000..32aa60f --- /dev/null +++ b/src/components/core/charts/art-dual-bar-compare-chart/index.vue @@ -0,0 +1,195 @@ + + + + diff --git a/src/components/core/charts/art-h-bar-chart/index.vue b/src/components/core/charts/art-h-bar-chart/index.vue new file mode 100644 index 0000000..2e34759 --- /dev/null +++ b/src/components/core/charts/art-h-bar-chart/index.vue @@ -0,0 +1,208 @@ + + + + diff --git a/src/components/core/charts/art-k-line-chart/index.vue b/src/components/core/charts/art-k-line-chart/index.vue new file mode 100644 index 0000000..0061b51 --- /dev/null +++ b/src/components/core/charts/art-k-line-chart/index.vue @@ -0,0 +1,152 @@ + + + + diff --git a/src/components/core/charts/art-line-chart/index.vue b/src/components/core/charts/art-line-chart/index.vue new file mode 100644 index 0000000..b70c2c3 --- /dev/null +++ b/src/components/core/charts/art-line-chart/index.vue @@ -0,0 +1,371 @@ + + + + diff --git a/src/components/core/charts/art-radar-chart/index.vue b/src/components/core/charts/art-radar-chart/index.vue new file mode 100644 index 0000000..e99fff6 --- /dev/null +++ b/src/components/core/charts/art-radar-chart/index.vue @@ -0,0 +1,105 @@ + + + + diff --git a/src/components/core/charts/art-ring-chart/index.vue b/src/components/core/charts/art-ring-chart/index.vue new file mode 100644 index 0000000..79115f7 --- /dev/null +++ b/src/components/core/charts/art-ring-chart/index.vue @@ -0,0 +1,133 @@ + + + + diff --git a/src/components/core/charts/art-scatter-chart/index.vue b/src/components/core/charts/art-scatter-chart/index.vue new file mode 100644 index 0000000..995b56a --- /dev/null +++ b/src/components/core/charts/art-scatter-chart/index.vue @@ -0,0 +1,115 @@ + + + + diff --git a/src/components/core/forms/art-button-more/index.vue b/src/components/core/forms/art-button-more/index.vue new file mode 100644 index 0000000..858d305 --- /dev/null +++ b/src/components/core/forms/art-button-more/index.vue @@ -0,0 +1,71 @@ + + + + diff --git a/src/components/core/forms/art-button-table/index.vue b/src/components/core/forms/art-button-table/index.vue new file mode 100644 index 0000000..c849901 --- /dev/null +++ b/src/components/core/forms/art-button-table/index.vue @@ -0,0 +1,59 @@ + + + + diff --git a/src/components/core/forms/art-drag-verify/index.vue b/src/components/core/forms/art-drag-verify/index.vue new file mode 100644 index 0000000..5306e04 --- /dev/null +++ b/src/components/core/forms/art-drag-verify/index.vue @@ -0,0 +1,430 @@ + + + + + + + + diff --git a/src/components/core/forms/art-excel-export/index.vue b/src/components/core/forms/art-excel-export/index.vue new file mode 100644 index 0000000..08207c2 --- /dev/null +++ b/src/components/core/forms/art-excel-export/index.vue @@ -0,0 +1,389 @@ + + + + + + diff --git a/src/components/core/forms/art-excel-import/index.vue b/src/components/core/forms/art-excel-import/index.vue new file mode 100644 index 0000000..8aa82fe --- /dev/null +++ b/src/components/core/forms/art-excel-import/index.vue @@ -0,0 +1,62 @@ + + + + diff --git a/src/components/core/forms/art-form/index.vue b/src/components/core/forms/art-form/index.vue new file mode 100644 index 0000000..1e76f14 --- /dev/null +++ b/src/components/core/forms/art-form/index.vue @@ -0,0 +1,311 @@ + + + + + + diff --git a/src/components/core/forms/art-search-bar/index.vue b/src/components/core/forms/art-search-bar/index.vue new file mode 100644 index 0000000..b25b5bb --- /dev/null +++ b/src/components/core/forms/art-search-bar/index.vue @@ -0,0 +1,437 @@ + + + + + + + + diff --git a/src/components/core/forms/art-wang-editor/index.vue b/src/components/core/forms/art-wang-editor/index.vue new file mode 100644 index 0000000..38741fe --- /dev/null +++ b/src/components/core/forms/art-wang-editor/index.vue @@ -0,0 +1,223 @@ + + + + + + diff --git a/src/components/core/forms/art-wang-editor/style.scss b/src/components/core/forms/art-wang-editor/style.scss new file mode 100644 index 0000000..fd5dbca --- /dev/null +++ b/src/components/core/forms/art-wang-editor/style.scss @@ -0,0 +1,210 @@ +$box-radius: calc(var(--custom-radius) / 3 + 2px); + +// 全屏容器 z-index 调整 +.w-e-full-screen-container { + z-index: 100 !important; +} + +/* 编辑器容器 */ +.editor-wrapper { + width: 100%; + height: 100%; + border: 1px solid var(--art-gray-300); + border-radius: $box-radius !important; + + .w-e-bar { + border-radius: $box-radius $box-radius 0 0 !important; + } + + .menu-item { + display: flex; + flex-direction: row; + align-items: center; + + i { + margin-right: 5px; + } + } + + /* 工具栏 */ + .editor-toolbar { + border-bottom: 1px solid var(--default-border); + } + + /* 下拉选择框配置 */ + .w-e-select-list { + min-width: 140px; + padding: 5px 10px 10px; + border: none; + border-radius: $box-radius; + } + + /* 下拉选择框元素配置 */ + .w-e-select-list ul li { + margin-top: 5px; + font-size: 15px !important; + border-radius: $box-radius; + } + + /* 下拉选择框 正文文字大小调整 */ + .w-e-select-list ul li:last-of-type { + font-size: 16px !important; + } + + /* 下拉选择框 hover 样式调整 */ + .w-e-select-list ul li:hover { + background-color: var(--art-gray-200); + } + + :root { + /* 激活颜色 */ + --w-e-toolbar-active-bg-color: var(--art-gray-200); + + /* toolbar 图标和文字颜色 */ + --w-e-toolbar-color: #000; + + /* 表格选中时候的边框颜色 */ + --w-e-textarea-selected-border-color: #ddd; + + /* 表格头背景颜色 */ + --w-e-textarea-slight-bg-color: var(--art-gray-200); + } + + /* 工具栏按钮样式 */ + .w-e-bar-item svg { + fill: var(--art-gray-800); + } + + .w-e-bar-item button { + color: var(--art-gray-800); + border-radius: $box-radius; + } + + /* 工具栏 hover 按钮背景颜色 */ + .w-e-bar-item button:hover { + background-color: var(--art-gray-200); + } + + /* 工具栏分割线 */ + .w-e-bar-divider { + height: 20px; + margin-top: 10px; + background-color: #ccc; + } + + /* 工具栏菜单 */ + .w-e-bar-item-group .w-e-bar-item-menus-container { + min-width: 120px; + padding: 10px 0; + border: none; + border-radius: $box-radius; + + .w-e-bar-item { + button { + width: 100%; + margin: 0 5px; + } + } + } + + /* 代码块 */ + .w-e-text-container [data-slate-editor] pre > code { + padding: 0.6rem 1rem; + background-color: var(--art-gray-50); + border-radius: $box-radius; + } + + /* 弹出框 */ + .w-e-drop-panel { + border: 0; + border-radius: $box-radius; + } + + a { + color: #318ef4; + } + + .w-e-text-container { + strong, + b { + font-weight: 500; + } + + i, + em { + font-style: italic; + } + } + + /* 表格样式优化 */ + .w-e-text-container [data-slate-editor] .table-container th { + border-right: none; + } + + .w-e-text-container [data-slate-editor] .table-container th:last-of-type { + border-right: 1px solid #ccc !important; + } + + /* 引用 */ + .w-e-text-container [data-slate-editor] blockquote { + background-color: var(--art-gray-200); + border-left: 4px solid var(--art-gray-300); + } + + /* 输入区域弹出 bar */ + .w-e-hover-bar { + border-radius: $box-radius; + } + + /* 超链接弹窗 */ + .w-e-modal { + border: none; + border-radius: $box-radius; + } + + /* 图片样式调整 */ + .w-e-text-container [data-slate-editor] .w-e-selected-image-container { + overflow: inherit; + + &:hover { + border: 0; + } + + img { + border: 1px solid transparent; + transition: border 0.3s; + + &:hover { + border: 1px solid #318ef4 !important; + } + } + + .w-e-image-dragger { + width: 12px; + height: 12px; + background-color: #318ef4; + border: 2px solid #fff; + border-radius: $box-radius; + } + + .left-top { + top: -6px; + left: -6px; + } + + .right-top { + top: -6px; + right: -6px; + } + + .left-bottom { + bottom: -6px; + left: -6px; + } + + .right-bottom { + right: -6px; + bottom: -6px; + } + } +} diff --git a/src/components/core/layouts/art-breadcrumb/index.vue b/src/components/core/layouts/art-breadcrumb/index.vue new file mode 100644 index 0000000..4b54859 --- /dev/null +++ b/src/components/core/layouts/art-breadcrumb/index.vue @@ -0,0 +1,142 @@ + + + + diff --git a/src/components/core/layouts/art-chat-window/index.vue b/src/components/core/layouts/art-chat-window/index.vue new file mode 100644 index 0000000..f3d9471 --- /dev/null +++ b/src/components/core/layouts/art-chat-window/index.vue @@ -0,0 +1,262 @@ + + + + diff --git a/src/components/core/layouts/art-fast-enter/index.vue b/src/components/core/layouts/art-fast-enter/index.vue new file mode 100644 index 0000000..fdde222 --- /dev/null +++ b/src/components/core/layouts/art-fast-enter/index.vue @@ -0,0 +1,113 @@ + + + + diff --git a/src/components/core/layouts/art-fireworks-effect/index.vue b/src/components/core/layouts/art-fireworks-effect/index.vue new file mode 100644 index 0000000..be85274 --- /dev/null +++ b/src/components/core/layouts/art-fireworks-effect/index.vue @@ -0,0 +1,633 @@ + + + + diff --git a/src/components/core/layouts/art-global-component/index.vue b/src/components/core/layouts/art-global-component/index.vue new file mode 100644 index 0000000..6908f94 --- /dev/null +++ b/src/components/core/layouts/art-global-component/index.vue @@ -0,0 +1,14 @@ + + + + diff --git a/src/components/core/layouts/art-global-search/index.vue b/src/components/core/layouts/art-global-search/index.vue new file mode 100644 index 0000000..a7d88df --- /dev/null +++ b/src/components/core/layouts/art-global-search/index.vue @@ -0,0 +1,426 @@ + + + + + + + diff --git a/src/components/core/layouts/art-header-bar/index.vue b/src/components/core/layouts/art-header-bar/index.vue new file mode 100644 index 0000000..2db0db8 --- /dev/null +++ b/src/components/core/layouts/art-header-bar/index.vue @@ -0,0 +1,521 @@ + + + + + + diff --git a/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue b/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue new file mode 100644 index 0000000..ab0ea0c --- /dev/null +++ b/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue @@ -0,0 +1,172 @@ + + + + + + diff --git a/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue b/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue new file mode 100644 index 0000000..edd1473 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue @@ -0,0 +1,110 @@ + + + + + + diff --git a/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue b/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue new file mode 100644 index 0000000..ff32c1e --- /dev/null +++ b/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/core/layouts/art-menus/art-mixed-menu/index.vue b/src/components/core/layouts/art-menus/art-mixed-menu/index.vue new file mode 100644 index 0000000..4e98246 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-mixed-menu/index.vue @@ -0,0 +1,279 @@ + + + + + + + + diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue b/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue new file mode 100644 index 0000000..39387dc --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue @@ -0,0 +1,355 @@ + + + + + + + + diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss b/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss new file mode 100644 index 0000000..b98011c --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss @@ -0,0 +1,253 @@ +.layout-sidebar { + display: flex; + height: 100vh; + user-select: none; + scrollbar-width: none; + border-right: 1px solid var(--art-card-border); + + &.no-border { + border-right: none !important; + } + + // 自定义滚动条宽度 + :deep(.el-scrollbar__bar.is-vertical) { + width: 4px; + } + + :deep(.el-scrollbar__thumb) { + right: -2px; + background-color: #ccc; + border-radius: 2px; + } + + .dual-menu-left { + position: relative; + width: 80px; + height: 100%; + border-right: 1px solid var(--art-card-border) !important; + transition: width 0.25s; + + .logo { + margin: auto; + margin-top: 12px; + margin-bottom: 3px; + cursor: pointer; + } + + ul { + li { + > div { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 8px; + overflow: hidden; + text-align: center; + cursor: pointer; + border-radius: 5px; + + .art-svg-icon { + display: block; + margin: 0 auto; + font-size: 20px; + } + + span { + display: -webkit-box; + width: 100%; + overflow: hidden; + font-size: 12px; + text-overflow: ellipsis; + -webkit-line-clamp: 1; + line-clamp: 1; + -webkit-box-orient: vertical; + } + + &.is-active { + background: var(--el-color-primary-light-9); + + .art-svg-icon, + span { + color: var(--theme-color) !important; + } + } + } + } + } + + .switch-btn { + position: absolute; + right: 0; + bottom: 15px; + left: 0; + margin: auto; + } + } + + .menu-left { + position: relative; + box-sizing: border-box; + height: 100vh; + + @media only screen and (width <= 640px) { + height: 100dvh; + } + + .el-menu { + height: 100%; + } + + &:hover { + .dual-menu-collapse-btn { + opacity: 1 !important; + } + } + + .dual-menu-collapse-btn { + position: absolute; + top: 50%; + right: -11px; + z-index: 10; + width: 11px; + height: 50px; + cursor: pointer; + background-color: var(--default-box-color); + border: 1px solid var(--art-card-border); + border-radius: 0 15px 15px 0; + opacity: 0; + transition: opacity 0.2s; + transform: translateY(-50%); + + &:hover { + .art-svg-icon { + color: var(--art-gray-800) !important; + } + } + + .art-svg-icon { + position: absolute; + top: 0; + bottom: 0; + left: -4px; + margin: auto; + transition: all 0.3s; + } + } + } + + .header { + position: relative; + box-sizing: border-box; + display: flex; + align-items: center; + width: 100%; + height: 60px; + overflow: hidden; + line-height: 60px; + cursor: pointer; + + .logo { + margin-left: 22px; + } + + p { + position: absolute; + top: 0; + bottom: 0; + left: 58px; + box-sizing: border-box; + margin-left: 10px; + font-size: 18px; + + &.is-dual-menu-name { + left: 25px; + margin: auto; + } + } + } + + .el-menu { + box-sizing: border-box; + height: calc(100vh - 60px); + overflow-y: auto; + // 防止菜单内的滚动影响整个页面滚动 + overscroll-behavior: contain; + border-right: 0; + scrollbar-width: none; + -ms-scroll-chaining: contain; + + &::-webkit-scrollbar { + width: 0 !important; + } + } + + .menu-model { + display: none; + } +} + +@media only screen and (width <= 800px) { + .layout-sidebar { + width: 0; + + .header { + height: 50px; + line-height: 50px; + } + + .el-menu { + height: calc(100vh - 60px); + } + + .el-menu--collapse { + width: 0; + } + + // 折叠状态下的header样式 + .menu-left-close .header { + .logo { + display: none; + } + + p { + left: 16px; + font-size: 0; + opacity: 0 !important; + } + } + + .menu-model { + position: fixed; + top: 0; + left: 0; + z-index: -1; + display: block; + width: 100%; + height: 100vh; + background: rgba($color: #000, $alpha: 50%); + transition: opacity 0.2s ease-in-out; + } + } +} + +@media only screen and (width <= 640px) { + .layout-sidebar { + border-right: 0 !important; + } +} + +.dark { + .layout-sidebar { + border-right: 1px solid rgb(255 255 255 / 13%); + + :deep(.el-scrollbar__thumb) { + background-color: #777; + } + + .dual-menu-left { + border-right: 1px solid rgb(255 255 255 / 9%) !important; + } + } +} diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss b/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss new file mode 100644 index 0000000..7626c42 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss @@ -0,0 +1,258 @@ +@use '@styles/core/mixin.scss' as *; + +// 菜单样式变量 +$menu-height: 42px; +$menu-icon-size: 20px; +$menu-font-size: 14px; +$hover-bg-color: var(--art-gray-200); +$popup-menu-height: 40px; +$popup-menu-padding: 8px; +$popup-menu-margin: 5px; +$popup-menu-radius: 6px; + +// 通用菜单项样式 +@mixin menu-item-base { + width: calc(100% - 16px); + margin-left: 8px; + border-radius: 6px; + + .menu-icon { + margin-left: -7px; + } +} + +// 通用 hover 样式 +@mixin menu-hover($bg-color) { + .el-sub-menu__title:hover, + .el-menu-item:not(.is-active):hover { + background: $bg-color !important; + } +} + +// 通用选中样式 +@mixin menu-active($color, $bg-color, $icon-color: var(--theme-color)) { + .el-menu-item.is-active { + color: $color !important; + background-color: $bg-color; + + .menu-icon { + .art-svg-icon { + color: $icon-color !important; + } + } + } +} + +// 弹窗菜单项样式 +@mixin popup-menu-item { + height: $popup-menu-height; + margin-bottom: $popup-menu-margin; + border-radius: $popup-menu-radius; + + .menu-icon { + margin-right: 5px; + } + + &:last-of-type { + margin-bottom: 0; + } +} + +// 主题菜单通用样式(合并 design 和 dark 主题的共同逻辑) +@mixin theme-menu-base { + .el-sub-menu__title, + .el-menu-item { + @include menu-item-base; + } +} + +// 弹窗菜单通用样式 +@mixin popup-menu-base($hover-bg, $active-color, $active-bg) { + .el-menu--popup { + padding: $popup-menu-padding; + + .el-sub-menu__title:hover, + .el-menu-item:hover { + background-color: $hover-bg !important; + border-radius: $popup-menu-radius; + } + + .el-menu-item { + @include popup-menu-item; + + &.is-active { + color: $active-color !important; + background-color: $active-bg !important; + } + } + + .el-sub-menu { + @include popup-menu-item; + + height: $popup-menu-height !important; + + .el-sub-menu__title { + height: $popup-menu-height !important; + border-radius: $popup-menu-radius; + } + } + } +} + +.layout-sidebar { + // ---------------------- Modify default style ---------------------- + + // 菜单折叠样式 + .menu-left-close { + .header { + .logo { + margin: 0 auto; + } + } + } + + // 菜单图标 + .menu-icon { + margin-right: 8px; + font-size: $menu-icon-size; + } + + // 菜单高度 + .el-sub-menu__title, + .el-menu-item { + height: $menu-height !important; + margin-bottom: 4px; + line-height: $menu-height !important; + + span { + font-size: $menu-font-size !important; + + @include ellipsis(); + } + } + + // 右侧箭头 + .el-sub-menu__icon-arrow { + width: 13px !important; + font-size: 13px !important; + } + + // 菜单折叠 + .el-menu--collapse { + .el-sub-menu.is-active { + .el-sub-menu__title { + .menu-icon { + .art-svg-icon { + // 选中菜单图标颜色 + color: var(--theme-color) !important; + } + } + } + } + } + + // ---------------------- Design theme menu ---------------------- + .el-menu-design { + @include theme-menu-base; + @include menu-active(var(--theme-color), var(--el-color-primary-light-9)); + @include menu-hover($hover-bg-color); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-600); + } + } + + // ---------------------- Dark theme menu ---------------------- + .el-menu-dark { + @include theme-menu-base; + @include menu-active(#fff, #27282d, #fff); + @include menu-hover(#0f1015); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-400); + } + } + + // ---------------------- Light theme menu ---------------------- + .el-menu-light { + .el-sub-menu__title, + .el-menu-item { + .menu-icon { + margin-left: 1px; + } + } + + .el-menu-item.is-active { + background-color: var(--el-color-primary-light-9); + + .art-svg-icon { + color: var(--theme-color) !important; + } + + &::before { + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + content: ''; + background: var(--theme-color); + } + } + + @include menu-hover($hover-bg-color); + + .el-sub-menu__icon-arrow { + color: var(--art-gray-600); + } + } +} + +@media only screen and (width <= 640px) { + .layout-sidebar { + .el-menu-design { + > .el-sub-menu { + margin-left: 0; + } + + .el-sub-menu { + width: 100% !important; + } + } + } +} + +// 菜单折叠 hover 弹窗样式(浅色主题) +.el-menu--vertical, +.el-menu--popup-container { + @include popup-menu-base(var(--art-gray-200), var(--art-gray-900), var(--art-gray-200)); +} + +// 暗黑模式菜单样式 +.dark { + .el-menu--vertical, + .el-menu--popup-container { + @include popup-menu-base(var(--art-gray-200), var(--art-gray-900), #292a2e); + } + + .layout-sidebar { + // 图标颜色、文字颜色 + .menu-icon .art-svg-icon, + .menu-name { + color: var(--art-gray-800) !important; + } + + // 选中的文字颜色跟图标颜色 + .el-menu-item.is-active { + span, + .menu-icon .art-svg-icon { + color: var(--theme-color) !important; + } + } + + // 右侧箭头颜色 + .el-sub-menu__icon-arrow { + color: #fff; + } + } +} diff --git a/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue b/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue new file mode 100644 index 0000000..a7ac6a9 --- /dev/null +++ b/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue @@ -0,0 +1,188 @@ + + + diff --git a/src/components/core/layouts/art-notification/index.vue b/src/components/core/layouts/art-notification/index.vue new file mode 100644 index 0000000..a58853c --- /dev/null +++ b/src/components/core/layouts/art-notification/index.vue @@ -0,0 +1,456 @@ + + + + + + diff --git a/src/components/core/layouts/art-page-content/index.vue b/src/components/core/layouts/art-page-content/index.vue new file mode 100644 index 0000000..a862df1 --- /dev/null +++ b/src/components/core/layouts/art-page-content/index.vue @@ -0,0 +1,136 @@ + + + diff --git a/src/components/core/layouts/art-screen-lock/index.vue b/src/components/core/layouts/art-screen-lock/index.vue new file mode 100644 index 0000000..a3bf58b --- /dev/null +++ b/src/components/core/layouts/art-screen-lock/index.vue @@ -0,0 +1,522 @@ + + + + + + diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts new file mode 100644 index 0000000..35e8066 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.ts @@ -0,0 +1,248 @@ +import { computed } from 'vue' +import { useI18n } from 'vue-i18n' +import { ContainerWidthEnum } from '@/enums/appEnum' +import AppConfig from '@/config' +import { headerBarConfig } from '@/config/modules/headerBar' + +/** + * 设置项配置选项管理 + */ +export function useSettingsConfig() { + const { t } = useI18n() + + // 标签页风格选项 + const tabStyleOptions = computed(() => [ + { + value: 'tab-default', + label: t('setting.tabStyle.default') + }, + { + value: 'tab-card', + label: t('setting.tabStyle.card') + }, + { + value: 'tab-google', + label: t('setting.tabStyle.google') + } + ]) + + // 页面切换动画选项 + const pageTransitionOptions = computed(() => [ + { + value: '', + label: t('setting.transition.list.none') + }, + { + value: 'fade', + label: t('setting.transition.list.fade') + }, + { + value: 'slide-left', + label: t('setting.transition.list.slideLeft') + }, + { + value: 'slide-bottom', + label: t('setting.transition.list.slideBottom') + }, + { + value: 'slide-top', + label: t('setting.transition.list.slideTop') + } + ]) + + // 圆角大小选项 + const customRadiusOptions = [ + { value: '0', label: '0' }, + { value: '0.25', label: '0.25' }, + { value: '0.5', label: '0.5' }, + { value: '0.75', label: '0.75' }, + { value: '1', label: '1' } + ] + + // 容器宽度选项 + const containerWidthOptions = computed(() => [ + { + value: ContainerWidthEnum.FULL, + label: t('setting.container.list[0]'), + icon: 'icon-park-outline:auto-width' + }, + { + value: ContainerWidthEnum.BOXED, + label: t('setting.container.list[1]'), + icon: 'ix:width' + } + ]) + + // 盒子样式选项 + const boxStyleOptions = computed(() => [ + { + value: 'border-mode', + label: t('setting.box.list[0]'), + type: 'border-mode' as const + }, + { + value: 'shadow-mode', + label: t('setting.box.list[1]'), + type: 'shadow-mode' as const + } + ]) + + // 从配置文件获取的选项 + const configOptions = { + // 主题色彩选项 + mainColors: AppConfig.systemMainColor, + + // 主题风格选项 + themeList: AppConfig.settingThemeList, + + // 菜单布局选项 + menuLayoutList: AppConfig.menuLayoutList + } + + // 基础设置项配置 + const basicSettingsConfig = computed(() => { + // 定义所有基础设置项 + const allSettings = [ + { + key: 'showWorkTab', + label: t('setting.basics.list.multiTab'), + type: 'switch' as const, + handler: 'workTab', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'uniqueOpened', + label: t('setting.basics.list.accordion'), + type: 'switch' as const, + handler: 'uniqueOpened', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'showMenuButton', + label: t('setting.basics.list.collapseSidebar'), + type: 'switch' as const, + handler: 'menuButton', + headerBarKey: 'menuButton' as const + }, + { + key: 'showFastEnter', + label: t('setting.basics.list.fastEnter'), + type: 'switch' as const, + handler: 'fastEnter', + headerBarKey: 'fastEnter' as const + }, + { + key: 'showRefreshButton', + label: t('setting.basics.list.reloadPage'), + type: 'switch' as const, + handler: 'refreshButton', + headerBarKey: 'refreshButton' as const + }, + { + key: 'showCrumbs', + label: t('setting.basics.list.breadcrumb'), + type: 'switch' as const, + handler: 'crumbs', + mobileHide: true, + headerBarKey: 'breadcrumb' as const + }, + { + key: 'showLanguage', + label: t('setting.basics.list.language'), + type: 'switch' as const, + handler: 'language', + headerBarKey: 'language' as const + }, + { + key: 'showNprogress', + label: t('setting.basics.list.progressBar'), + type: 'switch' as const, + handler: 'nprogress', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'colorWeak', + label: t('setting.basics.list.weakMode'), + type: 'switch' as const, + handler: 'colorWeak', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'watermarkVisible', + label: t('setting.basics.list.watermark'), + type: 'switch' as const, + handler: 'watermark', + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'menuOpenWidth', + label: t('setting.basics.list.menuWidth'), + type: 'input-number' as const, + handler: 'menuOpenWidth', + min: 180, + max: 320, + step: 10, + style: { width: '120px' }, + controlsPosition: 'right' as const, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'tabStyle', + label: t('setting.basics.list.tabStyle'), + type: 'select' as const, + handler: 'tabStyle', + options: tabStyleOptions.value, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'pageTransition', + label: t('setting.basics.list.pageTransition'), + type: 'select' as const, + handler: 'pageTransition', + options: pageTransitionOptions.value, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + }, + { + key: 'customRadius', + label: t('setting.basics.list.borderRadius'), + type: 'select' as const, + handler: 'customRadius', + options: customRadiusOptions, + style: { width: '120px' }, + headerBarKey: null // 不依赖headerBar配置 + } + ] + + // 根据 headerBarConfig 过滤设置项 + return ( + allSettings + .filter((setting) => { + // 如果设置项不依赖headerBar配置,则始终显示 + if (setting.headerBarKey === null) { + return true + } + + // 如果依赖headerBar配置,检查对应的功能是否启用 + const headerBarFeature = headerBarConfig[setting.headerBarKey] + return headerBarFeature?.enabled !== false + }) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(({ headerBarKey: _headerBarKey, ...setting }) => setting) + ) + }) + + return { + // 选项配置 + tabStyleOptions, + pageTransitionOptions, + customRadiusOptions, + containerWidthOptions, + boxStyleOptions, + configOptions, + + // 设置项配置 + basicSettingsConfig + } +} diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts new file mode 100644 index 0000000..392c690 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.ts @@ -0,0 +1,167 @@ +import { useSettingStore } from '@/store/modules/setting' +import { storeToRefs } from 'pinia' +import type { ContainerWidthEnum } from '@/enums/appEnum' + +/** + * 设置项通用处理逻辑 + */ +export function useSettingsHandlers() { + const settingStore = useSettingStore() + + // DOM 操作相关 + const domOperations = { + // 设置HTML类名 + setHtmlClass: (className: string, add: boolean) => { + const el = document.getElementsByTagName('html')[0] + if (add) { + el.classList.add(className) + } else { + el.classList.remove(className) + } + }, + + // 设置根元素属性 + setRootAttribute: (attribute: string, value: string) => { + const el = document.documentElement + el.setAttribute(attribute, value) + }, + + // 设置body类名 + setBodyClass: (className: string, add: boolean) => { + const el = document.getElementsByTagName('body')[0] + if (add) { + el.classList.add(className) + } else { + el.classList.remove(className) + } + } + } + + // 通用切换处理器 + const createToggleHandler = (storeMethod: () => void, callback?: () => void) => { + return () => { + storeMethod() + callback?.() + } + } + + // 通用值变更处理器 + const createValueHandler = ( + storeMethod: (value: T) => void, + callback?: (value: T) => void + ) => { + return (value: T) => { + if (value !== undefined && value !== null) { + storeMethod(value) + callback?.(value) + } + } + } + + // 基础设置处理器 + const basicHandlers = { + // 工作台标签页 + workTab: createToggleHandler(() => settingStore.setWorkTab(!settingStore.showWorkTab)), + + // 菜单手风琴 + uniqueOpened: createToggleHandler(() => settingStore.setUniqueOpened()), + + // 显示菜单按钮 + menuButton: createToggleHandler(() => settingStore.setButton()), + + // 显示快速入口 + fastEnter: createToggleHandler(() => settingStore.setFastEnter()), + + // 显示刷新按钮 + refreshButton: createToggleHandler(() => settingStore.setShowRefreshButton()), + + // 显示面包屑 + crumbs: createToggleHandler(() => settingStore.setCrumbs()), + + // 显示语言切换 + language: createToggleHandler(() => settingStore.setLanguage()), + + // 显示进度条 + nprogress: createToggleHandler(() => settingStore.setNprogress()), + + // 色弱模式 + colorWeak: createToggleHandler( + () => settingStore.setColorWeak(), + () => { + domOperations.setHtmlClass('color-weak', settingStore.colorWeak) + } + ), + + // 水印显示 + watermark: createToggleHandler(() => + settingStore.setWatermarkVisible(!settingStore.watermarkVisible) + ), + + // 菜单展开宽度 + menuOpenWidth: createValueHandler((width: number) => + settingStore.setMenuOpenWidth(width) + ), + + // 标签页风格 + tabStyle: createValueHandler((style: string) => settingStore.setTabStyle(style)), + + // 页面切换动画 + pageTransition: createValueHandler((transition: string) => + settingStore.setPageTransition(transition) + ), + + // 圆角大小 + customRadius: createValueHandler((radius: string) => + settingStore.setCustomRadius(radius) + ) + } + + // 盒子样式处理器 + const boxStyleHandlers = { + // 设置盒子模式 + setBoxMode: (type: 'border-mode' | 'shadow-mode') => { + const { boxBorderMode } = storeToRefs(settingStore) + + // 防止重复设置 + if ( + (type === 'shadow-mode' && boxBorderMode.value === false) || + (type === 'border-mode' && boxBorderMode.value === true) + ) { + return + } + + setTimeout(() => { + domOperations.setRootAttribute('data-box-mode', type) + settingStore.setBorderMode() + }, 50) + } + } + + // 颜色设置处理器 + const colorHandlers = { + // 选择主题色 + selectColor: (theme: string) => { + settingStore.setElementTheme(theme) + settingStore.reload() + } + } + + // 容器设置处理器 + const containerHandlers = { + // 设置容器宽度 + setWidth: (type: ContainerWidthEnum) => { + settingStore.setContainerWidth(type) + settingStore.reload() + } + } + + return { + domOperations, + basicHandlers, + boxStyleHandlers, + colorHandlers, + containerHandlers, + createToggleHandler, + createValueHandler + } +} diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts new file mode 100644 index 0000000..358ef57 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.ts @@ -0,0 +1,207 @@ +import { ref, computed, watch } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { storeToRefs } from 'pinia' +import { useBreakpoints } from '@vueuse/core' +import AppConfig from '@/config' +import { SystemThemeEnum, MenuTypeEnum } from '@/enums/appEnum' +import { mittBus } from '@/utils/sys' +import { useTheme } from '@/hooks/core/useTheme' +import { useCeremony } from '@/hooks/core/useCeremony' +import { useSettingsState } from './useSettingsState' +import { useSettingsHandlers } from './useSettingsHandlers' + +/** + * 设置面板核心逻辑管理 + */ +export function useSettingsPanel() { + const settingStore = useSettingStore() + const { systemThemeType, systemThemeMode, menuType } = storeToRefs(settingStore) + + // Composables + const { openFestival, cleanup } = useCeremony() + const { setSystemTheme, setSystemAutoTheme } = useTheme() + const { initColorWeak } = useSettingsState() + const { domOperations } = useSettingsHandlers() + + // 响应式状态 + const showDrawer = ref(false) + + // 使用 VueUse breakpoints 优化性能 + const breakpoints = useBreakpoints({ tablet: 1000 }) + const isMobile = breakpoints.smaller('tablet') + + // 记录窗口宽度变化前的菜单类型 + const beforeMenuType = ref() + const hasChangedMenu = ref(false) + + // 计算属性 + const systemThemeColor = computed(() => settingStore.systemThemeColor as string) + + // 主题相关处理 + const useThemeHandlers = () => { + // 初始化系统颜色 + const initSystemColor = () => { + if (!AppConfig.systemMainColor.includes(systemThemeColor.value)) { + settingStore.setElementTheme(AppConfig.systemMainColor[0]) + settingStore.reload() + } + } + + // 初始化系统主题 + const initSystemTheme = () => { + if (systemThemeMode.value === SystemThemeEnum.AUTO) { + setSystemAutoTheme() + } else { + setSystemTheme(systemThemeType.value) + } + } + + // 监听系统主题变化 + const listenerSystemTheme = () => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + mediaQuery.addEventListener('change', initSystemTheme) + return () => { + mediaQuery.removeEventListener('change', initSystemTheme) + } + } + + return { + initSystemColor, + initSystemTheme, + listenerSystemTheme + } + } + + // 响应式布局处理 + const useResponsiveLayout = () => { + // 使用 watch 监听断点变化,性能更优 + const stopWatch = watch( + isMobile, + (mobile: boolean) => { + if (mobile) { + // 切换到移动端布局 + if (!hasChangedMenu.value) { + beforeMenuType.value = menuType.value + useSettingsState().switchMenuLayouts(MenuTypeEnum.LEFT) + settingStore.setMenuOpen(false) + hasChangedMenu.value = true + } + } else { + // 恢复桌面端布局 + if (hasChangedMenu.value && beforeMenuType.value) { + useSettingsState().switchMenuLayouts(beforeMenuType.value) + settingStore.setMenuOpen(true) + hasChangedMenu.value = false + } + } + }, + { immediate: true } + ) + + return { stopWatch } + } + + // 抽屉控制 + const useDrawerControl = () => { + // 用于存储 setTimeout 的 ID,以便在需要时清除 + let themeChangeTimer: ReturnType | null = null + + // 打开抽屉 + const handleOpen = () => { + // 清除可能存在的旧定时器 + if (themeChangeTimer) { + clearTimeout(themeChangeTimer) + } + // 延迟添加 theme-change class,避免抽屉打开动画受影响 + themeChangeTimer = setTimeout(() => { + domOperations.setBodyClass('theme-change', true) + themeChangeTimer = null + }, 500) + } + + // 关闭抽屉 + const handleClose = () => { + // 清除未执行的定时器,防止关闭后才添加 class + if (themeChangeTimer) { + clearTimeout(themeChangeTimer) + themeChangeTimer = null + } + // 立即移除 theme-change class + domOperations.setBodyClass('theme-change', false) + } + + // 打开设置 + const openSetting = () => { + showDrawer.value = true + } + + // 关闭设置 + const closeDrawer = () => { + showDrawer.value = false + } + + return { + handleOpen, + handleClose, + openSetting, + closeDrawer + } + } + + // Props 变化监听 + const usePropsWatcher = (props: { open?: boolean }) => { + watch( + () => props.open, + (val: boolean | undefined) => { + if (val !== undefined) { + showDrawer.value = val + } + } + ) + } + + // 初始化设置 + const useSettingsInitializer = () => { + const themeHandlers = useThemeHandlers() + const { openSetting } = useDrawerControl() + const { stopWatch } = useResponsiveLayout() + let themeCleanup: (() => void) | null = null + + const initializeSettings = () => { + mittBus.on('openSetting', openSetting) + themeHandlers.initSystemColor() + themeCleanup = themeHandlers.listenerSystemTheme() + initColorWeak() + + // 设置盒子模式 + const boxMode = settingStore.boxBorderMode ? 'border-mode' : 'shadow-mode' + domOperations.setRootAttribute('data-box-mode', boxMode) + + themeHandlers.initSystemTheme() + openFestival() + } + + const cleanupSettings = () => { + stopWatch() + themeCleanup?.() + cleanup() + } + + return { + initializeSettings, + cleanupSettings + } + } + + return { + // 状态 + showDrawer, + + // 方法组合 + useThemeHandlers, + useResponsiveLayout, + useDrawerControl, + usePropsWatcher, + useSettingsInitializer + } +} diff --git a/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts b/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts new file mode 100644 index 0000000..65352d2 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/composables/useSettingsState.ts @@ -0,0 +1,37 @@ +import { useSettingStore } from '@/store/modules/setting' +import { MenuThemeEnum, MenuTypeEnum } from '@/enums/appEnum' + +/** + * 设置状态管理 + */ +export function useSettingsState() { + const settingStore = useSettingStore() + + // 色弱模式初始化 + const initColorWeak = () => { + if (settingStore.colorWeak) { + const el = document.getElementsByTagName('html')[0] + setTimeout(() => { + el.classList.add('color-weak') + }, 100) + } + } + + // 菜单布局切换 + const switchMenuLayouts = (type: MenuTypeEnum) => { + if (type === MenuTypeEnum.LEFT || type === MenuTypeEnum.TOP_LEFT) { + settingStore.setMenuOpen(true) + } + settingStore.switchMenuLayouts(type) + if (type === MenuTypeEnum.DUAL_MENU) { + settingStore.switchMenuStyles(MenuThemeEnum.DESIGN) + settingStore.setMenuOpen(true) + } + } + + return { + // 方法 + initColorWeak, + switchMenuLayouts + } +} diff --git a/src/components/core/layouts/art-settings-panel/index.vue b/src/components/core/layouts/art-settings-panel/index.vue new file mode 100644 index 0000000..0cbf344 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/index.vue @@ -0,0 +1,72 @@ + + + + + + diff --git a/src/components/core/layouts/art-settings-panel/style.scss b/src/components/core/layouts/art-settings-panel/style.scss new file mode 100644 index 0000000..e863074 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/style.scss @@ -0,0 +1,92 @@ +@use '@styles/core/mixin.scss' as *; + +// 设置抽屉模态框样式 +.setting-modal { + background: transparent !important; + + .el-drawer { + // 背景滤镜效果 + background: rgba($color: #fff, $alpha: 50%) !important; + box-shadow: 0 0 30px rgb(0 0 0 / 10%) !important; + + @include backdropBlur(); + + .setting-box-wrap { + display: flex; + flex-wrap: wrap; + align-items: center; + width: calc(100% + 15px); + margin-bottom: 10px; + + .setting-item { + box-sizing: border-box; + width: calc(33.333% - 15px); + margin-right: 15px; + text-align: center; + + .box { + position: relative; + box-sizing: border-box; + display: flex; + height: 52px; + overflow: hidden; + cursor: pointer; + border: 2px solid var(--default-border); + border-radius: 8px; + box-shadow: 0 0 8px 0 rgb(0 0 0 / 10%); + transition: box-shadow 0.1s; + + &.mt-16 { + margin-top: 16px; + } + + &.is-active { + border: 2px solid var(--theme-color); + } + + img { + width: 100%; + height: 100%; + } + } + + .name { + margin-top: 6px; + font-size: 14px; + text-align: center; + } + } + } + } + + // 去除滚动条 + .el-drawer__body::-webkit-scrollbar { + width: 0 !important; + } +} + +.dark { + .setting-modal { + .el-drawer { + background: rgba($color: #000, $alpha: 50%) !important; + + .setting-item { + .box { + border: 2px solid transparent; + } + } + } + } +} + +// 去除火狐浏览器滚动条 +:deep(.el-drawer__body) { + scrollbar-width: none; +} + +// 移动端隐藏 +@media screen and (width <= 800px) { + .mobile-hide { + display: none !important; + } +} diff --git a/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue b/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue new file mode 100644 index 0000000..b6dc9d3 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue @@ -0,0 +1,77 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue b/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue new file mode 100644 index 0000000..86c7a9e --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue b/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue new file mode 100644 index 0000000..05a4b41 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue b/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue new file mode 100644 index 0000000..1f5be72 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue b/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue new file mode 100644 index 0000000..dbcae46 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue b/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue new file mode 100644 index 0000000..61237eb --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue b/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue new file mode 100644 index 0000000..31ef00c --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue b/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue new file mode 100644 index 0000000..7b47d1a --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue @@ -0,0 +1,235 @@ + + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue b/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue new file mode 100644 index 0000000..85372be --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue b/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue new file mode 100644 index 0000000..e3ead9e --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue b/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue new file mode 100644 index 0000000..5721027 --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue b/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue new file mode 100644 index 0000000..4b46fcd --- /dev/null +++ b/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/core/layouts/art-work-tab/index.vue b/src/components/core/layouts/art-work-tab/index.vue new file mode 100644 index 0000000..152ff63 --- /dev/null +++ b/src/components/core/layouts/art-work-tab/index.vue @@ -0,0 +1,584 @@ + + + + + + diff --git a/src/components/core/media/art-cutter-img/index.vue b/src/components/core/media/art-cutter-img/index.vue new file mode 100644 index 0000000..191ceed --- /dev/null +++ b/src/components/core/media/art-cutter-img/index.vue @@ -0,0 +1,350 @@ + + + + + + diff --git a/src/components/core/media/art-video-player/index.vue b/src/components/core/media/art-video-player/index.vue new file mode 100644 index 0000000..4f681ea --- /dev/null +++ b/src/components/core/media/art-video-player/index.vue @@ -0,0 +1,111 @@ + + + + diff --git a/src/components/core/others/art-menu-right/index.vue b/src/components/core/others/art-menu-right/index.vue new file mode 100644 index 0000000..1cc92ab --- /dev/null +++ b/src/components/core/others/art-menu-right/index.vue @@ -0,0 +1,415 @@ + + + + + + diff --git a/src/components/core/others/art-watermark/index.vue b/src/components/core/others/art-watermark/index.vue new file mode 100644 index 0000000..1d7f06b --- /dev/null +++ b/src/components/core/others/art-watermark/index.vue @@ -0,0 +1,64 @@ + + + + diff --git a/src/components/core/tables/art-table-header/index.vue b/src/components/core/tables/art-table-header/index.vue new file mode 100644 index 0000000..281dc51 --- /dev/null +++ b/src/components/core/tables/art-table-header/index.vue @@ -0,0 +1,340 @@ + + + + + + diff --git a/src/components/core/tables/art-table/index.vue b/src/components/core/tables/art-table/index.vue new file mode 100644 index 0000000..2392d96 --- /dev/null +++ b/src/components/core/tables/art-table/index.vue @@ -0,0 +1,342 @@ + + + + + + + + + diff --git a/src/components/core/tables/art-table/style.scss b/src/components/core/tables/art-table/style.scss new file mode 100644 index 0000000..67459e8 --- /dev/null +++ b/src/components/core/tables/art-table/style.scss @@ -0,0 +1,99 @@ +.art-table { + position: relative; + height: 100%; + + .el-table { + height: 100%; + margin-top: 10px; + } + + :deep(.el-loading-mask) { + z-index: 100; + background-color: var(--default-box-color) !important; + } + + // Loading 过渡动画 - 消失时淡出 + .loading-fade-leave-active { + transition: opacity 0.3s ease-out; + } + + .loading-fade-leave-to { + opacity: 0; + } + + // 空状态垂直居中 + &.is-empty { + :deep(.el-scrollbar__wrap) { + display: flex; + } + } + + .pagination { + display: flex; + margin-top: 13px; + + :deep(.el-select) { + width: 102px !important; + } + + // 分页对齐方式 + &.left { + justify-content: flex-start; + } + + &.center { + justify-content: center; + } + + &.right { + justify-content: flex-end; + } + + // 自定义分页组件样式 + &.custom-pagination { + :deep(.el-pagination) { + .btn-prev, + .btn-next { + background-color: transparent; + border: 1px solid var(--art-gray-300); + transition: border-color 0.15s; + + &:hover:not(.is-disabled) { + color: var(--theme-color); + border-color: var(--theme-color); + } + } + + li { + box-sizing: border-box; + font-weight: 400 !important; + background-color: transparent; + border: 1px solid var(--art-gray-300); + transition: border-color 0.15s; + + &.is-active { + font-weight: 400; + color: #fff; + background-color: var(--theme-color); + border: 1px solid var(--theme-color); + } + + &:hover:not(.is-disabled) { + border-color: var(--theme-color); + } + } + } + } + } +} + +// 移动端分页 +@media (width <= 640px) { + :deep(.el-pagination) { + display: flex; + flex-wrap: wrap; + gap: 15px 0; + align-items: center; + justify-content: center; + } +} diff --git a/src/components/core/text-effect/art-count-to/index.vue b/src/components/core/text-effect/art-count-to/index.vue new file mode 100644 index 0000000..7fb104b --- /dev/null +++ b/src/components/core/text-effect/art-count-to/index.vue @@ -0,0 +1,310 @@ + + + + diff --git a/src/components/core/text-effect/art-festival-text-scroll/index.vue b/src/components/core/text-effect/art-festival-text-scroll/index.vue new file mode 100644 index 0000000..770b457 --- /dev/null +++ b/src/components/core/text-effect/art-festival-text-scroll/index.vue @@ -0,0 +1,32 @@ + + + + diff --git a/src/components/core/text-effect/art-text-scroll/index.vue b/src/components/core/text-effect/art-text-scroll/index.vue new file mode 100644 index 0000000..90be30f --- /dev/null +++ b/src/components/core/text-effect/art-text-scroll/index.vue @@ -0,0 +1,285 @@ + + + + diff --git a/src/components/core/theme/theme-svg/index.vue b/src/components/core/theme/theme-svg/index.vue new file mode 100644 index 0000000..0b565a9 --- /dev/null +++ b/src/components/core/theme/theme-svg/index.vue @@ -0,0 +1,100 @@ + + + + + + + diff --git a/src/components/core/views/exception/ArtException.vue b/src/components/core/views/exception/ArtException.vue new file mode 100644 index 0000000..699228f --- /dev/null +++ b/src/components/core/views/exception/ArtException.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/core/views/login/AuthTopBar.vue b/src/components/core/views/login/AuthTopBar.vue new file mode 100644 index 0000000..9455253 --- /dev/null +++ b/src/components/core/views/login/AuthTopBar.vue @@ -0,0 +1,149 @@ + + + + + + diff --git a/src/components/core/views/login/LoginLeftView.vue b/src/components/core/views/login/LoginLeftView.vue new file mode 100644 index 0000000..aac488e --- /dev/null +++ b/src/components/core/views/login/LoginLeftView.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/src/components/core/views/result/ArtResultPage.vue b/src/components/core/views/result/ArtResultPage.vue new file mode 100644 index 0000000..b2eca48 --- /dev/null +++ b/src/components/core/views/result/ArtResultPage.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/core/widget/art-icon-button/index.vue b/src/components/core/widget/art-icon-button/index.vue new file mode 100644 index 0000000..760888b --- /dev/null +++ b/src/components/core/widget/art-icon-button/index.vue @@ -0,0 +1,23 @@ + + + + diff --git a/src/components/merchant/AuditTimeline.vue b/src/components/merchant/AuditTimeline.vue new file mode 100644 index 0000000..98eec5a --- /dev/null +++ b/src/components/merchant/AuditTimeline.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/config/assets/images.ts b/src/config/assets/images.ts new file mode 100644 index 0000000..f3e89dd --- /dev/null +++ b/src/config/assets/images.ts @@ -0,0 +1,61 @@ +/** + * 配置图片资源 + * + * 统一管理设置中心使用的预览图片资源。 + * 包含主题样式、菜单布局、菜单风格的预览图。 + * + * ## 图片分类 + * + * - themeStyles: 系统主题预览图(亮色/暗色/自动) + * - menuLayouts: 菜单布局预览图(左侧/顶部/混合/双栏) + * - menuStyles: 菜单风格预览图(设计/暗色/亮色) + * + * @module config/assets/images + * @author Art Design Pro Team + */ + +import lightTheme from '@imgs/settings/theme_styles/light.png' +import darkTheme from '@imgs/settings/theme_styles/dark.png' +import systemTheme from '@imgs/settings/theme_styles/system.png' +import verticalLayout from '@imgs/settings/menu_layouts/vertical.png' +import horizontalLayout from '@imgs/settings/menu_layouts/horizontal.png' +import mixedLayout from '@imgs/settings/menu_layouts/mixed.png' +import dualColumnLayout from '@imgs/settings/menu_layouts/dual_column.png' +import designStyle from '@imgs/settings/menu_styles/design.png' +import darkStyle from '@imgs/settings/menu_styles/dark.png' +import lightStyle from '@imgs/settings/menu_styles/light.png' + +/** + * 配置中心图片资源对象 + */ +export const configImages = { + /** 系统主题预览图 */ + themeStyles: { + /** 亮色主题 */ + light: lightTheme, + /** 暗色主题 */ + dark: darkTheme, + /** 自动主题(跟随系统) */ + system: systemTheme + }, + /** 菜单布局预览图 */ + menuLayouts: { + /** 左侧菜单 */ + vertical: verticalLayout, + /** 顶部菜单 */ + horizontal: horizontalLayout, + /** 混合菜单 */ + mixed: mixedLayout, + /** 双栏菜单 */ + dualColumn: dualColumnLayout + }, + /** 菜单风格预览图 */ + menuStyles: { + /** 设计风格 */ + design: designStyle, + /** 暗色风格 */ + dark: darkStyle, + /** 亮色风格 */ + light: lightStyle + } +} diff --git a/src/config/fastEnter.ts b/src/config/fastEnter.ts new file mode 100644 index 0000000..ccade16 --- /dev/null +++ b/src/config/fastEnter.ts @@ -0,0 +1,79 @@ +/** + * 快速入口配置 + * 包含:应用列表、快速链接等配置 + */ +import { WEB_LINKS } from '@/utils/constants' +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '工作台', + description: '系统概览与数据统计', + icon: 'ri:pie-chart-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '官方文档', + description: '使用指南与开发文档', + icon: 'ri:bill-line', + iconColor: '#ffb100', + enabled: true, + order: 2, + link: WEB_LINKS.DOCS + }, + { + name: '技术支持', + description: '技术支持与问题反馈', + icon: 'ri:user-location-line', + iconColor: '#ff6b6b', + enabled: true, + order: 3, + link: WEB_LINKS.COMMUNITY + }, + { + name: '哔哩哔哩', + description: '技术分享与交流', + icon: 'ri:bilibili-line', + iconColor: '#FB7299', + enabled: true, + order: 4, + link: WEB_LINKS.BILIBILI + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '注册', + enabled: true, + order: 2, + routeName: 'Register' + }, + { + name: '忘记密码', + enabled: true, + order: 3, + routeName: 'ForgetPassword' + }, + { + name: '个人中心', + enabled: true, + order: 4, + routeName: 'UserCenter' + } + ] +} + +export default Object.freeze(fastEnterConfig) diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..e6466bc --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,135 @@ +/** + * 系统全局配置 + * + * 这是系统的核心配置文件,集中管理所有全局配置项。 + * 包含系统信息、主题样式、菜单布局、颜色方案等所有可配置项。 + * + * ## 主要功能 + * + * - 系统信息 - 系统名称等基础信息 + * - 主题配置 - 亮色/暗色/自动主题的样式配置 + * - 菜单配置 - 菜单布局、主题、宽度等配置 + * - 颜色方案 - 系统主色和预设颜色列表 + * - 快速入口 - 快速入口应用和链接配置 + * - 顶部栏配置 - 顶部栏功能模块配置 + * + * ## 配置项说明 + * + * - systemInfo: 系统基础信息(名称等) + * - systemThemeStyles: 系统主题样式映射 + * - settingThemeList: 可选的系统主题列表 + * - menuLayoutList: 可选的菜单布局列表 + * - themeList: 菜单主题样式列表 + * - darkMenuStyles: 暗黑模式下的菜单样式 + * - systemMainColor: 预设的系统主色列表 + * - fastEnter: 快速入口配置 + * - headerBar: 顶部栏功能配置 + * + * @module config + * @author Art Design Pro Team + */ + +import { MenuThemeEnum, MenuTypeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { SystemConfig } from '@/types/config' +import { configImages } from './assets/images' +import fastEnterConfig from './modules/fastEnter' +import { headerBarConfig } from './modules/headerBar' + +const appConfig: SystemConfig = { + // 系统信息 + systemInfo: { + name: 'TakeoutSaaS AdminUI' // 系统名称 + }, + // 系统主题 + systemThemeStyles: { + [SystemThemeEnum.LIGHT]: { className: '' }, + [SystemThemeEnum.DARK]: { className: SystemThemeEnum.DARK } + }, + // 系统主题列表 + settingThemeList: [ + { + name: 'Light', + theme: SystemThemeEnum.LIGHT, + color: ['#fff', '#fff'], + leftLineColor: '#EDEEF0', + rightLineColor: '#EDEEF0', + img: configImages.themeStyles.light + }, + { + name: 'Dark', + theme: SystemThemeEnum.DARK, + color: ['#22252A'], + leftLineColor: '#3F4257', + rightLineColor: '#3F4257', + img: configImages.themeStyles.dark + }, + { + name: 'System', + theme: SystemThemeEnum.AUTO, + color: ['#fff', '#22252A'], + leftLineColor: '#EDEEF0', + rightLineColor: '#3F4257', + img: configImages.themeStyles.system + } + ], + // 菜单布局列表 + menuLayoutList: [ + { name: 'Left', value: MenuTypeEnum.LEFT, img: configImages.menuLayouts.vertical }, + { name: 'Top', value: MenuTypeEnum.TOP, img: configImages.menuLayouts.horizontal }, + { name: 'Mixed', value: MenuTypeEnum.TOP_LEFT, img: configImages.menuLayouts.mixed }, + { name: 'Dual Column', value: MenuTypeEnum.DUAL_MENU, img: configImages.menuLayouts.dualColumn } + ], + // 菜单主题列表 + themeList: [ + { + theme: MenuThemeEnum.DESIGN, + background: '#FFFFFF', + systemNameColor: 'var(--art-gray-800)', + iconColor: '#6B6B6B', + textColor: '#29343D', + img: configImages.menuStyles.design + }, + { + theme: MenuThemeEnum.DARK, + background: '#191A23', + systemNameColor: '#D9DADB', + iconColor: '#BABBBD', + textColor: '#BABBBD', + img: configImages.menuStyles.dark + }, + { + theme: MenuThemeEnum.LIGHT, + background: '#ffffff', + systemNameColor: 'var(--art-gray-800)', + iconColor: '#6B6B6B', + textColor: '#29343D', + img: configImages.menuStyles.light + } + ], + // 暗黑模式菜单样式 + darkMenuStyles: [ + { + theme: MenuThemeEnum.DARK, + background: 'var(--default-box-color)', + systemNameColor: '#DDDDDD', + iconColor: '#BABBBD', + textColor: 'rgba(#FFFFFF, 0.7)' + } + ], + // 系统主色 + systemMainColor: [ + '#5D87FF', + '#B48DF3', + '#1D84FF', + '#60C041', + '#38C0FC', + '#F9901F', + '#FF80C8' + ] as const, + // 快速入口配置 + fastEnter: fastEnterConfig, + // 顶部栏功能配置 + headerBar: headerBarConfig +} + +export default Object.freeze(appConfig) diff --git a/src/config/modules/component.ts b/src/config/modules/component.ts new file mode 100644 index 0000000..bc709e0 --- /dev/null +++ b/src/config/modules/component.ts @@ -0,0 +1,105 @@ +/** + * 全局组件配置 + * + * 统一管理系统级全局组件的注册。 + * 这些组件会在应用启动时全局注册,可在任何地方使用。 + * + * ## 主要功能 + * + * - 组件配置 - 集中管理全局组件的配置信息 + * - 异步加载 - 使用 defineAsyncComponent 实现按需加载 + * - 开关控制 - 支持通过 enabled 字段启用/禁用组件 + * - 配置查询 - 提供工具函数快速查询组件配置 + * + * @module config/component + * @author Art Design Pro Team + */ + +import { defineAsyncComponent } from 'vue' + +/** + * 全局组件配置列表 + */ +export const globalComponentsConfig: GlobalComponentConfig[] = [ + { + name: '设置面板', + key: 'settings-panel', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-settings-panel/index.vue') + ), + enabled: true + }, + { + name: '全局搜索', + key: 'global-search', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-global-search/index.vue') + ), + enabled: true + }, + { + name: '锁屏', + key: 'screen-lock', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-screen-lock/index.vue') + ), + enabled: true + }, + { + name: '聊天窗口', + key: 'chat-window', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-chat-window/index.vue') + ), + enabled: true + }, + { + name: '礼花效果', + key: 'fireworks-effect', + component: defineAsyncComponent( + () => import('@/components/core/layouts/art-fireworks-effect/index.vue') + ), + enabled: true + }, + { + name: '水印效果', + key: 'watermark', + component: defineAsyncComponent( + () => import('@/components/core/others/art-watermark/index.vue') + ), + enabled: true + } +] + +/** + * 全局组件配置接口 + */ +export interface GlobalComponentConfig { + /** 组件名称 */ + name: string + /** 组件标识 */ + key: string + /** 组件 */ + component: any + /** 是否启用 */ + enabled?: boolean + /** 组件描述 */ + description?: string +} + +/** + * 获取启用的全局组件 + * @returns 已启用的组件配置列表 + */ +export const getEnabledGlobalComponents = () => { + return globalComponentsConfig.filter((config) => config.enabled !== false) +} + +/** + * 根据 key 获取组件配置 + * @param key 组件标识 + * @returns 组件配置对象 + */ +export const getGlobalComponentByKey = (key: string) => { + return globalComponentsConfig.find((config) => config.key === key) +} diff --git a/src/config/modules/fastEnter.ts b/src/config/modules/fastEnter.ts new file mode 100644 index 0000000..6b9740c --- /dev/null +++ b/src/config/modules/fastEnter.ts @@ -0,0 +1,127 @@ +/** + * 快速入口配置 + * 包含:应用列表、快速链接等配置 + */ +import { WEB_LINKS } from '@/utils/constants' +import type { FastEnterConfig } from '@/types/config' + +const fastEnterConfig: FastEnterConfig = { + // 显示条件(屏幕宽度) + minWidth: 1200, + // 应用列表 + applications: [ + { + name: '工作台', + description: '系统概览与数据统计', + icon: 'ri:pie-chart-line', + iconColor: '#377dff', + enabled: true, + order: 1, + routeName: 'Console' + }, + { + name: '分析页', + description: '数据分析与可视化', + icon: 'ri:game-line', + iconColor: '#ff3b30', + enabled: true, + order: 2, + routeName: 'Analysis' + }, + { + name: '礼花效果', + description: '动画特效展示', + icon: 'ri:loader-line', + iconColor: '#7A7FFF', + enabled: true, + order: 3, + routeName: 'Fireworks' + }, + { + name: '聊天', + description: '即时通讯功能', + icon: 'ri:user-line', + iconColor: '#13DEB9', + enabled: true, + order: 4, + routeName: 'Chat' + }, + { + name: '官方文档', + description: '使用指南与开发文档', + icon: 'ri:bill-line', + iconColor: '#ffb100', + enabled: true, + order: 5, + link: WEB_LINKS.DOCS + }, + { + name: '技术支持', + description: '技术支持与问题反馈', + icon: 'ri:user-location-line', + iconColor: '#ff6b6b', + enabled: true, + order: 6, + link: WEB_LINKS.COMMUNITY + }, + { + name: '更新日志', + description: '版本更新与变更记录', + icon: 'ri:gamepad-line', + iconColor: '#38C0FC', + enabled: true, + order: 7, + routeName: 'ChangeLog' + }, + { + name: '哔哩哔哩', + description: '技术分享与交流', + icon: 'ri:bilibili-line', + iconColor: '#FB7299', + enabled: true, + order: 8, + link: WEB_LINKS.BILIBILI + } + ], + // 快速链接 + quickLinks: [ + { + name: '登录', + enabled: true, + order: 1, + routeName: 'Login' + }, + { + name: '注册', + enabled: true, + order: 2, + routeName: 'Register' + }, + { + name: '忘记密码', + enabled: true, + order: 3, + routeName: 'ForgetPassword' + }, + { + name: '定价', + enabled: true, + order: 4, + routeName: 'Pricing' + }, + { + name: '个人中心', + enabled: true, + order: 5, + routeName: 'UserCenter' + }, + { + name: '留言管理', + enabled: true, + order: 6, + routeName: 'ArticleComment' + } + ] +} + +export default Object.freeze(fastEnterConfig) diff --git a/src/config/modules/festival.ts b/src/config/modules/festival.ts new file mode 100644 index 0000000..39cd790 --- /dev/null +++ b/src/config/modules/festival.ts @@ -0,0 +1,51 @@ +/** + * 节日庆祝配置 + * + * 配置系统的节日烟花效果和祝福文本。 + * 支持单日节日和跨日期节日,可自定义烟花播放次数。 + * + * ## 配置说明 + * + * - name: 节日名称 + * - date: 节日开始日期(格式:YYYY-MM-DD) + * - endDate: 节日结束日期(可选,用于跨日期节日) + * - image: 烟花图片(需要预先导入) + * - scrollText: 滚动显示的祝福文本 + * - count: 烟花播放次数(可选,默认为 3 次) + * + * ## 注意事项 + * + * - 图片需要预先导入并在配置中引用 + * - 跨日期节日会在整个日期范围内生效 + * - 每个用户每天只会播放一次烟花效果 + * + * @module config/modules/festival + * @author Art Design Pro Team + */ + +import { FestivalConfig } from '@/types/config' + +// 导入烟花图片(根据需要取消注释) +// import sd from '@imgs/ceremony/sd.png' +// import yd from '@imgs/ceremony/yd.png' + +export const festivalConfigList: FestivalConfig[] = [ + // 跨日期示例 + // { + // name: 'v3.0 Sass 升级至 TailwindCSS', + // date: '2025-11-03', + // endDate: '2025-11-09', + // image: '', + // count: 3, + // scrollText: + // '🚀 系统 v3.0 测试阶段正式开启!测试周期为 11 月 3 日 - 11 月 16 日,通过 TailwindCSS 重构样式体系、统一 Iconify 图标方案,带来更高效现代的开发体验,正式发布敬请期待~' + // } + // 单日示例:圣诞节 + // { + // name: '圣诞节', + // date: '2024-12-25', + // image: sd, + // count: 3 // 可选,不设置则使用默认值 3 次 + // scrollText: 'Merry Christmas!Art Design Pro 祝您圣诞快乐,愿节日的欢乐与祝福如雪花般纷至沓来!', + // } +] diff --git a/src/config/modules/headerBar.ts b/src/config/modules/headerBar.ts new file mode 100644 index 0000000..a420e82 --- /dev/null +++ b/src/config/modules/headerBar.ts @@ -0,0 +1,63 @@ +/** + * 顶部栏功能配置 + * + * 统一管理顶部栏各个功能模块的启用状态。 + * 通过修改此配置文件可以快速启用或禁用顶部栏的功能按钮。 + * + * @module config/headerBar + * @author Art Design Pro Team + */ + +import { HeaderBarFeatureConfig } from '@/types' + +/** + * 顶部栏功能配置对象 + */ +export const headerBarConfig: HeaderBarFeatureConfig = { + menuButton: { + enabled: true, + description: '控制左侧菜单的展开/收起按钮' + }, + refreshButton: { + enabled: true, + description: '页面刷新按钮' + }, + fastEnter: { + enabled: true, + description: '快速入口功能,提供常用应用和链接的快速访问' + }, + breadcrumb: { + enabled: true, + description: '面包屑导航,显示当前页面路径' + }, + globalSearch: { + enabled: true, + description: '全局搜索功能,支持快捷键 Ctrl+K 或 Cmd+K' + }, + fullscreen: { + enabled: true, + description: '全屏切换功能' + }, + notification: { + enabled: true, + description: '通知中心,显示系统通知和消息' + }, + chat: { + enabled: true, + description: '聊天功能,提供实时沟通' + }, + language: { + enabled: true, + description: '多语言切换功能' + }, + settings: { + enabled: true, + description: '系统设置面板' + }, + themeToggle: { + enabled: true, + description: '主题切换功能(明暗主题)' + } +} + +export default headerBarConfig diff --git a/src/config/setting.ts b/src/config/setting.ts new file mode 100644 index 0000000..94f2d2c --- /dev/null +++ b/src/config/setting.ts @@ -0,0 +1,109 @@ +/** + * 系统设置默认值配置 + * + * 统一管理系统设置的所有默认值 + * + * ## 主要功能 + * + * - 菜单相关默认配置 + * - 主题相关默认配置 + * - 界面显示默认配置 + * - 功能开关默认配置 + * - 样式相关默认配置 + * + * ## 注意事项 + * + * 1. 修改此文件的配置项时,需要同步更新以下文件: + * - src/components/core/layouts/art-settings-panel/widget/SettingActions.vue(复制配置和重置配置逻辑) + * - src/store/modules/setting.ts(Store 状态定义) + * 2. 可以通过设置面板的"复制配置"按钮快速生成配置代码 + * 3. 枚举类型的值需要与 src/enums/appEnum.ts 中的定义保持一致 + */ + +import AppConfig from '@/config' +import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from '@/enums/appEnum' + +/** + * 系统设置默认值配置 + */ +export const SETTING_DEFAULT_CONFIG = { + /** 菜单类型 */ + menuType: MenuTypeEnum.LEFT, + /** 菜单展开宽度 */ + menuOpenWidth: 230, + /** 菜单是否展开 */ + menuOpen: true, + /** 双菜单是否显示文本 */ + dualMenuShowText: false, + /** 系统主题类型 */ + systemThemeType: SystemThemeEnum.AUTO, + /** 系统主题模式 */ + systemThemeMode: SystemThemeEnum.AUTO, + /** 菜单风格 */ + menuThemeType: MenuThemeEnum.DESIGN, + /** 系统主题颜色 */ + systemThemeColor: AppConfig.systemMainColor[0], + /** 是否显示菜单按钮 */ + showMenuButton: true, + /** 是否显示快速入口 */ + showFastEnter: true, + /** 是否显示刷新按钮 */ + showRefreshButton: true, + /** 是否显示面包屑 */ + showCrumbs: true, + /** 是否显示工作台标签 */ + showWorkTab: true, + /** 是否显示语言切换 */ + showLanguage: true, + /** 是否显示进度条 */ + showNprogress: false, + /** 是否显示设置引导 */ + showSettingGuide: true, + /** 是否显示节日文本 */ + showFestivalText: false, + /** 是否显示水印 */ + watermarkVisible: false, + /** 是否自动关闭 */ + autoClose: false, + /** 是否唯一展开 */ + uniqueOpened: true, + /** 是否色弱模式 */ + colorWeak: false, + /** 是否刷新 */ + refresh: false, + /** 是否加载节日烟花 */ + holidayFireworksLoaded: false, + /** 边框模式 */ + boxBorderMode: true, + /** 页面过渡效果 */ + pageTransition: 'slide-left', + /** 标签页样式 */ + tabStyle: 'tab-default', + /** 自定义圆角 */ + customRadius: '0.75', + /** 容器宽度 */ + containerWidth: ContainerWidthEnum.FULL, + /** 节日日期 */ + festivalDate: '' +} + +/** + * 获取设置默认值 + * @returns 设置默认值对象 + */ +export function getSettingDefaults() { + return { ...SETTING_DEFAULT_CONFIG } +} + +/** + * 重置为默认设置 + * @param currentSettings 当前设置对象 + */ +export function resetToDefaults(currentSettings: Record) { + const defaults = getSettingDefaults() + Object.keys(defaults).forEach((key) => { + if (key in currentSettings) { + currentSettings[key] = defaults[key as keyof typeof defaults] + } + }) +} diff --git a/src/directives/business/highlight.ts b/src/directives/business/highlight.ts new file mode 100644 index 0000000..13af225 --- /dev/null +++ b/src/directives/business/highlight.ts @@ -0,0 +1,248 @@ +/** + * v-highlight 代码高亮指令 + * + * 为代码块提供语法高亮、行号显示和一键复制功能。 + * 基于 highlight.js 实现,支持多种编程语言的语法高亮。 + * + * ## 主要功能 + * + * - 语法高亮 - 使用 highlight.js 自动识别并高亮代码 + * - 行号显示 - 自动为每行代码添加行号 + * - 一键复制 - 提供复制按钮,点击即可复制代码(自动过滤行号) + * - 性能优化 - 批量处理代码块,避免阻塞渲染 + * - 动态监听 - 使用 MutationObserver 监听新增代码块 + * - 防重复处理 - 自动标记已处理的代码块,避免重复处理 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 性能优化 + * + * - 批量处理:每次处理 10 个代码块,避免长时间阻塞 + * - 延迟处理:使用 requestAnimationFrame 分批处理 + * - 重试机制:自动重试处理失败的代码块 + * - 智能监听:只在有新代码块时才触发处理 + * + * @module directives/highlight + * @author Art Design Pro Team + */ + +import { App, Directive } from 'vue' +import hljs from 'highlight.js' + +// 高亮代码 +function highlightCode(block: HTMLElement) { + hljs.highlightElement(block) +} + +// 插入行号 +function insertLineNumbers(block: HTMLElement) { + const lines = block.innerHTML.split('\n') + const numberedLines = lines + .map((line, index) => { + return `${index + 1} ${line}` + }) + .join('\n') + block.innerHTML = numberedLines +} + +// 添加复制按钮:调整 DOM 结构,将代码部分包裹在 .code-wrapper 内 +function addCopyButton(block: HTMLElement) { + const copyButton = document.createElement('i') + copyButton.className = 'copy-button' + copyButton.innerHTML = + '' + copyButton.onclick = () => { + // 过滤掉行号,只复制代码内容 + const codeContent = block.innerText.replace(/^\d+\s+/gm, '') + navigator.clipboard.writeText(codeContent).then(() => { + ElMessage.success('复制成功') + }) + } + + const preElement = block.parentElement + if (preElement) { + let codeWrapper: HTMLElement + // 如果代码块还没有被包裹,则创建包裹容器 + if (!block.parentElement.classList.contains('code-wrapper')) { + codeWrapper = document.createElement('div') + codeWrapper.className = 'code-wrapper' + preElement.replaceChild(codeWrapper, block) + codeWrapper.appendChild(block) + } else { + codeWrapper = block.parentElement + } + // 将复制按钮添加到 pre 元素(而非 codeWrapper 内),这样它不会随滚动条滚动 + preElement.appendChild(copyButton) + } +} + +// 检查代码块是否已经被处理过 +function isBlockProcessed(block: HTMLElement): boolean { + return ( + block.hasAttribute('data-highlighted') || + !!block.querySelector('.line-number') || + !!block.parentElement?.querySelector('.copy-button') + ) +} + +// 标记代码块为已处理 +function markBlockAsProcessed(block: HTMLElement) { + block.setAttribute('data-highlighted', 'true') +} + +// 处理单个代码块 +function processBlock(block: HTMLElement) { + if (isBlockProcessed(block)) { + return + } + + try { + highlightCode(block) + insertLineNumbers(block) + addCopyButton(block) + markBlockAsProcessed(block) + } catch (error) { + console.warn('处理代码块时出错:', error) + } +} + +// 查找并处理所有代码块 +function processAllCodeBlocks(el: HTMLElement) { + const blocks = Array.from(el.querySelectorAll('pre code')) + const unprocessedBlocks = blocks.filter((block) => !isBlockProcessed(block)) + + if (unprocessedBlocks.length === 0) { + return + } + + if (unprocessedBlocks.length <= 10) { + // 如果代码块数量少于等于10,直接处理所有代码块 + unprocessedBlocks.forEach((block) => processBlock(block)) + } else { + // 定义每次处理的代码块数 + const batchSize = 10 + let currentIndex = 0 + + const processBatch = () => { + const batch = unprocessedBlocks.slice(currentIndex, currentIndex + batchSize) + + batch.forEach((block) => { + processBlock(block) + }) + + // 更新索引并继续处理下一批 + currentIndex += batchSize + if (currentIndex < unprocessedBlocks.length) { + // 使用 requestAnimationFrame 确保下一帧再处理 + requestAnimationFrame(processBatch) + } + } + + // 开始处理第一批代码块 + processBatch() + } +} + +// 重试处理函数 +function retryProcessing(el: HTMLElement, maxRetries: number = 3, delay: number = 200) { + let retryCount = 0 + + const tryProcess = () => { + processAllCodeBlocks(el) + + // 检查是否还有未处理的代码块 + const remainingBlocks = Array.from(el.querySelectorAll('pre code')).filter( + (block) => !isBlockProcessed(block) + ) + + if (remainingBlocks.length > 0 && retryCount < maxRetries) { + retryCount++ + setTimeout(tryProcess, delay * retryCount) // 递增延迟 + } + } + + tryProcess() +} + +// 代码高亮、插入行号、复制按钮 +const highlightDirective: Directive = { + mounted(el: HTMLElement) { + // 立即尝试处理一次 + processAllCodeBlocks(el) + + // 延迟处理,确保 v-html 内容已经渲染 + setTimeout(() => { + retryProcessing(el) + }, 100) + + // 使用 MutationObserver 监听 DOM 变化 + const observer = new MutationObserver((mutations) => { + let hasNewCodeBlocks = false + + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as HTMLElement + // 检查新添加的节点是否包含代码块 + if (element.tagName === 'PRE' || element.querySelector('pre code')) { + hasNewCodeBlocks = true + } + } + }) + } + }) + + if (hasNewCodeBlocks) { + // 延迟处理新添加的代码块 + setTimeout(() => { + processAllCodeBlocks(el) + }, 50) + } + }) + + // 开始观察 + observer.observe(el, { + childList: true, + subtree: true + }) + + // 将 observer 存储到元素上,以便在 unmounted 时清理 + ;(el as any)._highlightObserver = observer + }, + + updated(el: HTMLElement) { + // 当组件更新时,重新处理代码块 + setTimeout(() => { + processAllCodeBlocks(el) + }, 50) + }, + + unmounted(el: HTMLElement) { + // 清理 MutationObserver + const observer = (el as any)._highlightObserver + if (observer) { + observer.disconnect() + delete (el as any)._highlightObserver + } + } +} + +export function setupHighlightDirective(app: App) { + app.directive('highlight', highlightDirective) +} diff --git a/src/directives/business/ripple.ts b/src/directives/business/ripple.ts new file mode 100644 index 0000000..8d7d8f9 --- /dev/null +++ b/src/directives/business/ripple.ts @@ -0,0 +1,114 @@ +/** + * v-ripple 水波纹效果指令 + * + * 为元素添加 Material Design 风格的水波纹点击效果。 + * 点击时从点击位置扩散出圆形水波纹动画,提升交互体验。 + * + * ## 主要功能 + * + * - 水波纹动画 - 点击时从点击位置扩散圆形波纹 + * - 自适应大小 - 根据元素尺寸自动调整波纹大小和动画时长 + * - 智能配色 - 自动识别按钮类型,使用合适的波纹颜色 + * - 自定义颜色 - 支持通过参数自定义波纹颜色 + * - 性能优化 - 使用 requestAnimationFrame 和自动清理机制 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 颜色规则 + * + * - 有色按钮(primary、success、warning 等):使用白色半透明波纹 + * - 默认按钮:使用主题色半透明波纹 + * - 自定义:通过 color 参数指定任意颜色 + * + * @module directives/ripple + * @author Art Design Pro Team + */ + +import type { App, Directive, DirectiveBinding } from 'vue' + +export interface RippleOptions { + /** 水波纹颜色 */ + color?: string +} + +export const vRipple: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + // 获取指令的配置参数 + const options: RippleOptions = binding.value || {} + + // 设置元素为相对定位,并隐藏溢出部分 + el.style.position = 'relative' + el.style.overflow = 'hidden' + + // 点击事件处理 + el.addEventListener('mousedown', (e: MouseEvent) => { + const rect = el.getBoundingClientRect() + const left = e.clientX - rect.left + const top = e.clientY - rect.top + + // 创建水波纹元素 + const ripple = document.createElement('div') + const diameter = Math.max(el.clientWidth, el.clientHeight) + const radius = diameter / 2 + + // 根据直径计算动画时间(直径越大,动画时间越长) + const baseTime = 600 // 基础动画时间(毫秒) + const scaleFactor = 0.5 // 缩放因子 + const animationDuration = baseTime + diameter * scaleFactor + + // 设置水波纹的尺寸和位置 + ripple.style.width = ripple.style.height = `${diameter}px` + ripple.style.left = `${left - radius}px` + ripple.style.top = `${top - radius}px` + ripple.style.position = 'absolute' + ripple.style.borderRadius = '50%' + ripple.style.pointerEvents = 'none' + + // 判断是否为有色按钮(Element Plus 按钮类型) + const buttonTypes = ['primary', 'info', 'warning', 'danger', 'success'].map( + (type) => `el-button--${type}` + ) + const isColoredButton = buttonTypes.some((type) => el.classList.contains(type)) + const defaultColor = isColoredButton + ? 'rgba(255, 255, 255, 0.25)' // 有色按钮使用白色水波纹 + : 'var(--el-color-primary-light-7)' // 默认按钮使用主题色水波纹 + + // 设置水波纹颜色、初始状态和过渡效果 + ripple.style.backgroundColor = options.color || defaultColor + ripple.style.transform = 'scale(0)' + ripple.style.transition = `transform ${animationDuration}ms cubic-bezier(0.3, 0, 0.2, 1), opacity ${animationDuration}ms cubic-bezier(0.3, 0, 0.5, 1)` + ripple.style.zIndex = '1' + + // 添加水波纹元素到DOM中 + el.appendChild(ripple) + + // 触发动画 + requestAnimationFrame(() => { + ripple.style.transform = 'scale(2)' + ripple.style.opacity = '0' + }) + + // 动画结束后移除水波纹元素 + setTimeout(() => { + ripple.remove() + }, animationDuration + 500) // 增加500ms缓冲时间 + }) + } +} + +export function setupRippleDirective(app: App) { + app.directive('ripple', vRipple) +} diff --git a/src/directives/core/auth.ts b/src/directives/core/auth.ts new file mode 100644 index 0000000..1829a0e --- /dev/null +++ b/src/directives/core/auth.ts @@ -0,0 +1,92 @@ +/** + * v-auth 权限指令 + * + * 适用于后端权限控制模式,基于权限标识控制 DOM 元素的显示和隐藏。 + * 如果用户没有对应权限,元素将从 DOM 中移除。 + * + * ## 主要功能 + * + * - 权限验证 - 根据路由 meta 中的权限列表验证用户权限 + * - DOM 控制 - 无权限时自动移除元素,而非隐藏 + * - 响应式更新 - 权限变化时自动更新元素状态 + * + * ## 使用示例 + * + * ```vue + * + * 新增 + * + * + * 编辑 + * + * + * 删除 + * ``` + * + * ## 注意事项 + * + * - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏 + * - 权限列表从当前路由的 meta.authList 中获取 + * + * @module directives/auth + * @author Art Design Pro Team + */ + +import { App, Directive, DirectiveBinding } from 'vue' + +import { useUserStore } from '@/store/modules/user' + +interface AuthBinding extends DirectiveBinding { + value: string | string[] +} + +function checkAuthPermission(el: HTMLElement, binding: AuthBinding): void { + const { value } = binding + const userStore = useUserStore() + const permissions = userStore.permissions + + if (value && value instanceof Array && value.length > 0) { + const hasPermission = permissions.some((permission) => { + return value.includes(permission) + }) + + if (!hasPermission) { + disableElement(el) + } + } else if (value && typeof value === 'string') { + const hasPermission = permissions.includes(value) + + if (!hasPermission) { + disableElement(el) + } + } +} + +function disableElement(el: HTMLElement): void { + // 添加置灰样式 + el.style.filter = 'grayscale(100%)' + el.style.opacity = '0.5' + el.style.pointerEvents = 'none' + el.style.cursor = 'not-allowed' + // 如果是按钮或输入框,添加 disabled 属性 + if (el instanceof HTMLButtonElement || el instanceof HTMLInputElement) { + el.disabled = true + } + + // 阻止点击事件 + el.addEventListener('click', stopEvent, true) +} + +function stopEvent(e: Event) { + e.preventDefault() + e.stopPropagation() +} + +const authDirective: Directive = { + mounted: checkAuthPermission, + updated: checkAuthPermission +} + +export function setupAuthDirective(app: App): void { + app.directive('auth', authDirective) +} diff --git a/src/directives/core/roles.ts b/src/directives/core/roles.ts new file mode 100644 index 0000000..2ab1029 --- /dev/null +++ b/src/directives/core/roles.ts @@ -0,0 +1,89 @@ +/** + * v-roles 角色权限指令 + * + * 基于用户角色控制 DOM 元素的显示和隐藏。 + * 只要用户拥有指定角色中的任意一个,元素就会显示,否则从 DOM 中移除。 + * + * ## 主要功能 + * + * - 角色验证 - 检查用户是否拥有指定角色 + * - 多角色支持 - 支持单个角色或多个角色(满足其一即可) + * - DOM 控制 - 无权限时自动移除元素,而非隐藏 + * - 响应式更新 - 角色变化时自动更新元素状态 + * + * ## 使用示例 + * + * ```vue + * + * ``` + * + * ## 权限逻辑 + * + * - 用户角色从 userStore.getUserInfo.roles 获取 + * - 只要用户拥有指定角色中的任意一个,元素就会显示 + * - 如果用户没有任何角色或不满足条件,元素将被移除 + * + * ## 注意事项 + * + * - 该指令会直接移除 DOM 元素,而不是使用 v-if 隐藏 + * - 适用于基于角色的粗粒度权限控制 + * - 如需基于具体操作的细粒度权限控制,请使用 v-auth 指令 + * + * @module directives/roles + * @author Art Design Pro Team + */ + +import { useUserStore } from '@/store/modules/user' +import { App, Directive, DirectiveBinding } from 'vue' + +interface RolesBinding extends DirectiveBinding { + value: string | string[] +} + +function checkRolePermission(el: HTMLElement, binding: RolesBinding): void { + const userStore = useUserStore() + const userRoles = userStore.getUserInfo.roles + + // 如果用户角色为空或未定义,移除元素 + if (!userRoles?.length) { + removeElement(el) + return + } + + // 确保指令值为数组格式 + const requiredRoles = Array.isArray(binding.value) ? binding.value : [binding.value] + + // 检查用户是否具有所需角色之一 + const hasPermission = requiredRoles.some((role: string) => userRoles.includes(role)) + + // 如果没有权限,安全地移除元素 + if (!hasPermission) { + removeElement(el) + } +} + +function removeElement(el: HTMLElement): void { + if (el.parentNode) { + el.parentNode.removeChild(el) + } +} + +const rolesDirective: Directive = { + mounted: checkRolePermission, + updated: checkRolePermission +} + +export function setupRolesDirective(app: App): void { + app.directive('roles', rolesDirective) +} diff --git a/src/directives/index.ts b/src/directives/index.ts new file mode 100644 index 0000000..780464b --- /dev/null +++ b/src/directives/index.ts @@ -0,0 +1,12 @@ +import type { App } from 'vue' +import { setupAuthDirective } from './core/auth' +import { setupHighlightDirective } from './business/highlight' +import { setupRippleDirective } from './business/ripple' +import { setupRolesDirective } from './core/roles' + +export function setupGlobDirectives(app: App) { + setupAuthDirective(app) // 权限指令 + setupRolesDirective(app) // 角色权限指令 + setupHighlightDirective(app) // 高亮指令 + setupRippleDirective(app) // 水波纹指令 +} diff --git a/src/enums/Billing.ts b/src/enums/Billing.ts new file mode 100644 index 0000000..9c53835 --- /dev/null +++ b/src/enums/Billing.ts @@ -0,0 +1,95 @@ +/** 账单状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantBillingStatus) */ +export enum TenantBillingStatus { + /** 待支付 */ + Pending = 0, + + /** 已支付 */ + Paid = 1, + + /** 已逾期 */ + Overdue = 2, + + /** 已取消 */ + Cancelled = 3 +} + +/** 账单类型枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.BillingType) */ +export enum BillingType { + /** 订阅账单 */ + Subscription = 0, + + /** 配额包购买 */ + QuotaPurchase = 1, + + /** 手动创建 */ + Manual = 2, + + /** 续费账单 */ + Renewal = 3 +} + +/** 租户账单支付方式枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantPaymentMethod) */ +export enum TenantPaymentMethod { + /** 在线支付 */ + Online = 0, + + /** 银行转账 */ + BankTransfer = 1, + + /** 其他方式 */ + Other = 2 +} + +/** 支付流水支付方式枚举(后端:TakeoutSaaS.Domain.Payments.Enums.PaymentMethod) */ +export enum PaymentMethod { + /** 在线支付 */ + Online = 0, + + /** 银行转账 */ + BankTransfer = 1, + + /** 其他方式 */ + Other = 2 +} + +/** 租户账单支付状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantPaymentStatus) */ +export enum TenantPaymentStatus { + /** 待处理 */ + Pending = 0, + + /** 成功 */ + Success = 1, + + /** 失败 */ + Failed = 2, + + /** 已退款 */ + Refunded = 3 +} + +/** 支付流水状态枚举(后端:TakeoutSaaS.Domain.Payments.Enums.PaymentStatus) */ +export enum PaymentStatus { + /** 待处理 */ + Pending = 0, + + /** 成功 */ + Success = 1, + + /** 失败 */ + Failed = 2, + + /** 已退款 */ + Refunded = 3 +} + +/** 导出格式枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.ExportFormat) */ +export enum ExportFormat { + /** Excel 格式 */ + Excel = 0, + + /** PDF 格式 */ + Pdf = 1, + + /** CSV 格式 */ + Csv = 2 +} diff --git a/src/enums/BusinessHourType.ts b/src/enums/BusinessHourType.ts new file mode 100644 index 0000000..ac030a6 --- /dev/null +++ b/src/enums/BusinessHourType.ts @@ -0,0 +1,11 @@ +/** 营业时段类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.BusinessHourType) */ +export enum BusinessHourType { + /** 正常营业时段。 */ + Normal = 0, + /** 仅预约时段。 */ + ReservationOnly = 1, + /** 自提/配送时段。 */ + PickupOrDelivery = 2, + /** 休息时段。 */ + Closed = 3 +} diff --git a/src/enums/Dictionary.ts b/src/enums/Dictionary.ts new file mode 100644 index 0000000..0c06eef --- /dev/null +++ b/src/enums/Dictionary.ts @@ -0,0 +1,32 @@ +/** 字典作用域枚举(后端:TakeoutSaaS.Domain.Dictionary.Enums.DictionaryScope) */ +export enum DictionaryScope { + /** 系统级字典 */ + System = 1, + + /** 业务级字典 */ + Business = 2 +} + +/** 导入冲突处理模式枚举 */ +export enum ConflictResolutionMode { + /** 跳过冲突项 */ + Skip = 1, + + /** 覆盖已有项 */ + Overwrite = 2, + + /** 追加新项 */ + Append = 3 +} + +/** 缓存失效操作类型枚举 */ +export enum CacheInvalidationOperation { + /** 创建操作 */ + Create = 1, + + /** 更新操作 */ + Update = 2, + + /** 删除操作 */ + Delete = 3 +} diff --git a/src/enums/MerchantStatus.ts b/src/enums/MerchantStatus.ts new file mode 100644 index 0000000..9f559d3 --- /dev/null +++ b/src/enums/MerchantStatus.ts @@ -0,0 +1,14 @@ +/** 商户状态枚举(后端:TakeoutSaaS.Domain.Merchants.Enums.MerchantStatus) */ +export enum MerchantStatus { + /** 待审核。 */ + Pending = 0, + + /** 审核通过。 */ + Approved = 1, + + /** 审核驳回。 */ + Rejected = 2, + + /** 业务冻结。 */ + Frozen = 3 +} diff --git a/src/enums/OperatingMode.ts b/src/enums/OperatingMode.ts new file mode 100644 index 0000000..f2ff6b9 --- /dev/null +++ b/src/enums/OperatingMode.ts @@ -0,0 +1,8 @@ +/** 经营模式枚举(后端:TakeoutSaaS.Domain.Common.Enums.OperatingMode) */ +export enum OperatingMode { + /** 同一主体。 */ + SameEntity = 1, + + /** 不同主体。 */ + DifferentEntity = 2 +} diff --git a/src/enums/OverrideType.ts b/src/enums/OverrideType.ts new file mode 100644 index 0000000..5427a30 --- /dev/null +++ b/src/enums/OverrideType.ts @@ -0,0 +1,9 @@ +/** 临时时段覆盖类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.OverrideType) */ +export enum OverrideType { + /** 闭店(歇业) */ + Closed = 0, + /** 临时营业 */ + TemporaryOpen = 1, + /** 调整营业时间 */ + ModifiedHours = 2 +} diff --git a/src/enums/PackagingFeeMode.ts b/src/enums/PackagingFeeMode.ts new file mode 100644 index 0000000..d0d957c --- /dev/null +++ b/src/enums/PackagingFeeMode.ts @@ -0,0 +1,7 @@ +/** 打包费模式枚举(后端:TakeoutSaaS.Domain.Stores.Enums.PackagingFeeMode) */ +export enum PackagingFeeMode { + /** 固定打包费。 */ + Fixed = 0, + /** 商品计费打包费。 */ + PerItem = 1 +} diff --git a/src/enums/ReviewAction.ts b/src/enums/ReviewAction.ts new file mode 100644 index 0000000..5fc876d --- /dev/null +++ b/src/enums/ReviewAction.ts @@ -0,0 +1,41 @@ +/** 商户审核动作枚举(后端:TakeoutSaaS.Domain.Merchants.Enums.MerchantAuditAction) */ +export enum ReviewAction { + /** 提交入驻申请或资料。 */ + ApplicationSubmitted = 0, + + /** 上传/更新证照。 */ + DocumentUploaded = 1, + + /** 证照审核。 */ + DocumentReviewed = 2, + + /** 合同创建或更新。 */ + ContractUpdated = 3, + + /** 合同状态变更。 */ + ContractStatusChanged = 4, + + /** 商户审核结果。 */ + MerchantReviewed = 5, + + /** 领取审核。 */ + ReviewClaimed = 6, + + /** 释放审核。 */ + ReviewReleased = 7, + + /** 审核通过。 */ + ReviewApproved = 8, + + /** 审核驳回。 */ + ReviewRejected = 9, + + /** 撤销审核。 */ + ReviewRevoked = 10, + + /** 关键信息变更进入待审核。 */ + ReviewPendingReApproval = 11, + + /** 强制接管审核。 */ + ReviewForceClaimed = 12 +} diff --git a/src/enums/StoreAuditAction.ts b/src/enums/StoreAuditAction.ts new file mode 100644 index 0000000..1419325 --- /dev/null +++ b/src/enums/StoreAuditAction.ts @@ -0,0 +1,17 @@ +/** 门店审核动作枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreAuditAction) */ +export enum StoreAuditAction { + /** 提交审核。 */ + Submit = 0, + /** 重新提交。 */ + Resubmit = 1, + /** 审核通过。 */ + Approve = 2, + /** 审核驳回。 */ + Reject = 3, + /** 强制关闭。 */ + ForceClose = 4, + /** 解除关闭。 */ + Reopen = 5, + /** 自动激活。 */ + AutoActivate = 6 +} diff --git a/src/enums/StoreAuditStatus.ts b/src/enums/StoreAuditStatus.ts new file mode 100644 index 0000000..eb2d013 --- /dev/null +++ b/src/enums/StoreAuditStatus.ts @@ -0,0 +1,11 @@ +/** 门店审核状态枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreAuditStatus) */ +export enum StoreAuditStatus { + /** 草稿/待提交。 */ + Draft = 0, + /** 审核中。 */ + Pending = 1, + /** 已激活。 */ + Activated = 2, + /** 已驳回。 */ + Rejected = 3 +} diff --git a/src/enums/StoreBusinessStatus.ts b/src/enums/StoreBusinessStatus.ts new file mode 100644 index 0000000..7545440 --- /dev/null +++ b/src/enums/StoreBusinessStatus.ts @@ -0,0 +1,9 @@ +/** 门店经营状态枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreBusinessStatus) */ +export enum StoreBusinessStatus { + /** 营业中。 */ + Open = 0, + /** 休息中。 */ + Resting = 1, + /** 强制关闭。 */ + ForceClosed = 2 +} diff --git a/src/enums/StoreClosureReason.ts b/src/enums/StoreClosureReason.ts new file mode 100644 index 0000000..1d13e5f --- /dev/null +++ b/src/enums/StoreClosureReason.ts @@ -0,0 +1,17 @@ +/** 门店歇业原因枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreClosureReason) */ +export enum StoreClosureReason { + /** 非营业时间。 */ + OutOfBusinessHours = 0, + /** 设备检修。 */ + EquipmentMaintenance = 1, + /** 老板休假。 */ + OwnerVacation = 2, + /** 食材告罄。 */ + OutOfStock = 3, + /** 暂停营业。 */ + TemporarilyClosed = 4, + /** 证照过期。 */ + LicenseExpired = 5, + /** 其他原因。 */ + Other = 99 +} diff --git a/src/enums/StoreOwnershipType.ts b/src/enums/StoreOwnershipType.ts new file mode 100644 index 0000000..3e75c1f --- /dev/null +++ b/src/enums/StoreOwnershipType.ts @@ -0,0 +1,7 @@ +/** 门店主体类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreOwnershipType) */ +export enum StoreOwnershipType { + /** 同一主体(自营)。 */ + SameEntity = 0, + /** 不同主体(外部入驻)。 */ + DifferentEntity = 1 +} diff --git a/src/enums/StoreQualificationType.ts b/src/enums/StoreQualificationType.ts new file mode 100644 index 0000000..d12be55 --- /dev/null +++ b/src/enums/StoreQualificationType.ts @@ -0,0 +1,11 @@ +/** 门店资质类型枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreQualificationType) */ +export enum StoreQualificationType { + /** 营业执照。 */ + BusinessLicense = 0, + /** 食品经营许可证。 */ + FoodServiceLicense = 1, + /** 门头实景照。 */ + StorefrontPhoto = 2, + /** 店内环境照。 */ + InteriorPhoto = 3 +} diff --git a/src/enums/StoreStatus.ts b/src/enums/StoreStatus.ts new file mode 100644 index 0000000..cf624eb --- /dev/null +++ b/src/enums/StoreStatus.ts @@ -0,0 +1,14 @@ +/** 门店状态枚举(后端:TakeoutSaaS.Domain.Stores.Enums.StoreStatus) */ +export enum StoreStatus { + /** 未开业或休眠。 */ + Closed = 0, + + /** 准备营业。 */ + Preparing = 1, + + /** 正常营业中。 */ + Operating = 2, + + /** 暂停营业。 */ + Suspended = 3 +} diff --git a/src/enums/SubscriptionStatus.ts b/src/enums/SubscriptionStatus.ts new file mode 100644 index 0000000..645c14c --- /dev/null +++ b/src/enums/SubscriptionStatus.ts @@ -0,0 +1,17 @@ +/** 订阅状态枚举(后端:TakeoutSaaS.Domain.Subscriptions.Enums.SubscriptionStatus) */ +export enum SubscriptionStatus { + /** 待激活 */ + Pending = 0, + + /** 生效中 */ + Active = 1, + + /** 宽限期 */ + GracePeriod = 2, + + /** 已取消 */ + Cancelled = 3, + + /** 已暂停 */ + Suspended = 4 +} diff --git a/src/enums/TenantPackageType.ts b/src/enums/TenantPackageType.ts new file mode 100644 index 0000000..2ad6b2a --- /dev/null +++ b/src/enums/TenantPackageType.ts @@ -0,0 +1,7 @@ +/** 租户套餐类型枚举(运行时常量,避免直接引用全局 Api 命名空间) */ +export enum TenantPackageTypeEnum { + Free = 0, + Standard = 1, + Professional = 2, + Enterprise = 3 +} diff --git a/src/enums/TenantStatus.ts b/src/enums/TenantStatus.ts new file mode 100644 index 0000000..eb6a3f9 --- /dev/null +++ b/src/enums/TenantStatus.ts @@ -0,0 +1,17 @@ +/** 租户服务状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantStatus) */ +export enum TenantStatus { + /** 已提交信息,等待审核。 */ + PendingReview = 0, + + /** 审核通过并正常运营。 */ + Active = 1, + + /** 因欠费或违规被暂时停用。 */ + Suspended = 2, + + /** 服务到期尚未续费。 */ + Expired = 3, + + /** 主动或被动注销,数据进入归档状态。 */ + Closed = 4 +} diff --git a/src/enums/TenantVerificationStatus.ts b/src/enums/TenantVerificationStatus.ts new file mode 100644 index 0000000..53c49bd --- /dev/null +++ b/src/enums/TenantVerificationStatus.ts @@ -0,0 +1,14 @@ +/** 租户实名认证状态枚举(后端:TakeoutSaaS.Domain.Tenants.Enums.TenantVerificationStatus) */ +export enum TenantVerificationStatus { + /** 草稿,未提交审核。 */ + Draft = 0, + + /** 已提交审核,等待运营处理。 */ + Pending = 1, + + /** 审核通过。 */ + Approved = 2, + + /** 审核驳回。 */ + Rejected = 3 +} diff --git a/src/enums/appEnum.ts b/src/enums/appEnum.ts new file mode 100644 index 0000000..a39c278 --- /dev/null +++ b/src/enums/appEnum.ts @@ -0,0 +1,81 @@ +/** + * 系统级别枚举定义模块 + * + * ## 主要功能 + * + * - 菜单类型枚举(左侧、顶部、混合、双栏) + * - 主题类型枚举(亮色、暗色、自动) + * - 菜单主题枚举(设计、亮色、暗色) + * - 语言类型枚举(中文、英文) + * - 容器宽度枚举(全屏、固定) + * - 菜单宽度枚举(收起宽度) + * + * @module enums/appEnum + * @author Art Design Pro Team + */ + +/** + * 菜单类型 + */ +export enum MenuTypeEnum { + /** 左侧菜单 */ + LEFT = 'left', + /** 顶部菜单 */ + TOP = 'top', + /** 顶部+左侧菜单 */ + TOP_LEFT = 'top-left', + /** 双栏菜单 */ + DUAL_MENU = 'dual-menu' +} + +/** + * 系统主题 + */ +export enum SystemThemeEnum { + /** 暗色主题 */ + DARK = 'dark', + /** 亮色主题 */ + LIGHT = 'light', + /** 自动主题(跟随系统) */ + AUTO = 'auto' +} + +/** + * 菜单主题 + */ +export enum MenuThemeEnum { + /** 暗色主题 */ + DARK = 'dark', + /** 亮色主题 */ + LIGHT = 'light', + /** 设计主题 */ + DESIGN = 'design' +} + +/** + * 菜单宽度 + */ +export enum MenuWidth { + /** 收起宽度 */ + CLOSE = '64px' +} + +/** + * 语言类型 + */ +export enum LanguageEnum { + /** 中文 */ + ZH = 'zh', + /** 英文 */ + EN = 'en' +} + +/** + * 容器宽度 + */ +export enum ContainerWidthEnum { + /** 全屏宽度 */ + FULL = '100%', + /** 固定宽度 */ + BOXED = '1200px' +} diff --git a/src/enums/formEnum.ts b/src/enums/formEnum.ts new file mode 100644 index 0000000..8e9b3b4 --- /dev/null +++ b/src/enums/formEnum.ts @@ -0,0 +1,24 @@ +/** + * 表单相关枚举定义模块 + * + * ## 主要功能 + * + * - 页面模式枚举(新增、编辑) + * - 表格尺寸枚举(默认、紧凑、宽松) + * + * @module enums/formEnum + * @author Art Design Pro Team + */ + +// 页面类型 +export enum PageModeEnum { + Add, // 新增 + Edit // 编辑 +} + +// 表格大小 +export enum TableSizeEnum { + DEFAULT = 'default', + SMALL = 'small', + LARGE = 'large' +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..4401f21 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,34 @@ +/// + +declare module 'nprogress' + +declare module 'crypto-js' + +declare module 'vue-img-cutter' + +declare module 'file-saver' + +declare module 'qrcode.vue' { + export type Level = 'L' | 'M' | 'Q' | 'H' + export type RenderAs = 'canvas' | 'svg' + export type GradientType = 'linear' | 'radial' + export interface ImageSettings { + src: string + height: number + width: number + excavate: boolean + } + export interface QRCodeProps { + value: string + size?: number + level?: Level + background?: string + foreground?: string + renderAs?: RenderAs + } + const QrcodeVue: any + export default QrcodeVue +} + +// 全局变量声明 +declare const __APP_VERSION__: string // 版本号 diff --git a/src/hooks/core/useAppMode.ts b/src/hooks/core/useAppMode.ts new file mode 100644 index 0000000..c39cd9e --- /dev/null +++ b/src/hooks/core/useAppMode.ts @@ -0,0 +1,45 @@ +/** + * useAppMode - 应用模式管理 + * + * 提供应用访问模式的判断和管理功能,支持前端和后端两种权限控制模式。 + * 根据环境变量 VITE_ACCESS_MODE 自动识别当前运行模式。 + * + * ## 主要功能 + * + * 1. 模式识别 - 自动识别前端模式或后端模式 + * 2. 前端模式 - 权限由前端路由配置控制,适合小型项目或演示环境 + * 3. 后端模式 - 权限由后端接口返回的菜单数据控制,适合企业级应用 + * 4. 响应式状态 - 提供响应式的模式判断,方便在组件中使用 + * + * @module useAppMode + * @author Art Design Pro Team + */ + +import { computed } from 'vue' + +export function useAppMode() { + // 获取访问模式配置 + const accessMode = import.meta.env.VITE_ACCESS_MODE + + /** + * 是否为前端控制模式 + * 前端模式:权限由前端路由配置控制 + */ + const isFrontendMode = computed(() => accessMode === 'frontend') + /** + * 是否为后端控制模式 + * 后端模式:权限由后端接口返回的菜单数据控制 + */ + const isBackendMode = computed(() => accessMode === 'backend') + + /** + * 当前应用模式 + */ + const currentMode = computed(() => accessMode) + + return { + isFrontendMode, + isBackendMode, + currentMode + } +} diff --git a/src/hooks/core/useAuth.ts b/src/hooks/core/useAuth.ts new file mode 100644 index 0000000..6e49415 --- /dev/null +++ b/src/hooks/core/useAuth.ts @@ -0,0 +1,78 @@ +/** + * useAuth - 权限验证管理 + * + * 提供统一的权限验证功能,支持前端和后端两种权限模式。 + * 用于控制页面按钮、操作等功能的显示和访问权限。 + * + * ## 主要功能 + * + * 1. 权限检查 - 检查用户是否拥有指定的权限标识 + * 2. 双模式支持 - 自动适配前端模式和后端模式的权限验证 + * 3. 前端模式 - 从用户信息中获取按钮权限列表(如 ['add', 'edit', 'delete']) + * 4. 后端模式 - 从路由 meta 配置中获取权限列表(如 [{ authMark: 'add' }]) + * + * ## 使用示例 + * + * ```typescript + * const { hasAuth } = useAuth() + * + * // 检查是否有新增权限 + * if (hasAuth('add')) { + * // 显示新增按钮 + * } + * + * // 在模板中使用 + * 编辑 + * 删除 + * ``` + * + * @module useAuth + * @author Art Design Pro Team + */ + +import { useRoute } from 'vue-router' +import { storeToRefs } from 'pinia' +import { useUserStore } from '@/store/modules/user' +import { useAppMode } from '@/hooks/core/useAppMode' +import type { AppRouteRecord } from '@/types/router' + +type AuthItem = NonNullable[number] + +const userStore = useUserStore() + +export const useAuth = () => { + const route = useRoute() + const { isFrontendMode } = useAppMode() + const { info } = storeToRefs(userStore) + + // 前端权限列表(使用后端返回的 permissions) + const frontendAuthList = info.value?.permissions ?? [] + + // 后端路由 meta 配置的权限列表(例如:[{ authMark: 'add' }]) + const backendAuthList: AuthItem[] = Array.isArray(route.meta.authList) + ? (route.meta.authList as AuthItem[]) + : [] + + /** + * 检查是否拥有某权限标识(前后端模式通用) + * @param auth 权限标识 + * @returns 是否有权限 + */ + const hasAuth = (auth: string): boolean => { + // 1. 前端模式:直接按用户权限列表判断 + if (isFrontendMode.value) { + return frontendAuthList.includes(auth) + } + + // 2. 后端模式:同时校验路由 meta 声明和用户真实权限 + const declaredInRoute = backendAuthList.some((item) => item?.authMark === auth) + const ownedByUser = frontendAuthList.includes(auth) + + // 路由未声明时,退化为只校验用户权限;声明了则需用户权限+声明同时满足 + return declaredInRoute ? ownedByUser : ownedByUser + } + + return { + hasAuth + } +} diff --git a/src/hooks/core/useCeremony.ts b/src/hooks/core/useCeremony.ts new file mode 100644 index 0000000..ead2630 --- /dev/null +++ b/src/hooks/core/useCeremony.ts @@ -0,0 +1,184 @@ +/** + * useCeremony - 节日庆祝管理 + * + * 提供节日烟花效果和祝福文本展示功能,为系统增添节日氛围。 + * 自动检测当前日期是否为节日,并在首次进入时播放烟花动画和显示祝福语。 + * + * ## 主要功能 + * + * 1. 节日检测 - 自动匹配当前日期与节日配置列表,支持单日和跨日期节日 + * 2. 烟花动画 - 播放节日烟花特效,支持自定义图片和触发次数 + * 3. 祝福文本 - 烟花结束后显示节日祝福文本 + * 4. 状态管理 - 记录烟花播放状态,避免重复播放 + * 5. 清理机制 - 提供清理方法,支持手动停止和重置 + * + * ## 使用示例 + * + * ```typescript + * // 在配置文件中定义节日 + * // 单日节日 + * { + * date: '2024-12-25', + * name: '圣诞节', + * image: christmasImage, + * count: 3 // 可选,不设置则使用默认值 3 次 + * scrollText: 'Merry Christmas!', + * } + * + * // 跨日期节日 + * { + * date: '2025-11-07', + * endDate: '2025-11-10', + * name: 'v3.0 测试阶段', + * image: '', + * count: 5 // 自定义烟花播放次数 + * scrollText: '系统 v3.0 测试阶段正式开启!', + * } + * ``` + * + * @module useCeremony + * @author Art Design Pro Team + */ + +import { useTimeoutFn, useIntervalFn, useDateFormat } from '@vueuse/core' +import { storeToRefs } from 'pinia' +import { computed } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { mittBus } from '@/utils/sys' +import { festivalConfigList } from '@/config/modules/festival' + +/** + * 节日庆祝配置常量 + */ +const FESTIVAL_CONFIG = { + /** 初始延迟(毫秒) */ + INITIAL_DELAY: 300, + /** 烟花播放间隔(毫秒) */ + FIREWORK_INTERVAL: 1000, + /** 文本显示延迟(毫秒) */ + TEXT_DELAY: 2000, + /** 默认烟花播放次数 */ + DEFAULT_FIREWORKS_COUNT: 3 +} as const + +/** + * 节日庆祝功能 + * 提供节日烟花效果和祝福文本展示 + */ +export function useCeremony() { + const settingStore = useSettingStore() + const { holidayFireworksLoaded, isShowFireworks } = storeToRefs(settingStore) + + let fireworksInterval: { pause: () => void } | null = null + + /** + * 检查日期是否在节日范围内 + * @param currentDate 当前日期 + * @param festivalDate 节日开始日期 + * @param festivalEndDate 节日结束日期(可选) + */ + const isDateInRange = ( + currentDate: string, + festivalDate: string, + festivalEndDate?: string + ): boolean => { + if (!festivalEndDate) { + // 单日节日 + return currentDate === festivalDate + } + + // 跨日期节日 + const current = new Date(currentDate) + const start = new Date(festivalDate) + const end = new Date(festivalEndDate) + + return current >= start && current <= end + } + + /** + * 获取当前日期对应的节日数据 + */ + const currentFestivalData = computed(() => { + const currentDate = useDateFormat(new Date(), 'YYYY-MM-DD').value + return festivalConfigList.find((item) => isDateInRange(currentDate, item.date, item.endDate)) + }) + + /** + * 更新节日日期到 store + */ + const updateFestivalDate = () => { + settingStore.setFestivalDate(currentFestivalData.value?.date || '') + } + + /** + * 触发烟花效果 + */ + const triggerFirework = () => { + mittBus.emit('triggerFireworks', currentFestivalData.value?.image) + } + + /** + * 完成烟花效果后显示文本 + */ + const showFestivalText = () => { + settingStore.setholidayFireworksLoaded(true) + + useTimeoutFn(() => { + settingStore.setShowFestivalText(true) + updateFestivalDate() + }, FESTIVAL_CONFIG.TEXT_DELAY) + } + + /** + * 启动烟花循环 + */ + const startFireworksLoop = () => { + let playedCount = 0 + // 使用节日配置的播放次数,如果没有则使用默认值 + const count = currentFestivalData.value?.count ?? FESTIVAL_CONFIG.DEFAULT_FIREWORKS_COUNT + + const { pause } = useIntervalFn(() => { + triggerFirework() + playedCount++ + + if (playedCount >= count) { + pause() + showFestivalText() + } + }, FESTIVAL_CONFIG.FIREWORK_INTERVAL) + + fireworksInterval = { pause } + } + + /** + * 开启节日庆祝 + */ + const openFestival = () => { + if (!currentFestivalData.value || !isShowFireworks.value) { + return + } + + const { start } = useTimeoutFn(startFireworksLoop, FESTIVAL_CONFIG.INITIAL_DELAY) + start() + } + + /** + * 清理烟花效果 + */ + const cleanup = () => { + if (fireworksInterval) { + fireworksInterval.pause() + fireworksInterval = null + } + settingStore.setShowFestivalText(false) + updateFestivalDate() + } + + return { + openFestival, + cleanup, + holidayFireworksLoaded, + currentFestivalData, + isShowFireworks + } +} diff --git a/src/hooks/core/useChart.ts b/src/hooks/core/useChart.ts new file mode 100644 index 0000000..29ba1d1 --- /dev/null +++ b/src/hooks/core/useChart.ts @@ -0,0 +1,745 @@ +/** + * useChart - ECharts 图表管理 + * + * 提供完整的 ECharts 图表生命周期管理和配置能力,简化图表开发流程。 + * 自动处理图表初始化、更新、销毁、主题切换、响应式调整等复杂逻辑。 + * + * ## 核心功能 + * + * 1. 图表生命周期管理 - 自动处理初始化、更新、销毁,支持延迟加载和可见性检测 + * 2. 主题自动适配 - 响应系统主题变化,自动更新图表样式和配色 + * 3. 响应式调整 - 监听窗口大小、菜单展开等变化,自动调整图表尺寸 + * 4. 空状态处理 - 优雅的空数据展示,自动显示"暂无数据"提示 + * 5. 样式配置统一 - 提供坐标轴、图例、提示框等统一的样式配置方法 + * 6. 性能优化 - 防抖处理、样式缓存、requestAnimationFrame 优化 + * 7. 高级组件抽象 - useChartComponent 提供更高层次的图表组件封装 + * + * ## 使用示例 + * + * ```typescript + * // 基础用法 + * const { + * chartRef, + * initChart, + * updateChart, + * getAxisLineStyle, + * getTooltipStyle + * } = useChart() + * + * onMounted(() => { + * initChart({ + * xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] }, + * yAxis: { type: 'value' }, + * series: [{ data: [120, 200, 150], type: 'bar' }] + * }) + * }) + * + * // 高级用法 - 组件抽象 + * const chart = useChartComponent({ + * props, + * generateOptions: () => ({ + * // ECharts 配置 + * }), + * checkEmpty: () => data.value.length === 0, + * watchSources: [() => props.data] + * }) + * ``` + * + * @module useChart + * @author Art Design Pro Team + */ + +import { echarts, type EChartsOption } from '@/plugins/echarts' +import { storeToRefs } from 'pinia' +import { useSettingStore } from '@/store/modules/setting' +import { getCssVar } from '@/utils/ui' +import type { BaseChartProps, ChartThemeConfig, UseChartOptions } from '@/types/component/chart' + +// 图表主题配置 +export const useChartOps = (): ChartThemeConfig => ({ + /** */ + chartHeight: '16rem', + /** 字体大小 */ + fontSize: 13, + /** 字体颜色 */ + fontColor: '#999', + /** 主题颜色 */ + themeColor: getCssVar('--el-color-primary-light-1'), + /** 颜色组 */ + colors: [ + getCssVar('--el-color-primary-light-1'), + '#4ABEFF', + '#EDF2FF', + '#14DEBA', + '#FFAF20', + '#FA8A6C', + '#FFAF20' + ] +}) + +// 常量定义 +const RESIZE_DELAYS = [50, 100, 200, 350] as const +const MENU_RESIZE_DELAYS = [50, 100, 200] as const +const RESIZE_DEBOUNCE_DELAY = 100 + +export function useChart(options: UseChartOptions = {}) { + const { initOptions, initDelay = 0, threshold = 0.1, autoTheme = true } = options + + const settingStore = useSettingStore() + const { isDark, menuOpen, menuType } = storeToRefs(settingStore) + + const chartRef = ref() + let chart: echarts.ECharts | null = null + let intersectionObserver: IntersectionObserver | null = null + let pendingOptions: EChartsOption | null = null + let resizeTimeoutId: number | null = null + let resizeFrameId: number | null = null + let isDestroyed = false + let emptyStateDiv: HTMLElement | null = null + + // 清理定时器的统一方法 + const clearTimers = () => { + if (resizeTimeoutId) { + clearTimeout(resizeTimeoutId) + resizeTimeoutId = null + } + if (resizeFrameId) { + cancelAnimationFrame(resizeFrameId) + resizeFrameId = null + } + } + + // 使用 requestAnimationFrame 优化 resize 处理 + const requestAnimationResize = () => { + if (resizeFrameId) { + cancelAnimationFrame(resizeFrameId) + } + resizeFrameId = requestAnimationFrame(() => { + handleResize() + resizeFrameId = null + }) + } + + // 防抖的resize处理(用于窗口resize事件) + const debouncedResize = () => { + if (resizeTimeoutId) { + clearTimeout(resizeTimeoutId) + } + resizeTimeoutId = window.setTimeout(() => { + requestAnimationResize() + resizeTimeoutId = null + }, RESIZE_DEBOUNCE_DELAY) + } + + // 多延迟resize处理 - 统一方法 + const multiDelayResize = (delays: readonly number[]) => { + // 立即调用一次,快速响应 + nextTick(requestAnimationResize) + + // 使用延迟时间,确保图表正确适应变化 + delays.forEach((delay) => { + setTimeout(requestAnimationResize, delay) + }) + } + + // 收缩菜单时,重新计算图表大小(仅在图表存在时监听) + let menuOpenStopHandle: (() => void) | null = null + let menuTypeStopHandle: (() => void) | null = null + + const setupMenuWatchers = () => { + menuOpenStopHandle = watch(menuOpen, () => multiDelayResize(RESIZE_DELAYS)) + menuTypeStopHandle = watch(menuType, () => { + nextTick(requestAnimationResize) + setTimeout(() => multiDelayResize(MENU_RESIZE_DELAYS), 0) + }) + } + + const cleanupMenuWatchers = () => { + menuOpenStopHandle?.() + menuTypeStopHandle?.() + menuOpenStopHandle = null + menuTypeStopHandle = null + } + + // 主题变化时重新设置图表选项 + let themeStopHandle: (() => void) | null = null + + const setupThemeWatcher = () => { + if (autoTheme) { + themeStopHandle = watch(isDark, () => { + // 更新空状态样式 + emptyStateManager.updateStyle() + + if (chart && !isDestroyed) { + // 使用 requestAnimationFrame 优化主题更新 + requestAnimationFrame(() => { + if (chart && !isDestroyed) { + const currentOptions = chart.getOption() + if (currentOptions) { + updateChart(currentOptions as EChartsOption) + } + } + }) + } + }) + } + } + + const cleanupThemeWatcher = () => { + themeStopHandle?.() + themeStopHandle = null + } + + // 样式生成器 - 统一的样式配置 + const createLineStyle = (color: string, width = 1, type?: 'solid' | 'dashed') => ({ + color, + width, + ...(type && { type }) + }) + + // 缓存样式配置以减少重复计算 + const styleCache = { + axisLine: null as any, + splitLine: null as any, + axisLabel: null as any, + lastDarkValue: isDark.value + } + + const clearStyleCache = () => { + styleCache.axisLine = null + styleCache.splitLine = null + styleCache.axisLabel = null + styleCache.lastDarkValue = isDark.value + } + + // 坐标轴线样式 + const getAxisLineStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.axisLine) { + styleCache.axisLine = { + show, + lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED') + } + } + return styleCache.axisLine + } + + // 分割线样式 + const getSplitLineStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.splitLine) { + styleCache.splitLine = { + show, + lineStyle: createLineStyle(isDark.value ? '#444' : '#EDEDED', 1, 'dashed') + } + } + return styleCache.splitLine + } + + // 坐标轴标签样式 + const getAxisLabelStyle = (show: boolean = true) => { + if (styleCache.lastDarkValue !== isDark.value) { + clearStyleCache() + } + if (!styleCache.axisLabel) { + const { fontColor, fontSize } = useChartOps() + styleCache.axisLabel = { + show, + color: fontColor, + fontSize + } + } + return styleCache.axisLabel + } + + // 坐标轴刻度样式(静态配置,无需缓存) + const getAxisTickStyle = () => ({ + show: false + }) + + // 获取动画配置 + const getAnimationConfig = (animationDelay: number = 50, animationDuration: number = 1500) => ({ + animationDelay: (idx: number) => idx * animationDelay + 200, + animationDuration: (idx: number) => animationDuration - idx * 50, + animationEasing: 'quarticOut' as const + }) + + // 获取统一的 tooltip 配置 + const getTooltipStyle = (trigger: 'item' | 'axis' = 'axis', customOptions: any = {}) => ({ + trigger, + backgroundColor: isDark.value ? 'rgba(0, 0, 0, 0.8)' : 'rgba(255, 255, 255, 0.9)', + borderColor: isDark.value ? '#333' : '#ddd', + borderWidth: 1, + textStyle: { + color: isDark.value ? '#fff' : '#333' + }, + ...customOptions + }) + + // 获取统一的图例配置 + const getLegendStyle = ( + position: 'bottom' | 'top' | 'left' | 'right' = 'bottom', + customOptions: any = {} + ) => { + const baseConfig = { + textStyle: { + color: isDark.value ? '#fff' : '#333' + }, + itemWidth: 12, + itemHeight: 12, + itemGap: 20, + ...customOptions + } + + // 根据位置设置不同的配置 + switch (position) { + case 'bottom': + return { + ...baseConfig, + bottom: 0, + left: 'center', + orient: 'horizontal', + icon: 'roundRect' + } + case 'top': + return { + ...baseConfig, + top: 0, + left: 'center', + orient: 'horizontal', + icon: 'roundRect' + } + case 'left': + return { + ...baseConfig, + left: 0, + top: 'center', + orient: 'vertical', + icon: 'roundRect' + } + case 'right': + return { + ...baseConfig, + right: 0, + top: 'center', + orient: 'vertical', + icon: 'roundRect' + } + default: + return baseConfig + } + } + + // 根据图例位置计算 grid 配置 + const getGridWithLegend = ( + showLegend: boolean, + legendPosition: 'bottom' | 'top' | 'left' | 'right' = 'bottom', + baseGrid: any = {} + ) => { + const defaultGrid = { + top: 15, + right: 15, + bottom: 8, + left: 0, + containLabel: true, + ...baseGrid + } + + if (!showLegend) { + return defaultGrid + } + + // 根据图例位置调整 grid + switch (legendPosition) { + case 'bottom': + return { + ...defaultGrid, + bottom: 40 + } + case 'top': + return { + ...defaultGrid, + top: 40 + } + case 'left': + return { + ...defaultGrid, + left: 120 + } + case 'right': + return { + ...defaultGrid, + right: 120 + } + default: + return defaultGrid + } + } + + // 创建IntersectionObserver + const createIntersectionObserver = () => { + if (intersectionObserver || !chartRef.value) return + + intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && pendingOptions && !isDestroyed) { + // 使用 requestAnimationFrame 确保在下一帧初始化图表 + requestAnimationFrame(() => { + if (!isDestroyed && pendingOptions) { + try { + // 元素变为可见,初始化图表 + if (!chart) { + chart = echarts.init(entry.target as HTMLElement) + } + + // 触发自定义事件,让组件处理动画逻辑 + const event = new CustomEvent('chartVisible', { + detail: { options: pendingOptions } + }) + entry.target.dispatchEvent(event) + + pendingOptions = null + cleanupIntersectionObserver() + } catch (error) { + console.error('图表初始化失败:', error) + } + } + }) + } + }) + }, + { threshold } + ) + + intersectionObserver.observe(chartRef.value) + } + + // 清理IntersectionObserver + const cleanupIntersectionObserver = () => { + if (intersectionObserver) { + intersectionObserver.disconnect() + intersectionObserver = null + } + } + + // 检查容器是否可见 + const isContainerVisible = (element: HTMLElement): boolean => { + const rect = element.getBoundingClientRect() + return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0 + } + + // 图表初始化核心逻辑 + const performChartInit = (options: EChartsOption) => { + if (!chart && chartRef.value && !isDestroyed) { + chart = echarts.init(chartRef.value) + // 图表创建后立即设置监听器 + setupMenuWatchers() + setupThemeWatcher() + } + if (chart && !isDestroyed) { + chart.setOption(options) + pendingOptions = null + } + } + + // 空状态管理器 + const emptyStateManager = { + create: () => { + if (!chartRef.value || emptyStateDiv) return + + emptyStateDiv = document.createElement('div') + emptyStateDiv.style.cssText = ` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 12px; + color: ${isDark.value ? '#555555' : '#B3B2B2'}; + background: transparent; + z-index: 10; + ` + emptyStateDiv.innerHTML = `暂无数据` + + // 确保父容器有相对定位 + if ( + chartRef.value.style.position !== 'relative' && + chartRef.value.style.position !== 'absolute' + ) { + chartRef.value.style.position = 'relative' + } + + chartRef.value.appendChild(emptyStateDiv) + }, + + remove: () => { + if (emptyStateDiv && chartRef.value) { + chartRef.value.removeChild(emptyStateDiv) + emptyStateDiv = null + } + }, + + updateStyle: () => { + if (emptyStateDiv) { + emptyStateDiv.style.color = isDark.value ? '#666' : '#999' + } + } + } + + // 初始化图表 + const initChart = (options: EChartsOption = {}, isEmpty: boolean = false) => { + if (!chartRef.value || isDestroyed) return + + const mergedOptions = { ...initOptions, ...options } + + try { + if (isEmpty) { + // 处理空数据情况 - 显示自定义空状态div + if (chart) { + chart.clear() + } + emptyStateManager.create() + return + } else { + // 有数据时移除空状态div + emptyStateManager.remove() + } + + if (isContainerVisible(chartRef.value)) { + // 容器可见,正常初始化 + if (initDelay > 0) { + setTimeout(() => performChartInit(mergedOptions), initDelay) + } else { + performChartInit(mergedOptions) + } + } else { + // 容器不可见,保存选项并设置监听器 + pendingOptions = mergedOptions + createIntersectionObserver() + } + } catch (error) { + console.error('图表初始化失败:', error) + } + } + + // 更新图表 + const updateChart = (options: EChartsOption) => { + if (isDestroyed) return + + try { + if (!chart) { + // 如果图表不存在,先初始化 + initChart(options) + return + } + chart.setOption(options) + } catch (error) { + console.error('图表更新失败:', error) + } + } + + // 处理窗口大小变化 + const handleResize = () => { + if (chart && !isDestroyed) { + try { + chart.resize() + } catch (error) { + console.error('图表resize失败:', error) + } + } + } + + // 销毁图表 + const destroyChart = () => { + isDestroyed = true + + if (chart) { + try { + chart.dispose() + } catch (error) { + console.error('图表销毁失败:', error) + } finally { + chart = null + } + } + + // 清理所有监听器和资源 + cleanupMenuWatchers() + cleanupThemeWatcher() + emptyStateManager.remove() + cleanupIntersectionObserver() + clearTimers() + clearStyleCache() + pendingOptions = null + } + + // 获取图表实例 + const getChartInstance = () => chart + + // 获取图表是否已初始化 + const isChartInitialized = () => chart !== null + + onMounted(() => { + window.addEventListener('resize', debouncedResize) + }) + + onBeforeUnmount(() => { + window.removeEventListener('resize', debouncedResize) + }) + + onUnmounted(() => { + destroyChart() + }) + + return { + isDark, + chartRef, + initChart, + updateChart, + handleResize, + destroyChart, + getChartInstance, + isChartInitialized, + emptyStateManager, + getAxisLineStyle, + getSplitLineStyle, + getAxisLabelStyle, + getAxisTickStyle, + getAnimationConfig, + getTooltipStyle, + getLegendStyle, + useChartOps, + getGridWithLegend + } +} + +// 高级图表组件抽象 +interface UseChartComponentOptions { + /** Props响应式对象 */ + props: T + /** 图表配置生成函数 */ + generateOptions: () => EChartsOption + /** 空数据检查函数 */ + checkEmpty?: () => boolean + /** 自定义监听的响应式数据 */ + watchSources?: (() => any)[] + /** 自定义可视事件处理 */ + onVisible?: () => void + /** useChart选项 */ + chartOptions?: UseChartOptions +} + +export function useChartComponent(options: UseChartComponentOptions) { + const { + props, + generateOptions, + checkEmpty, + watchSources = [], + onVisible, + chartOptions = {} + } = options + + const chart = useChart(chartOptions) + const { chartRef, initChart, isDark, emptyStateManager } = chart + + // 检查是否为空数据 + const isEmpty = computed(() => { + if (props.isEmpty) return true + if (checkEmpty) return checkEmpty() + return false + }) + + // 更新图表 + const updateChart = () => { + nextTick(() => { + if (isEmpty.value) { + // 处理空数据情况 - 显示自定义空状态div + if (chart.getChartInstance()) { + chart.getChartInstance()?.clear() + } + emptyStateManager.create() + } else { + // 有数据时移除空状态div并初始化图表 + emptyStateManager.remove() + initChart(generateOptions()) + } + }) + } + + // 处理图表进入可视区域时的逻辑 + const handleChartVisible = () => { + if (onVisible) { + onVisible() + } else { + updateChart() + } + } + + // 存储监听器停止函数 + const stopHandles: (() => void)[] = [] + + // 设置数据监听 + const setupWatchers = () => { + // 监听自定义数据源 + if (watchSources.length > 0) { + const stopHandle = watch(watchSources, updateChart, { deep: true }) + stopHandles.push(stopHandle) + } + + // 监听主题变化 + const themeStopHandle = watch(isDark, () => { + emptyStateManager.updateStyle() + updateChart() + }) + stopHandles.push(themeStopHandle) + } + + // 清理所有监听器 + const cleanupWatchers = () => { + stopHandles.forEach((stop) => stop()) + stopHandles.length = 0 + } + + // 设置生命周期 + const setupLifecycle = () => { + onMounted(() => { + updateChart() + + // 监听图表可见事件 + if (chartRef.value) { + chartRef.value.addEventListener('chartVisible', handleChartVisible) + } + }) + + onBeforeUnmount(() => { + // 清理事件监听器 + if (chartRef.value) { + chartRef.value.removeEventListener('chartVisible', handleChartVisible) + } + // 清理所有监听器 + cleanupWatchers() + // 清理空状态div + emptyStateManager.remove() + }) + } + + // 初始化 + setupWatchers() + setupLifecycle() + + return { + ...chart, + isEmpty, + updateChart, + handleChartVisible + } +} diff --git a/src/hooks/core/useCommon.ts b/src/hooks/core/useCommon.ts new file mode 100644 index 0000000..c936854 --- /dev/null +++ b/src/hooks/core/useCommon.ts @@ -0,0 +1,87 @@ +/** + * useCommon - 通用功能集合 + * + * 提供常用的页面操作功能,包括页面刷新、滚动控制、路径获取等。 + * 这些功能在多个页面和组件中都会用到,统一封装便于复用。 + * + * ## 主要功能 + * + * 1. 首页路径 - 获取系统配置的首页路径 + * 2. 页面刷新 - 刷新当前页面内容 + * 3. 滚动控制 - 提供多种滚动到顶部和指定位置的方法 + * 4. 平滑滚动 - 支持平滑滚动动画效果 + * + * @module useCommon + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import { useMenuStore } from '@/store/modules/menu' +import { useSettingStore } from '@/store/modules/setting' + +export function useCommon() { + const menuStore = useMenuStore() + const settingStore = useSettingStore() + + /** + * 首页路径 + * 从菜单 store 中获取配置的首页路径 + */ + const homePath = computed(() => menuStore.getHomePath()) + + /** + * 刷新当前页面 + * 通过切换 setting store 中的 refresh 状态触发页面重新渲染 + */ + const refresh = () => { + settingStore.reload() + } + + /** + * 滚动到页面顶部 + * 查找主内容区域并将其滚动位置重置为顶部 + */ + const scrollToTop = () => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTop = 0 + } + } + + /** + * 平滑滚动到页面顶部 + * 使用 smooth 行为实现平滑滚动效果 + */ + const smoothScrollToTop = () => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTo({ + top: 0, + behavior: 'smooth' + }) + } + } + + /** + * 滚动到指定位置 + * @param top 目标滚动位置(像素) + * @param smooth 是否使用平滑滚动 + */ + const scrollTo = (top: number, smooth: boolean = false) => { + const scrollContainer = document.getElementById('app-main') + if (scrollContainer) { + scrollContainer.scrollTo({ + top, + behavior: smooth ? 'smooth' : 'auto' + }) + } + } + + return { + homePath, + refresh, + scrollTo, + scrollToTop, + smoothScrollToTop + } +} diff --git a/src/hooks/core/useFastEnter.ts b/src/hooks/core/useFastEnter.ts new file mode 100644 index 0000000..555eb65 --- /dev/null +++ b/src/hooks/core/useFastEnter.ts @@ -0,0 +1,55 @@ +/** + * useFastEnter - 快速入口管理 + * + * 管理顶部栏的快速入口功能,提供应用列表和快速链接的配置和过滤。 + * 支持动态启用/禁用、自定义排序、响应式宽度控制等功能。 + * + * ## 主要功能 + * + * 1. 应用列表管理 - 获取启用的应用列表,自动按排序权重排序 + * 2. 快速链接管理 - 获取启用的快速链接,支持自定义排序 + * 3. 响应式配置 - 所有配置自动响应变化,无需手动更新 + * 4. 宽度控制 - 提供最小显示宽度配置,支持响应式布局 + * + * @module useFastEnter + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import appConfig from '@/config' +import type { FastEnterApplication, FastEnterQuickLink } from '@/types/config' + +export function useFastEnter() { + // 获取快速入口配置 + const fastEnterConfig = computed(() => appConfig.fastEnter) + + // 获取启用的应用列表(按排序权重排序) + const enabledApplications = computed(() => { + if (!fastEnterConfig.value?.applications) return [] + + return fastEnterConfig.value.applications + .filter((app) => app.enabled !== false) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }) + + // 获取启用的快速链接(按排序权重排序) + const enabledQuickLinks = computed(() => { + if (!fastEnterConfig.value?.quickLinks) return [] + + return fastEnterConfig.value.quickLinks + .filter((link) => link.enabled !== false) + .sort((a, b) => (a.order || 0) - (b.order || 0)) + }) + + // 获取最小显示宽度 + const minWidth = computed(() => { + return fastEnterConfig.value?.minWidth || 1200 + }) + + return { + fastEnterConfig, + enabledApplications, + enabledQuickLinks, + minWidth + } +} diff --git a/src/hooks/core/useHeaderBar.ts b/src/hooks/core/useHeaderBar.ts new file mode 100644 index 0000000..be10712 --- /dev/null +++ b/src/hooks/core/useHeaderBar.ts @@ -0,0 +1,201 @@ +/** + * useHeaderBar - 顶部栏功能管理 + * + * 统一管理顶部栏各个功能模块的显示状态和配置信息。 + * 提供灵活的功能开关控制,支持动态显示/隐藏顶部栏的各个功能按钮。 + * + * ## 主要功能 + * + * 1. 功能开关控制 - 统一管理菜单按钮、刷新按钮、快速入口等功能的显示状态 + * 2. 配置信息获取 - 获取各个功能模块的详细配置信息 + * 3. 功能列表查询 - 快速获取所有启用或禁用的功能列表 + * 4. 响应式状态 - 所有状态自动响应配置和 store 变化 + * + * @module useHeaderBar + * @author Art Design Pro Team + */ + +import { computed } from 'vue' +import { storeToRefs } from 'pinia' +import { useSettingStore } from '@/store/modules/setting' +import { headerBarConfig } from '@/config/modules/headerBar' +import { HeaderBarFeatureConfig } from '@/types' + +/** + * 顶部栏功能管理 + * @returns 顶部栏功能相关的状态和方法 + */ +export function useHeaderBar() { + const settingStore = useSettingStore() + + // 获取顶部栏配置 + const headerBarConfigRef = computed(() => headerBarConfig) + + // 从store中获取相关状态 + const { showMenuButton, showFastEnter, showRefreshButton, showCrumbs, showLanguage } = + storeToRefs(settingStore) + + /** + * 检查特定功能是否启用 + * @param feature 功能名称 + * @returns 是否启用 + */ + const isFeatureEnabled = (feature: keyof HeaderBarFeatureConfig): boolean => { + return headerBarConfigRef.value[feature]?.enabled ?? false + } + + /** + * 获取功能配置信息 + * @param feature 功能名称 + * @returns 功能配置信息 + */ + const getFeatureConfig = (feature: keyof HeaderBarFeatureConfig) => { + return headerBarConfigRef.value[feature] + } + + // 检查菜单按钮是否显示 + const shouldShowMenuButton = computed(() => { + return isFeatureEnabled('menuButton') && showMenuButton.value + }) + + // 检查刷新按钮是否显示 + const shouldShowRefreshButton = computed(() => { + return isFeatureEnabled('refreshButton') && showRefreshButton.value + }) + + // 检查快速入口是否显示 + const shouldShowFastEnter = computed(() => { + return isFeatureEnabled('fastEnter') && showFastEnter.value + }) + + // 检查面包屑是否显示 + const shouldShowBreadcrumb = computed(() => { + return isFeatureEnabled('breadcrumb') && showCrumbs.value + }) + + // 检查全局搜索是否显示 + const shouldShowGlobalSearch = computed(() => { + return isFeatureEnabled('globalSearch') + }) + + // 检查全屏按钮是否显示 + const shouldShowFullscreen = computed(() => { + return isFeatureEnabled('fullscreen') + }) + + // 检查通知中心是否显示 + const shouldShowNotification = computed(() => { + return isFeatureEnabled('notification') + }) + + // 检查聊天功能是否显示 + const shouldShowChat = computed(() => { + return isFeatureEnabled('chat') + }) + + // 检查语言切换是否显示 + const shouldShowLanguage = computed(() => { + return isFeatureEnabled('language') && showLanguage.value + }) + + // 检查设置面板是否显示 + const shouldShowSettings = computed(() => { + return isFeatureEnabled('settings') + }) + + // 检查主题切换是否显示 + const shouldShowThemeToggle = computed(() => { + return isFeatureEnabled('themeToggle') + }) + + // 获取快速入口的最小宽度 + const fastEnterMinWidth = computed(() => { + const config = getFeatureConfig('fastEnter') + return (config as any)?.minWidth || 1200 + }) + + /** + * 检查功能是否启用(别名) + * @param feature 功能名称 + * @returns 是否启用 + */ + const isFeatureActive = (feature: keyof HeaderBarFeatureConfig): boolean => { + return isFeatureEnabled(feature) + } + + /** + * 获取功能配置(别名) + * @param feature 功能名称 + * @returns 功能配置 + */ + const getFeatureInfo = (feature: keyof HeaderBarFeatureConfig) => { + return getFeatureConfig(feature) + } + + /** + * 获取所有启用的功能列表 + * @returns 启用的功能名称数组 + */ + const getEnabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => { + return Object.keys(headerBarConfigRef.value).filter( + (key) => headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled + ) as (keyof HeaderBarFeatureConfig)[] + } + + /** + * 获取所有禁用的功能列表 + * @returns 禁用的功能名称数组 + */ + const getDisabledFeatures = (): (keyof HeaderBarFeatureConfig)[] => { + return Object.keys(headerBarConfigRef.value).filter( + (key) => !headerBarConfigRef.value[key as keyof HeaderBarFeatureConfig]?.enabled + ) as (keyof HeaderBarFeatureConfig)[] + } + + /** + * 获取所有启用的功能(别名) + * @returns 启用的功能列表 + */ + const getActiveFeatures = () => { + return getEnabledFeatures() + } + + /** + * 获取所有禁用的功能(别名) + * @returns 禁用的功能列表 + */ + const getInactiveFeatures = () => { + return getDisabledFeatures() + } + + return { + // 配置 + headerBarConfig: headerBarConfigRef, + + // 显示状态计算属性 + shouldShowMenuButton, // 是否显示菜单按钮 + shouldShowRefreshButton, // 是否显示刷新按钮 + shouldShowFastEnter, // 是否显示快速入口 + shouldShowBreadcrumb, // 是否显示面包屑 + shouldShowGlobalSearch, // 是否显示全局搜索 + shouldShowFullscreen, // 是否显示全屏按钮 + shouldShowNotification, // 是否显示通知中心 + shouldShowChat, // 是否显示聊天功能 + shouldShowLanguage, // 是否显示语言切换 + shouldShowSettings, // 是否显示设置面板 + shouldShowThemeToggle, // 是否显示主题切换 + + // 配置相关 + fastEnterMinWidth, // 快速入口最小宽度 + + // 方法 + isFeatureEnabled, // 检查功能是否启用 + isFeatureActive, // 检查功能是否启用(别名) + getFeatureConfig, // 获取功能配置 + getFeatureInfo, // 获取功能配置(别名) + getEnabledFeatures, // 获取所有启用的功能 + getDisabledFeatures, // 获取所有禁用的功能 + getActiveFeatures, // 获取所有启用的功能(别名) + getInactiveFeatures // 获取所有禁用的功能(别名) + } +} diff --git a/src/hooks/core/useLayoutHeight.ts b/src/hooks/core/useLayoutHeight.ts new file mode 100644 index 0000000..4b1171a --- /dev/null +++ b/src/hooks/core/useLayoutHeight.ts @@ -0,0 +1,148 @@ +/** + * useLayoutHeight - 页面布局高度管理 + * + * 自动计算和管理页面内容区域的高度,确保内容区域能够正确填充剩余空间。 + * 监听头部元素高度变化,动态调整内容区域高度,避免出现滚动条或布局错乱。 + * + * ## 主要功能 + * + * 1. 动态高度计算 - 根据头部元素高度自动计算内容区域高度 + * 2. 响应式监听 - 自动监听元素尺寸变化并更新高度 + * 3. CSS 变量同步 - 自动更新 CSS 变量,方便全局使用 + * 4. 灵活配置 - 支持自定义间距、CSS 变量名等 + * 5. 自动查找模式 - 提供通过 ID 自动查找元素的便捷方式 + * + * @module useLayoutHeight + * @author Art Design Pro Team + */ + +import { ref, computed, watch, onMounted } from 'vue' +import { useElementSize } from '@vueuse/core' + +/** + * 页面容器高度配置 + */ +interface LayoutHeightOptions { + /** 额外的间距(默认 15px) */ + extraSpacing?: number + /** 是否自动更新 CSS 变量(默认 true) */ + updateCssVar?: boolean + /** CSS 变量名称(默认 '--art-full-height') */ + cssVarName?: string +} + +export function useLayoutHeight(options: LayoutHeightOptions = {}) { + const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options + + // 元素引用 + const headerRef = ref() + const contentHeaderRef = ref() + + // 使用 VueUse 自动监听元素尺寸变化 + const { height: headerHeight } = useElementSize(headerRef) + const { height: contentHeaderHeight } = useElementSize(contentHeaderRef) + + // 计算容器最小高度(响应式) + const containerMinHeight = computed(() => { + const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing + return `calc(100vh - ${totalHeight}px)` + }) + + if (updateCssVar) { + watch( + containerMinHeight, + (newHeight) => { + requestAnimationFrame(() => { + document.documentElement.style.setProperty(cssVarName, newHeight) + }) + }, + { immediate: true } + ) + } + + return { + /** 容器最小高度(响应式) */ + containerMinHeight, + /** 头部元素引用 */ + headerRef, + /** 内容头部元素引用 */ + contentHeaderRef, + /** 头部高度(响应式) */ + headerHeight, + /** 内容头部高度(响应式) */ + contentHeaderHeight + } +} + +/** + * 通过 ID 自动查找元素的布局高度管理 + * 适用于无法直接获取元素引用的场景 + * + * @param headerIds 头部元素的 ID 数组 + * @param options 配置选项 + * + * ``` + */ +export function useAutoLayoutHeight( + headerIds: string[] = ['app-header', 'app-content-header'], + options: LayoutHeightOptions = {} +) { + const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options + + // 创建元素引用 + const headerRef = ref() + const contentHeaderRef = ref() + + // 使用 VueUse 自动监听元素尺寸变化 + const { height: headerHeight } = useElementSize(headerRef) + const { height: contentHeaderHeight } = useElementSize(contentHeaderRef) + + // 计算容器最小高度(响应式) + const containerMinHeight = computed(() => { + const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing + return `calc(100vh - ${totalHeight}px)` + }) + + if (updateCssVar) { + watch( + containerMinHeight, + (newHeight) => { + requestAnimationFrame(() => { + document.documentElement.style.setProperty(cssVarName, newHeight) + }) + }, + { immediate: true } + ) + } + + // 在 DOM 挂载后查找元素 + onMounted(() => { + if (typeof document !== 'undefined') { + // 使用 nextTick 确保 DOM 完全渲染 + requestAnimationFrame(() => { + const header = document.getElementById(headerIds[0]) + const contentHeader = document.getElementById(headerIds[1]) + + if (header) { + headerRef.value = header + } + if (contentHeader) { + contentHeaderRef.value = contentHeader + } + }) + } + }) + + return { + /** 容器最小高度(响应式) */ + containerMinHeight, + /** 头部元素引用 */ + headerRef, + /** 内容头部元素引用 */ + contentHeaderRef, + /** 头部高度(响应式) */ + headerHeight, + /** 内容头部高度(响应式) */ + contentHeaderHeight + } +} diff --git a/src/hooks/core/useTable.ts b/src/hooks/core/useTable.ts new file mode 100644 index 0000000..bf06c4d --- /dev/null +++ b/src/hooks/core/useTable.ts @@ -0,0 +1,737 @@ +/** + * useTable - 企业级表格数据管理方案 + * + * 功能完整的表格数据管理解决方案,专为后台管理系统设计。 + * 封装了表格开发中的所有常见需求,让你专注于业务逻辑。 + * + * ## 主要功能 + * + * 1. 数据管理 - 自动处理 API 请求、响应转换、加载状态和错误处理 + * 2. 分页控制 - 自动同步分页状态、移动端适配、智能页码边界处理 + * 3. 搜索功能 - 防抖搜索优化、参数管理、一键重置、参数过滤 + * 4. 缓存系统 - 智能请求缓存、多种清理策略、自动过期管理、统计信息 + * 5. 刷新策略 - 提供 5 种刷新方法适配不同业务场景(新增/更新/删除/手动/定时) + * 6. 列配置管理 - 动态显示/隐藏列、列排序、配置持久化、批量操作(可选) + * + * @module useTable + * @author Art Design Pro Team + */ + +import { ref, reactive, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue' +import { useWindowSize } from '@vueuse/core' +import { useTableColumns } from './useTableColumns' +import type { ColumnOption } from '@/types/component' +import { + TableCache, + CacheInvalidationStrategy, + type ApiResponse +} from '../../utils/table/tableCache' +import { + type TableError, + defaultResponseAdapter, + extractTableData, + updatePaginationFromResponse, + createSmartDebounce, + createErrorHandler +} from '../../utils/table/tableUtils' +import { tableConfig } from '../../utils/table/tableConfig' + +// 类型推导工具类型 +type InferApiParams = T extends (params: infer P) => any ? P : never +type InferApiResponse = T extends (params: any) => Promise ? R : never +type InferRecordType = T extends Api.Common.PaginatedResponse ? U : never + +// 优化的配置接口 - 支持自动类型推导 +export interface UseTableConfig< + TApiFn extends (params: any) => Promise = (params: any) => Promise, + TRecord = InferRecordType>, + TParams = InferApiParams, + TResponse = InferApiResponse +> { + // 核心配置 + core: { + /** API 请求函数 */ + apiFn: TApiFn + /** 默认请求参数 */ + apiParams?: Partial + /** 排除 apiParams 中的属性 */ + excludeParams?: string[] + /** 是否立即加载数据 */ + immediate?: boolean + /** 列配置工厂函数 */ + columnsFactory?: () => ColumnOption[] + /** 自定义分页字段映射 */ + paginationKey?: { + /** 当前页码字段名,默认为 'current' */ + current?: string + /** 每页条数字段名,默认为 'size' */ + size?: string + } + } + + // 数据处理 + transform?: { + /** 数据转换函数 */ + dataTransformer?: (data: TRecord[]) => TRecord[] + /** 响应数据适配器 */ + responseAdapter?: (response: TResponse) => ApiResponse + } + + // 性能优化 + performance?: { + /** 是否启用缓存 */ + enableCache?: boolean + /** 缓存时间(毫秒) */ + cacheTime?: number + /** 防抖延迟时间(毫秒) */ + debounceTime?: number + /** 最大缓存条数限制 */ + maxCacheSize?: number + } + + // 生命周期钩子 + hooks?: { + /** 数据加载成功回调(仅网络请求成功时触发) */ + onSuccess?: (data: TRecord[], response: ApiResponse) => void + /** 错误处理回调 */ + onError?: (error: TableError) => void + /** 缓存命中回调(从缓存获取数据时触发) */ + onCacheHit?: (data: TRecord[], response: ApiResponse) => void + /** 加载状态变化回调 */ + onLoading?: (loading: boolean) => void + /** 重置表单回调函数 */ + resetFormCallback?: () => void + } + + // 调试配置 + debug?: { + /** 是否启用日志输出 */ + enableLog?: boolean + /** 日志级别 */ + logLevel?: 'info' | 'warn' | 'error' + } +} + +export function useTable< + TApiFn extends (params: any) => Promise, + TRecord = InferRecordType> +>(config: UseTableConfig) { + return useTableImpl(config) +} + +/** + * useTable 的核心实现 - 强大的表格数据管理 Hook + * + * 提供完整的表格解决方案,包括: + * - 数据获取与缓存 + * - 分页控制 + * - 搜索功能 + * - 智能刷新策略 + * - 错误处理 + * - 列配置管理 + */ +function useTableImpl< + TApiFn extends (params: any) => Promise, + TRecord = InferRecordType> +>(config: UseTableConfig) { + type TParams = InferApiParams + const { + core: { + apiFn, + apiParams = {} as Partial, + excludeParams = [], + immediate = true, + columnsFactory, + paginationKey + }, + transform: { dataTransformer, responseAdapter = defaultResponseAdapter } = {}, + performance: { + enableCache = false, + cacheTime = 5 * 60 * 1000, + debounceTime = 300, + maxCacheSize = 50 + } = {}, + hooks: { onSuccess, onError, onCacheHit, resetFormCallback } = {}, + debug: { enableLog = false } = {} + } = config + + // 分页字段名配置:优先使用传入的配置,否则使用全局配置 + const pageKey = paginationKey?.current || tableConfig.paginationKey.current + const sizeKey = paginationKey?.size || tableConfig.paginationKey.size + + // 响应式触发器,用于手动更新缓存统计信息 + const cacheUpdateTrigger = ref(0) + + // 日志工具函数 + const logger = { + log: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.log(`[useTable] ${message}`, ...args) + } + }, + warn: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.warn(`[useTable] ${message}`, ...args) + } + }, + error: (message: string, ...args: unknown[]) => { + if (enableLog) { + console.error(`[useTable] ${message}`, ...args) + } + } + } + + // 缓存实例 + const cache = enableCache ? new TableCache(cacheTime, maxCacheSize, enableLog) : null + + // 加载状态机 + type LoadingState = 'idle' | 'loading' | 'success' | 'error' + const loadingState = ref('idle') + const loading = computed(() => loadingState.value === 'loading') + + // 错误状态 + const error = ref(null) + + // 表格数据 + const data = ref([]) + + // 请求取消控制器 + let abortController: AbortController | null = null + + // 缓存清理定时器 + let cacheCleanupTimer: NodeJS.Timeout | null = null + + // 搜索参数 + const searchParams = reactive( + Object.assign( + { + [pageKey]: 1, + [sizeKey]: 10 + }, + apiParams || {} + ) as TParams + ) + + // 分页配置 + const pagination = reactive({ + current: ((searchParams as Record)[pageKey] as number) || 1, + size: ((searchParams as Record)[sizeKey] as number) || 10, + total: 0 + }) + + // 移动端分页 (响应式) + const { width } = useWindowSize() + const mobilePagination = computed(() => ({ + ...pagination, + small: width.value < 768 + })) + + // 列配置 + const columnConfig = columnsFactory ? useTableColumns(columnsFactory) : null + const columns = columnConfig?.columns + const columnChecks = columnConfig?.columnChecks + + // 是否有数据 + const hasData = computed(() => data.value.length > 0) + + // 缓存统计信息 + const cacheInfo = computed(() => { + // 依赖触发器,确保缓存变化时重新计算 + void cacheUpdateTrigger.value + if (!cache) return { total: 0, size: '0KB', hitRate: '0 avg hits' } + return cache.getStats() + }) + + // 错误处理函数 + const handleError = createErrorHandler(onError, enableLog) + + // 清理缓存,根据不同的业务场景选择性地清理缓存 + const clearCache = (strategy: CacheInvalidationStrategy, context?: string): void => { + if (!cache) return + + let clearedCount = 0 + + switch (strategy) { + case CacheInvalidationStrategy.CLEAR_ALL: + cache.clear() + logger.log(`清空所有缓存 - ${context || ''}`) + break + + case CacheInvalidationStrategy.CLEAR_CURRENT: + clearedCount = cache.clearCurrentSearch(searchParams) + logger.log(`清空当前搜索缓存 ${clearedCount} 条 - ${context || ''}`) + break + + case CacheInvalidationStrategy.CLEAR_PAGINATION: + clearedCount = cache.clearPagination() + logger.log(`清空分页缓存 ${clearedCount} 条 - ${context || ''}`) + break + + case CacheInvalidationStrategy.KEEP_ALL: + default: + logger.log(`保持缓存不变 - ${context || ''}`) + break + } + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + + // 获取数据的核心方法 + const fetchData = async ( + params?: Partial, + useCache = enableCache + ): Promise> => { + // 取消上一个请求 + if (abortController) { + abortController.abort() + } + + // 创建新的取消控制器 + const currentController = new AbortController() + abortController = currentController + + // 状态机:进入 loading 状态 + loadingState.value = 'loading' + error.value = null + + try { + let requestParams = Object.assign( + {}, + searchParams, + { + [pageKey]: pagination.current, + [sizeKey]: pagination.size + }, + params || {} + ) as TParams + + // 剔除不需要的参数 + if (excludeParams.length > 0) { + const filteredParams = { ...requestParams } + excludeParams.forEach((key) => { + delete (filteredParams as Record)[key] + }) + requestParams = filteredParams as TParams + } + + // 检查缓存 + if (useCache && cache) { + const cachedItem = cache.get(requestParams) + if (cachedItem) { + data.value = cachedItem.data + updatePaginationFromResponse(pagination, cachedItem.response) + + // 修复:避免重复设置相同的值,防止响应式循环更新 + const paramsRecord = searchParams as Record + if (paramsRecord[pageKey] !== pagination.current) { + paramsRecord[pageKey] = pagination.current + } + if (paramsRecord[sizeKey] !== pagination.size) { + paramsRecord[sizeKey] = pagination.size + } + + // 状态机:缓存命中,进入 success 状态 + loadingState.value = 'success' + + // 缓存命中时触发专门的回调,而不是 onSuccess + if (onCacheHit) { + onCacheHit(cachedItem.data, cachedItem.response) + } + + logger.log(`缓存命中`) + return cachedItem.response + } + } + + const response = await apiFn(requestParams) + + // 检查请求是否被取消 + if (currentController.signal.aborted) { + throw new Error('请求已取消') + } + + // 使用响应适配器转换为标准格式 + const standardResponse = responseAdapter(response) + + // 处理响应数据 + let tableData = extractTableData(standardResponse) + + // 应用数据转换函数 + if (dataTransformer) { + tableData = dataTransformer(tableData) + } + + // 更新状态 + data.value = tableData + updatePaginationFromResponse(pagination, standardResponse) + + // 修复:避免重复设置相同的值,防止响应式循环更新 + const paramsRecord = searchParams as Record + if (paramsRecord[pageKey] !== pagination.current) { + paramsRecord[pageKey] = pagination.current + } + if (paramsRecord[sizeKey] !== pagination.size) { + paramsRecord[sizeKey] = pagination.size + } + + // 缓存数据 + if (useCache && cache) { + cache.set(requestParams, tableData, standardResponse) + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + logger.log(`数据已缓存`) + } + + // 状态机:请求成功,进入 success 状态 + loadingState.value = 'success' + + // 成功回调 + if (onSuccess) { + onSuccess(tableData, standardResponse) + } + + return standardResponse + } catch (err) { + if (err instanceof Error && err.message === '请求已取消') { + // 请求被取消,回到 idle 状态 + loadingState.value = 'idle' + return { records: [], total: 0, current: 1, size: 10 } + } + + // 状态机:请求失败,进入 error 状态 + loadingState.value = 'error' + data.value = [] + const tableError = handleError(err, '获取表格数据失败') + throw tableError + } finally { + // 只有当前控制器是活跃的才清空 + if (abortController === currentController) { + abortController = null + } + } + } + + // 获取数据 (保持当前页) + const getData = async (params?: Partial): Promise | void> => { + try { + return await fetchData(params) + } catch { + // 错误已在 fetchData 中处理 + return Promise.resolve() + } + } + + // 分页获取数据 (重置到第一页) - 专门用于搜索场景 + const getDataByPage = async (params?: Partial): Promise | void> => { + pagination.current = 1 + ;(searchParams as Record)[pageKey] = 1 + + // 搜索时清空当前搜索条件的缓存,确保获取最新数据 + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') + + try { + return await fetchData(params, false) // 搜索时不使用缓存 + } catch { + // 错误已在 fetchData 中处理 + return Promise.resolve() + } + } + + // 智能防抖搜索函数 + const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime) + + // 重置搜索参数 + const resetSearchParams = async (): Promise => { + // 取消防抖的搜索 + debouncedGetDataByPage.cancel() + + // 保存分页相关的默认值 + const paramsRecord = searchParams as Record + const defaultPagination = { + [pageKey]: 1, + [sizeKey]: (paramsRecord[sizeKey] as number) || 10 + } + + // 清空所有搜索参数 + Object.keys(searchParams).forEach((key) => { + delete paramsRecord[key] + }) + + // 重新设置默认参数 + Object.assign(searchParams, apiParams || {}, defaultPagination) + + // 重置分页 + pagination.current = 1 + pagination.size = defaultPagination[sizeKey] as number + + // 清空错误状态 + error.value = null + + // 清空缓存 + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '重置搜索') + + // 重新获取数据 + await getData() + + // 执行重置回调 + if (resetFormCallback) { + await nextTick() + resetFormCallback() + } + } + + // 防重复调用的标志 + let isCurrentChanging = false + + // 处理分页大小变化 + const handleSizeChange = async (newSize: number): Promise => { + if (newSize <= 0) return + + debouncedGetDataByPage.cancel() + + const paramsRecord = searchParams as Record + pagination.size = newSize + pagination.current = 1 + paramsRecord[sizeKey] = newSize + paramsRecord[pageKey] = 1 + + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '分页大小变化') + + await getData() + } + + // 处理当前页变化 + const handleCurrentChange = async (newCurrent: number): Promise => { + if (newCurrent <= 0) return + + // 修复:防止重复调用 + if (isCurrentChanging) { + return + } + + // 修复:如果当前页没有变化,不需要重新请求 + if (pagination.current === newCurrent) { + logger.log('分页页码未变化,跳过请求') + return + } + + try { + isCurrentChanging = true + + // 修复:只更新必要的状态 + const paramsRecord = searchParams as Record + pagination.current = newCurrent + // 只有当 searchParams 的分页字段与新值不同时才更新 + if (paramsRecord[pageKey] !== newCurrent) { + paramsRecord[pageKey] = newCurrent + } + + await getData() + } finally { + isCurrentChanging = false + } + } + + // 针对不同业务场景的刷新方法 + + // 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) + const refreshCreate = async (): Promise => { + debouncedGetDataByPage.cancel() + pagination.current = 1 + ;(searchParams as Record)[pageKey] = 1 + clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') + await getData() + } + + // 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) + const refreshUpdate = async (): Promise => { + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '编辑数据') + await getData() + } + + // 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) + const refreshRemove = async (): Promise => { + const { current } = pagination + + // 清除缓存并获取最新数据 + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '删除数据') + await getData() + + // 如果当前页为空且不是第一页,回到上一页 + if (data.value.length === 0 && current > 1) { + pagination.current = current - 1 + ;(searchParams as Record)[pageKey] = current - 1 + await getData() + } + } + + // 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) + const refreshData = async (): Promise => { + debouncedGetDataByPage.cancel() + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') + await getData() + } + + // 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) + const refreshSoft = async (): Promise => { + clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '软刷新') + await getData() + } + + // 取消当前请求 + const cancelRequest = (): void => { + if (abortController) { + abortController.abort() + } + debouncedGetDataByPage.cancel() + } + + // 清空数据 + const clearData = (): void => { + data.value = [] + error.value = null + clearCache(CacheInvalidationStrategy.CLEAR_ALL, '清空数据') + } + + // 清理已过期的缓存条目,释放内存空间 + const clearExpiredCache = (): number => { + if (!cache) return 0 + const cleanedCount = cache.cleanupExpired() + if (cleanedCount > 0) { + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + return cleanedCount + } + + // 设置定期清理过期缓存 + if (enableCache && cache) { + cacheCleanupTimer = setInterval(() => { + const cleanedCount = cache.cleanupExpired() + if (cleanedCount > 0) { + logger.log(`自动清理 ${cleanedCount} 条过期缓存`) + // 手动触发缓存状态更新 + cacheUpdateTrigger.value++ + } + }, cacheTime / 2) // 每半个缓存周期清理一次 + } + + // 挂载时自动加载数据 + if (immediate) { + onMounted(async () => { + await getData() + }) + } + + // 组件卸载时彻底清理 + onUnmounted(() => { + cancelRequest() + if (cache) { + cache.clear() + } + if (cacheCleanupTimer) { + clearInterval(cacheCleanupTimer) + } + }) + + // 优化的返回值结构 + return { + // 数据相关 + /** 表格数据 */ + data, + /** 数据加载状态 */ + loading: readonly(loading), + /** 错误状态 */ + error: readonly(error), + /** 数据是否为空 */ + isEmpty: computed(() => data.value.length === 0), + /** 是否有数据 */ + hasData, + + // 分页相关 + /** 分页状态信息 */ + pagination: readonly(pagination), + /** 移动端分页配置 */ + paginationMobile: mobilePagination, + /** 页面大小变化处理 */ + handleSizeChange, + /** 当前页变化处理 */ + handleCurrentChange, + + // 搜索相关 - 统一前缀 + /** 搜索参数 */ + searchParams, + /** 重置搜索参数 */ + resetSearchParams, + + // 数据操作 - 更明确的操作意图 + /** 加载数据 */ + fetchData: getData, + /** 获取数据 */ + getData: getDataByPage, + /** 获取数据(防抖) */ + getDataDebounced: debouncedGetDataByPage, + /** 清空数据 */ + clearData, + + // 刷新策略 + /** 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) */ + refreshData, + /** 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) */ + refreshSoft, + /** 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) */ + refreshCreate, + /** 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) */ + refreshUpdate, + /** 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) */ + refreshRemove, + + // 缓存控制 + /** 缓存统计信息 */ + cacheInfo, + /** 清除缓存,根据不同的业务场景选择性地清理缓存: */ + clearCache, + // 支持4种清理策略 + // clearCache(CacheInvalidationStrategy.CLEAR_ALL, '手动刷新') // 清空所有缓存 + // clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '搜索数据') // 只清空当前搜索条件的缓存 + // clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '新增数据') // 清空分页相关缓存 + // clearCache(CacheInvalidationStrategy.KEEP_ALL, '保持缓存') // 不清理任何缓存 + /** 清理已过期的缓存条目,释放内存空间 */ + clearExpiredCache, + + // 请求控制 + /** 取消当前请求 */ + cancelRequest, + + // 列配置 (如果提供了 columnsFactory) + ...(columnConfig && { + /** 表格列配置 */ + columns, + /** 列显示控制 */ + columnChecks, + /** 新增列 */ + addColumn: columnConfig.addColumn, + /** 删除列 */ + removeColumn: columnConfig.removeColumn, + /** 切换列显示状态 */ + toggleColumn: columnConfig.toggleColumn, + /** 更新列配置 */ + updateColumn: columnConfig.updateColumn, + /** 批量更新列配置 */ + batchUpdateColumns: columnConfig.batchUpdateColumns, + /** 重新排序列 */ + reorderColumns: columnConfig.reorderColumns, + /** 获取指定列配置 */ + getColumnConfig: columnConfig.getColumnConfig, + /** 获取所有列配置 */ + getAllColumns: columnConfig.getAllColumns, + /** 重置所有列配置到默认状态 */ + resetColumns: columnConfig.resetColumns + }) + } +} + +// 重新导出类型和枚举,方便使用 +export { CacheInvalidationStrategy } from '../../utils/table/tableCache' +export type { ApiResponse, CacheItem } from '../../utils/table/tableCache' +export type { BaseRequestParams, TableError } from '../../utils/table/tableUtils' diff --git a/src/hooks/core/useTableColumns.ts b/src/hooks/core/useTableColumns.ts new file mode 100644 index 0000000..84b6e13 --- /dev/null +++ b/src/hooks/core/useTableColumns.ts @@ -0,0 +1,312 @@ +/** + * useTableColumns - 表格列配置管理 + * + * 提供动态的表格列配置管理能力,支持运行时灵活控制列的显示、隐藏、排序等操作。 + * 通常与 useTable 配合使用,为表格提供完整的列管理功能。 + * + * ## 主要功能 + * + * 1. 列显示控制 - 动态显示/隐藏列,支持批量操作 + * 2. 列排序 - 拖拽或编程方式重新排列列顺序 + * 3. 列配置管理 - 新增、删除、更新列配置 + * 4. 特殊列支持 - 自动处理 selection、expand、index 等特殊列 + * 5. 状态持久化 - 保持列的显示状态,支持重置到初始状态 + * + * ## 使用示例 + * + * ```typescript + * const { columns, columnChecks, toggleColumn, reorderColumns } = useTableColumns(() => [ + * { prop: 'name', label: '姓名', visible: true }, + * { prop: 'email', label: '邮箱', visible: true }, + * { prop: 'status', label: '状态', visible: false } + * ]) + * + * // 切换列显示 + * toggleColumn('email', false) + * + * // 重新排序 + * reorderColumns(0, 2) + * ``` + * + * @module useTableColumns + * @author Art Design Pro Team + */ + +import { ref, computed, watch } from 'vue' +import { $t } from '@/locales' +import type { ColumnOption } from '@/types/component' + +/** + * 特殊列类型 + */ +const SPECIAL_COLUMNS: Record = { + selection: { prop: '__selection__', label: $t('table.column.selection') }, + expand: { prop: '__expand__', label: $t('table.column.expand') }, + index: { prop: '__index__', label: $t('table.column.index') } +} + +/** + * 获取列的唯一标识 + */ +export const getColumnKey = (col: ColumnOption) => + SPECIAL_COLUMNS[col.type as keyof typeof SPECIAL_COLUMNS]?.prop ?? (col.prop as string) + +/** + * 获取列的显示状态 + * 优先使用 visible 字段,如果不存在则使用 checked 字段 + */ +export const getColumnVisibility = (col: ColumnOption): boolean => { + // visible 优先级高于 checked + if (col.visible !== undefined) { + return col.visible + } + // 如果 visible 未定义,使用 checked,默认为 true + return col.checked ?? true +} + +/** + * 获取列的检查状态 + */ +export const getColumnChecks = (columns: ColumnOption[]) => + columns.map((col) => { + const special = col.type && SPECIAL_COLUMNS[col.type] + const visibility = getColumnVisibility(col) + + if (special) { + return { ...col, prop: special.prop, label: special.label, checked: true, visible: true } + } + return { ...col, checked: visibility, visible: visibility } + }) + +/** + * 动态列配置接口 + */ +export interface DynamicColumnConfig { + /** + * 新增列(支持单个或批量) + * @param column 列配置或列配置数组 + * @param index 可选的插入位置,默认末尾(批量时为第一个列的位置) + */ + addColumn: (column: ColumnOption | ColumnOption[], index?: number) => void + /** + * 删除列(支持单个或批量) + * @param prop 列的唯一标识或标识数组 + */ + removeColumn: (prop: string | string[]) => void + /** + * 切换列显示状态(支持单个或批量) + * @param prop 列的唯一标识或标识数组 + * @param visible 可选的显示状态,默认取反 + */ + toggleColumn: (prop: string | string[], visible?: boolean) => void + + /** + * 更新列(支持单个或批量) + * @param prop 列的唯一标识或更新配置数组 + * @param updates 列配置更新(当 prop 为字符串时使用) + */ + updateColumn: ( + prop: string | Array<{ prop: string; updates: Partial> }>, + updates?: Partial> + ) => void + /** + * 批量更新列(兼容旧版本,推荐使用 updateColumn 的数组模式) + * @param updates 列更新配置 + * @deprecated 推荐使用 updateColumn 的数组模式 + */ + batchUpdateColumns: (updates: Array<{ prop: string; updates: Partial> }>) => void + /** + * 重新排序列 + * @param fromIndex 源索引 + * @param toIndex 目标索引 + */ + reorderColumns: (fromIndex: number, toIndex: number) => void + /** + * 获取列配置 + * @param prop 列的唯一标识 + * @returns 列配置 + */ + getColumnConfig: (prop: string) => ColumnOption | undefined + /** + * 获取所有列配置 + * @returns 所有列配置 + */ + getAllColumns: () => ColumnOption[] + /** + * 重置所有列 + */ + resetColumns: () => void +} + +export function useTableColumns( + columnsFactory: () => ColumnOption[] +): { + columns: any + columnChecks: any +} & DynamicColumnConfig { + const dynamicColumns = ref[]>(columnsFactory()) + const columnChecks = ref[]>(getColumnChecks(dynamicColumns.value)) + + // 当 dynamicColumns 变动时,重新生成 columnChecks 且保留已存在的显示状态 + watch( + dynamicColumns, + (newCols) => { + const visibilityMap = new Map( + columnChecks.value.map((c) => [getColumnKey(c), getColumnVisibility(c)]) + ) + const newChecks = getColumnChecks(newCols).map((c) => { + const key = getColumnKey(c) + const visibility = visibilityMap.has(key) ? visibilityMap.get(key) : getColumnVisibility(c) + return { + ...c, + checked: visibility, + visible: visibility + } + }) + columnChecks.value = newChecks + }, + { deep: true } + ) + + // 当前显示列(基于 columnChecks 的 checked 或 visible) + const columns = computed(() => { + const colMap = new Map(dynamicColumns.value.map((c) => [getColumnKey(c), c])) + return columnChecks.value + .filter((c) => getColumnVisibility(c)) + .map((c) => colMap.get(getColumnKey(c))) + .filter(Boolean) as ColumnOption[] + }) + + // 支持 updater 返回新数组或直接在传入数组上 mutate + const setDynamicColumns = (updater: (cols: ColumnOption[]) => void | ColumnOption[]) => { + const copy = [...dynamicColumns.value] + const result = updater(copy) + dynamicColumns.value = Array.isArray(result) ? result : copy + } + + return { + columns, + columnChecks, + + /** + * 新增列(支持单个或批量) + */ + addColumn: (column: ColumnOption | ColumnOption[], index?: number) => + setDynamicColumns((cols) => { + const next = [...cols] + const columnsToAdd = Array.isArray(column) ? column : [column] + const insertIndex = + typeof index === 'number' && index >= 0 && index <= next.length ? index : next.length + + // 批量插入 + next.splice(insertIndex, 0, ...columnsToAdd) + return next + }), + + /** + * 删除列(支持单个或批量) + */ + removeColumn: (prop: string | string[]) => + setDynamicColumns((cols) => { + const propsToRemove = Array.isArray(prop) ? prop : [prop] + return cols.filter((c) => !propsToRemove.includes(getColumnKey(c))) + }), + + /** + * 更新列(支持单个或批量) + */ + updateColumn: ( + prop: string | Array<{ prop: string; updates: Partial> }>, + updates?: Partial> + ) => { + // 批量模式:prop 是数组 + if (Array.isArray(prop)) { + setDynamicColumns((cols) => { + const map = new Map(prop.map((u) => [u.prop, u.updates])) + return cols.map((c) => { + const key = getColumnKey(c) + const upd = map.get(key) + return upd ? { ...c, ...upd } : c + }) + }) + } + // 单个模式:prop 是字符串 + else if (updates) { + setDynamicColumns((cols) => + cols.map((c) => (getColumnKey(c) === prop ? { ...c, ...updates } : c)) + ) + } + }, + + /** + * 切换列显示状态(支持单个或批量) + */ + toggleColumn: (prop: string | string[], visible?: boolean) => { + const propsToToggle = Array.isArray(prop) ? prop : [prop] + const next = [...columnChecks.value] + + propsToToggle.forEach((p) => { + const i = next.findIndex((c) => getColumnKey(c) === p) + if (i > -1) { + const currentVisibility = getColumnVisibility(next[i]) + const newVisibility = visible ?? !currentVisibility + // 同时更新 checked 和 visible 以保持兼容性 + next[i] = { ...next[i], checked: newVisibility, visible: newVisibility } + } + }) + + columnChecks.value = next + }, + + /** + * 重置所有列 + */ + resetColumns: () => { + dynamicColumns.value = columnsFactory() + }, + + /** + * 批量更新列(兼容旧版本) + * @deprecated 推荐使用 updateColumn 的数组模式 + */ + batchUpdateColumns: (updates) => + setDynamicColumns((cols) => { + const map = new Map(updates.map((u) => [u.prop, u.updates])) + return cols.map((c) => { + const key = getColumnKey(c) + const upd = map.get(key) + return upd ? { ...c, ...upd } : c + }) + }), + + /** + * 重新排序列 + */ + reorderColumns: (fromIndex: number, toIndex: number) => + setDynamicColumns((cols) => { + if ( + fromIndex < 0 || + fromIndex >= cols.length || + toIndex < 0 || + toIndex >= cols.length || + fromIndex === toIndex + ) { + return cols + } + const next = [...cols] + const [moved] = next.splice(fromIndex, 1) + next.splice(toIndex, 0, moved) + return next + }), + + /** + * 获取列配置 + */ + getColumnConfig: (prop: string) => dynamicColumns.value.find((c) => getColumnKey(c) === prop), + + /** + * 获取所有列配置 + */ + getAllColumns: () => [...dynamicColumns.value] + } +} diff --git a/src/hooks/core/useTableHeight.ts b/src/hooks/core/useTableHeight.ts new file mode 100644 index 0000000..8fdf6da --- /dev/null +++ b/src/hooks/core/useTableHeight.ts @@ -0,0 +1,105 @@ +/** + * useTableHeight - 表格高度自动计算 + * + * 自动计算表格容器的最佳高度,确保表格在不同布局场景下都能正确显示。 + * 根据表格头部、分页器等元素的高度动态调整容器高度,避免出现滚动条或布局错乱。 + * + * ## 主要功能 + * + * 1. 动态高度计算 - 根据表格头部、分页器高度自动计算容器高度 + * 2. 响应式更新 - 配置变化时自动重新计算高度 + * 3. 灵活配置 - 支持自定义各部分高度和间距 + * 4. 智能适配 - 无额外元素时自动使用 100% 高度 + * + * @module useTableHeight + * @author Art Design Pro Team + */ + +import { computed, type Ref } from 'vue' + +/** + * 表格高度计算器配置接口 + */ +interface TableHeightOptions { + /** 是否显示表格头部 */ + showTableHeader: Ref + /** 分页器高度 */ + paginationHeight: Ref + /** 表格头部高度 */ + tableHeaderHeight: Ref + /** 分页器间距 */ + paginationSpacing: Ref +} + +/** + * 表格高度计算器类 + */ +class TableHeightCalculator { + // 常量配置 + private static readonly DEFAULT_TABLE_HEADER_HEIGHT = 44 + private static readonly TABLE_HEADER_SPACING = 12 + + constructor(private options: TableHeightOptions) {} + + /** + * 计算容器高度 + */ + calculate(): { height: string } { + const offset = this.calculateOffset() + return { + height: offset === 0 ? '100%' : `calc(100% - ${offset}px)` + } + } + + /** + * 计算偏移量 + */ + private calculateOffset(): number { + if (!this.options.showTableHeader.value) { + return this.calculatePaginationOffset() + } + + const headerHeight = this.getHeaderHeight() + const paginationOffset = this.calculatePaginationOffset() + + return headerHeight + paginationOffset + TableHeightCalculator.TABLE_HEADER_SPACING + } + + /** + * 获取表格头部高度 + */ + private getHeaderHeight(): number { + return this.options.tableHeaderHeight.value || TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT + } + + /** + * 计算分页器偏移量 + */ + private calculatePaginationOffset(): number { + const { paginationHeight, paginationSpacing } = this.options + return paginationHeight.value === 0 ? 0 : paginationHeight.value + paginationSpacing.value + } +} + +/** + * 表格高度计算 Hook + * + * 提供表格容器高度的自动计算功能,支持: + * - 表格头部高度 + * - 分页器高度 + * - 动态间距计算 + * + * @param options 配置选项 + * @returns 容器高度计算结果 + */ +export function useTableHeight(options: TableHeightOptions) { + const containerHeight = computed(() => { + const calculator = new TableHeightCalculator(options) + return calculator.calculate() + }) + + return { + /** 容器高度样式对象 */ + containerHeight + } +} diff --git a/src/hooks/core/useTheme.ts b/src/hooks/core/useTheme.ts new file mode 100644 index 0000000..187c3e0 --- /dev/null +++ b/src/hooks/core/useTheme.ts @@ -0,0 +1,174 @@ +/** + * useTheme - 系统主题管理 + * + * 提供完整的主题切换和管理功能,支持亮色、暗色和自动模式。 + * 自动处理主题切换时的过渡效果,确保切换流畅无闪烁。 + * + * ## 主要功能 + * + * 1. 主题切换 - 支持亮色、暗色、自动三种主题模式 + * 2. 自动模式 - 根据系统偏好自动切换主题 + * 3. 颜色适配 - 自动调整主题色的明暗变体(9 个层级) + * 4. 过渡优化 - 切换时临时禁用过渡效果,避免闪烁 + * 5. 状态持久化 - 主题设置自动保存到 store + * + * ## 使用示例 + * + * ```typescript + * const { switchThemeStyles } = useTheme() + * + * // 切换到暗色主题 + * switchThemeStyles(SystemThemeEnum.DARK) + * + * // 切换到亮色主题 + * switchThemeStyles(SystemThemeEnum.LIGHT) + * + * // 切换到自动模式(跟随系统) + * switchThemeStyles(SystemThemeEnum.AUTO) + * ``` + * + * @module useTheme + * @author Art Design Pro Team + */ + +import { useSettingStore } from '@/store/modules/setting' +import { SystemThemeEnum } from '@/enums/appEnum' +import AppConfig from '@/config' +import { SystemThemeTypes } from '@/types/store' +import { getDarkColor, getLightColor, setElementThemeColor } from '@/utils/ui' +import { usePreferredDark } from '@vueuse/core' +import { watch } from 'vue' + +export function useTheme() { + const settingStore = useSettingStore() + + // 禁用过渡效果 + const disableTransitions = () => { + const style = document.createElement('style') + style.setAttribute('id', 'disable-transitions') + style.textContent = '* { transition: none !important; }' + document.head.appendChild(style) + } + + // 启用过渡效果 + const enableTransitions = () => { + const style = document.getElementById('disable-transitions') + if (style) { + style.remove() + } + } + + // 设置系统主题 + const setSystemTheme = (theme: SystemThemeEnum, themeMode?: SystemThemeEnum) => { + // 临时禁用过渡效果 + disableTransitions() + + const el = document.getElementsByTagName('html')[0] + const isDark = theme === SystemThemeEnum.DARK + + if (!themeMode) { + themeMode = theme + } + + const currentTheme = AppConfig.systemThemeStyles[theme as keyof SystemThemeTypes] + + if (currentTheme) { + el.setAttribute('class', currentTheme.className) + } + + // 设置按钮颜色加深或变浅 + const primary = settingStore.systemThemeColor + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-light-${i}`, + isDark ? `${getDarkColor(primary, i / 10)}` : `${getLightColor(primary, i / 10)}` + ) + } + + // 更新store中的主题设置 + settingStore.setGlopTheme(theme, themeMode) + + // 使用 requestAnimationFrame 确保在下一帧恢复过渡效果 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + enableTransitions() + }) + }) + } + + // 使用 VueUse 的 usePreferredDark 检测系统主题偏好 + const prefersDark = usePreferredDark() + + // 自动设置系统主题 + const setSystemAutoTheme = () => { + const theme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT + setSystemTheme(theme, SystemThemeEnum.AUTO) + } + + // 切换主题 + const switchThemeStyles = (theme: SystemThemeEnum) => { + if (theme === SystemThemeEnum.AUTO) { + setSystemAutoTheme() + } else { + setSystemTheme(theme) + } + } + + return { + setSystemTheme, + setSystemAutoTheme, + switchThemeStyles, + prefersDark + } +} + +/** + * 初始化主题系统 + */ +export function initializeTheme() { + const settingStore = useSettingStore() + const prefersDark = usePreferredDark() + + // 根据系统偏好应用主题 + const applyThemeByMode = () => { + const el = document.getElementsByTagName('html')[0] + let actualTheme = settingStore.systemThemeType + + // 如果是 AUTO 模式,检测系统偏好 + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + actualTheme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT + // 更新实际应用的主题类型 + settingStore.systemThemeType = actualTheme + } + + // 设置主题 class + const currentTheme = AppConfig.systemThemeStyles[actualTheme as keyof SystemThemeTypes] + if (currentTheme) { + el.setAttribute('class', currentTheme.className) + } + + // 设置主题颜色 + setElementThemeColor(settingStore.systemThemeColor) + + // 设置圆角 + document.documentElement.style.setProperty('--custom-radius', `${settingStore.customRadius}rem`) + } + + // 应用主题 + applyThemeByMode() + + // 如果是 AUTO 模式,监听系统主题变化(使用 VueUse 的响应式特性) + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + watch( + prefersDark, + () => { + // 只有在 AUTO 模式下才响应系统主题变化 + if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) { + applyThemeByMode() + } + }, + { immediate: false } + ) + } +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..472b09c --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,32 @@ +// 通用功能集合 +export { useCommon } from './core/useCommon' + +// 应用模式 +export { useAppMode } from './core/useAppMode' + +// 权限控制 +export { useAuth } from './core/useAuth' + +// 表格数据管理方案 +export { useTable } from './core/useTable' + +// 表格列配置管理 +export { useTableColumns } from './core/useTableColumns' + +// 主题相关 +export { useTheme } from './core/useTheme' + +// 礼花+文字滚动 +export { useCeremony } from './core/useCeremony' + +// 顶栏快速入口 +export { useFastEnter } from './core/useFastEnter' + +// 顶栏功能管理 +export { useHeaderBar } from './core/useHeaderBar' + +// 图表相关 +export { useChart, useChartComponent, useChartOps } from './core/useChart' + +// 布局高度 +export { useLayoutHeight, useAutoLayoutHeight } from './core/useLayoutHeight' diff --git a/src/locales/en-US/billing.json b/src/locales/en-US/billing.json new file mode 100644 index 0000000..1545b9e --- /dev/null +++ b/src/locales/en-US/billing.json @@ -0,0 +1,244 @@ +{ + "table": { + "column": { + "action": "Action" + } + }, + "billing": { + "quickFilter": { + "title": "Quick Filters", + "all": "All", + "pending": "Pending", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant (optional)", + "billingType": "Bill Type", + "billingTypePlaceholder": "Select bill type", + "status": "Status", + "statusPlaceholder": "Select status", + "dateRange": "Date range", + "dateRangePlaceholder": "Select date range", + "amountRange": "Amount Range", + "minAmountPlaceholder": "Min amount", + "maxAmountPlaceholder": "Max amount", + "keyword": "Keyword", + "keywordPlaceholder": "Statement no / tenant name" + }, + "action": { + "export": "Export", + "exportPdf": "Export PDF", + "create": "Create", + "detail": "Detail", + "cancel": "Cancel", + "recordPayment": "Confirm Payment", + "verifyPayment": "Verify Payment", + "addLineItem": "Add line item", + "uploadProof": "Upload proof" + }, + "dialog": { + "createTitle": "Create Bill", + "recordPaymentTitle": "Confirm Payment", + "proofPreviewTitle": "Proof Preview" + }, + "field": { + "statementNo": "Statement No", + "tenantName": "Tenant", + "billingType": "Bill Type", + "amount": "Amount", + "amountDue": "Amount Due", + "amountPaid": "Amount Paid", + "status": "Status", + "dueDate": "Due date", + "createdAt": "Created at", + "periodStart": "Period start", + "periodEnd": "Period end", + "notes": "Notes" + }, + "drawer": { + "tabs": { + "basic": "Basic", + "lineItems": "Line Items", + "payments": "Payments", + "statusFlow": "Status Flow" + }, + "title": "Bill Detail", + "basicInfo": "Basic info", + "paymentList": "Payment records", + "openProof": "Open proof", + "noPayments": "No payment records", + "noStatusFlow": "No status flow records" + }, + "view": { + "timeline": "Timeline", + "table": "Table" + }, + "status": { + "draft": "Draft", + "pending": "Pending", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "statusFlow": { + "created": "Created", + "due": "Due", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "lineItem": { + "itemType": "Type", + "description": "Description", + "quantity": "Qty", + "unitPrice": "Unit Price", + "amount": "Amount" + }, + "billingType": { + "subscription": "Subscription", + "quotaPurchase": "Quota Purchase", + "manual": "Manual", + "renewal": "Renewal" + }, + "placeholder": { + "enterItemType": "Enter type", + "enterDescription": "Enter description", + "selectTenant": "Select tenant", + "selectBillingType": "Select bill type", + "selectDueDate": "Select due date", + "enterNotes": "Enter notes (optional)", + "selectPaymentMethod": "Select payment method", + "enterPaymentAmount": "Enter payment amount", + "enterTransactionNo": "Enter transaction no (optional)", + "enterPaymentNotes": "Enter notes (optional)" + }, + "hint": { + "amountAutoSum": "Amount is auto-calculated from line items" + }, + "validation": { + "tenantRequired": "Please select tenant", + "billingTypeRequired": "Please select bill type", + "dueDateRequired": "Please select due date", + "lineItemsRequired": "Please add at least one line item", + "lineItemDescriptionRequired": "Line item #{index}: description is required", + "lineItemQuantityInvalid": "Line item #{index}: quantity is invalid", + "lineItemUnitPriceInvalid": "Line item #{index}: unit price is invalid", + "amountMin": "Amount due must be greater than 0", + "paymentAmountRequired": "Please enter payment amount", + "paymentAmountMin": "Payment amount must be greater than 0", + "paymentAmountExceedRemain": "Payment amount cannot exceed remaining amount", + "paymentMethodRequired": "Please select payment method" + }, + "message": { + "exportFailed": "Export failed, please retry", + "exportNoData": "No data to export", + "exportSuccess": "Export successful", + "exportExcelSuccess": "Exported {count} rows", + "exportPdfNeedSingleSelection": "Export PDF supports single selection only", + "exportPdfOpened": "PDF opened in a new window", + "exportPdfPopupBlocked": "Popup blocked by browser, please allow popups and retry", + "batchMarkPaidSuccess": "Batch marked as paid", + "batchMarkPaidNotes": "Batch marked as paid", + "batchNoPending": "No pending bills selected", + "batchCancelNotes": "Batch cancelled", + "batchCancelSuccess": "Batch cancelled successfully", + "cancelByAdmin": "Cancelled by admin", + "cancelConfirm": "Cancel this bill?", + "cancelSuccess": "Cancelled", + "createSuccess": "Created successfully", + "loadDetailFailed": "Failed to load bill detail", + "recordPaymentSuccess": "Payment confirmed", + "verifyPaymentSuccess": "Payment verified", + "verifyPaymentConfirm": "Are you sure to verify this payment? After verification, the paid amount of the bill will be updated accordingly.", + "proofTypeNotAllowed": "Only JPG/PNG images are allowed", + "proofTooLarge": "File is too large (max {size}MB)", + "proofUploadSuccess": "Proof uploaded", + "sortFieldNotSupported": "Sort field is not supported", + "generatingPdf": "Generating PDF, please wait...", + "exportCancelled": "Export cancelled" + }, + "batch": { + "confirmPayment": "Confirm Payment", + "confirmPaymentTip": "Mark selected pending bills as paid", + "cancel": "Batch Cancel", + "cancelTip": "Cancel selected bills (irreversible)", + "export": "Batch Export", + "selectedCount": "Selected {count}" + }, + "payment": { + "amount": "Amount", + "remainAmount": "Remaining", + "method": "Method", + "transactionNo": "Transaction No", + "proofUrl": "Proof", + "proofUploadHint": "JPG/PNG, single file ≤ 10MB", + "paidAt": "Paid at", + "notes": "Notes", + "status": "Status" + }, + "paymentMethod": { + "online": "Online", + "bankTransfer": "Bank transfer", + "other": "Other" + }, + "paymentStatus": { + "pending": "Pending", + "success": "Success", + "failed": "Failed", + "refunded": "Refunded" + }, + "export": { + "title": "Export Bills", + "format": "Format", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "scope": "Scope", + "currentPage": "Current page", + "selected": "Selected", + "all": "All", + "fields": "Fields", + "dateRange": "Date range", + "dateRangePlaceholder": "Select date range", + "confirm": "Export", + "sheetName": "Bills", + "fileName": "billing_export", + "noPayments": "No payment records", + "formatRequired": "Please select format", + "scopeRequired": "Please select scope", + "fieldsRequired": "Please select at least one field" + }, + "statistics": { + "title": "Billing Statistics", + "startDate": "Start date", + "endDate": "End date", + "groupBy": { + "day": "Daily", + "week": "Weekly", + "month": "Monthly" + }, + "totalRevenue": "Collected", + "pendingAmount": "Uncollected", + "overdueAmount": "Overdue", + "totalAmountDue": "Amount Due", + "statusDistribution": "Status Distribution", + "paymentMethodDistribution": "Payment Method Share", + "paymentMethodNoData": "No payment method data", + "revenueTrend": "Revenue Trend", + "topDebtors": "Top Debtors", + "overdueDays": "Overdue Days" + }, + "timeline": { + "created": "Created", + "dueDate": "Due date", + "overdueByDays": "Overdue by {days} days", + "payment": "Payment", + "paymentDesc": "{method} ${amount}", + "currentStatus": "Current status: {status}", + "currentStatusDesc": "Current status is authoritative" + } + } +} diff --git a/src/locales/en/announcement.ts b/src/locales/en/announcement.ts new file mode 100644 index 0000000..3a336cf --- /dev/null +++ b/src/locales/en/announcement.ts @@ -0,0 +1,155 @@ +/** + * Announcement module i18n (en) + * + * Note: This file is merged into `announcement.*` namespace at i18n bootstrap. + */ +export default { + common: { + empty: '-' + }, + create: { + title: 'Create Platform Announcement' + }, + edit: { + title: 'Edit Platform Announcement', + notEditable: 'This announcement is published or revoked and cannot be edited.', + readonlyFieldsHint: + 'Current API only supports editing title, content and audience. Other fields are read-only.' + }, + list: { + total: '{total} announcement(s)' + }, + action: { + create: 'Create', + refresh: 'Refresh', + detail: 'Detail', + edit: 'Edit', + publish: 'Publish', + revoke: 'Revoke', + delete: 'Delete', + save: 'Save', + back: 'Back' + }, + search: { + status: 'Status', + statusPlaceholder: 'All statuses', + keyword: 'Keyword', + keywordPlaceholder: 'Title/Content', + dateRange: 'Date Range', + dateRangeStart: 'Start date', + dateRangeEnd: 'End date' + }, + table: { + title: 'Title', + type: 'Type', + priority: 'Priority', + status: 'Status', + effectiveRange: 'Effective Range', + publishedAt: 'Published At', + actions: 'Actions', + effectiveOpenEnded: 'Open-ended' + }, + form: { + title: 'Title', + titlePlaceholder: 'Enter announcement title', + type: 'Type', + typePlaceholder: 'Select announcement type', + priority: 'Priority', + effectiveFrom: 'Effective From', + effectiveFromPlaceholder: 'Select start time', + effectiveTo: 'Effective To', + effectiveToPlaceholder: 'Select end time (optional)', + target: 'Audience', + targetTypePlaceholder: 'Select audience type', + content: 'Content' + }, + status: { + draft: 'Draft', + published: 'Published', + revoked: 'Revoked', + unknown: 'Unknown' + }, + type: { + system: 'System', + billing: 'Billing/Subscription', + operation: 'Operations', + platformUpdate: 'Platform Update', + security: 'Security Notice', + compliance: 'Compliance Notice', + tenantInternal: 'Tenant Internal', + tenantFinance: 'Tenant Finance', + tenantOperation: 'Tenant Operations', + unknown: 'Unknown Type' + }, + targetType: { + all: 'All Users', + rules: 'Rules', + roles: 'Roles', + users: 'Users', + manual: 'Manual' + }, + audience: { + placeholder: 'Audience selector will be integrated in a later release.', + rulesPlaceholder: 'Enter rules JSON (e.g. {"roles":["roleId"]})', + usersPlaceholder: 'Enter user IDs separated by commas' + }, + draft: { + notSaved: 'Not saved', + savedAt: 'Saved · {time}', + readOnly: 'Read-only state, auto-save stopped' + }, + validation: { + titleRequired: 'Please enter a title', + titleMax: 'Title cannot exceed 128 characters', + contentRequired: 'Please enter content', + typeRequired: 'Please select a type', + priorityRequired: 'Please enter priority', + priorityRange: 'Priority must be between 1 and 5', + effectiveFromRequired: 'Please select effective start time', + effectiveToAfter: 'End time must be later than start time', + targetTypeRequired: 'Please select audience type', + targetRulesRequired: 'Please complete rule conditions', + targetUsersRequired: 'Please select at least one user', + targetRulesInvalid: 'Invalid rules JSON format' + }, + message: { + loadFailed: 'Failed to load announcement', + createSuccess: 'Created successfully', + createFailed: 'Create failed, please retry', + updateSuccess: 'Updated successfully', + updateFailed: 'Update failed, please retry', + publishConfirm: 'Publish this announcement?', + publishSuccess: 'Published successfully', + publishFailed: 'Publish failed, please retry', + revokeConfirm: 'Revoke this announcement?', + revokeSuccess: 'Revoked successfully', + revokeFailed: 'Revoke failed, please retry', + deleteNotSupported: 'Delete is not supported for platform announcements', + rowVersionMissing: 'Missing row version, operation cancelled', + validationFailed: 'Please complete required fields', + missingId: 'Missing announcement ID', + targetParseFailed: 'Failed to parse audience parameters', + concurrencyConflict: 'Concurrency conflict: {message}', + refreshAndRetry: 'Data has been modified by another user, please refresh and retry', + cannotEditPublished: 'Cannot edit announcement with status {status}', + invalidId: 'Invalid announcement ID' + }, + detail: { + id: 'Announcement ID', + status: 'Status', + type: 'Type', + priority: 'Priority', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + publishedAt: 'Published At', + revokedAt: 'Revoked At', + targetType: 'Audience', + targetParameters: 'Audience Parameters', + publisherScope: 'Publisher Scope', + content: 'Content', + contentPlaceholder: 'No content', + contentHint: 'Rich text preview will be integrated with a safe renderer (DOMPurify).', + readStats: 'Read Stats', + readStatsPlaceholder: 'Read stats API is not integrated yet.' + } +} diff --git a/src/locales/en/dictionary.json b/src/locales/en/dictionary.json new file mode 100644 index 0000000..28b4d16 --- /dev/null +++ b/src/locales/en/dictionary.json @@ -0,0 +1,196 @@ +{ + "dictionary": { + "common": { + "system": "System", + "business": "Business", + "refresh": "Refresh", + "warning": "Warning", + "confirm": "Confirm", + "cancel": "Cancel", + "edit": "Edit", + "delete": "Delete", + "actions": "Actions", + "key": "Key", + "value": "Value", + "enabled": "Enabled", + "description": "Description", + "default": "Default", + "sortOrder": "Sort Order", + "code": "Code", + "name": "Name", + "scope": "Scope", + "order": "Order", + "hidden": "Hidden", + "source": "Source", + "tenant": "Tenant", + "systemLabel": "System", + "selectGroupFirst": "Select a group first" + }, + "group": { + "searchPlaceholder": "Search by code or name", + "new": "New Group", + "createTitle": "Create Dictionary Group", + "editTitle": "Edit Dictionary Group", + "codePlaceholder": "e.g. ORDER_STATUS", + "namePlaceholder": "Dictionary group name", + "scopePlaceholder": "Scope", + "allowOverride": "Allow Override", + "enabled": "Enabled", + "description": "Description", + "empty": "No groups found.", + "deleteConfirm": "Deleting a group also removes all its items. Continue?", + "deleteSuccess": "Deleted", + "rowVersionMissing": "Row version is missing. Please refresh.", + "codeRequired": "Code is required", + "codeLength": "Length {min}-{max}", + "codePattern": "Only letters, numbers, and underscore are allowed", + "nameRequired": "Name is required", + "nameTooLong": "Name is too long", + "descriptionTooLong": "Description is too long" + }, + "item": { + "new": "New Item", + "createTitle": "Create Dictionary Item", + "editTitle": "Edit Dictionary Item", + "keyPlaceholder": "e.g. PENDING", + "value": "Value", + "default": "Default", + "enabled": "Enabled", + "sortOrder": "Sort Order", + "deleteConfirm": "Delete this item?", + "deleteSuccess": "Deleted", + "import": "Import", + "export": "Export", + "importCompleted": "Import completed", + "rowVersionMissing": "Row version is missing. Please refresh.", + "groupNotSelected": "Group is not selected", + "keyRequired": "Key is required", + "keyTooLong": "Key is too long", + "valueRequired": "At least one language is required", + "descriptionTooLong": "Description is too long", + "sortUpdated": "Sort order updated" + }, + "i18n": { + "zh": "中文", + "en": "English", + "zhPlaceholder": "请输入中文内容", + "enPlaceholder": "Enter English content", + "hint": "Fill at least one language. Both recommended for full coverage." + }, + "import": { + "title": "Batch Import Dictionary Items", + "dropHere": "Drop file here or click to upload", + "tip": "Max file size: 10MB. CSV or JSON only.", + "conflictMode": "Conflict Mode", + "skip": "Skip", + "overwrite": "Overwrite", + "append": "Append", + "successSummary": "Success: {success}, Skipped: {skip}, Errors: {error}", + "row": "Row", + "field": "Field", + "message": "Message", + "start": "Start Import", + "fileTooLarge": "File size exceeds 10MB", + "selectFile": "Please select a file", + "unsupportedFormat": "Unsupported file format" + }, + "override": { + "toggleEnabled": "Override Enabled", + "toggleDisabled": "Override Disabled", + "systemItems": "System Items", + "customView": "Custom View", + "newCustomItem": "New Custom Item", + "saveSortOrder": "Save Sort Order", + "tenantGroupNotReady": "Tenant group is not ready", + "hiddenSaved": "Hidden items saved", + "sortSaved": "Sort order saved", + "selectSystemDictionary": "Select a system dictionary", + "selectGroupHint": "Select a dictionary group to continue.", + "unsavedSortConfirm": "You have unsaved sort changes. Leave without saving?" + }, + "metrics": { + "cacheHitRatio": "Cache Hit Ratio", + "totalQueries": "Total Queries", + "hits": "Hits", + "misses": "Misses", + "avgResponse": "Avg Response", + "last1h": "Last 1 hour", + "hitRatioTrend": "Hit Ratio Trend (L1 vs L2)", + "timeRange1h": "1 Hour", + "timeRange24h": "24 Hours", + "timeRange7d": "7 Days", + "invalidationEvents": "Invalidation Events", + "rangeTo": "To", + "start": "Start", + "end": "End", + "timestamp": "Timestamp", + "dictionary": "Dictionary", + "operation": "Operation", + "keys": "Keys", + "operator": "Operator", + "create": "Create", + "update": "Update", + "delete": "Delete", + "l1HitRatio": "L1 Hit Ratio", + "l2HitRatio": "L2 Hit Ratio", + "target": "Target" + }, + "labelOverride": { + "title": "Label Override Management", + "tenantTitle": "Tenant Label Override", + "platformTitle": "Platform Label Override", + "selectTenant": "Select Tenant", + "selectTenantHint": "Select a tenant to view their label overrides", + "dictionaryItem": "Dictionary Item", + "dictionaryItemKey": "Dictionary Item Key", + "originalValue": "Original Value", + "overrideValue": "Override Value", + "overrideType": "Override Type", + "tenantCustomization": "Tenant Customization", + "platformEnforcement": "Platform Enforcement", + "reason": "Override Reason", + "reasonPlaceholder": "Enter override reason (optional)", + "reasonHint": "Recommended for audit purposes", + "createdAt": "Created At", + "updatedAt": "Updated At", + "operator": "Operator", + "newOverride": "New Override", + "editOverride": "Edit Override", + "deleteOverride": "Delete Override", + "deleteConfirm": "Are you sure to delete this label override? The original value will be restored.", + "noOverrides": "No label overrides found", + "selectDictionaryItem": "Please select a dictionary item", + "overrideValueRequired": "Override value is required", + "onlySystemDictionary": "Tenant can only override system dictionary items", + "filterAll": "All", + "filterTenantCustomization": "Tenant Customization", + "filterPlatformEnforcement": "Platform Enforcement" + }, + "errors": { + "loadGroups": "Failed to load dictionary groups.", + "loadGroup": "Failed to load dictionary group.", + "createGroup": "Failed to create dictionary group.", + "updateGroup": "Failed to update dictionary group.", + "deleteGroup": "Failed to delete dictionary group.", + "loadItems": "Failed to load dictionary items.", + "createItem": "Failed to create dictionary item.", + "updateItem": "Failed to update dictionary item.", + "deleteItem": "Failed to delete dictionary item.", + "loadOverrides": "Failed to load overrides.", + "loadOverride": "Failed to load override.", + "enableOverride": "Failed to enable override.", + "disableOverride": "Failed to disable override.", + "updateHidden": "Failed to update hidden items.", + "updateSort": "Failed to update sort order.", + "dataConflict": "Data has been updated by someone else. Refresh and retry.", + "loadLabelOverrides": "Failed to load label overrides.", + "saveLabelOverride": "Failed to save label override.", + "deleteLabelOverride": "Failed to delete label override." + }, + "messages": { + "deleted": "Deleted", + "importCompleted": "Import completed", + "sortUpdated": "Sort order updated" + } + } +} diff --git a/src/locales/en/merchant.ts b/src/locales/en/merchant.ts new file mode 100644 index 0000000..5e24412 --- /dev/null +++ b/src/locales/en/merchant.ts @@ -0,0 +1,135 @@ +/** + * Merchant module i18n (en) + * + * NOTE: This file is merged into `merchant.*` namespace. + */ +export default { + list: { + title: 'Merchants', + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Merchant name or license number', + status: 'Status', + operatingMode: 'Operating mode', + tenant: 'Tenant', + tenantPlaceholder: 'Enter tenant ID' + }, + table: { + name: 'Merchant', + tenantName: 'Tenant', + operatingMode: 'Operating mode', + status: 'Status', + frozen: 'Frozen', + storeCount: 'Stores', + createdAt: 'Created at' + } + }, + review: { + title: 'Merchant Review', + claim: 'Claim', + release: 'Release', + approve: 'Approve', + reject: 'Reject', + revoke: 'Revoke', + reason: 'Reason', + comment: 'Comment', + result: 'Result', + claimed: 'Claimed', + unclaimed: 'Unclaimed', + readonlyTip: 'This review is claimed by another operator', + reasonPlaceholder: 'Enter rejection reason', + selectResult: 'Select review result', + needClaim: 'Please claim before reviewing', + success: 'Review submitted', + pendingReApproval: 'Pending re-approval', + empty: 'No audit records', + section: { + action: 'Review Action', + history: 'Audit History' + } + }, + detail: { + title: 'Merchant Detail', + basicInfo: 'Basic info', + subjectInfo: 'Entity info', + stores: 'Stores', + auditHistory: 'Audit history', + changeHistory: 'Change history' + }, + fields: { + name: 'Merchant name', + tenant: 'Tenant', + status: 'Status', + operatingMode: 'Operating mode', + licenseNumber: 'License number', + legalRepresentative: 'Legal representative', + registeredAddress: 'Registered address', + contactPhone: 'Contact phone', + contactEmail: 'Contact email', + isFrozen: 'Frozen', + frozenReason: 'Frozen reason', + approvedAt: 'Approved at', + approvedBy: 'Approved by', + createdAt: 'Created at', + updatedAt: 'Updated at' + }, + status: { + pending: 'Pending', + approved: 'Approved', + rejected: 'Rejected', + frozen: 'Frozen', + unknown: 'Unknown' + }, + operatingMode: { + same: 'Same entity', + different: 'Different entity' + }, + frozen: { + yes: 'Frozen', + no: 'Normal' + }, + action: { + edit: 'Edit', + detail: 'Detail', + export: 'Export PDF' + }, + placeholder: { + name: 'Enter merchant name', + licenseNumber: 'Enter license number', + legalRepresentative: 'Enter legal representative', + registeredAddress: 'Enter registered address', + contactPhone: 'Enter contact phone', + contactEmail: 'Enter contact email' + }, + rules: { + nameRequired: 'Merchant name is required', + contactPhoneRequired: 'Contact phone is required', + contactEmailInvalid: 'Invalid email format' + }, + message: { + rowVersionMissing: 'Missing row version, please refresh', + updateSuccess: 'Update successful', + updateRequiresReview: 'Critical changes require re-approval' + }, + store: { + name: 'Store name', + status: 'Store status', + address: 'Address', + contactPhone: 'Contact phone', + licenseNumber: 'License number', + empty: 'No stores', + statusOperating: 'Operating', + statusPreparing: 'Preparing', + statusClosed: 'Closed', + statusSuspended: 'Suspended', + statusUnknown: 'Unknown' + }, + change: { + field: 'Field', + oldValue: 'Old value', + newValue: 'New value', + changedBy: 'Changed by', + changedAt: 'Changed at', + empty: 'No change records' + } +} diff --git a/src/locales/en/store.ts b/src/locales/en/store.ts new file mode 100644 index 0000000..f703256 --- /dev/null +++ b/src/locales/en/store.ts @@ -0,0 +1,506 @@ +/** + * Store module i18n (en) + * + * Note: merged into `store.*` namespace during i18n init. + */ +export default { + list: { + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Store name/code', + merchantId: 'Merchant ID', + merchantIdPlaceholder: 'Enter merchant ID', + auditStatus: 'Audit Status', + businessStatus: 'Business Status', + ownershipType: 'Ownership Type' + }, + table: { + name: 'Store Name', + code: 'Store Code', + ownershipType: 'Ownership Type', + auditStatus: 'Audit Status', + businessStatus: 'Business Status', + phone: 'Phone', + createdAt: 'Created At' + }, + action: { + create: 'Create Store', + detail: 'View Detail', + edit: 'Edit', + toggleStatus: 'Toggle Status', + submitAudit: 'Submit Audit' + } + }, + form: { + createTitle: 'Create Store', + editTitle: 'Edit Store', + tabs: { + basic: 'Basic Info', + location: 'Location', + settings: 'Settings', + services: 'Services' + }, + merchantId: 'Merchant ID', + merchantIdPlaceholder: 'Enter merchant ID', + code: 'Store Code', + codePlaceholder: 'Enter store code', + name: 'Store Name', + namePlaceholder: 'Enter store name', + phone: 'Phone', + phonePlaceholder: 'Enter phone number', + managerName: 'Manager Name', + managerNamePlaceholder: 'Enter manager name', + status: 'Store Status', + signboardImageUrl: 'Signboard Image', + signboardImageUrlPlaceholder: 'Enter signboard image URL', + ownershipType: 'Ownership Type', + categoryId: 'Category ID', + categoryIdPlaceholder: 'Enter category ID', + deliveryRadiusKm: 'Delivery Radius (km)', + locationTitle: 'Location', + province: 'Province', + provincePlaceholder: 'Enter province', + city: 'City', + cityPlaceholder: 'Enter city', + district: 'District', + districtPlaceholder: 'Enter district', + address: 'Address', + addressPlaceholder: 'Enter address', + coordinate: 'Coordinates', + coordinatePlaceholder: 'e.g. 39.695954,116.074058', + coordinatePasteAction: 'Paste', + coordinatePickerAction: 'Open coordinate picker', + coordinateHint: 'Supports lat,lng or lng,lat; copy from Tencent picker', + coordinatePasteUnavailable: 'Clipboard access is unavailable', + coordinatePasteEmpty: 'Clipboard is empty, copy coordinates first', + coordinateInvalid: 'Invalid coordinate format', + settingsTitle: 'Service Settings', + announcement: 'Announcement', + announcementPlaceholder: 'Enter announcement', + tags: 'Tags', + tagsPlaceholder: 'Comma-separated tags', + supportsDineIn: 'Supports Dine-in', + supportsPickup: 'Supports Pickup', + supportsDelivery: 'Supports Delivery', + supportsReservation: 'Supports Reservation', + supportsQueueing: 'Supports Queueing' + }, + rules: { + merchantId: 'Merchant ID is required', + code: 'Store code is required', + name: 'Store name is required', + signboardImageUrl: 'Signboard image is required', + ownershipType: 'Select ownership type', + coordinateText: 'Coordinates are required', + coordinateTextFormat: 'Use lat,lng or lng,lat format' + }, + detail: { + title: 'Store Detail', + empty: 'No store data', + back: 'Back to List', + fields: { + name: 'Store Name', + code: 'Store Code', + auditStatus: 'Audit Status', + businessStatus: 'Business Status', + ownershipType: 'Ownership Type', + phone: 'Phone', + address: 'Address', + createdAt: 'Created At' + }, + tabs: { + basic: 'Basic Info', + qualification: 'Qualifications', + businessHours: 'Business Hours', + temporaryHours: 'Temporary Adjustments', + deliveryZone: 'Delivery Zones', + fee: 'Fee Config' + } + }, + qualification: { + warningTitle: 'Qualification Alerts', + complete: 'Complete', + incomplete: 'Incomplete', + expiringSoon: 'Expiring soon {count}', + expired: 'Expired {count}', + action: { + add: 'Add Qualification', + edit: 'Edit Qualification' + }, + table: { + type: 'Type', + documentNumber: 'Document No.', + expiresAt: 'Expires At', + status: 'Status' + }, + form: { + type: 'Type', + fileUrl: 'File URL', + fileUrlPlaceholder: 'Enter file URL', + documentNumber: 'Document No.', + documentNumberPlaceholder: 'Enter document number', + issuedAt: 'Issued At', + expiresAt: 'Expires At', + sortOrder: 'Sort Order' + }, + rules: { + type: 'Select qualification type', + fileUrl: 'File URL is required', + documentNumber: 'Document number is required', + expiresAt: 'Select expiry date' + }, + status: { + valid: 'Valid', + expiringSoon: 'Expiring Soon', + expired: 'Expired' + }, + type: { + businessLicense: 'Business License', + foodService: 'Food Service License', + storefront: 'Storefront Photo', + interior: 'Interior Photo' + }, + deleteTitle: 'Delete Qualification', + deleteConfirm: 'Are you sure to delete this qualification?' + }, + businessHours: { + tip: 'Cross-day hours are supported and overlap validation will run on save.', + action: { + add: 'Add Slot', + save: 'Save' + }, + table: { + dayOfWeek: 'Day', + hourType: 'Type', + startTime: 'Start Time', + endTime: 'End Time', + capacityLimit: 'Capacity', + notes: 'Notes', + notesPlaceholder: 'Notes' + }, + days: { + sunday: 'Sunday', + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday' + }, + hourType: { + normal: 'Normal', + reservation: 'Reservation Only', + pickup: 'Pickup/Delivery', + closed: 'Closed' + } + }, + temporaryHours: { + tip: 'Set closures, temporary openings, or adjusted hours for specific dates', + allDay: 'All Day', + action: { + add: 'Add Adjustment' + }, + table: { + dateRange: 'Date Range', + timeRange: 'Time Range', + overrideType: 'Type', + reason: 'Note' + }, + overrideType: { + closed: 'Closed', + temporaryOpen: 'Temporary Open', + modifiedHours: 'Modified Hours' + }, + dialog: { + addTitle: 'Add Temporary Adjustment', + editTitle: 'Edit Temporary Adjustment' + }, + form: { + dateRange: 'Date Range', + isAllDay: 'All Day', + timeRange: 'Time Range', + overrideType: 'Type', + reason: 'Note', + reasonPlaceholder: "e.g., Chinese New Year closure, Valentine's extended hours" + }, + rules: { + dateRequired: 'Please select date range', + typeRequired: 'Please select adjustment type', + timeRequired: 'Time range is required when not all-day' + }, + deleteConfirm: 'Are you sure you want to delete this adjustment?' + }, + deliveryZone: { + tip: 'Use GeoJSON polygons to define delivery zones and test delivery checks.', + action: { + add: 'Add Zone', + edit: 'Edit Zone' + }, + table: { + zoneName: 'Zone Name', + minimumOrderAmount: 'Minimum Order', + deliveryFee: 'Delivery Fee', + estimatedMinutes: 'ETA (min)' + }, + form: { + zoneName: 'Zone Name', + zoneNamePlaceholder: 'Enter zone name', + polygonGeoJson: 'GeoJSON', + polygonPlaceholder: 'Enter GeoJSON polygon', + drawPolygon: 'Draw Area', + drawPolygonTitle: 'Draw Delivery Zone', + editPolygon: 'Edit', + clearPolygon: 'Clear', + drawHint: 'Finish drawing and confirm to apply to GeoJSON', + mapKeyMissing: 'Tencent map key is not configured, cannot load the drawing tool.', + mapKeyMissingHint: 'Set VITE_TENCENT_MAP_KEY in your environment variables.', + minimumOrderAmount: 'Minimum Order', + deliveryFee: 'Delivery Fee', + estimatedMinutes: 'ETA (min)', + sortOrder: 'Sort Order' + }, + rules: { + zoneName: 'Zone name is required', + polygonGeoJson: 'GeoJSON is required' + }, + deleteTitle: 'Delete Delivery Zone', + deleteConfirm: 'Are you sure to delete this zone?', + check: { + title: 'Delivery Check', + action: 'Check', + longitude: 'Longitude', + latitude: 'Latitude', + inRange: 'In range', + outOfRange: 'Out of range', + distance: 'Distance', + zoneName: 'Matched Zone' + } + }, + fee: { + title: 'Fee Config', + minimumOrderAmount: 'Minimum Order', + deliveryFee: 'Delivery Fee', + packagingFeeMode: 'Packaging Fee Mode', + fixedPackagingFee: 'Fixed Packaging Fee', + freeDeliveryThreshold: 'Free Delivery Threshold', + packagingMode: { + fixed: 'Fixed', + perItem: 'Per Item' + }, + preview: { + title: 'Fee Preview', + action: 'Preview', + orderAmount: 'Order Amount', + itemCount: 'Item Count', + itemsTitle: 'Items', + addItem: 'Add Item', + skuId: 'SKU', + quantity: 'Qty', + packagingFee: 'Packaging Fee', + result: { + totalAmount: 'Total Amount', + totalFee: 'Total Fee', + deliveryFee: 'Delivery Fee', + packagingFee: 'Packaging Fee', + meetsMinimum: 'Meets Minimum', + shortfall: 'Shortfall' + } + } + }, + auditStatus: { + draft: 'Draft', + pending: 'Pending', + activated: 'Activated', + rejected: 'Rejected', + unknown: 'Unknown' + }, + businessStatus: { + title: 'Business Status', + targetStatus: 'Target Status', + closureReason: 'Closure Reason', + closureReasonText: 'Details', + closureReasonTextPlaceholder: 'Enter details', + forceClosedTip: 'This store is force closed and cannot be changed by tenant.', + rules: { + status: 'Select status', + reason: 'Select a reason' + }, + open: 'Open', + resting: 'Resting', + forceClosed: 'Force Closed', + unknown: 'Unknown' + }, + ownership: { + same: 'Same Entity', + different: 'Different Entity' + }, + closureReason: { + equipment: 'Equipment Maintenance', + vacation: 'Owner Vacation', + outOfStock: 'Out of Stock', + temporarilyClosed: 'Temporarily Closed', + other: 'Other' + }, + status: { + closed: 'Closed', + preparing: 'Preparing', + operating: 'Operating', + suspended: 'Suspended' + }, + action: { + save: 'Save', + edit: 'Edit', + delete: 'Delete', + refresh: 'Refresh' + }, + message: { + createSuccess: 'Created successfully', + updateSuccess: 'Updated successfully', + deleteSuccess: 'Deleted successfully', + statusUpdated: 'Status updated', + submitAuditSuccess: 'Audit submitted' + }, + audit: { + stats: { + pending: 'Pending', + overdue: 'Overdue', + approved: 'Approved', + rejected: 'Rejected', + avgProcessing: 'Avg Processing (hours)' + }, + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Store name/merchant name', + tenantId: 'Tenant ID', + tenantIdPlaceholder: 'Enter tenant ID', + dateRange: 'Submitted Range', + dateRangeStart: 'Start', + dateRangeEnd: 'End', + overdueOnly: 'Overdue Only' + }, + table: { + storeName: 'Store Name', + storeCode: 'Store Code', + tenantName: 'Tenant', + merchantName: 'Merchant', + ownershipType: 'Ownership', + submittedAt: 'Submitted At', + waitingDays: 'Waiting Days', + qualificationCount: 'Qualifications', + overdue: 'Overdue' + }, + action: { + detail: 'View Detail', + approve: 'Approve', + reject: 'Reject', + riskControl: 'Risk Control' + }, + overdue: 'Overdue', + notOverdue: 'Normal', + detail: { + title: 'Audit Detail', + empty: 'No data', + loadFailed: 'Failed to load detail', + viewFile: 'View File', + tabs: { + basic: 'Basic', + qualification: 'Qualifications', + history: 'History' + }, + sections: { + tenant: 'Tenant', + merchant: 'Merchant' + }, + fields: { + storeName: 'Store Name', + storeCode: 'Store Code', + auditStatus: 'Audit Status', + ownershipType: 'Ownership Type', + phone: 'Phone', + address: 'Address', + submittedAt: 'Submitted At', + signboard: 'Signboard', + tenantName: 'Tenant', + tenantContact: 'Contact', + tenantPhone: 'Phone', + merchantName: 'Merchant', + merchantLegal: 'Legal Name', + merchantCredit: 'Credit Code', + file: 'File' + } + }, + history: { + action: 'Action', + operator: 'Operator', + remark: 'Remark', + createdAt: 'Time' + }, + approve: { + prompt: 'Optional remark for approval', + placeholder: 'Enter remark', + success: 'Approved successfully' + }, + reject: { + title: 'Reject Audit', + reason: 'Reason', + reasonText: 'Details', + reasonTextPlaceholder: 'Enter details', + remark: 'Remark', + remarkPlaceholder: 'Enter remark', + success: 'Rejected successfully', + rules: { + reason: 'Select a reason', + reasonText: 'Enter reason details' + }, + reasonOptions: { + licenseMissing: 'Missing documents', + photoBlur: 'Blurry photos', + inconsistent: 'Inconsistent information', + other: 'Other' + } + }, + riskControl: { + title: 'Risk Control', + storeName: 'Current store: {name}', + action: 'Action', + forceClose: 'Force Close', + reopen: 'Reopen', + reason: 'Reason', + reasonPlaceholder: 'Enter reason', + remark: 'Remark', + remarkPlaceholder: 'Enter remark', + forceCloseSuccess: 'Store force closed', + reopenSuccess: 'Store reopened', + rules: { + action: 'Select action', + reason: 'Reason is required' + } + } + }, + alerts: { + summary: { + expiringSoon: 'Expiring Soon', + expired: 'Expired' + }, + search: { + tenantId: 'Tenant ID', + tenantIdPlaceholder: 'Enter tenant ID', + daysThreshold: 'Threshold (days)', + expired: 'Expired' + }, + table: { + storeName: 'Store Name', + storeCode: 'Store Code', + tenantName: 'Tenant', + qualificationType: 'Qualification', + expiresAt: 'Expires At', + daysUntilExpiry: 'Days Left', + status: 'Status', + businessStatus: 'Business Status' + }, + status: { + expiring: 'Expiring Soon', + expired: 'Expired' + } + } +} diff --git a/src/locales/en/tenant.ts b/src/locales/en/tenant.ts new file mode 100644 index 0000000..3c4552a --- /dev/null +++ b/src/locales/en/tenant.ts @@ -0,0 +1,267 @@ +/** + * Tenant module i18n (en) + * + * Note: This file is merged into `tenant.*` namespace at i18n bootstrap. + */ +export default { + // Detail Drawer + detail: { + title: 'Tenant Detail', + basicInfo: 'Basic Info', + statusInfo: 'Status Info', + subscriptionInfo: 'Subscription', + quotaInfo: 'Quota', + billingInfo: 'Billing', + + tenantId: 'Tenant ID', + name: 'Tenant Name', + code: 'Tenant Code', + shortName: 'Short Name', + industry: 'Industry', + contactName: 'Contact Name', + contactPhone: 'Contact Phone', + contactEmail: 'Contact Email', + + status: 'Tenant Status', + verificationStatus: 'Verification Status', + autoRenew: 'Auto Renew', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + currentPackage: 'Current Plan' + }, + + // Tabs + tabs: { + basicInfo: 'Basic Info', + statusInfo: 'Status Info', + subscription: 'Subscription', + quotaOverview: 'Quota Overview', + billing: 'Billing' + }, + + // Quota + quota: { + title: 'Quota Overview', + usageSummary: 'Quota Usage', + usageHistory: 'Quota Usage Details', + purchaseHistory: 'Quota Purchase History', + + recordedAt: 'Recorded At', + changeType: 'Change Type', + snapshot: 'Snapshot', + historyNotice: 'Current data is real-time snapshot. History feature pending backend API.', + fullOrderId: 'Full Order ID', + + orderNo: 'Order No', + quotaType: 'Quota Type', + quotaPackage: 'Quota Package', + limitValue: 'Limit', + usedValue: 'Used', + remainingValue: 'Remaining', + usagePercentage: 'Usage Rate', + resetCycle: 'Reset Cycle', + lastResetAt: 'Last Reset At', + + purchaseValue: 'Quantity', + price: 'Price', + purchasedAt: 'Purchased At', + expiredAt: 'Expired At', + notes: 'Notes', + + type: { + store: 'Stores', + account: 'Accounts', + storageGb: 'Storage (GB)', + smsCredits: 'SMS Credits', + deliveryOrders: 'Delivery Orders', + unknown: 'Unknown ({value})' + } + }, + + // 公告管理 + announcement: { + title: 'Tenant Announcements', + listTitle: 'Announcement List', + createTitle: 'Create Announcement', + editTitle: 'Edit Announcement', + detailTitle: 'Announcement Detail', + + search: { + status: 'Status', + statusPlaceholder: 'All Statuses', + keyword: 'Keyword', + keywordPlaceholder: 'Title/Content keyword', + dateRange: 'Date Range', + dateRangePlaceholder: 'Select date range' + }, + + field: { + title: 'Title', + content: 'Content', + announcementType: 'Type', + priority: 'Priority', + status: 'Status', + effectiveRange: 'Effective Range', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + targetType: 'Audience', + targetParameters: 'Target Parameters', + departmentIds: 'Department IDs', + roleIds: 'Role IDs', + tagIds: 'Tag IDs', + publishedAt: 'Published At', + revokedAt: 'Revoked At', + isActive: 'Active', + rowVersion: 'Row Version' + }, + + action: { + create: 'Create', + edit: 'Edit', + detail: 'Detail', + publish: 'Publish', + revoke: 'Revoke', + delete: 'Delete', + save: 'Save', + back: 'Back', + more: 'More' + }, + + status: { + draft: 'Draft', + published: 'Published', + revoked: 'Revoked' + }, + + type: { + system: 'System', + billing: 'Billing/Subscription', + operation: 'Operation', + systemPlatformUpdate: 'Platform Update', + systemSecurityNotice: 'Security Notice', + systemCompliance: 'Compliance', + tenantInternal: 'Tenant Internal', + tenantFinance: 'Tenant Finance', + tenantOperation: 'Tenant Operation' + }, + + targetType: { + all: 'All', + roles: 'Roles', + users: 'Users', + rules: 'Rules', + manual: 'Manual' + }, + + placeholder: { + title: 'Enter announcement title', + content: 'Enter announcement content', + type: 'Select announcement type', + priority: 'Enter priority', + effectiveRange: 'Select effective range', + targetType: 'Select audience', + roleIds: 'Enter role IDs, separated by commas', + userIds: 'Enter user IDs, separated by commas', + departmentIds: 'Enter department IDs, separated by commas', + tagIds: 'Enter tag IDs, separated by commas' + }, + + validation: { + titleRequired: 'Please enter title', + titleLength: 'Title must be 2-128 characters', + contentRequired: 'Please enter content', + typeRequired: 'Please select type', + priorityRequired: 'Please enter priority', + effectiveRangeRequired: 'Please select effective range', + targetTypeRequired: 'Please select audience', + roleIdsRequired: 'Please enter at least one role ID', + userIdsRequired: 'Please enter at least one user ID', + targetRuleRequired: 'Please fill at least one rule field' + }, + + message: { + loadListFailed: 'Failed to load list', + loadDetailFailed: 'Failed to load detail', + detailNotFound: 'Announcement not found. Please return to the list.', + tenantIdMissing: 'Tenant ID not found', + publishConfirm: 'Publish this announcement?', + revokeConfirm: 'Revoke this announcement?', + deleteConfirm: 'Delete this announcement?', + publishSuccess: 'Announcement published', + revokeSuccess: 'Announcement revoked', + deleteSuccess: 'Announcement deleted', + createSuccess: 'Announcement created', + updateSuccess: 'Announcement updated', + actionNotAllowed: 'Action not allowed for current status', + empty: 'No announcements' + }, + + tip: { + tenantScope: 'This is a tenant-level announcement and will be sent to this tenant only.', + draftAutoSave: 'Auto-saving draft', + draftSaved: 'Draft saved at {time}', + draftLoaded: 'Draft loaded', + draftNotEditable: 'Only draft announcements can be edited' + }, + + section: { + content: 'Content', + audience: 'Audience' + } + }, + + // Edit/Create Dialog + edit: { + titleEdit: 'Edit Tenant', + titleCreate: 'Add Tenant', + + placeholder: { + code: 'Enter tenant code', + name: 'Enter tenant name', + shortName: 'Enter short name', + industry: 'Enter industry', + contactName: 'Enter contact name', + contactPhone: 'Enter contact phone', + contactEmail: 'Enter contact email', + tenantPackageId: 'Select plan', + effectiveFrom: 'Select effective date' + }, + + package: { + title: 'Plan Info', + select: 'Plan', + durationMonths: 'Duration' + }, + + packageType: { + free: 'Free', + paid: 'Paid' + }, + + unit: { + month: 'month(s)' + }, + + validation: { + codeRequired: 'Please enter tenant code', + nameRequired: 'Please enter tenant name', + tenantPackageIdRequired: 'Please select plan' + } + }, + + // Messages + warning: { + updateApiNotReady: 'Tenant update API is under development. Please try again later.' + }, + error: { + loadDetailFailed: 'Failed to load tenant detail', + loadQuotaFailed: 'Failed to load quota info', + loadBillingFailed: 'Failed to load billing info', + loadSubscriptionFailed: 'Failed to load subscription info', + updateFailed: 'Update failed, please retry' + }, + success: { + registerSuccess: 'Registered successfully', + updateSuccess: 'Updated successfully' + } +} diff --git a/src/locales/en/tenantPackage.ts b/src/locales/en/tenantPackage.ts new file mode 100644 index 0000000..f7bc6b6 --- /dev/null +++ b/src/locales/en/tenantPackage.ts @@ -0,0 +1,137 @@ +/** + * Tenant Package Module Locale (en) + */ +export default { + actions: { + more: 'More', + copy: 'Copy', + toggleActive: 'Enable/Disable', + toggleVisible: 'Show/Hide', + togglePurchasable: 'List/Unlist', + publish: 'Publish', + rollbackDraft: 'Rollback Draft', + quotaEdit: 'Edit Quota', + viewTenants: 'View Tenants', + delete: 'Delete', + view: 'View', + edit: 'Edit' + }, + drawer: { + title: 'Associated Tenants - {name}', + expiringTitle: 'Associated Tenants - {name} (Expiring in {days} days)', + searchPlaceholder: 'Search Tenant Name/Code', + tenantName: 'Tenant Name', + tenantCode: 'Tenant Code', + tenantStatus: 'Tenant Status', + contact: 'Contact', + phone: 'Phone', + effectiveTo: 'Effective To' + }, + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Search Package Name', + status: 'Status', + statusAll: 'All', + statusEnabled: 'Enabled', + statusDisabled: 'Disabled' + }, + form: { + name: 'Package Name', + namePlaceholder: 'Enter package name', + packageType: 'Package Type', + packageTypePlaceholder: 'Select package type', + monthlyPrice: 'Monthly Price', + yearlyPrice: 'Yearly Price', + pricePlaceholder: 'Enter price', + maxStoreCount: 'Max Stores', + maxAccountCount: 'Max Accounts', + maxStorageGb: 'Max Storage (GB)', + maxSmsCredits: 'SMS Credits', + maxDeliveryOrders: 'Max Delivery Orders', + quotaPlaceholder: 'Enter quantity (-1 for unlimited)', + featurePoliciesJson: 'Feature Policies', + featurePlaceholder: 'Enter JSON configuration', + isActive: 'Active', + publicVisible: 'Public Visible', + allowNewPurchase: 'Allow New Purchase', + publishStatus: 'Publish Status', + isRecommended: 'Recommended', + sortOrder: 'Sort Order', + submit: 'Submit' + }, + status: { + enabled: 'Enabled', + disabled: 'Disabled' + }, + publishStatus: { + published: 'Published', + draft: 'Draft' + }, + type: { + free: 'Free', + standard: 'Standard', + professional: 'Professional', + enterprise: 'Enterprise' + }, + tags: { + recommended: 'Recommended', + bestValue: 'Best Value', + flagship: 'Flagship' + }, + table: { + name: 'Package Name', + packageType: 'Type', + publishStatus: 'Status', + publicVisible: 'Public', + allowNewPurchase: 'Buyable', + monthlyPrice: 'Monthly', + yearlyPrice: 'Yearly', + quota: 'Quota', + usage: 'Usage', + status: 'Active', + actions: 'Actions', + quotaEmpty: 'Unlimited' + }, + message: { + updateSuccess: 'Update Successful', + + toggleConfirmEnable: 'Enable this package?', + toggleConfirmDisable: 'Disable this package? New purchases will be blocked.', + toggleTitleEnable: 'Enable', + toggleTitleDisable: 'Disable', + toggleSuccessEnable: 'Enabled', + toggleSuccessDisable: 'Disabled', + + toggleVisibleConfirmEnable: 'Make public visible?', + toggleVisibleConfirmDisable: 'Hide package?', + toggleVisibleTitleEnable: 'Show', + toggleVisibleTitleDisable: 'Hide', + toggleVisibleSuccessEnable: 'Package is now visible', + toggleVisibleSuccessDisable: 'Package is now hidden', + + togglePurchasableConfirmEnable: 'Allow new purchases?', + togglePurchasableConfirmDisable: 'Disallow new purchases?', + togglePurchasableTitleEnable: 'Allow Purchase', + togglePurchasableTitleDisable: 'Disallow Purchase', + togglePurchasableSuccessEnable: 'Purchases Allowed', + togglePurchasableSuccessDisable: 'Purchases Disabled', + + publishConfirm: 'Publish this package? It will be visible to tenants.', + publishTitle: 'Publish', + publishSuccess: 'Published Successfully', + + rollbackDraftConfirm: 'Rollback to draft? New users cannot see it.', + rollbackDraftTitle: 'Rollback', + rollbackDraftSuccess: 'Rolled back to draft', + + deleteConfirm: 'Delete this package? This cannot be undone.', + deleteTitle: 'Delete', + deleteSuccess: 'Deleted Successfully' + }, + dialog: { + quotaTitle: 'Quota Management - {name}' + }, + featurePolicy: { + invalidJson: 'Invalid JSON format' + } +} diff --git a/src/locales/index.ts b/src/locales/index.ts new file mode 100644 index 0000000..668f1b0 --- /dev/null +++ b/src/locales/index.ts @@ -0,0 +1,193 @@ +/** + * 国际化配置 + * + * 基于 vue-i18n 实现的多语言国际化解决方案。 + * 支持中文和英文切换,自动从本地存储恢复用户的语言偏好。 + * + * ## 主要功能 + * + * - 多语言支持 - 支持中文(简体)和英文两种语言 + * - 语言切换 - 运行时动态切换语言,无需刷新页面 + * - 持久化存储 - 自动保存和恢复用户的语言偏好 + * - 全局注入 - 在任何组件中都可以使用 $t 函数进行翻译 + * - 类型安全 - 提供 TypeScript 类型支持 + * + * ## 支持的语言 + * + * - zh: 简体中文 + * - en: English + * + * @module locales + * @author Art Design Pro Team + */ + +import { createI18n } from 'vue-i18n' +import type { I18n, I18nOptions } from 'vue-i18n' +import { LanguageEnum } from '@/enums/appEnum' +import { getSystemStorage } from '@/utils/storage' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +// 同步导入语言文件 +import enMessages from './langs/en.json' +import zhMessages from './langs/zh.json' + +// 业务模块语言包(按模块拆分,避免主语言文件过大) +import enBillingMessages from './en-US/billing.json' +import zhBillingMessages from './zh-CN/billing.json' +import enDictionaryMessages from './en/dictionary.json' +import zhDictionaryMessages from './zh-CN/dictionary.json' +import enTenantMessages from './en/tenant' +import zhTenantMessages from './zh-CN/tenant' +import enAnnouncementMessages from './en/announcement' +import zhAnnouncementMessages from './zh-CN/announcement' +import enMerchantMessages from './en/merchant' +import zhMerchantMessages from './zh-CN/merchant' +import enStoreMessages from './en/store' +import zhStoreMessages from './zh-CN/store' +import enTenantPackageMessages from './en/tenantPackage' +import zhTenantPackageMessages from './zh-CN/tenantPackage' + +/** + * 存储键管理器实例 + */ +const storageKeyManager = new StorageKeyManager() + +/** + * 语言消息对象 + */ +type JsonObject = Record + +const isPlainObject = (value: unknown): value is JsonObject => { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * 深度合并语言包(用于按模块拆分语言文件) + * - 数组:直接覆盖 + * - 对象:递归合并 + * - 其他:直接覆盖 + */ +const deepMerge = (target: T, source: U): T & U => { + const result: JsonObject = { ...target } + + Object.keys(source).forEach((key) => { + const sourceValue = source[key] + const targetValue = result[key] + + if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { + result[key] = deepMerge(targetValue, sourceValue) + return + } + + result[key] = sourceValue + }) + + return result as T & U +} + +const messages = { + [LanguageEnum.EN]: deepMerge( + deepMerge( + deepMerge(enMessages as unknown as JsonObject, enBillingMessages as unknown as JsonObject), + enDictionaryMessages as unknown as JsonObject + ), + { + tenant: enTenantMessages, + tenantPackage: enTenantPackageMessages, + announcement: enAnnouncementMessages, + merchant: enMerchantMessages, + store: enStoreMessages + } as unknown as JsonObject + ), + [LanguageEnum.ZH]: deepMerge( + deepMerge( + deepMerge(zhMessages as unknown as JsonObject, zhBillingMessages as unknown as JsonObject), + zhDictionaryMessages as unknown as JsonObject + ), + { + tenant: zhTenantMessages, + tenantPackage: zhTenantPackageMessages, + announcement: zhAnnouncementMessages, + merchant: zhMerchantMessages, + store: zhStoreMessages + } as unknown as JsonObject + ) +} as unknown as NonNullable + +/** + * 语言选项列表 + * 用于语言切换下拉框 + */ +export const languageOptions = [ + { value: LanguageEnum.ZH, label: '简体中文' }, + { value: LanguageEnum.EN, label: 'English' } +] + +/** + * 从存储中获取语言设置 + * @returns 语言设置,如果获取失败则返回默认语言 + */ +const getDefaultLanguage = (): LanguageEnum => { + // 尝试从版本化的存储中获取语言设置 + try { + const storageKey = storageKeyManager.getStorageKey('user') + const userStore = localStorage.getItem(storageKey) + + if (userStore) { + const { language } = JSON.parse(userStore) + if (language && Object.values(LanguageEnum).includes(language)) { + return language + } + } + } catch (error) { + console.warn('[i18n] 从版本化存储获取语言设置失败:', error) + } + + // 尝试从系统存储中获取语言设置 + try { + const sys = getSystemStorage() + if (sys) { + const { user } = JSON.parse(sys) + if (user?.language && Object.values(LanguageEnum).includes(user.language)) { + return user.language + } + } + } catch (error) { + console.warn('[i18n] 从系统存储获取语言设置失败:', error) + } + + // 返回默认语言 + console.debug('[i18n] 使用默认语言:', LanguageEnum.ZH) + return LanguageEnum.ZH +} + +/** + * i18n 配置选项 + */ +const i18nOptions: I18nOptions = { + locale: getDefaultLanguage(), + legacy: false, + globalInjection: true, + fallbackLocale: LanguageEnum.ZH, + messages +} + +/** + * i18n 实例 + */ +const i18n: I18n = createI18n(i18nOptions) + +/** + * 翻译函数类型 + */ +interface Translation { + (key: string): string +} + +/** + * 全局翻译函数 + * 可在任何地方使用,无需导入 useI18n + */ +export const $t = i18n.global.t as Translation + +export default i18n diff --git a/src/locales/lang/en/announcement.ts b/src/locales/lang/en/announcement.ts new file mode 100644 index 0000000..7084b68 --- /dev/null +++ b/src/locales/lang/en/announcement.ts @@ -0,0 +1,227 @@ +/** + * 公告模块国际化(en) + * + * 注意:该文件会在 i18n 初始化时合并到 `announcement.*` 命名空间下 + */ +export default { + title: 'Announcement Management', + + platform: { + title: 'Platform Announcements', + list: 'Platform Announcement List', + create: 'Create Platform Announcement', + edit: 'Edit Platform Announcement', + detail: 'Platform Announcement Detail' + }, + + tenant: { + title: 'Tenant Announcements', + list: 'Tenant Announcement List', + create: 'Create Tenant Announcement', + edit: 'Edit Tenant Announcement', + detail: 'Tenant Announcement Detail' + }, + + app: { + title: 'App Announcements', + list: 'Announcements', + detail: 'Announcement Detail', + center: 'Announcement Center', + unread: 'Unread', + all: 'All', + markAllRead: 'Mark All as Read', + emptyUnread: 'No unread announcements', + emptyAll: 'No announcements' + }, + + drafts: { + title: 'Draft Center', + list: 'Draft List', + empty: 'No drafts', + save: 'Save Draft', + saving: 'Saving', + savedAt: 'Saved · {time}', + restore: 'Restore Draft', + discard: 'Discard Draft', + autoSaveOn: 'Auto-save enabled', + autoSaveOff: 'Auto-save disabled' + }, + + list: { + title: 'Announcement List', + total: '{count} items', + selected: '{count} selected', + empty: 'No announcements' + }, + + table: { + title: 'Title', + type: 'Type', + priority: 'Priority', + status: 'Status', + effectiveRange: 'Effective Range', + publishedAt: 'Published At', + actions: 'Actions' + }, + + detail: { + title: 'Announcement Detail', + readStatus: 'Read Status', + unreadTip: 'Unread', + readTip: 'Read' + }, + + form: { + title: 'Announcement Info', + base: 'Basic Info', + audience: 'Audience Settings', + schedule: 'Publish Settings', + preview: 'Preview Settings' + }, + + fields: { + title: 'Title', + summary: 'Summary', + content: 'Content', + type: 'Type', + priority: 'Priority', + status: 'Status', + publisher: 'Publisher', + publisherScope: 'Publisher Scope', + targetType: 'Audience', + effectiveFrom: 'Effective From', + effectiveTo: 'Effective To', + publishedAt: 'Published At', + revokedAt: 'Revoked At', + scheduledPublishAt: 'Scheduled Publish At', + createdAt: 'Created At', + updatedAt: 'Updated At', + readStatus: 'Read Status', + readAt: 'Read At', + tenantId: 'Tenant ID' + }, + + search: { + keyword: 'Keyword', + keywordPlaceholder: 'Search title/content', + status: 'Status', + statusPlaceholder: 'All statuses', + dateRange: 'Date Range', + dateFrom: 'Start Date', + dateTo: 'End Date', + targetType: 'Audience', + publisherScope: 'Publisher Scope', + announcementType: 'Announcement Type', + priority: 'Priority' + }, + + status: { + draft: 'Draft', + published: 'Published', + revoked: 'Revoked', + scheduled: 'Scheduled', + active: 'Active', + inactive: 'Inactive', + read: 'Read', + unread: 'Unread' + }, + + priority: { + low: 'Low', + normal: 'Normal', + high: 'High', + urgent: 'Urgent' + }, + + targetType: { + all: 'All Users', + roles: 'Roles', + users: 'Users', + rules: 'Rules', + manual: 'Manual' + }, + + publish: { + mode: 'Publish Mode', + immediate: 'Publish Now', + scheduled: 'Schedule', + scheduleTime: 'Scheduled Time' + }, + + action: { + create: 'Create', + edit: 'Edit', + view: 'View', + delete: 'Delete', + publish: 'Publish', + revoke: 'Revoke', + preview: 'Preview', + save: 'Save', + saveDraft: 'Save Draft', + markRead: 'Mark as Read', + markAllRead: 'Mark All as Read', + refresh: 'Refresh', + reset: 'Reset', + back: 'Back', + close: 'Close' + }, + + messages: { + createSuccess: 'Created successfully', + updateSuccess: 'Updated successfully', + deleteSuccess: 'Deleted successfully', + publishSuccess: 'Published successfully', + revokeSuccess: 'Revoked successfully', + saveDraftSuccess: 'Draft saved', + loadFailed: 'Failed to load', + empty: 'No data', + deleteConfirm: 'Are you sure you want to delete this announcement?', + revokeConfirm: 'Are you sure you want to revoke this announcement?', + publishConfirm: 'Are you sure you want to publish this announcement?', + discardDraftConfirm: 'Discard this draft?', + audienceEstimateFailed: 'Failed to estimate audience. Please try again.', + permissionDenied: 'You do not have permission to perform this action', + deleteNotSupported: 'Platform announcements cannot be deleted directly' + }, + + validation: { + titleRequired: 'Please enter a title', + contentRequired: 'Please enter content', + effectiveFromRequired: 'Please select effective start time', + targetTypeRequired: 'Please select audience', + announcementTypeRequired: 'Please select announcement type', + priorityRequired: 'Please select priority' + }, + + tabs: { + base: 'Basic Info', + audience: 'Audience', + schedule: 'Publish', + preview: 'Preview' + }, + + preview: { + openPreview: 'Preview', + drawerTitle: 'Announcement Preview', + titlePlaceholder: 'Enter announcement title', + emptyContent: 'No announcement content', + publishAt: 'Publish Time', + publishAtUnknown: 'Pending', + priorityTag: 'Priority P{level}', + priorityUnknown: 'Not set', + resizeHandle: 'Resize preview width' + }, + + type: { + system: 'System Announcement', + billing: 'Billing Notice', + operation: 'Operations Notice', + systemPlatformUpdate: 'Platform System Update', + systemSecurityNotice: 'System Security Notice', + systemCompliance: 'System Compliance Notice', + tenantInternal: 'Tenant Internal Notice', + tenantFinance: 'Tenant Finance Notice', + tenantOperation: 'Tenant Operations Notice', + unknown: 'Unknown Type' + } +} diff --git a/src/locales/lang/zh-CN/announcement.ts b/src/locales/lang/zh-CN/announcement.ts new file mode 100644 index 0000000..df5c7c8 --- /dev/null +++ b/src/locales/lang/zh-CN/announcement.ts @@ -0,0 +1,227 @@ +/** + * 公告模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `announcement.*` 命名空间下 + */ +export default { + title: '公告管理', + + platform: { + title: '平台公告', + list: '平台公告列表', + create: '创建平台公告', + edit: '编辑平台公告', + detail: '平台公告详情' + }, + + tenant: { + title: '租户公告', + list: '租户公告列表', + create: '创建租户公告', + edit: '编辑租户公告', + detail: '租户公告详情' + }, + + app: { + title: '应用端公告', + list: '公告列表', + detail: '公告详情', + center: '公告中心', + unread: '未读公告', + all: '全部公告', + markAllRead: '全部标记已读', + emptyUnread: '暂无未读公告', + emptyAll: '暂无公告' + }, + + drafts: { + title: '草稿中心', + list: '草稿列表', + empty: '暂无草稿', + save: '保存草稿', + saving: '保存中', + savedAt: '已保存 · {time}', + restore: '恢复草稿', + discard: '丢弃草稿', + autoSaveOn: '自动保存已开启', + autoSaveOff: '自动保存已关闭' + }, + + list: { + title: '公告列表', + total: '共 {count} 条', + selected: '已选 {count} 条', + empty: '暂无公告' + }, + + table: { + title: '标题', + type: '类型', + priority: '优先级', + status: '状态', + effectiveRange: '生效时间', + publishedAt: '发布时间', + actions: '操作' + }, + + detail: { + title: '公告详情', + readStatus: '阅读状态', + unreadTip: '未读', + readTip: '已读' + }, + + form: { + title: '公告信息', + base: '基础信息', + audience: '受众设置', + schedule: '发布设置', + preview: '预览设置' + }, + + fields: { + title: '标题', + summary: '摘要', + content: '内容', + type: '公告类型', + priority: '优先级', + status: '状态', + publisher: '发布人', + publisherScope: '发布范围', + targetType: '目标受众', + effectiveFrom: '生效开始时间', + effectiveTo: '生效结束时间', + publishedAt: '发布时间', + revokedAt: '撤销时间', + scheduledPublishAt: '计划发布时间', + createdAt: '创建时间', + updatedAt: '更新时间', + readStatus: '已读状态', + readAt: '已读时间', + tenantId: '租户ID' + }, + + search: { + keyword: '关键词', + keywordPlaceholder: '标题/内容关键词', + status: '状态', + statusPlaceholder: '全部状态', + dateRange: '日期范围', + dateFrom: '开始日期', + dateTo: '结束日期', + targetType: '受众类型', + publisherScope: '发布范围', + announcementType: '公告类型', + priority: '优先级' + }, + + status: { + draft: '草稿', + published: '已发布', + revoked: '已撤销', + scheduled: '待发布', + active: '生效中', + inactive: '未生效', + read: '已读', + unread: '未读' + }, + + priority: { + low: '低', + normal: '普通', + high: '高', + urgent: '紧急' + }, + + targetType: { + all: '全部用户', + roles: '指定角色', + users: '指定用户', + rules: '规则模式', + manual: '手动选择' + }, + + publish: { + mode: '发布方式', + immediate: '立即发布', + scheduled: '定时发布', + scheduleTime: '定时发布时间' + }, + + action: { + create: '新建', + edit: '编辑', + view: '查看', + delete: '删除', + publish: '发布', + revoke: '撤销', + preview: '预览', + save: '保存', + saveDraft: '保存草稿', + markRead: '标记已读', + markAllRead: '全部已读', + refresh: '刷新', + reset: '重置', + back: '返回', + close: '关闭' + }, + + messages: { + createSuccess: '创建成功', + updateSuccess: '更新成功', + deleteSuccess: '删除成功', + publishSuccess: '发布成功', + revokeSuccess: '撤销成功', + saveDraftSuccess: '草稿已保存', + loadFailed: '加载失败', + empty: '暂无数据', + deleteConfirm: '确认删除该公告?', + revokeConfirm: '确认撤销该公告?', + publishConfirm: '确认发布该公告?', + discardDraftConfirm: '确认丢弃该草稿?', + audienceEstimateFailed: '受众预估失败,请稍后重试', + permissionDenied: '暂无权限执行该操作', + deleteNotSupported: '平台公告不支持直接删除' + }, + + validation: { + titleRequired: '请输入标题', + contentRequired: '请输入内容', + effectiveFromRequired: '请选择生效开始时间', + targetTypeRequired: '请选择目标受众', + announcementTypeRequired: '请选择公告类型', + priorityRequired: '请选择优先级' + }, + + tabs: { + base: '基本信息', + audience: '受众设置', + schedule: '发布设置', + preview: '预览' + }, + + preview: { + openPreview: '预览', + drawerTitle: '公告预览', + titlePlaceholder: '请输入公告标题', + emptyContent: '暂无公告内容', + publishAt: '发布时间', + publishAtUnknown: '待发布', + priorityTag: '优先级 P{level}', + priorityUnknown: '未设置', + resizeHandle: '拖动调整预览宽度' + }, + + type: { + system: '系统公告', + billing: '账单提醒', + operation: '运营通知', + systemPlatformUpdate: '平台系统更新公告', + systemSecurityNotice: '系统安全公告', + systemCompliance: '系统合规公告', + tenantInternal: '租户内部公告', + tenantFinance: '租户财务公告', + tenantOperation: '租户运营公告', + unknown: '未知类型' + } +} diff --git a/src/locales/langs/en.json b/src/locales/langs/en.json new file mode 100644 index 0000000..571f780 --- /dev/null +++ b/src/locales/langs/en.json @@ -0,0 +1,2114 @@ +{ + "httpMsg": { + "unauthorized": "Unauthorized access, please login again", + "forbidden": "Access to this resource is forbidden", + "notFound": "The requested resource does not exist", + "methodNotAllowed": "Request method not allowed", + "conflict": "Data has been updated by someone else. Refresh and retry.", + "validationFailed": "Request validation failed", + "requestTimeout": "Request timeout, please try again later", + "internalServerError": "Internal server error, please try again later", + "badGateway": "Bad gateway error, please try again later", + "serviceUnavailable": "Service temporarily unavailable, please try again later", + "gatewayTimeout": "Gateway timeout, please try again later", + "requestCancelled": "Request cancelled", + "networkError": "Network connection error, please check your connection", + "requestFailed": "Request failed", + "requestConfigError": "Request configuration error" + }, + "topBar": { + "search": { + "title": "Search" + }, + "user": { + "userCenter": "User center", + "docs": "Document", + "github": "Github", + "lockScreen": "Lock screen", + "logout": "Log out" + }, + "guide": { + "title": "Click here to view", + "theme": "Theme style", + "menu": "Open top menu", + "description": "More configurations" + }, + "impersonation": { + "active": "Impersonating (Tenant {tenantId})", + "exit": "Exit", + "confirm": "Exit impersonation and return to the platform console?" + } + }, + "common": { + "id": "ID", + "tips": "Prompt", + "cancel": "Cancel", + "confirm": "Confirm", + "yes": "Yes", + "no": "No", + "days": "days", + "delete": "Delete", + "edit": "Edit", + "action": "Action", + "uploadImage": "Upload Image", + "fileTooLarge": "File too large, max {size}MB allowed", + "uploadFailed": "Upload failed", + "logOutTips": "Do you want to log out?", + "prevStep": "Previous Step", + "nextStep": "Next Step", + "unlimited": "Unlimited", + "startDate": "Start Date", + "endDate": "End Date", + "startTime": "Start Time", + "endTime": "End Time" + }, + "search": { + "placeholder": "Search page", + "historyTitle": "Search history", + "switchKeydown": "Navigate", + "selectKeydown": "Select", + "exitKeydown": "Close" + }, + "setting": { + "menuType": { + "title": "Menu Layout", + "list": [ + "Vertical", + "Horizontal", + "Mixed", + "Dual" + ] + }, + "theme": { + "title": "Theme Style", + "list": [ + "Light", + "Dark", + "System" + ] + }, + "menu": { + "title": "Menu Style" + }, + "color": { + "title": "Theme Color" + }, + "box": { + "title": "Box Style", + "list": [ + "Border", + "Shadow" + ] + }, + "container": { + "title": "Container Width", + "list": [ + "Full", + "Boxed" + ] + }, + "basics": { + "title": "Basic Config", + "list": { + "multiTab": "Show work tab", + "accordion": "Sidebar opens accordion", + "collapseSidebar": "Show sidebar button", + "reloadPage": "Show reload page button", + "fastEnter": "Show fast enter", + "breadcrumb": "Show crumb navigation", + "language": "Show multilingual selection", + "progressBar": "Show top progress bar", + "weakMode": "Color Weakness Mode", + "watermark": "Global watermark", + "menuWidth": "Menu width", + "tabStyle": "Tab style", + "pageTransition": "Page animation", + "borderRadius": "Custom radius" + } + }, + "tabStyle": { + "default": "Default", + "card": "Card", + "google": "Chrome" + }, + "transition": { + "list": { + "none": "None", + "fade": "Fade", + "slideLeft": "Slide Left", + "slideBottom": "Slide Bottom", + "slideTop": "Slide Top" + } + }, + "actions": { + "resetConfig": "Reset Config", + "copyConfig": "Copy Config", + "copySuccess": "Configuration copied to clipboard, paste it into src/config/setting.ts file", + "copyFailed": "Copy failed, please try again", + "resetFailed": "Reset failed, please refresh the page and try again" + } + }, + "notice": { + "title": "Notice", + "btnRead": "Mark as read", + "bar": [ + "Notice", + "Message", + "Todo" + ], + "text": [ + "No" + ], + "viewAll": "View all" + }, + "appAnnouncement": { + "list": { + "title": "Announcements", + "unreadCount": "Unread {count}", + "typePlaceholder": "Select announcement type", + "refresh": "Refresh", + "viewDetail": "View Detail", + "empty": "No announcements", + "publisher": "Publisher: ", + "publishedAt": "Published at: ", + "effective": "Effective: " + }, + "detail": { + "title": "Announcement Detail", + "back": "Back to List", + "publisher": "Publisher: ", + "publishedAt": "Published at: ", + "effective": "Effective: ", + "content": "Content" + }, + "type": { + "all": "All Types", + "system": "System Announcement", + "billing": "Billing Reminder", + "operation": "Operation Notice", + "platformUpdate": "Platform Update", + "security": "Security Notice", + "compliance": "Compliance Notice", + "tenantInternal": "Tenant Internal", + "tenantFinance": "Tenant Finance", + "tenantOperation": "Tenant Operation" + }, + "publisher": { + "platform": "Platform", + "tenant": "Tenant", + "unknown": "Unknown" + }, + "common": { + "notAvailable": "N/A", + "dateRange": "{start} ~ {end}" + }, + "message": { + "loadFailed": "Failed to load announcements, please try again.", + "missingId": "Missing announcement ID.", + "detailNotFound": "Announcement not found, please return to the list." + } + }, + "worktab": { + "btn": { + "refresh": "Refresh", + "fixed": "Fixed", + "unfixed": "Unfixed", + "closeLeft": "Close left", + "closeRight": "Close right", + "closeOther": "Close other", + "closeAll": "Close all" + } + }, + "login": { + "leftView": { + "title": "A backend system of beauty and efficiency", + "subTitle": "A sleek and practical interface for a great user experience" + }, + "title": "Welcome back", + "subTitle": "Please enter your account and password to login", + "roles": { + "super": "Super Admin", + "admin": "Admin", + "user": "User" + }, + "placeholder": { + "account": "Enter account", + "phone": "Enter phone number", + "password": "Please enter your password", + "slider": "Please slide to verify" + }, + "sliderText": "Please slide to verify", + "sliderSuccessText": "Verification successful", + "rememberPwd": "Remember password", + "forgetPwd": "Forgot password", + "btnText": "Login", + "noAccount": "No account yet?", + "register": "Register", + "success": { + "title": "Login successful", + "message": "Welcome back" + } + }, + "forgetPassword": { + "title": "Forgot password?", + "subTitle": "Enter your email to reset your password", + "placeholder": "Please enter your email", + "submitBtnText": "Submit", + "backBtnText": "Back" + }, + "resetPassword": { + "title": "Reset Password", + "subTitle": "Set a new login password", + "invalidToken": "The reset link is invalid or expired. Please ask the platform admin to generate a new link.", + "newPassword": "New Password", + "newPasswordPlaceholder": "Enter new password (6-32 chars)", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Re-enter new password", + "submitBtnText": "Reset", + "backBtnText": "Back to Login", + "success": "Password reset succeeded. Please log in with the new password.", + "rules": { + "newPasswordRequired": "Please enter a new password", + "confirmPasswordRequired": "Please confirm the new password", + "passwordLength": "Password length must be 6-32 characters", + "passwordNotMatch": "Passwords do not match" + } + }, + "register": { + "title": "Self-service onboarding", + "subTitle": "Provide admin info to spin up your space quickly", + "layout": { + "brand": "CloudSaaS", + "badge": "Self-Service Onboarding", + "badgeDesc": "Password is only for login and will not return", + "title": "Start Your", + "titleHighlight": "Digital Management Journey", + "desc": "Open your dedicated workspace in just a few steps. Enjoy secure, stable, and efficient enterprise-grade management.", + "pillSecurity": "Enterprise Security", + "pillData": "Data Connectivity", + "pillDeploy": "Rapid Deployment", + "footer": "© CloudSaaS Inc. All rights reserved.", + "formTitle": "Fill in administrator information", + "formDesc": "Get your tenant ready fast. The password is only for login and will not be returned.", + "optional": "Optional" + }, + "alert": { + "title": "Save your admin account and password", + "desc": "Password is only used for login and will not be returned. You can fill verification and subscription later on the progress page." + }, + "field": { + "code": "Tenant code", + "name": "Tenant name", + "shortName": "Short name", + "industry": "Industry", + "contactName": "Contact name", + "contactPhone": "Contact phone", + "contactEmail": "Contact email", + "tenantPackageId": "Package ID", + "durationMonths": "Duration (months)", + "autoRenew": "Auto renew on expiry", + "adminAccount": "Admin account", + "adminDisplayName": "Admin display name", + "adminEmail": "Admin email", + "adminPhone": "Admin phone", + "adminPassword": "Admin password", + "confirmPassword": "Confirm password" + }, + "placeholder": { + "code": "Enter tenant code, e.g. your-tenant", + "name": "Enter tenant name", + "shortName": "Optional short name", + "industry": "Optional industry", + "contactName": "Enter contact name", + "contactPhone": "Enter contact phone", + "contactEmail": "Optional contact email", + "tenantPackageId": "Enter package ID", + "durationMonths": "Subscription duration, default 12 months", + "adminAccount": "Enter admin account", + "adminDisplayName": "Optional admin nickname", + "adminEmail": "Optional admin email", + "adminPhone": "Enter admin phone", + "adminPassword": "Enter admin password, at least 8 chars", + "confirmPassword": "Re-enter admin password" + }, + "rule": { + "adminPhoneRequired": "Please enter admin phone", + "adminPhoneFormat": "Please enter a valid phone number", + "adminAccountRequired": "Please enter admin account", + "adminAccountFormat": "Admin account can only contain letters and numbers", + "adminPasswordRequired": "Please enter admin password", + "adminPasswordLength": "Admin password must be at least 8 chars", + "adminEmailFormat": "Please enter a valid email", + "adminEmailRequired": "Please enter admin email", + "adminDisplayNameRequired": "Please enter admin display name", + "confirmPasswordRequired": "Please confirm admin password", + "passwordMismatch": "The two admin passwords do not match", + "agreementRequired": "Please agree to the privacy policy first" + }, + "agreeText": "I have read and agree", + "privacyPolicy": "Privacy policy", + "submitBtnText": "Submit Registration Now", + "hasAccount": "Already have an account?", + "toLogin": "Go to login", + "success": "Registered successfully, redirecting to progress", + "failed": "Registration failed, please try again later", + "rateLimit": "Too many requests, please try again later" + }, + "onboarding": { + "breadcrumb": "Self-service onboarding", + "title": "Onboarding progress", + "waiting": { + "title": "Waiting for review", + "tip": "Your information has been submitted. Please wait for review." + }, + "error": { + "title": "Status error", + "tip": "The tenant status is abnormal. Please contact an administrator.", + "loading": "Loading status...", + "suspended": { + "title": "Service suspended", + "desc": "Your account has been suspended due to unpaid bills or policy violations. Please contact support.", + "primaryAction": "Contact support", + "secondaryAction": "View billing" + }, + "expired": { + "title": "Subscription expired", + "desc": "Your subscription has expired. Please renew to continue using the service.", + "primaryAction": "Renew now", + "secondaryAction": "Contact sales" + }, + "closed": { + "title": "Account closed", + "desc": "This account has been closed or archived and cannot be used. Please register again if needed.", + "primaryAction": "Register again", + "secondaryAction": "Back to home" + }, + "rejected": { + "title": "Verification rejected", + "desc": "Your onboarding information was rejected. Please update and resubmit.", + "reasonTitle": "Rejection reason", + "defaultReason": "No detailed reason provided. Please contact support.", + "primaryAction": "Update and resubmit", + "secondaryAction": "Contact support" + }, + "fallback": { + "title": "Status", + "desc": "This status cannot be displayed here. Please refresh and try again.", + "primaryAction": "Refresh" + } + }, + "placeholder": { + "tenantId": "Enter tenant ID", + "businessLicenseNumber": "Enter business license number", + "businessLicenseUrl": "Enter business license URL", + "legalPersonName": "Enter legal name", + "legalPersonIdNumber": "Enter legal ID number", + "legalPersonIdFrontUrl": "Enter ID front URL", + "legalPersonIdBackUrl": "Enter ID back URL", + "bankAccountName": "Enter bank account name", + "bankAccountNumber": "Enter bank account number", + "bankName": "Enter bank name", + "additionalDataJson": "Optional extra data JSON", + "tenantPackageId": "Enter package ID", + "durationMonths": "Enter duration (months)", + "notes": "Optional notes" + }, + "actions": { + "load": "Query", + "refresh": "Refresh", + "submitVerification": "Submit verification", + "goConsole": "Go to console", + "goLogin": "Back to login" + }, + "messages": { + "missingTenantInfo": "Missing tenant or package info, please go back and try again", + "submitSuccess": "Submitted for review and package bound successfully" + }, + "card": { + "tenantId": "Tenant ID", + "verificationStatus": "Verification status", + "tenantStatus": "Tenant status" + }, + "status": { + "verification": { + "draft": "Draft", + "pending": "Pending", + "rejected": "Rejected", + "approved": "Approved" + }, + "tenant": { + "active": "Active", + "pendingReview": "Pending review", + "suspended": "Suspended", + "expired": "Expired", + "closed": "Closed" + }, + "hintDraft": "Please submit verification", + "hintPending": "Waiting for review", + "hintRejected": "Review rejected, please resubmit", + "hintApproved": "Approved, you can enter console", + "unknown": "Unknown" + }, + "form": { + "title": "Submit verification", + "tip": "Fill business license and legal info to speed up review", + "rejected": "Review rejected, please update and resubmit", + "businessLicenseNumber": "Business license number", + "businessLicenseUrl": "Business license URL", + "legalPersonName": "Legal name", + "legalPersonIdNumber": "Legal ID number", + "legalPersonIdFrontUrl": "ID front URL", + "legalPersonIdBackUrl": "ID back URL", + "bankAccountName": "Bank account name", + "bankAccountNumber": "Bank account number", + "bankName": "Bank name", + "additionalDataJson": "Additional data JSON", + "tenantPackageId": "Package ID", + "durationMonths": "Duration (months)", + "notes": "Notes", + "autoRenew": "Auto renew", + "securityTip": "Data is only used for review and will not be leaked." + }, + "subscription": { + "title": "Plan subscription", + "tip": "Sign in to submit subscription and lock quota", + "loginTip": "Please log in to submit subscription", + "submit": "Submit subscription", + "failed": "Create subscription failed, please try again later" + }, + "pricing": { + "badge": "Self-service onboarding", + "freeTag": "Free for commercial use", + "title": "Trusted by 53,476+ developers", + "subtitle": "And chosen by many tech giants", + "defaultDesc": "For cloud products, billed annually by user.", + "perTime": "one-time payment", + "selectCta": "Choose this plan", + "empty": "No plans available, please try again later", + "features": { + "code": "Full source code", + "docs": "Technical docs", + "saasAuth": "SaaS license", + "singleProject": "Single project use", + "unlimitedProjects": "Unlimited projects", + "support": "1-year support", + "update": "1-year updates", + "limitStore": "Store limit: {value}", + "limitAccount": "Account limit: {value}", + "limitStorage": "Storage: {value}GB", + "unlimited": "Unlimited" + } + }, + "pending": { + "title": "Under review", + "subtitle": "We received your info, we will notify when done", + "polling": "Auto refresh in {seconds}s", + "tipsTitle": "Tips", + "tip1": "Avoid repeated submissions; update after a short wait.", + "tip2": "If you see 429, wait 1 minute before retry." + }, + "ready": { + "title": "Verification approved", + "subtitle": "You can enter the console now", + "tipsTitle": "Next steps", + "tip1": "Check roles and permissions after login.", + "tip2": "Adjust plan in console or contact support if needed." + }, + "fallback": { + "title": "Cannot enter console", + "tipsTitle": "Possible reasons", + "tip1": "Tenant is suspended or closed, please contact admin.", + "tip2": "Renew the plan after login if expired." + }, + "message": { + "noTenantId": "Please enter tenant ID first", + "rateLimit": "Too many requests, please try again later", + "loadFailed": "Failed to load progress, please retry", + "verificationSubmitted": "Verification submitted", + "submitFailed": "Submit failed, please retry later", + "subscriptionCreated": "Subscription created" + }, + "rule": { + "businessLicenseNumber": "Please enter business license number", + "legalPersonName": "Please enter legal name", + "legalPersonIdNumber": "Please enter valid ID number", + "bankAccountNumber": "Please enter valid bank account number", + "packageRequired": "Please select a package", + "durationRequired": "Please enter duration", + "durationRange": "Duration range 1-120 months" + } + }, + "lockScreen": { + "pwdError": "Password error", + "lock": { + "inputPlaceholder": "Please input lock screen password", + "btnText": "Lock" + }, + "unlock": { + "inputPlaceholder": "Please input unlock password", + "btnText": "Unlock", + "backBtnText": "Back to login" + } + }, + "greeting": { + "dawn": "Good morning!", + "morning": "Good morning!", + "afternoon": "Good afternoon!", + "evening": "Good evening!" + }, + "exceptionPage": { + "403": "Sorry, you do not have permission to access this page", + "404": "Sorry, the page you are trying to access does not exist", + "500": "Sorry, there was an error on the server", + "gohome": "Go Home" + }, + "termsOfService": { + "title": "Terms of Service" + }, + "menus": { + "login": { + "title": "Login" + }, + "register": { + "title": "Register" + }, + "termsOfService": { + "title": "Terms of Service" + }, + "onboarding": { + "status": "Onboarding progress", + "pricing": "Plan selection", + "waiting": "Waiting for review", + "error": "Status error" + }, + "forgetPassword": { + "title": "Forget Password" + }, + "resetPassword": { + "title": "Reset Password" + }, + "outside": { + "title": "Outside" + }, + "dashboard": { + "title": "Dashboard", + "console": "Console" + }, + "result": { + "title": "Result Page", + "success": "Success", + "fail": "Fail" + }, + "exception": { + "title": "Exception", + "forbidden": "403", + "notFound": "404", + "serverError": "500" + }, + "system": { + "title": "System Settings", + "user": "User Manage", + "role": "Role Manage", + "tenantRole": "Tenant Role", + "userCenter": "User Center", + "menu": "Menu Manage", + "tenant": "Tenant Manage", + "tenantPackage": "Package Management" + }, + "dictionary": { + "title": "Dictionary", + "system": "System Dictionary", + "tenant": "Tenant Dictionary", + "override": "Dictionary Overrides", + "labelOverride": "Label Override Management", + "metrics": "Cache Metrics" + }, + "tenant": { + "title": "Tenant Management", + "subscription": "Subscription Management", + "billing": "Billing Management", + "billingStatistics": "Billing Statistics" + }, + "announcement": { + "title": "Announcement Management", + "platform": { + "title": "Platform Announcements", + "list": "Platform Announcement List", + "create": "Create Platform Announcement", + "edit": "Edit Platform Announcement", + "detail": "Announcement Detail" + }, + "tenant": { + "title": "Tenant Announcements", + "list": "Tenant Announcement List", + "create": "Create Tenant Announcement", + "edit": "Edit Tenant Announcement", + "detail": "Announcement Detail" + }, + "app": { + "title": "App Announcements", + "list": "Announcement List", + "detail": "Announcement Detail" + }, + "drafts": { + "title": "Draft Center", + "list": "Draft List" + } + }, + "merchant": { + "title": "Merchant Management", + "list": "Merchants", + "review": "Merchant Review", + "detail": "Merchant Detail" + }, + "store": { + "title": "Store Management", + "list": "Store List", + "detail": "Store Detail" + }, + "platform": { + "title": "Platform Ops", + "storeAudits": "Store Audits", + "qualificationAlerts": "Qualification Alerts" + } + }, + "tenant": { + "search": { + "keyword": "Keyword", + "keywordPlaceholder": "Tenant name/code", + "status": "Status", + "statusPlaceholder": "All", + "tenantName": "Tenant Name", + "tenantNamePlaceholder": "Enter tenant name", + "contactName": "Contact Name", + "contactNamePlaceholder": "Enter contact name", + "contactPhone": "Contact Phone", + "contactPhonePlaceholder": "Enter contact phone", + "verificationStatus": "Verification Status" + }, + "action": { + "addTenant": "Add Tenant", + "edit": "Edit", + "detail": "Details", + "more": "More", + "impersonateLogin": "Impersonate Login", + "quotaOverview": "Quota Overview", + "toggleFreeze": "Freeze/Unfreeze", + "freeze": "Freeze", + "unfreeze": "Unfreeze", + "extendOrGift": "Extend/Gift Time", + "resetAdmin": "Reset Admin" + }, + "more": { + "impersonate": { + "title": "Impersonate Login", + "tip": "This will switch to the tenant console and log in as the tenant primary admin.", + "confirm": "Confirm", + "success": "Switched to tenant console", + "alreadyImpersonating": "You are already impersonating. Please exit first." + }, + "freeze": { + "freezeTitle": "Freeze Tenant", + "unfreezeTitle": "Unfreeze Tenant", + "freezeTip": "After freezing, the tenant will not be able to use the system. Please proceed with caution.", + "unfreezeTip": "After unfreezing, the tenant service will be restored (expired subscription will still show as expired).", + "reason": "Reason", + "reasonRequired": "Please enter the freeze reason", + "freezePlaceholder": "Enter freeze reason (required)", + "unfreezePlaceholder": "Enter unfreeze note (optional)", + "success": "Success" + }, + "extend": { + "title": "Extend/Gift Time", + "months": "Months", + "monthUnit": "months", + "monthsRequired": "Please enter months", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "expireUnknown": "Current expiration time is unknown. The extension will start from now.", + "currentExpireAt": "Current expiration time: {date}", + "success": "Success" + }, + "resetAdmin": { + "title": "Reset Admin", + "tip": "Generate a password reset link for the primary admin (shown only once).", + "generate": "Generate Link", + "resetUrl": "Reset Link", + "copy": "Copy", + "copied": "Copied", + "empty": "Please generate the link first", + "success": "Generated", + "failed": "Failed to generate. Please try again." + }, + "todo": { + "impersonateLogin": "Impersonate login is not implemented yet", + "quotaOverview": "Quota overview is not implemented yet", + "resetAdmin": "Reset admin is not implemented yet" + } + }, + "status": { + "pendingReview": "Pending review", + "active": "Active", + "suspended": "Suspended", + "expired": "Expired", + "closed": "Closed", + "unknown": "Unknown" + }, + "verificationStatus": { + "draft": "Draft", + "pending": "Pending", + "approved": "Approved", + "rejected": "Rejected", + "unknown": "Unknown" + }, + "subscriptionStatus": { + "active": "Active", + "expired": "Expired", + "cancelled": "Cancelled", + "suspended": "Suspended", + "unknown": "Unknown" + }, + "review": { + "title": "Tenant Review", + "dialogTitle": "Tenant Review", + "tenantName": "Tenant Name", + "result": "Review Result", + "approve": "Approve", + "reject": "Reject", + "rejectReason": "Reject Reason", + "rejectReasonPlaceholder": "Please enter reject reason", + "renewMonths": "Renew Months", + "renewMonthsPlaceholder": "Please enter renew months", + "monthUnit": "months", + "rejectReasonCustom": "Custom Reason", + "rejectReasons": { + "materialsIncomplete": "Incomplete materials / Missing documents", + "licenseInvalid": "Invalid or unclear business license", + "identityMismatch": "Legal person info mismatch", + "contactUnreachable": "Unreachable contact / Invalid info", + "duplicateRegistration": "Possible duplicate registration / Risk related", + "other": "Other (custom)" + }, + "claim": { + "label": "Claim", + "unclaimed": "Unclaimed", + "claimed": "Claimed", + "claimedBy": "Claimed by: {name}", + "needClaimTip": "Please claim before reviewing", + "claimButton": "Claim", + "releaseButton": "Release", + "forceButton": "Force Takeover", + "readonlyTip": "This review is claimed by {name}. You can view only.", + "needClaimFirst": "Please claim this review first", + "claimedByTip": "This review has been claimed by {name}", + "forceDenied": "No permission to force takeover" + }, + "selectResult": "Please select review result", + "success": "Review completed", + "action": "Review", + "openMaterial": "Open materials", + "section": { + "basic": "Application Info", + "package": "Package Info", + "subscription": "Subscription Info", + "verification": "Verification Info", + "action": "Review Action", + "history": "Review History" + }, + "history": { + "empty": "No records" + }, + "field": { + "name": "Tenant Name", + "code": "Tenant Code", + "packageId": "Package ID", + "packageName": "Selected Package", + "packageType": "Package Type", + "packageDescription": "Description", + "monthlyPrice": "Monthly Price", + "yearlyPrice": "Yearly Price", + "packageActive": "Enabled", + "packageQuota": "Quota", + "featurePoliciesJson": "Feature Policies (JSON)", + "subscriptionId": "Subscription ID", + "subscriptionStatus": "Subscription Status", + "subscriptionPackageId": "Subscription Package ID", + "subscriptionEffectiveFrom": "Effective From", + "subscriptionEffectiveTo": "Effective To", + "nextBillingDate": "Next Billing Date", + "autoRenew": "Auto Renew", + "contactName": "Contact Name", + "contactPhone": "Contact Phone", + "contactEmail": "Contact Email", + "effectiveFrom": "Submitted At", + "verificationStatus": "Verification Status", + "verificationSubmittedAt": "Verification Submitted At", + "businessLicenseNumber": "Business License No.", + "businessLicenseUrl": "Materials", + "legalPersonName": "Legal Person Name", + "legalPersonIdNumber": "Legal Person ID", + "legalPersonIdFrontUrl": "Legal Person ID (Front)", + "legalPersonIdBackUrl": "Legal Person ID (Back)", + "bankName": "Bank Name", + "bankAccountName": "Account Name", + "bankAccountNumber": "Bank Account", + "additionalDataJson": "Additional Info (JSON)", + "reviewRemarks": "Review Remarks", + "reviewedBy": "Reviewer ID", + "reviewedByName": "Reviewed By", + "reviewedAt": "Reviewed At" + }, + "operatingMode": "Operating mode", + "operatingModePlaceholder": "Select operating mode", + "operatingModeOptions": { + "same": "Same entity", + "different": "Different entity" + } + }, + "manualCreate": { + "title": "Create Tenant (Manual, Active)", + "success": "Created successfully", + "unit": { + "month": "month(s)" + }, + "section": { + "tenant": "Tenant Info", + "subscription": "Plan & Subscription", + "verification": "Verification Info", + "admin": "Admin Account", + "system": "System Fields (Read-only)" + }, + "field": { + "region": "Province/City/District", + "code": "Tenant Code", + "name": "Tenant Name", + "shortName": "Short Name", + "legalEntityName": "Legal Entity Name", + "industry": "Industry", + "status": "Tenant Status", + "contactName": "Contact Name", + "contactPhone": "Contact Phone", + "contactEmail": "Contact Email", + "website": "Website", + "logoUrl": "LogoUrl", + "coverImageUrl": "CoverImageUrl", + "country": "Country/Region", + "province": "Province/State", + "city": "City", + "district": "District", + "address": "Address", + "tags": "Tags", + "remarks": "Remarks", + "suspendedAt": "Suspended At", + "suspensionReason": "Suspension Reason", + "tenantPackageId": "Package", + "durationMonths": "Duration", + "subscriptionEffectiveFrom": "Effective From", + "subscriptionEffectiveToPreview": "Effective To", + "nextBillingDate": "Next Billing Date", + "autoRenew": "Auto Renew", + "subscriptionStatus": "Subscription Status", + "scheduledPackageId": "Scheduled Package ID", + "subscriptionNotes": "Subscription Notes", + "verificationStatus": "Verification Status", + "businessLicenseNumber": "Business License No.", + "businessLicenseUrl": "Business License URL", + "legalPersonName": "Legal Person Name", + "legalPersonIdNumber": "Legal Person ID No.", + "legalPersonIdFrontUrl": "ID Front URL", + "legalPersonIdBackUrl": "ID Back URL", + "bankAccountName": "Bank Account Name", + "bankAccountNumber": "Bank Account Number", + "bankName": "Bank Name", + "additionalDataJson": "Additional Data (JSON)", + "submittedAt": "Submitted At", + "reviewedAt": "Reviewed At", + "reviewedBy": "Reviewer ID", + "reviewedByName": "Reviewer Name", + "reviewRemarks": "Review Remarks", + "adminAccount": "Admin Account", + "adminDisplayName": "Admin Name", + "adminPassword": "Initial Password", + "adminAvatar": "Admin Avatar", + "adminMerchantId": "Merchant ID", + "primaryOwnerUserId": "Primary Owner UserId", + "tenantId": "TenantId", + "tenantCreatedAt": "Tenant.CreatedAt", + "tenantUpdatedAt": "Tenant.UpdatedAt", + "tenantDeletedAt": "Tenant.DeletedAt", + "subscriptionId": "SubscriptionId", + "verificationId": "VerificationId", + "tenantCreatedBy": "Tenant.CreatedBy", + "tenantUpdatedBy": "Tenant.UpdatedBy", + "tenantDeletedBy": "Tenant.DeletedBy", + "subscriptionDeletedAt": "Subscription.DeletedAt" + }, + "placeholder": { + "region": "Select province/city/district", + "code": "Enter tenant code (unique)", + "name": "Enter tenant name", + "shortName": "Enter short name", + "legalEntityName": "Enter legal entity name", + "industry": "Enter industry", + "contactName": "Enter contact name", + "contactPhone": "Enter contact phone (unique)", + "contactEmail": "Enter contact email", + "website": "Enter website", + "logoUrl": "Enter logo URL", + "coverImageUrl": "Enter cover URL", + "country": "Enter country/region", + "province": "Enter province/state", + "city": "Enter city", + "address": "Enter address", + "tags": "Comma-separated", + "remarks": "Enter remarks", + "suspensionReason": "Enter suspension reason", + "scheduledPackageId": "Optional", + "subscriptionNotes": "Optional", + "businessLicenseNumber": "Enter business license no.", + "businessLicenseUrl": "Enter business license URL", + "legalPersonName": "Enter legal person name", + "legalPersonIdNumber": "Enter legal person ID no.", + "legalPersonIdFrontUrl": "Enter ID front URL", + "legalPersonIdBackUrl": "Enter ID back URL", + "bankAccountName": "Enter bank account name", + "bankAccountNumber": "Enter bank account number", + "bankName": "Enter bank name", + "additionalDataJson": "Optional", + "reviewedByName": "Defaults to current user if empty", + "reviewRemarks": "Optional", + "adminAccount": "Enter admin account (unique)", + "adminDisplayName": "Enter admin name", + "adminPassword": "Enter initial password", + "adminAvatar": "Optional", + "adminMerchantId": "Optional", + "systemGenerated": "Generated by system", + "systemAutoFill": "Auto-filled by system" + }, + "action": { + "upload": "Upload", + "reupload": "Re-upload", + "remove": "Remove", + "noImage": "No image", + "tip": "jpg/png/webp supported, auto-fill after upload" + }, + "rule": { + "code": "Tenant code is required", + "name": "Tenant name is required", + "tenantStatus": "Please select tenant status", + "tenantPackageId": "Please select package", + "durationMonths": "Please enter duration (months)", + "subscriptionEffectiveFrom": "Please select effective from", + "subscriptionStatus": "Please select subscription status", + "verificationStatus": "Please select verification status", + "adminAccount": "Admin account is required", + "adminDisplayName": "Admin name is required", + "adminPassword": "Initial password is required" + }, + "subscriptionStatus": { + "pending": "Pending", + "active": "Active", + "gracePeriod": "Grace Period", + "cancelled": "Cancelled", + "suspended": "Suspended" + } + }, + "table": { + "index": "No.", + "name": "Tenant Name", + "code": "Tenant Code", + "contactName": "Contact Name", + "contactPhone": "Contact Phone", + "status": "Status", + "verificationStatus": "Verification Status", + "effectiveFrom": "Effective From", + "effectiveTo": "Effective To", + "action": "Actions" + } + }, + "tenantPackage": { + "actions": { + "create": "New Package", + "close": "Close", + "view": "View", + "edit": "Edit", + "more": "More", + "copy": "Copy", + "saveDraft": "Save Draft", + "publish": "Publish", + "rollbackDraft": "Revert to Draft", + "toggleActive": "Enable/Disable", + "toggleVisible": "Public Visible", + "togglePurchasable": "Allow Purchase", + "quotaEdit": "Quota", + "viewTenants": "Tenants", + "delete": "Delete" + }, + "usage": { + "activeSubscription": "Active Subscriptions", + "totalSubscription": "Total Subscriptions", + "activeTenant": "Active Tenants", + "mrr": "MRR", + "arr": "ARR", + "expiringIn": "Expiring" + }, + "search": { + "keyword": "Keyword", + "keywordPlaceholder": "Enter package name or description", + "status": "Status", + "statusAll": "All", + "statusEnabled": "Enabled", + "statusDisabled": "Disabled" + }, + "table": { + "name": "Name", + "packageType": "Package Type", + "publishStatus": "Publish Status", + "publicVisible": "Visible", + "allowNewPurchase": "Purchasable", + "monthlyPrice": "Monthly Price", + "yearlyPrice": "Yearly Price", + "quota": "Quota", + "usage": "Usage", + "status": "Status", + "description": "Description", + "actions": "Actions", + "quotaEmpty": "Not set" + }, + "featurePolicy": { + "open": "Visual Config", + "clear": "Clear", + "configured": "Configured", + "notConfigured": "Not configured", + "titleEdit": "Feature Policy", + "titleView": "View Feature Policy", + "hint": "Configure feature toggles and quotas visually. The result will be saved to featurePoliciesJson for future feature control by package.", + "saved": "Feature policy saved", + "invalidJson": "Invalid feature policy JSON", + "applyPreset": "Apply Preset", + "applyPresetTitle": "Confirm Apply", + "applyPresetConfirm": "Applying a preset will overwrite current feature toggles and quotas. Continue?", + "presetApplied": "Preset applied", + "validationPassed": "Validation passed", + "validationFailed": "Validation failed ({count})", + "tabs": { + "config": "Config", + "custom": "Custom", + "preview": "JSON Preview" + }, + "presets": { + "blank": "Blank", + "standard": "Standard", + "pro": "Pro" + }, + "customHint": "Add custom extension items for future expansion (e.g. hardware gifts, special services). Keys should use English/underscore and will be saved to extra.customItems.", + "custom": { + "add": "Add Item", + "key": "Key", + "label": "Label", + "type": "Type", + "value": "Value", + "typeBoolean": "Boolean", + "typeNumber": "Number", + "typeString": "Text" + }, + "sections": { + "features": "Features", + "quotas": "Quotas", + "preview": "JSON Preview (Read-only)" + }, + "features": { + "reportsExport": "Reports / Export", + "printing": "Printing / Receipt", + "apiAccess": "API Access", + "marketing": "Marketing", + "coupon": "Coupon", + "fullReduction": "Full Reduction", + "member": "Membership", + "points": "Points" + }, + "quotas": { + "maxProducts": "Max Products", + "maxMenus": "Max Menus", + "maxApiCallsPerDay": "API Calls / Day", + "unlimitedPlaceholder": "Empty means unlimited" + } + }, + "detail": { + "featureStrategy": { + "title": "Feature Strategy", + "invalidJson": "Invalid feature strategy JSON", + "empty": "No strategies to display", + "columns": { + "name": "Strategy", + "value": "Value", + "status": "Status", + "conditions": "Constraints" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "value": { + "unlimited": "Unlimited" + }, + "noConditions": "None", + "conditions": { + "dependsOnEnabled": "Depends on: {name}=Enabled", + "min": "Min: {min}", + "max": "Max: {max}", + "unknownDependency": "Unknown dependency ({key})" + } + } + }, + "form": { + "createTitle": "Create Package", + "editTitle": "Edit Package", + "viewTitle": "Package Details", + "copyTitle": "Copy Package", + "publishStatus": "Publish Status", + "publicVisible": "Public Visible", + "allowNewPurchase": "Allow Purchase", + "isRecommended": "Recommended", + "tags": "Tags", + "tagsPlaceholder": "Select tags (multiple)", + "name": "Package Name", + "namePlaceholder": "Enter package name", + "description": "Description", + "descriptionPlaceholder": "Enter package description", + "packageType": "Package Type", + "packageTypePlaceholder": "Select package type", + "monthlyPrice": "Monthly Price (CNY)", + "yearlyPrice": "Yearly Price (CNY)", + "pricePlaceholder": "Enter amount", + "maxStoreCount": "Store Limit (count)", + "maxAccountCount": "Employee Limit (count)", + "maxStorageGb": "Storage (GB)", + "maxSmsCredits": "SMS (messages)", + "maxDeliveryOrders": "Delivery Order Limit", + "quotaPlaceholder": "Enter limit or leave blank for unlimited", + "featurePoliciesJson": "Feature Policies (JSON)", + "featurePlaceholder": "Optional custom feature policy JSON", + "isActive": "Active", + "sortOrder": "Sort Order", + "sortOrderPlaceholder": "Smaller values come first", + "submit": "Submit", + "submitDraft": "Save Draft", + "submitPublish": "Publish", + "validation": { + "nameRequired": "Please enter package name", + "packageTypeRequired": "Please select package type" + } + }, + "tags": { + "recommended": "Recommended", + "bestValue": "Best Value", + "flagship": "Flagship" + }, + "drawer": { + "title": "Using Tenants ({name})", + "expiringTitle": "Expiring Tenants ({name} · within {days} days)", + "detailTitle": "Package Details ({name})", + "searchPlaceholder": "Tenant name/code/contact/phone", + "tenantName": "Tenant Name", + "tenantCode": "Tenant Code", + "tenantStatus": "Status", + "contact": "Contact", + "phone": "Phone", + "effectiveTo": "Expires At" + }, + "dialog": { + "quotaTitle": "Quota ({name})" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "publishStatus": { + "draft": "Draft", + "published": "Published" + }, + "message": { + "createSuccess": "Created successfully", + "saveDraftSuccess": "Draft saved", + "updateSuccess": "Updated successfully", + "publishConfirm": "Publish this package? After publishing (and visible/purchasable/enabled), new tenants can choose/purchase it.", + "publishTitle": "Publish Confirmation", + "publishSuccess": "Published", + "rollbackDraftConfirm": "Revert this package to draft? Draft packages are not publicly visible/sellable.", + "rollbackDraftTitle": "Revert Confirmation", + "rollbackDraftSuccess": "Reverted to draft", + "toggleVisibleConfirmEnable": "Set this package to publicly visible?", + "toggleVisibleConfirmDisable": "Set this package to not publicly visible?", + "toggleVisibleTitleEnable": "Visible Confirmation", + "toggleVisibleTitleDisable": "Hidden Confirmation", + "toggleVisibleSuccessEnable": "Now publicly visible", + "toggleVisibleSuccessDisable": "Now hidden", + "togglePurchasableConfirmEnable": "Allow new tenants to purchase/choose this package?", + "togglePurchasableConfirmDisable": "Disallow new tenants to purchase/choose this package?", + "togglePurchasableTitleEnable": "Allow Purchase Confirmation", + "togglePurchasableTitleDisable": "Disallow Purchase Confirmation", + "togglePurchasableSuccessEnable": "Purchase allowed", + "togglePurchasableSuccessDisable": "Purchase disallowed", + "toggleConfirmEnable": "Enable this package? It will be available for new tenants to choose/purchase.", + "toggleConfirmDisable": "Disable this package? New tenants will not be able to choose it (existing subscriptions are not affected).", + "toggleTitleEnable": "Enable Confirmation", + "toggleTitleDisable": "Disable Confirmation", + "toggleSuccessEnable": "Enabled", + "toggleSuccessDisable": "Disabled", + "deleteSuccess": "Deleted successfully", + "deleteConfirm": "Are you sure to delete this package? It will be soft deleted.", + "deleteTitle": "Delete Confirmation" + }, + "type": { + "free": "Free", + "standard": "Standard", + "professional": "Professional", + "enterprise": "Enterprise", + "unknown": "Unknown" + } + }, + "subscription": { + "title": "Subscription Management", + "search": { + "status": "Status", + "statusPlaceholder": "All Status", + "package": "Package", + "packagePlaceholder": "All Packages", + "tenantKeyword": "Tenant Keyword", + "tenantKeywordPlaceholder": "Tenant name / code", + "expireDateRange": "Expire Date", + "expireDateRangePlaceholder": "Select date range" + }, + "table": { + "index": "No.", + "tenantName": "Tenant Name", + "currentPackage": "Current Package", + "status": "Status", + "effectiveDate": "Effective Date", + "expireDate": "Expire Date", + "createdAt": "Created At", + "autoRenew": "Auto Renew", + "action": "Actions" + }, + "action": { + "detail": "View Details", + "more": "More", + "extend": "Extend", + "changePlan": "Change Plan", + "changeStatus": "Change Status" + }, + "status": { + "pending": "Pending", + "active": "Active", + "gracePeriod": "Grace Period", + "cancelled": "Cancelled", + "suspended": "Suspended" + }, + "detail": { + "title": "Subscription Details", + "basicInfo": "Basic Information", + "tenantName": "Tenant Name", + "currentPackage": "Current Package", + "status": "Status", + "effectiveDate": "Effective Date", + "expireDate": "Expire Date", + "autoRenew": "Auto Renew", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "saveNotes": "Save Notes", + "saveNotesSuccess": "Notes saved", + "autoRenewUpdateSuccess": "Auto renew updated", + "autoRenewUpdateFailed": "Failed to update auto renew", + "quotaUsage": "Quota Usage", + "noQuota": "No quota data", + "stores": "Stores", + "accounts": "Accounts", + "storage": "Storage", + "sms": "SMS Credits", + "deliveryOrders": "Delivery Orders", + "promotionSlots": "Promotion Slots", + "unlimited": "Unlimited", + "changeHistory": "Change History", + "noHistory": "No history records", + "changeType": { + "new": "New", + "renew": "Renew", + "upgrade": "Upgrade", + "downgrade": "Downgrade", + "unknown": "Change" + } + }, + "extend": { + "title": "Extend Subscription", + "duration": "Duration", + "durationPlaceholder": "Enter duration", + "unit": "Unit", + "unitDay": "Day(s)", + "unitMonth": "Month(s)", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "preview": "New Expire Date", + "success": "Extended successfully", + "validation": { + "durationRequired": "Please enter duration", + "durationMin": "Duration must be at least 1" + } + }, + "changePlan": { + "title": "Change Plan", + "currentPackage": "Current Package", + "newPackage": "New Package", + "newPackagePlaceholder": "Select new package", + "effectiveTime": "Effective Time", + "effectiveNow": "Immediately", + "effectiveNextCycle": "Next Cycle", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "success": "Plan changed successfully", + "validation": { + "packageRequired": "Please select new package" + } + }, + "changeStatus": { + "title": "Change Status", + "currentStatus": "Current Status", + "newStatus": "New Status", + "newStatusPlaceholder": "Select new status", + "reason": "Reason", + "reasonPlaceholder": "Enter reason", + "confirm": "Change subscription status to {status}?", + "success": "Status changed successfully", + "validation": { + "statusRequired": "Please select new status", + "reasonRequired": "Please enter reason" + } + } + }, + "quotaPackage": { + "title": "Quota Package Management", + "tabs": { + "packages": "Packages", + "purchases": "Purchase Records", + "dashboard": "Quota Dashboard", + "alertConfig": "Alert Settings" + }, + "search": { + "quotaType": "Quota Type", + "quotaTypePlaceholder": "All Types", + "status": "Status", + "statusPlaceholder": "All Status" + }, + "table": { + "index": "No.", + "name": "Package Name", + "quotaType": "Quota Type", + "quotaValue": "Quota Value", + "price": "Price", + "status": "Status", + "sortOrder": "Sort Order", + "action": "Actions" + }, + "action": { + "create": "Create Package" + }, + "quotaType": { + "storeCount": "Store Count", + "accountCount": "Account Count", + "storage": "Storage", + "smsCredits": "SMS Credits", + "deliveryOrders": "Delivery Orders", + "promotionSlots": "Promotion Slots" + }, + "status": { + "active": "Active", + "inactive": "Inactive" + }, + "unit": { + "sms": "messages", + "orders": "orders" + }, + "benefit": { + "featureStrategy": { + "title": "Feature policy", + "invalidJson": "Invalid feature policy JSON", + "notConfigured": "Feature policy not configured", + "empty": "No policy items to display", + "columns": { + "name": "Policy", + "value": "Value", + "status": "Status", + "conditions": "Conditions" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "value": { + "unlimited": "Unlimited" + }, + "noConditions": "None", + "conditions": { + "dependsOnEnabled": "Depends on: {name}=enabled", + "min": "Min: {min}", + "max": "Max: {max}", + "unknownDependency": "Unknown dependency ({key})" + } + } + }, + "form": { + "createTitle": "Create Quota Package", + "editTitle": "Edit Quota Package", + "name": "Package Name", + "namePlaceholder": "Enter package name", + "quotaType": "Quota Type", + "quotaTypePlaceholder": "Select quota type", + "quotaValue": "Quota Value", + "quotaValuePlaceholder": "Enter quota value", + "price": "Price", + "pricePlaceholder": "Enter price", + "description": "Description", + "descriptionPlaceholder": "Enter description (optional)", + "sortOrder": "Sort Order", + "isActive": "Active Status" + }, + "purchase": { + "title": "Purchase Quota Package", + "tenant": "Tenant", + "quotaPackage": "Quota Package", + "quotaPackagePlaceholder": "Select quota package", + "price": "Price", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "confirm": "Confirm Purchase", + "success": "Purchased successfully" + }, + "purchases": { + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant" + }, + "table": { + "index": "No.", + "tenantName": "Tenant Name", + "quotaPackageName": "Quota Package", + "quotaType": "Quota Type", + "quotaValue": "Quota Value", + "price": "Price", + "purchasedAt": "Purchased At", + "expiredAt": "Expired At" + }, + "message": { + "selectTenantFirst": "Please select a tenant first" + } + }, + "dashboard": { + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant" + }, + "empty": { + "selectTenant": "Please select a tenant to view quota usage" + }, + "meta": { + "used": "Used", + "total": "Total" + }, + "threshold": { + "reached": "Reached {threshold}%" + }, + "message": { + "selectTenantFirst": "Please select a tenant first" + } + }, + "alertConfig": { + "tip": { + "title": "Alert Threshold", + "description": "When quota usage reaches or exceeds the threshold, the dashboard will show an alert indicator." + }, + "action": { + "reset": "Reset", + "resetDefault": "Restore Default", + "save": "Save" + }, + "message": { + "saveSuccess": "Saved successfully", + "resetSuccess": "Reset successfully", + "resetDefaultSuccess": "Default thresholds restored" + } + }, + "validation": { + "nameRequired": "Please enter package name", + "nameMaxLength": "Name cannot exceed 50 characters", + "quotaTypeRequired": "Please select quota type", + "quotaValueRequired": "Please enter quota value", + "priceRequired": "Please enter price", + "packageRequired": "Please select quota package" + }, + "message": { + "createSuccess": "Created successfully", + "updateSuccess": "Updated successfully", + "deleteConfirm": "Are you sure you want to delete this quota package?", + "deleteSuccess": "Deleted successfully", + "statusUpdateSuccess": "Status updated successfully" + } + }, + "announcementDrafts": { + "title": "Draft Center", + "filter": { + "type": "Type", + "typePlaceholder": "All Types", + "date": "Date", + "dateStart": "Start date", + "dateEnd": "End date" + }, + "table": { + "title": "Title", + "type": "Type", + "lastSaved": "Last Saved", + "author": "Author", + "empty": "No drafts" + }, + "action": { + "continueEdit": "Continue Editing", + "publish": "Publish" + }, + "message": { + "loadFailed": "Failed to load drafts, please try again later", + "deleteConfirm": "Delete this draft?", + "deleteTitle": "Delete Draft", + "deleteSuccess": "Draft deleted", + "publishConfirm": "Publish this draft?", + "publishTitle": "Publish Draft", + "publishSuccess": "Draft published", + "missingRowVersion": "Missing version info, cannot publish", + "localPublishHint": "Local drafts must be published from the editor", + "draftNotFound": "Draft not found or expired", + "routeNotFound": "Editor route not found, please check routing" + }, + "pagination": { + "total": "Total {count} drafts" + }, + "source": { + "local": "Local Draft", + "server": "Server Draft" + }, + "type": { + "all": "All", + "system": "System Announcement", + "billing": "Billing/Subscription Reminder", + "operation": "Operations Notice", + "systemPlatformUpdate": "Platform System Update", + "systemSecurityNotice": "System Security Notice", + "systemCompliance": "System Compliance Notice", + "tenantInternal": "Tenant Internal Notice", + "tenantFinance": "Tenant Finance Notice", + "tenantOperation": "Tenant Operations Notice", + "unknown": "Unknown Type" + }, + "field": { + "untitled": "Untitled Draft", + "unknownAuthor": "Unknown", + "unknown": "Unknown" + } + }, + "table": { + "form": { + "reset": "Reset", + "submit": "Submit" + }, + "searchBar": { + "reset": "Reset", + "search": "Search", + "expand": "Expand", + "collapse": "Collapse", + "searchInputPlaceholder": "Please enter", + "searchSelectPlaceholder": "Please select" + }, + "selection": "Select", + "sizeOptions": { + "small": "Compact", + "default": "Default", + "large": "Loose" + }, + "column": { + "selection": "Select", + "expand": "Expand", + "index": "Index" + }, + "zebra": "Zebra", + "border": "Border", + "headerBackground": "Header BG" + }, + "billing": { + "title": "Billing Management", + "quickFilter": { + "title": "Quick Filter", + "all": "All" + }, + "search": { + "tenant": "Tenant", + "tenantPlaceholder": "Search tenant name", + "billingType": "Billing Type", + "billingTypePlaceholder": "Select billing type", + "status": "Status", + "statusPlaceholder": "Select status", + "dateRange": "Date Range", + "dateRangePlaceholder": "Select date range", + "amountRange": "Amount Range", + "minAmountPlaceholder": "Min amount", + "maxAmountPlaceholder": "Max amount", + "keyword": "Keyword", + "keywordPlaceholder": "Statement No. or Tenant Name" + }, + "field": { + "statementNo": "Statement No.", + "tenantName": "Tenant Name", + "billingType": "Billing Type", + "amount": "Amount", + "amountDue": "Amount Due", + "amountPaid": "Amount Paid", + "status": "Status", + "dueDate": "Due Date", + "periodStart": "Period Start", + "periodEnd": "Period End", + "createdAt": "Created At", + "notes": "Notes" + }, + "billingType": { + "subscription": "Subscription", + "quotaPurchase": "Quota Purchase", + "manual": "Manual", + "renewal": "Renewal" + }, + "status": { + "pending": "Pending", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "action": { + "export": "Export", + "create": "Create Bill", + "detail": "View Detail", + "markPaid": "Mark Paid", + "cancel": "Cancel", + "recordPayment": "Record Payment", + "exportExcel": "Export Excel", + "exportPdf": "Export PDF", + "addLineItem": "Add Line Item", + "batch": "Batch Actions", + "batchMarkPaid": "Batch Mark Paid", + "batchExportExcel": "Batch Export Excel", + "batchExportPdf": "Batch Export PDF", + "uploadProof": "Upload Proof" + }, + "dialog": { + "createTitle": "Create Bill Manually", + "recordPaymentTitle": "Confirm Payment", + "proofPreviewTitle": "Proof Preview" + }, + "drawer": { + "title": "Bill Detail", + "basicInfo": "Basic Information", + "paymentList": "Payment Records", + "tabs": { + "basic": "Basic", + "lineItems": "Line Items", + "payments": "Payments", + "statusFlow": "Status Flow" + }, + "openProof": "Open Proof", + "noPayments": "No payment records", + "noStatusFlow": "No status flow records" + }, + "view": { + "timeline": "Timeline", + "table": "Table" + }, + "lineItem": { + "itemType": "Item Type", + "description": "Description", + "quantity": "Quantity", + "unitPrice": "Unit Price", + "amount": "Amount" + }, + "statusFlow": { + "created": "Created", + "due": "Due", + "paid": "Paid", + "overdue": "Overdue", + "cancelled": "Cancelled" + }, + "hint": { + "amountAutoSum": "Amount is summed from line items" + }, + "placeholder": { + "selectTenant": "Select tenant", + "selectBillingType": "Select billing type", + "enterAmount": "Enter amount", + "selectDueDate": "Select due date", + "enterNotes": "Enter notes", + "enterItemType": "e.g., Subscription/Quota/Manual", + "enterDescription": "Enter description", + "enterPaymentAmount": "Enter payment amount", + "selectPaymentMethod": "Select payment method", + "enterTransactionNo": "Enter transaction no. (optional)", + "enterProofUrl": "Enter proof URL (optional)", + "enterPaymentNotes": "Enter payment notes (optional)" + }, + "validation": { + "tenantRequired": "Please select tenant", + "billingTypeRequired": "Please select billing type", + "amountRequired": "Please enter amount", + "amountMin": "Amount must be greater than 0", + "dueDateRequired": "Please select due date", + "lineItemsRequired": "Please add at least 1 line item", + "lineItemDescriptionRequired": "Line item #{index}: description is required", + "lineItemQuantityInvalid": "Line item #{index}: quantity must be greater than 0", + "lineItemUnitPriceInvalid": "Line item #{index}: unit price cannot be negative", + "paymentAmountRequired": "Please enter payment amount", + "paymentAmountMin": "Payment amount must be greater than 0", + "paymentAmountExceedRemain": "Payment amount cannot exceed remaining amount", + "paymentMethodRequired": "Please select payment method" + }, + "message": { + "createSuccess": "Bill created successfully", + "loadDetailFailed": "Failed to load billing detail", + "sortFieldNotSupported": "This sort field is not supported", + "cancelConfirm": "Are you sure to cancel this bill?", + "cancelByAdmin": "Cancelled by admin", + "cancelSuccess": "Bill cancelled", + "recordPaymentSuccess": "Payment confirmed", + "exportNoData": "No data to export", + "exportExcelSuccess": "Exported {count} records", + "exportSuccess": "Exported {count} records", + "exportFailed": "Export failed, please retry later", + "exportPdfNeedDetail": "Please open bill detail or select exactly 1 bill before exporting PDF", + "exportPdfNeedSingleSelection": "Please select exactly 1 bill to export PDF", + "exportPdfPopupBlocked": "Popup blocked by the browser. Please allow popups and retry.", + "exportPdfOpened": "Print window opened. Choose “Save as PDF” in the print dialog.", + "noSelection": "Please select bills first", + "batchNoPending": "No pending bills in selection", + "batchMarkPaidNotes": "Batch marked as paid", + "batchMarkPaidSuccess": "Confirmed payment for {count} bills", + "batchCancelNotes": "Batch cancelled", + "batchCancelSuccess": "Cancelled {count} bills", + "proofTypeNotAllowed": "Only JPG/PNG files are allowed", + "proofTooLarge": "File too large. Max {size}MB", + "proofUploadSuccess": "Proof uploaded successfully" + }, + "export": { + "title": "Export Settings", + "format": "Format", + "scope": "Scope", + "fields": "Fields", + "currentPage": "Current Page", + "selected": "Selected", + "all": "All", + "confirm": "Export", + "dateRange": "Date Range", + "dateRangePlaceholder": "Select date range", + "formatRequired": "Please select format", + "scopeRequired": "Please select scope", + "fieldsRequired": "Please select at least one field", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "sheetName": "Bills", + "fileName": "bills_export", + "selectedSuffix": "selected", + "noPayments": "No payment records" + }, + "batch": { + "confirmPayment": "Batch Confirm Payment", + "cancel": "Batch Cancel", + "export": "Batch Export", + "confirmPaymentTip": "Confirm payment for {count} bills in batch?", + "cancelTip": "Cancel {count} bills in batch?", + "selectedCount": "Selected {count} items" + }, + "payment": { + "amount": "Amount", + "method": "Method", + "status": "Status", + "transactionNo": "Transaction No.", + "paidAt": "Paid At", + "notes": "Notes", + "remainAmount": "Remain Amount", + "proofUrl": "Proof URL", + "proofUrlHint": "URL link to payment proof image", + "proofUploadHint": "JPG/PNG only, max 10MB. URL will be filled after upload." + }, + "paymentMethod": { + "online": "Online", + "bankTransfer": "Bank Transfer", + "other": "Other" + }, + "paymentStatus": { + "pending": "Pending", + "success": "Success", + "failed": "Failed", + "refunded": "Refunded" + } + }, + "dashboard": { + "title": "Operations Dashboard", + "overview": { + "title": "Subscription Overview", + "totalActive": "Active Subscriptions", + "expiringIn7Days": "Expiring in 7 Days", + "expiringIn3Days": "Expiring in 3 Days", + "expiringIn1Day": "Expiring Tomorrow", + "expired": "Expired", + "pending": "Pending", + "suspended": "Suspended" + }, + "expiring": { + "title": "Expiring Subscriptions", + "viewAll": "View All", + "noData": "No expiring subscriptions", + "daysLeft": "Days Left" + }, + "revenue": { + "title": "Revenue Statistics", + "total": "Total Revenue", + "monthly": "Monthly Revenue", + "quarterly": "Quarterly Revenue" + }, + "quotaRanking": { + "title": "Quota Usage Ranking", + "tenant": "Tenant", + "usage": "Usage", + "limit": "Limit", + "percentage": "Usage Rate", + "rank": "Rank", + "quotaType": "Quota Type", + "selectType": "Select Quota Type", + "type": { + "orders": "Orders", + "storage": "Storage", + "users": "Users" + } + } + }, + "batch": { + "selectedCount": "Selected {count} items", + "extend": { + "title": "Batch Extend", + "selectedCount": "Selected {count} subscriptions", + "duration": "Duration", + "notes": "Notes", + "notesPlaceholder": "Enter notes (optional)", + "success": "Batch extend successful", + "partialSuccess": "Batch extend partially successful, {success} succeeded, {failed} failed", + "result": "{success} succeeded, {failed} failed", + "noSelection": "Please select subscriptions to extend", + "failedItems": "Failed Items" + }, + "remind": { + "title": "Batch Remind", + "selectedCount": "Selected {count} subscriptions", + "content": "Reminder Content", + "contentPlaceholder": "Enter reminder content", + "success": "Batch reminders sent successfully", + "partialSuccess": "Batch reminders partially successful, {success} succeeded, {failed} failed", + "result": "{success} sent, {failed} failed", + "noSelection": "Please select subscriptions to remind", + "failedItems": "Failed Items", + "validation": { + "contentRequired": "Please enter reminder content", + "contentMin": "Reminder content must be at least 10 characters" + } + } + }, + "user": { + "action": { + "create": "Create User", + "view": "View", + "edit": "Edit", + "more": "More", + "enable": "Enable", + "disable": "Disable", + "unlock": "Unlock", + "resetPassword": "Reset Password", + "restore": "Restore", + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm" + }, + "avatar": { + "upload": "Upload", + "reupload": "Reupload", + "remove": "Remove", + "noImage": "No Image", + "tip": "Supports jpg/png/webp, auto-filled after upload" + }, + "batch": { + "title": "Batch Actions", + "selected": "{count} selected", + "enable": "Batch Enable", + "disable": "Batch Disable", + "delete": "Batch Delete", + "restore": "Batch Restore", + "export": "Batch Export" + }, + "detail": { + "title": "User Details", + "rolesTitle": "Roles", + "permissionsTitle": "Permissions" + }, + "empty": { + "roles": "No roles", + "permissions": "No permissions" + }, + "field": { + "userId": "User ID", + "tenant": "Tenant", + "account": "Account", + "displayName": "Display Name", + "phone": "Phone", + "email": "Email", + "status": "Status", + "createdAt": "Created At", + "lastLoginAt": "Last Login", + "password": "Password", + "roles": "Roles", + "avatar": "Avatar" + }, + "status": { + "active": "Active", + "disabled": "Disabled", + "locked": "Locked", + "deleted": "Deleted", + "unknown": "Unknown" + }, + "table": { + "avatar": "Avatar", + "account": "Account", + "displayName": "Display Name", + "phone": "Phone", + "email": "Email", + "roles": "Roles", + "status": "Status", + "createdAt": "Created At", + "lastLoginAt": "Last Login", + "actions": "Actions", + "tenant": "Tenant" + }, + "search": { + "keyword": "Keyword", + "keywordPlaceholder": "Account/Name/Phone/Email", + "status": "Status", + "statusPlaceholder": "All Statuses", + "role": "Role", + "rolePlaceholder": "Select role", + "createdRange": "Created At", + "createdRangeStart": "Start", + "createdRangeEnd": "End", + "lastLoginRange": "Last Login", + "lastLoginRangeStart": "Start", + "lastLoginRangeEnd": "End", + "includeDeleted": "Include Deleted", + "includeDeletedOn": "Include", + "includeDeletedOff": "Exclude", + "tenant": "Tenant", + "tenantPlaceholder": "Select tenant" + }, + "placeholder": { + "tenant": "Select tenant", + "account": "Enter account", + "displayName": "Enter display name", + "password": "Enter password", + "phone": "Enter phone", + "email": "Enter email", + "avatar": "Enter avatar URL", + "roles": "Select roles", + "status": "Select status" + }, + "dialog": { + "createTitle": "Create User", + "editTitle": "Edit User" + }, + "validation": { + "tenantRequired": "Please select a tenant", + "accountRequired": "Please enter an account", + "displayNameRequired": "Please enter a display name", + "passwordRequired": "Please enter a password", + "passwordLength": "Password length must be 6-32 characters", + "phoneInvalid": "Please enter a valid phone number", + "emailInvalid": "Please enter a valid email address" + }, + "message": { + "createSuccess": "User created successfully", + "missingUserId": "Missing user ID, cannot submit", + "rowVersionMissing": "Missing row version, please refresh and retry", + "updateSuccess": "User updated successfully", + "concurrencyConflict": "Data has been updated. Please refresh and retry: {message}", + "statusConfirm": "Confirm change status for {name}?", + "statusSuccess": "Status updated successfully", + "deleteConfirm": "Confirm delete {name}?", + "deleteSuccess": "User deleted", + "restoreConfirm": "Confirm restore {name}?", + "restoreSuccess": "User restored", + "resetConfirm": "Confirm reset password for {name}?", + "resetToken": "Reset token: {token}\nExpires at: {expiresAt}", + "batchEmpty": "Please select users first", + "batchLimit": "Up to 100 users can be processed at once", + "batchConfirm": "Confirm batch action for {count} users?", + "batchResult": "Succeeded {success}, failed {failure}.\nFailure details:\n{details}", + "batchSuccess": "Batch action succeeded for {count} users", + "exportSuccess": "Exported {count} user records" + }, + "export": { + "fileName": "users" + } + }, + "announcement": { + "audience": { + "title": "Audience Selector", + "targetType": "Target Type", + "targetTypeHint": "Choose the audience for this announcement", + "type": { + "all": "All Tenants/Users", + "roles": "Specific Roles", + "users": "Specific Users", + "rules": "Rule-Based", + "manual": "Manual Selection" + }, + "helper": { + "all": "Announcement will be sent to all tenants or users", + "roles": "Filter recipients by role", + "users": "Add specific users by search", + "rules": "Filter by department, role, and tags", + "manual": "Select users via search and transfer list" + }, + "rules": { + "departments": "Department", + "roles": "Role", + "tags": "Tags", + "departmentsPlaceholder": "Select departments", + "tagsPlaceholder": "Select or type tags", + "emptyDepartments": "No departments available", + "emptyRoles": "No roles available", + "estimateLabel": "Estimated Audience", + "estimateLoading": "Estimating..." + }, + "users": { + "placeholder": "Search users by name/phone/email", + "unknownUser": "User {id}" + }, + "manual": { + "searchPlaceholder": "Search by name/phone/email", + "transferLeft": "Candidates", + "transferRight": "Selected", + "filterPlaceholder": "Type to filter", + "estimateLabel": "Selected Count" + }, + "errors": { + "loadRolesFailed": "Failed to load roles", + "loadUsersFailed": "Failed to load users" + } + } + } +} diff --git a/src/locales/langs/zh.json b/src/locales/langs/zh.json new file mode 100644 index 0000000..9bad553 --- /dev/null +++ b/src/locales/langs/zh.json @@ -0,0 +1,2211 @@ +{ + "httpMsg": { + "unauthorized": "未授权访问,请重新登录", + "forbidden": "禁止访问该资源", + "notFound": "请求的资源不存在", + "methodNotAllowed": "请求方法不允许", + "conflict": "数据已被他人修改,请刷新后重试", + "validationFailed": "请求参数验证失败", + "requestTimeout": "请求超时,请稍后重试", + "internalServerError": "服务器内部错误,请稍后重试", + "badGateway": "网关错误,请稍后重试", + "serviceUnavailable": "服务暂时不可用,请稍后重试", + "gatewayTimeout": "网关超时,请稍后重试", + "requestCancelled": "请求已取消", + "networkError": "网络连接异常,请检查网络连接", + "requestFailed": "请求失败", + "requestConfigError": "请求配置错误" + }, + "topBar": { + "search": { + "title": "搜索" + }, + "user": { + "userCenter": "个人中心", + "docs": "使用文档", + "github": "Github", + "lockScreen": "锁定屏幕", + "logout": "退出登录" + }, + "guide": { + "title": "点击这里查看", + "theme": "主题风格", + "menu": "开启顶栏菜单", + "description": "等更多配置" + }, + "impersonation": { + "active": "伪装中(租户 {tenantId})", + "exit": "退出伪装", + "confirm": "确认退出伪装并返回平台后台?" + } + }, + "common": { + "id": "ID", + "tips": "提示", + "cancel": "取消", + "confirm": "确定", + "yes": "是", + "no": "否", + "days": "天", + "delete": "删除", + "edit": "编辑", + "action": "操作", + "uploadImage": "上传图片", + "fileTooLarge": "文件体积过大,最大支持 {size}MB", + "uploadFailed": "上传失败", + "logOutTips": "您是否要退出登录?", + "prevStep": "上一步", + "nextStep": "下一步", + "unlimited": "不限", + "startDate": "开始日期", + "endDate": "结束日期", + "startTime": "开始时间", + "endTime": "结束时间", + "tips": "提示", + "error": { + "operationFailed": "操作失败,请重试" + } + }, + "search": { + "placeholder": "搜索页面", + "historyTitle": "搜索历史", + "switchKeydown": "切换", + "selectKeydown": "选择", + "exitKeydown": "关闭" + }, + "setting": { + "menuType": { + "title": "菜单布局", + "list": ["垂直", "水平", "混合", "双列"] + }, + "theme": { + "title": "主题风格", + "list": ["浅色", "深色", "系统"] + }, + "menu": { + "title": "菜单风格" + }, + "color": { + "title": "系统主题色" + }, + "box": { + "title": "盒子样式", + "list": ["边框", "阴影"] + }, + "container": { + "title": "容器宽度", + "list": ["铺满", "定宽"] + }, + "basics": { + "title": "基础配置", + "list": { + "multiTab": "开启多标签栏", + "accordion": "侧边栏开启手风琴模式", + "collapseSidebar": "显示折叠侧边栏按钮", + "fastEnter": "显示快速入口", + "reloadPage": "显示重载页面按钮", + "breadcrumb": "显示全局面包屑导航", + "language": "显示多语言选择", + "progressBar": "显示顶部进度条", + "weakMode": "色弱模式", + "watermark": "全局水印", + "menuWidth": "菜单宽度", + "tabStyle": "标签页风格", + "pageTransition": "页面切换动画", + "borderRadius": "自定义圆角" + } + }, + "tabStyle": { + "default": "默认", + "card": "卡片", + "google": "谷歌" + }, + "transition": { + "list": { + "none": "无动画", + "fade": "淡入淡出", + "slideLeft": "左侧滑入", + "slideBottom": "下方滑入", + "slideTop": "上方滑入" + } + }, + "actions": { + "resetConfig": "重置配置", + "copyConfig": "复制配置", + "copySuccess": "配置已复制到剪贴板,可粘贴到 src/config/setting.ts 文件中", + "copyFailed": "复制失败,请重试", + "resetFailed": "重置失败,请刷新页面后重试" + } + }, + "notice": { + "title": "通知", + "btnRead": "标为已读", + "bar": ["通知", "消息", "代办"], + "text": ["暂无"], + "viewAll": "查看全部" + }, + "appAnnouncement": { + "list": { + "title": "公告中心", + "unreadCount": "未读 {count} 条", + "typePlaceholder": "请选择公告类型", + "refresh": "刷新列表", + "viewDetail": "查看详情", + "empty": "暂无可读公告", + "publisher": "发布者:", + "publishedAt": "发布时间:", + "effective": "有效期:" + }, + "detail": { + "title": "公告详情", + "back": "返回列表", + "publisher": "发布者:", + "publishedAt": "发布时间:", + "effective": "有效期:", + "content": "公告内容" + }, + "type": { + "all": "全部类型", + "system": "系统公告", + "billing": "账单提醒", + "operation": "运营通知", + "platformUpdate": "平台系统更新", + "security": "安全公告", + "compliance": "合规公告", + "tenantInternal": "租户内部公告", + "tenantFinance": "租户财务公告", + "tenantOperation": "租户运营公告" + }, + "publisher": { + "platform": "平台", + "tenant": "租户", + "unknown": "未知" + }, + "common": { + "notAvailable": "暂无", + "dateRange": "{start} ~ {end}" + }, + "message": { + "loadFailed": "加载公告失败,请稍后重试", + "missingId": "缺少公告ID,无法加载详情", + "detailNotFound": "未获取到公告详情,请返回列表重试" + } + }, + "worktab": { + "btn": { + "refresh": "刷新", + "fixed": "固定", + "unfixed": "取消固定", + "closeLeft": "关闭左侧", + "closeRight": "关闭右侧", + "closeOther": "关闭其他", + "closeAll": "关闭全部" + } + }, + "login": { + "leftView": { + "title": "一款兼具设计美学与高效开发的后台系统", + "subTitle": "美观实用的界面,经过视觉优化,确保卓越的用户体验" + }, + "title": "欢迎回来", + "subTitle": "输入您的账号和密码登录", + "roles": { + "super": "超级管理员", + "admin": "管理员", + "user": "普通用户" + }, + "placeholder": { + "account": "请输入账号名", + "phone": "请输入手机号", + "password": "请输入密码", + "slider": "请拖动滑块完成验证" + }, + "sliderText": "按住滑块拖动", + "sliderSuccessText": "验证成功", + "rememberPwd": "记住密码", + "forgetPwd": "忘记密码", + "btnText": "登录", + "noAccount": "还没有账号?", + "register": "注册", + "success": { + "title": "登录成功", + "message": "欢迎回来" + } + }, + "forgetPassword": { + "title": "忘记密码?", + "subTitle": "输入您的电子邮件来重置您的密码", + "placeholder": "请输入您的电子邮件", + "submitBtnText": "提交", + "backBtnText": "返回" + }, + "resetPassword": { + "title": "重置密码", + "subTitle": "请设置一个新的登录密码", + "invalidToken": "重置链接无效或已过期,请联系平台管理员重新生成链接", + "newPassword": "新密码", + "newPasswordPlaceholder": "请输入新密码(6~32 位)", + "confirmPassword": "确认新密码", + "confirmPasswordPlaceholder": "请再次输入新密码", + "submitBtnText": "确认重置", + "backBtnText": "返回登录", + "success": "密码重置成功,请使用新密码登录", + "rules": { + "newPasswordRequired": "请输入新密码", + "confirmPasswordRequired": "请再次输入新密码", + "passwordLength": "密码长度需为 6~32 位", + "passwordNotMatch": "两次输入的密码不一致" + } + }, + "register": { + "title": "自助入驻", + "subTitle": "填写管理员信息,快速开通独立空间", + "layout": { + "brand": "CloudSaaS", + "badge": "自助入驻", + "badgeDesc": "密码仅用于登录不会返回", + "title": "开启您的", + "titleHighlight": "数字化管理 新篇章", + "desc": "仅需几步,即可快速开通您的独立空间。为您提供安全、稳定、高效的企业级管理体验。", + "pillSecurity": "企业级安全", + "pillData": "数据联接", + "pillDeploy": "极速部署", + "footer": "© CloudSaaS Inc. All rights reserved.", + "formTitle": "填写管理员信息", + "formDesc": "快速开启独立空间,密码仅用于登录不会返回。", + "optional": "可选" + }, + "alert": { + "title": "请保存管理员账号与密码", + "desc": "密码仅用于登录不会返回,注册后可前往进度页补充实名与订阅信息。" + }, + "field": { + "code": "租户编码", + "name": "租户名称", + "shortName": "租户简称", + "industry": "所属行业", + "contactName": "联系人姓名", + "contactPhone": "联系电话", + "contactEmail": "联系邮箱", + "tenantPackageId": "套餐ID", + "durationMonths": "订阅时长(月)", + "autoRenew": "到期自动续费", + "adminAccount": "管理员账号", + "adminDisplayName": "管理员名称", + "adminEmail": "管理员邮箱", + "adminPhone": "管理员手机号", + "adminPassword": "管理员密码", + "confirmPassword": "确认密码" + }, + "placeholder": { + "code": "请输入租户编码,例如 your-tenant", + "name": "请输入租户名称", + "shortName": "可选,方便展示的简称", + "industry": "可选,所属行业", + "contactName": "请输入联系人姓名", + "contactPhone": "请输入联系电话", + "contactEmail": "可选,接收通知的邮箱", + "tenantPackageId": "请输入套餐ID", + "durationMonths": "订阅时长,默认12个月", + "adminAccount": "请输入管理员账号", + "adminDisplayName": "可选,管理员昵称", + "adminEmail": "可选,管理员邮箱", + "adminPhone": "请输入管理员手机号", + "adminPassword": "请输入管理员密码,至少8位", + "confirmPassword": "请再次输入管理员密码" + }, + "rule": { + "adminPhoneRequired": "请输入管理员手机号", + "adminPhoneFormat": "请输入正确的手机号", + "adminAccountRequired": "请输入管理员账号", + "adminAccountFormat": "管理员账号仅允许大小写字母与数字", + "adminPasswordRequired": "请输入管理员密码", + "adminPasswordLength": "管理员密码至少8位", + "adminEmailFormat": "请输入正确的邮箱地址", + "adminEmailRequired": "请输入管理员邮箱", + "adminDisplayNameRequired": "请输入管理员名称", + "confirmPasswordRequired": "请再次输入管理员密码", + "passwordMismatch": "两次输入的管理员密码不一致", + "agreementRequired": "请先同意隐私政策" + }, + "agreeText": "我已阅读并同意", + "privacyPolicy": "《隐私政策》", + "submitBtnText": "立即提交注册", + "hasAccount": "已有账号?", + "toLogin": "去登录", + "success": "注册成功,正在跳转进度页", + "failed": "注册失败,请稍后再试", + "rateLimit": "请求过于频繁,请稍后再试" + }, + "onboarding": { + "breadcrumb": "自助入驻", + "title": "租户入驻进度", + "waiting": { + "title": "等待审核", + "tip": "资料已提交,请等待管理员审核" + }, + "error": { + "title": "状态异常", + "tip": "当前租户状态异常,请联系管理员处理", + "loading": "正在加载状态...", + "suspended": { + "title": "账号服务已暂停", + "desc": "您的账号因欠费或违反平台规则已被暂停服务。如有疑问,请联系客服处理。", + "primaryAction": "联系客服", + "secondaryAction": "查看账单" + }, + "expired": { + "title": "订阅服务已到期", + "desc": "您的订阅服务已到期。为不影响正常使用,请尽快续费。", + "primaryAction": "立即续费", + "secondaryAction": "联系销售" + }, + "closed": { + "title": "账号已注销", + "desc": "该账号已被注销或归档,无法继续使用。如需使用本平台服务,请重新注册。", + "primaryAction": "重新注册", + "secondaryAction": "返回首页" + }, + "rejected": { + "title": "实名资质审核未通过", + "desc": "很抱歉,您提交的入驻资料未通过审核。请根据原因修改后重新提交。", + "reasonTitle": "驳回原因", + "defaultReason": "未提供详细原因,请联系客服。", + "primaryAction": "修改并重新提交", + "secondaryAction": "联系人工客服" + }, + "fallback": { + "title": "状态信息", + "desc": "当前状态不支持在此页面展示,请点击刷新重试。", + "primaryAction": "刷新" + } + }, + "placeholder": { + "tenantId": "请输入租户ID", + "businessLicenseNumber": "请输入营业执照编号", + "businessLicenseUrl": "请输入营业执照链接", + "legalPersonName": "请输入法人姓名", + "legalPersonIdNumber": "请输入法人证件号", + "legalPersonIdFrontUrl": "请输入身份证人像面链接", + "legalPersonIdBackUrl": "请输入身份证国徽面链接", + "bankAccountName": "请输入开户名称", + "bankAccountNumber": "请输入对公账号", + "bankName": "请输入开户银行", + "additionalDataJson": "可选,附加信息 JSON", + "tenantPackageId": "请输入套餐ID", + "durationMonths": "请输入订阅时长(月)", + "notes": "可选,订阅备注" + }, + "actions": { + "load": "查询", + "refresh": "刷新", + "submitVerification": "提交实名资料", + "goConsole": "进入控制台", + "goLogin": "返回登录" + }, + "messages": { + "missingTenantInfo": "缺少租户或套餐信息,请返回上一步重试", + "submitSuccess": "资料已提交审核,套餐已成功绑定" + }, + "card": { + "tenantId": "租户ID", + "verificationStatus": "实名状态", + "tenantStatus": "租户状态" + }, + "status": { + "verification": { + "draft": "草稿未提交", + "pending": "审核中", + "rejected": "已驳回", + "approved": "已通过" + }, + "tenant": { + "active": "服务可用", + "pendingReview": "待审核", + "suspended": "已暂停", + "expired": "已过期", + "closed": "已关闭" + }, + "hintDraft": "请补充实名资料后提交", + "hintPending": "正在审核,请耐心等待", + "hintRejected": "审核未通过,请修改后重提", + "hintApproved": "已通过审核,可进入控制台", + "unknown": "状态未知" + }, + "form": { + "title": "提交实名资料", + "tip": "填写营业执照与法人信息,加速审核", + "rejected": "审核未通过,请重新提交", + "businessLicenseNumber": "营业执照编号", + "businessLicenseUrl": "营业执照链接", + "legalPersonName": "法人姓名", + "legalPersonIdNumber": "法人证件号", + "legalPersonIdFrontUrl": "身份证人像面链接", + "legalPersonIdBackUrl": "身份证国徽面链接", + "bankAccountName": "开户名称", + "bankAccountNumber": "对公账号", + "bankName": "开户银行", + "additionalDataJson": "附加信息 JSON", + "tenantPackageId": "套餐ID", + "durationMonths": "订阅时长(月)", + "notes": "备注", + "autoRenew": "到期自动续费", + "securityTip": "资料仅用于审核,不会泄露。" + }, + "subscription": { + "title": "套餐订阅", + "tip": "登录后可提交套餐订阅,提前锁定额度", + "loginTip": "请登录后提交套餐订阅", + "submit": "提交订阅", + "failed": "创建订阅失败,请稍后再试" + }, + "pricing": { + "badge": "自助入驻", + "freeTag": "免费商用", + "title": "超过 53,476 位信赖的开发者", + "subtitle": "以及众多科技巨头的选择", + "defaultDesc": "适用于云端产品,能够用户需按年付费。", + "perTime": "一次性付款", + "selectCta": "立即购买", + "empty": "暂无可选套餐,请稍后重试", + "features": { + "code": "完整源代码", + "docs": "技术文档", + "saasAuth": "SaaS应用授权", + "singleProject": "单个项目使用", + "unlimitedProjects": "无限项目使用", + "support": "一年技术支持", + "update": "一年免费更新", + "limitStore": "门店数上限:{value}", + "limitAccount": "账号数上限:{value}", + "limitStorage": "存储空间:{value}GB", + "unlimited": "不限" + } + }, + "pending": { + "title": "资料审核中", + "subtitle": "我们已收到您的资料,审核通过后可立即使用", + "polling": "将于 {seconds}s 后自动刷新进度", + "tipsTitle": "温馨提示", + "tip1": "避免重复提交,若需补充请稍后再试。", + "tip2": "如遇 429,请稍等 1 分钟后再刷新。" + }, + "ready": { + "title": "审核已通过", + "subtitle": "您可以进入控制台开始使用", + "tipsTitle": "下一步建议", + "tip1": "在控制台检查账号与权限是否符合预期。", + "tip2": "如需调整套餐,可在控制台续费或联系管理员。" + }, + "fallback": { + "title": "无法进入控制台", + "tipsTitle": "可能原因", + "tip1": "租户已暂停或关闭,请联系管理员恢复。", + "tip2": "如需续费,请先登录后续订。" + }, + "message": { + "noTenantId": "请先输入租户ID", + "rateLimit": "请求过于频繁,请稍后再试", + "loadFailed": "获取进度失败,请稍后再试", + "verificationSubmitted": "实名资料已提交", + "submitFailed": "提交失败,请稍后再试", + "subscriptionCreated": "订阅创建成功" + }, + "rule": { + "businessLicenseNumber": "请输入营业执照编号", + "legalPersonName": "请输入法人姓名", + "legalPersonIdNumber": "请输入合法的证件号", + "bankAccountNumber": "请输入正确的对公账号", + "packageRequired": "请选择套餐", + "durationRequired": "请输入订阅时长", + "durationRange": "订阅时长范围 1-120 个月" + } + }, + "lockScreen": { + "pwdError": "密码错误", + "lock": { + "inputPlaceholder": "请输入锁屏密码", + "btnText": "锁定" + }, + "unlock": { + "inputPlaceholder": "请输入解锁密码", + "btnText": "解锁", + "backBtnText": "返回登录" + } + }, + "greeting": { + "dawn": "凌晨了!", + "morning": "上午好!", + "afternoon": "下午好!", + "evening": "晚上好!" + }, + "exceptionPage": { + "403": "抱歉,您无权访问该页面", + "404": "抱歉,您访问的页面不存在", + "500": "抱歉,服务器出错了", + "gohome": "返回首页" + }, + "termsOfService": { + "title": "服务条款" + }, + "menus": { + "login": { + "title": "登录" + }, + "register": { + "title": "注册" + }, + "termsOfService": { + "title": "服务条款" + }, + "onboarding": { + "status": "入驻进度", + "pricing": "套餐选择", + "waiting": "等待审核", + "error": "状态异常" + }, + "forgetPassword": { + "title": "忘记密码" + }, + "resetPassword": { + "title": "重置密码" + }, + "outside": { + "title": "内嵌页面" + }, + "dashboard": { + "title": "仪表盘", + "console": "工作台" + }, + "result": { + "title": "结果页面", + "success": "成功页", + "fail": "失败页" + }, + "exception": { + "title": "异常页面", + "forbidden": "403", + "notFound": "404", + "serverError": "500" + }, + "system": { + "title": "系统管理", + "user": "用户管理", + "role": "角色管理", + "tenantRole": "租户角色", + "userCenter": "个人中心", + "menu": "菜单管理", + "tenant": "租户管理", + "tenantPackage": "套餐管理" + }, + "dictionary": { + "title": "字典管理", + "system": "系统字典", + "tenant": "租户字典", + "override": "字典覆盖", + "labelOverride": "标签覆盖管理", + "metrics": "缓存监控" + }, + "tenant": { + "title": "租户管理", + "subscription": "订阅管理", + "billing": "账单管理", + "billingStatistics": "账单统计" + }, + "announcement": { + "title": "公告管理", + "platform": { + "title": "平台公告", + "list": "平台公告列表", + "create": "创建平台公告", + "edit": "编辑平台公告", + "detail": "公告详情" + }, + "tenant": { + "title": "租户公告", + "list": "租户公告列表", + "create": "创建租户公告", + "edit": "编辑租户公告", + "detail": "公告详情" + }, + "app": { + "title": "应用端公告", + "list": "公告列表", + "detail": "公告详情" + }, + "drafts": { + "title": "草稿中心", + "list": "草稿列表" + } + }, + "merchant": { + "title": "商户管理", + "list": "商户列表", + "review": "商户审核", + "detail": "商户详情" + }, + "store": { + "title": "门店管理", + "list": "门店列表" + } + }, + "tenant": { + "search": { + "keyword": "关键词", + "keywordPlaceholder": "租户名称/代码", + "status": "状态", + "statusPlaceholder": "全部", + "tenantName": "租户名称", + "tenantNamePlaceholder": "请输入租户名称", + "contactName": "联系人", + "contactNamePlaceholder": "请输入联系人", + "contactPhone": "联系电话", + "contactPhonePlaceholder": "请输入联系电话", + "verificationStatus": "认证状态" + }, + "action": { + "addTenant": "新增租户", + "edit": "编辑", + "detail": "详情", + "more": "更多", + "impersonateLogin": "伪装登录", + "quotaOverview": "资源配额概览", + "toggleFreeze": "一键冻结/解冻", + "freeze": "冻结", + "unfreeze": "解冻", + "extendOrGift": "延期/赠送时长", + "resetAdmin": "重置管理员" + }, + "more": { + "impersonate": { + "title": "伪装登录", + "tip": "将切换到该租户后台,并以租户主管理员身份登录。", + "confirm": "确认伪装", + "success": "已切换到租户后台", + "alreadyImpersonating": "当前已处于伪装态,请先退出后再继续" + }, + "freeze": { + "freezeTitle": "冻结租户", + "unfreezeTitle": "解冻租户", + "freezeTip": "冻结后该租户将无法正常使用系统,请谨慎操作。", + "unfreezeTip": "解冻后将恢复租户服务状态(若订阅已到期仍会显示到期)。", + "reason": "原因", + "reasonRequired": "请输入冻结原因", + "freezePlaceholder": "请输入冻结原因(必填)", + "unfreezePlaceholder": "请输入解冻备注(可选)", + "success": "操作成功" + }, + "extend": { + "title": "延期/赠送时长", + "months": "续费时长", + "monthUnit": "月", + "monthsRequired": "请输入续费时长", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "expireUnknown": "当前订阅到期时间未知,将从当前时间开始顺延。", + "currentExpireAt": "当前订阅到期时间:{date}", + "success": "操作成功" + }, + "resetAdmin": { + "title": "重置管理员", + "tip": "将生成主管理员的重置密码链接(仅展示一次)。", + "generate": "生成重置链接", + "resetUrl": "重置链接", + "copy": "复制", + "copied": "已复制", + "empty": "请先生成链接", + "success": "生成成功", + "failed": "生成失败,请重试" + }, + "todo": { + "impersonateLogin": "伪装登录功能待实现", + "quotaOverview": "资源配额概览功能待实现", + "resetAdmin": "重置管理员功能待实现" + } + }, + "status": { + "pendingReview": "待审核", + "active": "正常运营", + "suspended": "已停用", + "expired": "已过期", + "closed": "已注销", + "unknown": "未知" + }, + "verificationStatus": { + "draft": "草稿未提交", + "pending": "审核中", + "approved": "已通过", + "rejected": "已驳回", + "unknown": "未知" + }, + "subscriptionStatus": { + "active": "生效中", + "expired": "已过期", + "cancelled": "已取消", + "suspended": "已暂停", + "unknown": "未知" + }, + "review": { + "title": "租户审核", + "dialogTitle": "租户审核", + "tenantName": "租户名称", + "result": "审核结果", + "approve": "通过", + "reject": "拒绝", + "rejectReason": "拒绝原因", + "rejectReasonPlaceholder": "请输入拒绝原因", + "renewMonths": "续费时长", + "renewMonthsPlaceholder": "请输入续费时长(月)", + "monthUnit": "月", + "rejectReasonCustom": "自定义原因", + "rejectReasons": { + "materialsIncomplete": "资料不完整/缺少材料", + "licenseInvalid": "营业执照信息无效/不清晰", + "identityMismatch": "法人信息不一致/证件不匹配", + "contactUnreachable": "联系电话无法联系/信息不真实", + "duplicateRegistration": "疑似重复入驻/关联风险", + "other": "其他(自定义)" + }, + "claim": { + "label": "领单", + "unclaimed": "未领取", + "claimed": "已领取", + "claimedBy": "领取人:{name}", + "needClaimTip": "请先领取后再进行审核操作", + "claimButton": "领取审核", + "releaseButton": "释放领取", + "forceButton": "强制接管", + "readonlyTip": "该审核已被 {name} 领取,你当前仅可查看", + "needClaimFirst": "请先领取该审核", + "claimedByTip": "该审核已被 {name} 领取", + "forceDenied": "无强制接管权限" + }, + "selectResult": "请选择审核结果", + "success": "审核完成", + "action": "审核", + "openMaterial": "查看材料", + "section": { + "basic": "申请信息", + "package": "套餐信息", + "subscription": "订阅信息", + "verification": "认证信息", + "action": "审核操作", + "history": "审核历史" + }, + "history": { + "empty": "暂无记录" + }, + "field": { + "name": "租户名称", + "code": "租户代码", + "packageId": "套餐ID", + "packageName": "选择套餐", + "packageType": "套餐类型", + "packageDescription": "套餐描述", + "monthlyPrice": "月费", + "yearlyPrice": "年费", + "packageActive": "是否启用", + "packageQuota": "套餐配额", + "featurePoliciesJson": "功能策略(JSON)", + "subscriptionId": "订阅ID", + "subscriptionStatus": "订阅状态", + "subscriptionPackageId": "订阅套餐ID", + "subscriptionEffectiveFrom": "订阅生效时间", + "subscriptionEffectiveTo": "订阅到期时间", + "nextBillingDate": "下次扣费时间", + "autoRenew": "自动续费", + "contactName": "联系人", + "contactPhone": "联系电话", + "contactEmail": "联系邮箱", + "effectiveFrom": "提交时间", + "verificationStatus": "认证状态", + "verificationSubmittedAt": "认证提交时间", + "businessLicenseNumber": "营业执照号", + "businessLicenseUrl": "资质材料", + "legalPersonName": "法人姓名", + "legalPersonIdNumber": "法人证件号", + "legalPersonIdFrontUrl": "法人证件正面", + "legalPersonIdBackUrl": "法人证件反面", + "bankName": "开户行", + "bankAccountName": "开户名", + "bankAccountNumber": "银行账号", + "additionalDataJson": "补充信息(JSON)", + "reviewRemarks": "审核备注", + "reviewedBy": "审核人ID", + "reviewedByName": "审核人", + "reviewedAt": "审核时间" + }, + "operatingMode": "经营模式", + "operatingModePlaceholder": "请选择经营模式", + "operatingModeOptions": { + "same": "同一主体", + "different": "不同主体" + } + }, + "manualCreate": { + "title": "手动新增租户(直接入驻)", + "success": "创建成功", + "unit": { + "month": "月" + }, + "section": { + "tenant": "租户信息", + "subscription": "套餐与订阅", + "verification": "认证信息", + "admin": "管理员账号", + "system": "系统字段(只读)" + }, + "field": { + "region": "省市县", + "code": "租户代码", + "name": "租户名称", + "shortName": "租户简称", + "legalEntityName": "主体名称", + "industry": "所属行业", + "status": "租户状态", + "contactName": "联系人", + "contactPhone": "联系电话", + "contactEmail": "联系邮箱", + "website": "官网", + "logoUrl": "LogoUrl", + "coverImageUrl": "CoverImageUrl", + "country": "国家/地区", + "province": "省份/州", + "city": "城市", + "district": "区/县", + "address": "详细地址", + "tags": "标签", + "remarks": "备注", + "suspendedAt": "暂停时间", + "suspensionReason": "暂停原因", + "tenantPackageId": "选择套餐", + "durationMonths": "订阅时长", + "subscriptionEffectiveFrom": "订阅生效时间", + "subscriptionEffectiveToPreview": "订阅到期时间", + "nextBillingDate": "下次计费时间", + "autoRenew": "自动续费", + "subscriptionStatus": "订阅状态", + "scheduledPackageId": "预定套餐ID", + "subscriptionNotes": "订阅备注", + "verificationStatus": "认证状态", + "businessLicenseNumber": "营业执照编号", + "businessLicenseUrl": "营业执照URL", + "legalPersonName": "法人姓名", + "legalPersonIdNumber": "法人身份证号", + "legalPersonIdFrontUrl": "身份证正面URL", + "legalPersonIdBackUrl": "身份证反面URL", + "bankAccountName": "对公账户户名", + "bankAccountNumber": "对公账号", + "bankName": "开户行", + "additionalDataJson": "补充资料(JSON)", + "submittedAt": "提交时间", + "reviewedAt": "审核时间", + "reviewedBy": "审核人ID", + "reviewedByName": "审核人姓名", + "reviewRemarks": "审核备注", + "adminAccount": "管理员账号", + "adminDisplayName": "管理员姓名", + "adminPassword": "初始密码", + "adminAvatar": "管理员头像", + "adminMerchantId": "关联商户ID", + "primaryOwnerUserId": "租户所有者UserId", + "tenantId": "TenantId", + "tenantCreatedAt": "Tenant.CreatedAt", + "tenantUpdatedAt": "Tenant.UpdatedAt", + "tenantDeletedAt": "Tenant.DeletedAt", + "subscriptionId": "SubscriptionId", + "verificationId": "VerificationId", + "tenantCreatedBy": "Tenant.CreatedBy", + "tenantUpdatedBy": "Tenant.UpdatedBy", + "tenantDeletedBy": "Tenant.DeletedBy", + "subscriptionDeletedAt": "Subscription.DeletedAt" + }, + "placeholder": { + "region": "请选择省/市/区(县)", + "code": "请输入租户代码(唯一)", + "name": "请输入租户名称", + "shortName": "请输入租户简称", + "legalEntityName": "请输入主体名称", + "industry": "请输入所属行业", + "contactName": "请输入联系人姓名", + "contactPhone": "请输入联系电话(唯一)", + "contactEmail": "请输入联系邮箱", + "website": "请输入官网地址", + "logoUrl": "请输入 Logo URL", + "coverImageUrl": "请输入封面图 URL", + "country": "请输入国家/地区", + "province": "请输入省份/州", + "city": "请输入城市", + "address": "请输入详细地址", + "tags": "多个标签用逗号分隔", + "remarks": "请输入备注", + "suspensionReason": "请输入暂停原因", + "scheduledPackageId": "请输入预定套餐ID(可选)", + "subscriptionNotes": "请输入订阅备注(可选)", + "businessLicenseNumber": "请输入营业执照编号", + "businessLicenseUrl": "请输入营业执照 URL", + "legalPersonName": "请输入法人姓名", + "legalPersonIdNumber": "请输入法人身份证号", + "legalPersonIdFrontUrl": "请输入身份证正面 URL", + "legalPersonIdBackUrl": "请输入身份证反面 URL", + "bankAccountName": "请输入对公账户户名", + "bankAccountNumber": "请输入对公账号", + "bankName": "请输入开户行", + "additionalDataJson": "请输入补充资料 JSON(可选)", + "reviewedByName": "为空则默认当前用户", + "reviewRemarks": "可选", + "adminAccount": "请输入管理员账号(唯一)", + "adminDisplayName": "请输入管理员姓名", + "adminPassword": "请输入初始密码", + "adminAvatar": "请输入头像 URL(可选)", + "adminMerchantId": "请输入关联商户ID(可选)", + "systemGenerated": "系统生成", + "systemAutoFill": "系统自动填充" + }, + "action": { + "upload": "上传", + "reupload": "重新上传", + "remove": "移除", + "noImage": "暂无图片", + "tip": "支持 jpg/png/webp,上传后自动填充" + }, + "rule": { + "code": "请输入租户代码", + "name": "请输入租户名称", + "tenantStatus": "请选择租户状态", + "tenantPackageId": "请选择套餐", + "durationMonths": "请输入订阅时长(月)", + "subscriptionEffectiveFrom": "请选择订阅生效时间", + "subscriptionStatus": "请选择订阅状态", + "verificationStatus": "请选择认证状态", + "adminAccount": "请输入管理员账号", + "adminDisplayName": "请输入管理员姓名", + "adminPassword": "请输入初始密码" + }, + "subscriptionStatus": { + "pending": "待生效", + "active": "生效中", + "gracePeriod": "宽限期", + "cancelled": "已取消", + "suspended": "已暂停" + } + }, + "table": { + "index": "序号", + "name": "租户名称", + "code": "租户代码", + "contactName": "联系人", + "contactPhone": "联系电话", + "status": "状态", + "verificationStatus": "认证状态", + "effectiveFrom": "申请生效时间", + "effectiveTo": "到期时间", + "action": "操作" + } + }, + "tenantPackage": { + "actions": { + "create": "新增套餐", + "close": "关闭", + "view": "查看", + "edit": "编辑", + "more": "更多", + "copy": "复制", + "saveDraft": "保存草稿", + "publish": "发布", + "rollbackDraft": "回滚草稿", + "toggleActive": "上架/下架", + "toggleVisible": "对外可见", + "togglePurchasable": "允许新购", + "quotaEdit": "权益配额", + "viewTenants": "使用租户", + "delete": "删除" + }, + "usage": { + "activeSubscription": "活跃订阅", + "totalSubscription": "总订阅", + "activeTenant": "使用租户", + "mrr": "MRR", + "arr": "ARR", + "expiringIn": "到期分布" + }, + "search": { + "keyword": "关键词", + "keywordPlaceholder": "请输入套餐名称或描述", + "status": "启用状态", + "statusAll": "全部状态", + "statusEnabled": "启用", + "statusDisabled": "停用" + }, + "table": { + "name": "套餐名称", + "packageType": "套餐类型", + "publishStatus": "发布状态", + "publicVisible": "对外可见", + "allowNewPurchase": "允许新购", + "monthlyPrice": "月费", + "yearlyPrice": "年费", + "quota": "配额", + "usage": "使用情况", + "status": "状态", + "description": "描述", + "actions": "操作", + "quotaEmpty": "未配置" + }, + "featurePolicy": { + "open": "可视化配置", + "clear": "清空", + "configured": "已配置", + "notConfigured": "未配置", + "titleEdit": "功能策略配置", + "titleView": "功能策略查看", + "hint": "建议使用可视化方式配置功能开关与配额;保存后将写入 featurePoliciesJson(用于后续按套餐控制功能)。", + "saved": "已保存功能策略", + "invalidJson": "功能策略 JSON 格式不正确", + "applyPreset": "应用模板", + "applyPresetTitle": "确认应用模板", + "applyPresetConfirm": "应用模板会覆盖当前已编辑的功能开关与配额,是否继续?", + "presetApplied": "已应用模板", + "validationPassed": "校验通过", + "validationFailed": "校验失败({count}项)", + "tabs": { + "config": "配置", + "custom": "自定义扩展", + "preview": "JSON 预览" + }, + "presets": { + "blank": "空白模板", + "standard": "标准版模板", + "pro": "专业版模板" + }, + "customHint": "可在此添加自定义扩展项,用于后续扩展(如:赠送硬件、专属服务等)。key 建议使用英文与下划线,保存后将写入 extra.customItems。", + "custom": { + "add": "新增扩展项", + "key": "Key", + "label": "名称", + "type": "类型", + "value": "值", + "typeBoolean": "布尔", + "typeNumber": "数字", + "typeString": "文本" + }, + "sections": { + "features": "功能开关", + "quotas": "功能配额", + "preview": "JSON 预览(只读)" + }, + "features": { + "reportsExport": "报表/导出权限", + "printing": "打印/小票能力", + "apiAccess": "API 调用能力", + "marketing": "营销功能", + "coupon": "优惠券", + "fullReduction": "满减", + "member": "会员", + "points": "积分" + }, + "quotas": { + "maxProducts": "商品上限", + "maxMenus": "菜单上限", + "maxApiCallsPerDay": "API 调用/天", + "unlimitedPlaceholder": "留空表示不限" + } + }, + "detail": { + "featureStrategy": { + "title": "功能策略", + "invalidJson": "功能策略 JSON 格式不正确", + "empty": "暂无可展示的策略项", + "columns": { + "name": "策略名称", + "value": "策略值", + "status": "启用状态", + "conditions": "限制条件" + }, + "status": { + "enabled": "启用", + "disabled": "停用" + }, + "value": { + "unlimited": "不限" + }, + "noConditions": "无", + "conditions": { + "dependsOnEnabled": "依赖:{name}=启用", + "min": "最小值:{min}", + "max": "最大值:{max}", + "unknownDependency": "未知依赖({key})" + } + } + }, + "form": { + "createTitle": "新增套餐", + "editTitle": "编辑套餐", + "viewTitle": "套餐详情", + "copyTitle": "复制套餐", + "publishStatus": "发布状态", + "publicVisible": "对外可见", + "allowNewPurchase": "允许新购", + "isRecommended": "推荐标识", + "tags": "标签", + "tagsPlaceholder": "请选择标签(可多选)", + "name": "套餐名称", + "namePlaceholder": "请输入套餐名称", + "description": "套餐描述", + "descriptionPlaceholder": "请输入套餐描述", + "packageType": "套餐类型", + "packageTypePlaceholder": "请选择套餐类型", + "monthlyPrice": "月费金额(¥)", + "yearlyPrice": "年费金额(¥)", + "pricePlaceholder": "请输入金额", + "maxStoreCount": "门店数(个)", + "maxAccountCount": "员工数(个)", + "maxStorageGb": "存储空间(GB)", + "maxSmsCredits": "短信条数(条)", + "maxDeliveryOrders": "外卖订单数", + "quotaPlaceholder": "请输入配额上限,留空表示不限", + "featurePoliciesJson": "功能策略(JSON)", + "featurePlaceholder": "可选:自定义功能策略 JSON 字符串", + "isActive": "启用状态", + "sortOrder": "排序", + "sortOrderPlaceholder": "数值越小越靠前", + "submit": "提交", + "submitDraft": "保存草稿", + "submitPublish": "发布", + "validation": { + "nameRequired": "请输入套餐名称", + "packageTypeRequired": "请选择套餐类型" + } + }, + "tags": { + "recommended": "推荐", + "bestValue": "性价比", + "flagship": "旗舰" + }, + "drawer": { + "title": "使用租户({name})", + "expiringTitle": "即将到期租户({name} · {days}天内)", + "detailTitle": "套餐详情({name})", + "searchPlaceholder": "租户名称/编码/联系人/电话", + "tenantName": "租户名称", + "tenantCode": "租户代码", + "tenantStatus": "状态", + "contact": "联系人", + "phone": "电话", + "effectiveTo": "到期时间" + }, + "dialog": { + "quotaTitle": "权益配额({name})" + }, + "status": { + "enabled": "启用", + "disabled": "停用" + }, + "publishStatus": { + "draft": "草稿", + "published": "已发布" + }, + "message": { + "createSuccess": "创建成功", + "saveDraftSuccess": "草稿保存成功", + "updateSuccess": "更新成功", + "publishConfirm": "确定要发布该套餐吗?发布后(且对外可见/允许新购/启用)才能被新租户选择/购买。", + "publishTitle": "发布确认", + "publishSuccess": "已发布", + "rollbackDraftConfirm": "确定要将该套餐回滚为草稿吗?草稿状态将不会对外展示/出售。", + "rollbackDraftTitle": "回滚确认", + "rollbackDraftSuccess": "已回滚为草稿", + "toggleVisibleConfirmEnable": "确定要设置为对外可见吗?", + "toggleVisibleConfirmDisable": "确定要设置为对外不可见吗?", + "toggleVisibleTitleEnable": "可见确认", + "toggleVisibleTitleDisable": "不可见确认", + "toggleVisibleSuccessEnable": "已设为对外可见", + "toggleVisibleSuccessDisable": "已设为对外不可见", + "togglePurchasableConfirmEnable": "确定要允许新租户购买/选择该套餐吗?", + "togglePurchasableConfirmDisable": "确定要禁止新租户购买/选择该套餐吗?", + "togglePurchasableTitleEnable": "允许新购确认", + "togglePurchasableTitleDisable": "禁止新购确认", + "togglePurchasableSuccessEnable": "已允许新购", + "togglePurchasableSuccessDisable": "已禁止新购", + "toggleConfirmEnable": "确定要上架该套餐吗?上架后可被新租户选择/购买。", + "toggleConfirmDisable": "确定要下架该套餐吗?下架后新租户将无法选择该套餐(已订阅租户不受影响)。", + "toggleTitleEnable": "上架确认", + "toggleTitleDisable": "下架确认", + "toggleSuccessEnable": "已上架", + "toggleSuccessDisable": "已下架", + "deleteSuccess": "删除成功", + "deleteConfirm": "确定要删除该套餐吗?删除后可在后台恢复。", + "deleteTitle": "删除确认" + }, + "type": { + "free": "免费版", + "standard": "标准版", + "professional": "专业版", + "enterprise": "企业版", + "unknown": "未知" + } + }, + "subscription": { + "title": "订阅管理", + "search": { + "status": "订阅状态", + "statusPlaceholder": "全部状态", + "package": "套餐", + "packagePlaceholder": "全部套餐", + "tenantKeyword": "租户关键词", + "tenantKeywordPlaceholder": "租户名/租户编码", + "expireDateRange": "到期时间", + "expireDateRangePlaceholder": "选择日期范围" + }, + "table": { + "index": "序号", + "tenantName": "租户名称", + "currentPackage": "当前套餐", + "status": "状态", + "effectiveDate": "生效期", + "expireDate": "到期时间", + "createdAt": "创建时间", + "autoRenew": "自动续费", + "action": "操作" + }, + "action": { + "detail": "查看详情", + "more": "更多", + "extend": "延期", + "changePlan": "变更套餐", + "changeStatus": "变更状态" + }, + "status": { + "pending": "待激活", + "active": "生效中", + "gracePeriod": "宽限期", + "cancelled": "已取消", + "suspended": "已暂停" + }, + "detail": { + "title": "订阅详情", + "basicInfo": "基本信息", + "tenantName": "租户名称", + "currentPackage": "当前套餐", + "status": "订阅状态", + "effectiveDate": "生效时间", + "expireDate": "到期时间", + "autoRenew": "自动续费", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "saveNotes": "保存备注", + "saveNotesSuccess": "备注已保存", + "autoRenewUpdateSuccess": "自动续费设置已更新", + "autoRenewUpdateFailed": "自动续费设置更新失败", + "quotaUsage": "配额使用情况", + "noQuota": "暂无配额数据", + "stores": "门店数", + "accounts": "员工数", + "storage": "存储空间", + "sms": "短信额度", + "deliveryOrders": "外卖订单", + "promotionSlots": "营销活动", + "unlimited": "不限", + "changeHistory": "变更历史", + "noHistory": "暂无变更记录", + "changeType": { + "new": "新订阅", + "renew": "续费", + "upgrade": "升级", + "downgrade": "降级", + "unknown": "变更" + } + }, + "extend": { + "title": "延期订阅", + "duration": "延期时长", + "durationPlaceholder": "请输入延期时长", + "unit": "单位", + "unitDay": "天", + "unitMonth": "月", + "notes": "备注", + "notesPlaceholder": "请输入延期备注(可选)", + "preview": "延期后到期时间", + "success": "延期成功", + "validation": { + "durationRequired": "请输入延期时长", + "durationMin": "延期时长至少为1" + } + }, + "changePlan": { + "title": "变更套餐", + "currentPackage": "当前套餐", + "newPackage": "新套餐", + "newPackagePlaceholder": "请选择新套餐", + "effectiveTime": "生效时间", + "effectiveNow": "立即生效", + "effectiveNextCycle": "下周期生效", + "notes": "备注", + "notesPlaceholder": "请输入变更备注(可选)", + "success": "套餐变更成功", + "validation": { + "packageRequired": "请选择新套餐" + } + }, + "changeStatus": { + "title": "变更状态", + "currentStatus": "当前状态", + "newStatus": "新状态", + "newStatusPlaceholder": "请选择新状态", + "reason": "变更原因", + "reasonPlaceholder": "请输入变更原因", + "confirm": "确定要将订阅状态变更为 {status} 吗?", + "success": "状态变更成功", + "validation": { + "statusRequired": "请选择新状态", + "reasonRequired": "请输入变更原因" + } + } + }, + "quotaPackage": { + "title": "配额包管理", + "tabs": { + "packages": "配额包列表", + "purchases": "购买记录", + "dashboard": "配额仪表盘", + "alertConfig": "告警配置" + }, + "search": { + "quotaType": "配额类型", + "quotaTypePlaceholder": "全部类型", + "status": "状态", + "statusPlaceholder": "全部状态" + }, + "table": { + "index": "序号", + "name": "配额包名称", + "quotaType": "配额类型", + "quotaValue": "配额值", + "price": "价格", + "status": "状态", + "sortOrder": "排序", + "action": "操作" + }, + "action": { + "create": "新增配额包" + }, + "quotaType": { + "storeCount": "门店数量", + "accountCount": "账号数量", + "storage": "存储空间", + "smsCredits": "短信额度", + "deliveryOrders": "配送订单", + "promotionSlots": "促销位" + }, + "status": { + "active": "已上架", + "inactive": "已下架" + }, + "unit": { + "sms": "条", + "orders": "单" + }, + "benefit": { + "featureStrategy": { + "title": "功能策略", + "invalidJson": "功能策略 JSON 格式不正确", + "notConfigured": "未配置功能策略", + "empty": "暂无可展示的策略项", + "columns": { + "name": "策略名称", + "value": "策略值", + "status": "启用状态", + "conditions": "限制条件" + }, + "status": { + "enabled": "启用", + "disabled": "停用" + }, + "value": { + "unlimited": "不限" + }, + "noConditions": "无", + "conditions": { + "dependsOnEnabled": "依赖:{name}=启用", + "min": "最小值:{min}", + "max": "最大值:{max}", + "unknownDependency": "未知依赖({key})" + } + } + }, + "form": { + "createTitle": "新增配额包", + "editTitle": "编辑配额包", + "name": "配额包名称", + "namePlaceholder": "请输入配额包名称", + "quotaType": "配额类型", + "quotaTypePlaceholder": "请选择配额类型", + "quotaValue": "配额值", + "quotaValuePlaceholder": "请输入配额值", + "price": "价格", + "pricePlaceholder": "请输入价格", + "description": "描述", + "descriptionPlaceholder": "请输入描述(可选)", + "sortOrder": "排序", + "isActive": "上架状态" + }, + "purchase": { + "title": "购买配额包", + "tenant": "租户", + "quotaPackage": "配额包", + "quotaPackagePlaceholder": "请选择配额包", + "price": "价格", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "confirm": "确认购买", + "success": "购买成功" + }, + "purchases": { + "search": { + "tenant": "租户", + "tenantPlaceholder": "请选择租户" + }, + "table": { + "index": "序号", + "tenantName": "租户名称", + "quotaPackageName": "配额包名称", + "quotaType": "配额类型", + "quotaValue": "配额值", + "price": "价格", + "purchasedAt": "购买时间", + "expiredAt": "过期时间" + }, + "message": { + "selectTenantFirst": "请先选择租户" + } + }, + "dashboard": { + "search": { + "tenant": "租户", + "tenantPlaceholder": "请选择租户" + }, + "empty": { + "selectTenant": "请选择租户查看配额使用情况" + }, + "meta": { + "used": "已用", + "total": "总量" + }, + "threshold": { + "reached": "已达 {threshold}%" + }, + "message": { + "selectTenantFirst": "请先选择租户" + } + }, + "alertConfig": { + "tip": { + "title": "告警阈值说明", + "description": "当某类配额使用率达到或超过阈值时,将在仪表盘中显示告警标识。" + }, + "action": { + "reset": "重置", + "resetDefault": "恢复默认", + "save": "保存" + }, + "message": { + "saveSuccess": "保存成功", + "resetSuccess": "已重置", + "resetDefaultSuccess": "已恢复默认阈值" + } + }, + "validation": { + "nameRequired": "请输入配额包名称", + "nameMaxLength": "名称不能超过50个字符", + "quotaTypeRequired": "请选择配额类型", + "quotaValueRequired": "请输入配额值", + "priceRequired": "请输入价格", + "packageRequired": "请选择配额包" + }, + "message": { + "createSuccess": "创建成功", + "updateSuccess": "更新成功", + "deleteConfirm": "确定要删除该配额包吗?", + "deleteSuccess": "删除成功", + "statusUpdateSuccess": "状态更新成功" + } + }, + "announcementDrafts": { + "title": "草稿中心", + "filter": { + "type": "类型", + "typePlaceholder": "全部类型", + "date": "日期", + "dateStart": "开始日期", + "dateEnd": "结束日期" + }, + "table": { + "title": "标题", + "type": "类型", + "lastSaved": "最后保存时间", + "author": "作者", + "empty": "暂无草稿" + }, + "action": { + "continueEdit": "继续编辑", + "publish": "发布" + }, + "message": { + "loadFailed": "草稿加载失败,请稍后重试", + "deleteConfirm": "确定要删除该草稿吗?", + "deleteTitle": "删除草稿", + "deleteSuccess": "草稿已删除", + "publishConfirm": "确定要发布该草稿吗?", + "publishTitle": "发布草稿", + "publishSuccess": "草稿已发布", + "missingRowVersion": "缺少版本信息,无法发布", + "localPublishHint": "本地草稿需进入编辑页发布", + "draftNotFound": "草稿不存在或已过期", + "routeNotFound": "未找到编辑页面,请检查路由配置" + }, + "pagination": { + "total": "共 {count} 条草稿" + }, + "source": { + "local": "本地草稿", + "server": "服务端草稿" + }, + "type": { + "all": "全部", + "system": "系统公告", + "billing": "账单/订阅相关提醒", + "operation": "运营通知", + "systemPlatformUpdate": "平台系统更新公告", + "systemSecurityNotice": "系统安全公告", + "systemCompliance": "系统合规公告", + "tenantInternal": "租户内部公告", + "tenantFinance": "租户财务公告", + "tenantOperation": "租户运营公告", + "unknown": "未知类型" + }, + "field": { + "untitled": "未命名草稿", + "unknownAuthor": "未知", + "unknown": "未知" + } + }, + "table": { + "form": { + "reset": "重置", + "submit": "提交" + }, + "searchBar": { + "reset": "重置", + "search": "查询", + "expand": "展开", + "collapse": "收起", + "searchInputPlaceholder": "请输入", + "searchSelectPlaceholder": "请选择" + }, + "selection": "选择", + "sizeOptions": { + "small": "紧凑", + "default": "默认", + "large": "宽松" + }, + "column": { + "selection": "勾选", + "expand": "展开", + "index": "序号" + }, + "zebra": "斑马纹", + "border": "边框", + "headerBackground": "表头背景" + }, + "billing": { + "title": "账单管理", + "quickFilter": { + "title": "快捷筛选", + "all": "全部" + }, + "search": { + "tenant": "租户", + "tenantPlaceholder": "输入租户名搜索", + "billingType": "账单类型", + "billingTypePlaceholder": "请选择账单类型", + "status": "账单状态", + "statusPlaceholder": "请选择账单状态", + "dateRange": "创建时间", + "dateRangePlaceholder": "选择日期范围", + "amountRange": "金额区间", + "minAmountPlaceholder": "最小金额", + "maxAmountPlaceholder": "最大金额", + "keyword": "关键词", + "keywordPlaceholder": "账单号或租户名" + }, + "field": { + "statementNo": "账单编号", + "tenantName": "租户名称", + "billingType": "账单类型", + "amount": "金额", + "amountDue": "应付金额", + "amountPaid": "已付金额", + "status": "状态", + "dueDate": "到期日", + "periodStart": "计费周期开始", + "periodEnd": "计费周期结束", + "createdAt": "创建时间", + "notes": "备注" + }, + "billingType": { + "subscription": "订阅账单", + "quotaPurchase": "配额包购买", + "manual": "手动创建", + "renewal": "续费账单" + }, + "status": { + "pending": "待支付", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已作废" + }, + "action": { + "export": "导出", + "create": "创建账单", + "detail": "查看详情", + "markPaid": "标记已付", + "cancel": "作废", + "recordPayment": "记录支付", + "exportExcel": "导出 Excel", + "exportPdf": "导出 PDF", + "addLineItem": "新增明细项", + "batch": "批量操作", + "batchMarkPaid": "批量标记已支付", + "batchExportExcel": "批量导出 Excel", + "batchExportPdf": "批量导出 PDF", + "uploadProof": "上传凭证" + }, + "dialog": { + "createTitle": "手动创建账单", + "recordPaymentTitle": "确认收款", + "proofPreviewTitle": "凭证预览" + }, + "drawer": { + "title": "账单详情", + "basicInfo": "基本信息", + "paymentList": "支付记录", + "tabs": { + "basic": "基本信息", + "lineItems": "账单明细", + "payments": "支付记录", + "statusFlow": "状态流转" + }, + "openProof": "打开凭证", + "noPayments": "暂无支付记录", + "noStatusFlow": "暂无状态流转记录" + }, + "view": { + "timeline": "时间线", + "table": "表格" + }, + "lineItem": { + "itemType": "项目类型", + "description": "描述", + "quantity": "数量", + "unitPrice": "单价", + "amount": "小计" + }, + "statusFlow": { + "created": "创建", + "due": "到期", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "hint": { + "amountAutoSum": "金额自动根据明细项汇总" + }, + "placeholder": { + "selectTenant": "请选择租户", + "selectBillingType": "请选择账单类型", + "enterAmount": "请输入金额", + "selectDueDate": "请选择到期日", + "enterNotes": "请输入备注", + "enterItemType": "例如:Subscription/Quota/Manual", + "enterDescription": "请输入明细描述", + "enterPaymentAmount": "请输入支付金额", + "selectPaymentMethod": "请选择支付方式", + "enterTransactionNo": "请输入交易号(可选)", + "enterProofUrl": "请输入支付凭证链接(可选)", + "enterPaymentNotes": "请输入支付备注(可选)" + }, + "validation": { + "tenantRequired": "请选择租户", + "billingTypeRequired": "请选择账单类型", + "amountRequired": "请输入金额", + "amountMin": "金额必须大于0", + "dueDateRequired": "请选择到期日", + "lineItemsRequired": "请至少添加 1 条明细项", + "lineItemDescriptionRequired": "第 {index} 条明细:请填写描述", + "lineItemQuantityInvalid": "第 {index} 条明细:数量必须大于 0", + "lineItemUnitPriceInvalid": "第 {index} 条明细:单价不能小于 0", + "paymentAmountRequired": "请输入支付金额", + "paymentAmountMin": "支付金额必须大于0", + "paymentAmountExceedRemain": "支付金额不能超过剩余应付", + "paymentMethodRequired": "请选择支付方式" + }, + "message": { + "createSuccess": "账单创建成功", + "loadDetailFailed": "账单详情加载失败", + "sortFieldNotSupported": "当前排序字段暂不支持", + "cancelConfirm": "确定要作废该账单吗?", + "cancelByAdmin": "管理员作废", + "cancelSuccess": "账单已作废", + "recordPaymentSuccess": "确认收款成功", + "exportNoData": "暂无可导出的数据", + "exportExcelSuccess": "已导出 {count} 条数据", + "exportSuccess": "已导出 {count} 条数据", + "exportFailed": "导出失败,请稍后重试", + "exportPdfNeedDetail": "请先打开账单详情或选中 1 条账单再导出 PDF", + "exportPdfNeedSingleSelection": "请仅选择 1 条账单用于导出 PDF", + "exportPdfPopupBlocked": "弹窗被浏览器拦截,请允许弹窗后重试", + "exportPdfOpened": "已打开打印窗口,请在打印对话框选择“另存为 PDF”", + "noSelection": "请先选择要操作的账单", + "batchNoPending": "所选账单中没有待支付项", + "batchMarkPaidNotes": "批量标记已支付", + "batchMarkPaidSuccess": "已批量确认支付 {count} 条账单", + "batchCancelNotes": "批量取消", + "batchCancelSuccess": "已批量取消 {count} 条账单", + "proofTypeNotAllowed": "仅支持 JPG/PNG 文件", + "proofTooLarge": "文件过大,最大 {size}MB", + "proofUploadSuccess": "凭证上传成功" + }, + "export": { + "title": "导出配置", + "format": "导出格式", + "scope": "导出范围", + "fields": "字段选择", + "currentPage": "当前页", + "selected": "已选择", + "all": "全部", + "confirm": "开始导出", + "dateRange": "日期范围", + "dateRangePlaceholder": "选择日期范围", + "formatRequired": "请选择导出格式", + "scopeRequired": "请选择导出范围", + "fieldsRequired": "请至少选择一个字段", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "sheetName": "账单列表", + "fileName": "账单导出", + "selectedSuffix": "已选", + "noPayments": "暂无支付记录" + }, + "batch": { + "confirmPayment": "批量确认支付", + "cancel": "批量取消", + "export": "批量导出", + "confirmPaymentTip": "确定要批量确认支付 {count} 条账单吗?", + "cancelTip": "确定要批量取消 {count} 条账单吗?", + "selectedCount": "已选择 {count} 项" + }, + "payment": { + "amount": "金额", + "method": "支付方式", + "status": "状态", + "transactionNo": "交易号", + "paidAt": "支付时间", + "notes": "备注", + "remainAmount": "剩余应付", + "proofUrl": "支付凭证", + "proofUrlHint": "可填写支付凭证图片的URL链接", + "proofUploadHint": "支持 JPG/PNG,最多 10MB,上传后自动写入凭证链接" + }, + "paymentMethod": { + "online": "线上支付", + "bankTransfer": "银行转账", + "other": "其他" + }, + "paymentStatus": { + "pending": "待支付", + "success": "支付成功", + "failed": "支付失败", + "refunded": "已退款" + } + }, + "dashboard": { + "title": "运营仪表盘", + "overview": { + "title": "订阅概览", + "totalActive": "活跃订阅", + "expiringIn7Days": "7天内到期", + "expiringIn3Days": "3天内到期", + "expiringIn1Day": "明天到期", + "expired": "已过期", + "pending": "待激活", + "suspended": "已暂停" + }, + "expiring": { + "title": "即将到期订阅", + "viewAll": "查看全部", + "noData": "暂无即将到期的订阅", + "daysLeft": "剩余天数" + }, + "revenue": { + "title": "收入统计", + "total": "总收入", + "monthly": "本月收入", + "quarterly": "本季度收入" + }, + "quotaRanking": { + "title": "配额使用率排行", + "tenant": "租户", + "usage": "使用量", + "limit": "配额上限", + "percentage": "使用率", + "rank": "排名", + "quotaType": "配额类型", + "selectType": "选择配额类型", + "type": { + "orders": "订单数", + "storage": "存储空间", + "users": "用户数" + } + } + }, + "batch": { + "selectedCount": "已选择 {count} 项", + "extend": { + "title": "批量延期", + "selectedCount": "已选择 {count} 个订阅", + "duration": "延期时长", + "notes": "备注", + "notesPlaceholder": "请输入备注(可选)", + "success": "批量延期成功", + "partialSuccess": "批量延期部分成功,成功 {success} 个,失败 {failed} 个", + "result": "成功 {success} 个,失败 {failed} 个", + "noSelection": "请先选择要延期的订阅", + "failedItems": "失败项" + }, + "remind": { + "title": "批量提醒", + "selectedCount": "已选择 {count} 个订阅", + "content": "提醒内容", + "contentPlaceholder": "请输入提醒内容", + "success": "批量提醒发送成功", + "partialSuccess": "批量提醒部分成功,成功 {success} 个,失败 {failed} 个", + "result": "成功发送 {success} 条,失败 {failed} 条", + "noSelection": "请先选择要提醒的订阅", + "failedItems": "失败项", + "validation": { + "contentRequired": "请输入提醒内容", + "contentMin": "提醒内容至少10个字符" + } + } + }, + "user": { + "action": { + "create": "新增用户", + "view": "详情", + "edit": "编辑", + "more": "更多", + "enable": "启用", + "disable": "禁用", + "unlock": "解锁", + "resetPassword": "重置密码", + "restore": "恢复", + "delete": "删除", + "cancel": "取消", + "confirm": "确定" + }, + "avatar": { + "upload": "上传", + "reupload": "重新上传", + "remove": "移除", + "noImage": "暂无图片", + "tip": "支持 jpg/png/webp,上传后自动填充" + }, + "batch": { + "title": "批量操作", + "selected": "已选 {count} 项", + "enable": "批量启用", + "disable": "批量禁用", + "delete": "批量删除", + "restore": "批量恢复", + "export": "批量导出" + }, + "detail": { + "title": "用户详情", + "rolesTitle": "角色", + "permissionsTitle": "权限" + }, + "empty": { + "roles": "暂无角色", + "permissions": "暂无权限" + }, + "field": { + "userId": "用户ID", + "tenant": "租户", + "account": "账号", + "displayName": "昵称", + "phone": "手机号", + "email": "邮箱", + "status": "状态", + "createdAt": "创建时间", + "lastLoginAt": "最后登录", + "password": "密码", + "roles": "角色", + "avatar": "头像" + }, + "status": { + "active": "启用", + "disabled": "禁用", + "locked": "锁定", + "deleted": "已删除", + "unknown": "未知" + }, + "table": { + "avatar": "头像", + "account": "账号", + "displayName": "昵称", + "phone": "手机号", + "email": "邮箱", + "roles": "角色", + "status": "状态", + "createdAt": "创建时间", + "lastLoginAt": "最后登录", + "actions": "操作", + "tenant": "租户" + }, + "search": { + "keyword": "关键词", + "keywordPlaceholder": "账号/昵称/手机号/邮箱", + "status": "状态", + "statusPlaceholder": "全部状态", + "role": "角色", + "rolePlaceholder": "请选择角色", + "createdRange": "创建时间", + "createdRangeStart": "开始时间", + "createdRangeEnd": "结束时间", + "lastLoginRange": "最后登录", + "lastLoginRangeStart": "开始时间", + "lastLoginRangeEnd": "结束时间", + "includeDeleted": "包含已删除", + "includeDeletedOn": "包含", + "includeDeletedOff": "不包含", + "tenant": "租户", + "tenantPlaceholder": "请选择租户" + }, + "placeholder": { + "tenant": "请选择租户", + "account": "请输入账号", + "displayName": "请输入昵称", + "password": "请输入密码", + "phone": "请输入手机号", + "email": "请输入邮箱", + "avatar": "请输入头像链接", + "roles": "请选择角色", + "status": "请选择状态" + }, + "dialog": { + "createTitle": "新增用户", + "editTitle": "编辑用户" + }, + "validation": { + "tenantRequired": "请选择租户", + "accountRequired": "请输入账号", + "displayNameRequired": "请输入昵称", + "passwordRequired": "请输入密码", + "passwordLength": "密码长度需为 6~32 位", + "phoneInvalid": "请输入正确的手机号", + "emailInvalid": "请输入正确的邮箱" + }, + "message": { + "createSuccess": "用户创建成功", + "missingUserId": "缺少用户ID,无法提交", + "rowVersionMissing": "缺少版本信息,请刷新后重试", + "updateSuccess": "用户更新成功", + "concurrencyConflict": "数据已被更新,请刷新后重试:{message}", + "statusConfirm": "确认要调整 {name} 的状态吗?", + "statusSuccess": "状态更新成功", + "deleteConfirm": "确认删除 {name} 吗?", + "deleteSuccess": "用户已删除", + "restoreConfirm": "确认恢复 {name} 吗?", + "restoreSuccess": "用户已恢复", + "resetConfirm": "确认重置 {name} 的密码吗?", + "resetToken": "重置令牌:{token}\n有效期至:{expiresAt}", + "batchEmpty": "请先选择需要操作的用户", + "batchLimit": "单次最多操作 100 个用户", + "batchConfirm": "确认对 {count} 个用户执行该批量操作吗?", + "batchResult": "成功 {success} 个,失败 {failure} 个。\n失败明细:\n{details}", + "batchSuccess": "批量操作成功,共 {count} 个用户", + "exportSuccess": "已导出 {count} 条用户记录" + }, + "export": { + "fileName": "用户列表" + } + }, + "announcement": { + "audience": { + "title": "受众选择", + "targetType": "目标类型", + "targetTypeHint": "选择公告发布的受众范围", + "type": { + "all": "全部租户/用户", + "roles": "指定角色", + "users": "指定用户", + "rules": "规则模式", + "manual": "手动选择" + }, + "helper": { + "all": "将向所有租户或用户发送公告", + "roles": "按角色筛选发送对象", + "users": "通过搜索添加指定用户", + "rules": "通过部门、角色与标签组合筛选", + "manual": "通过搜索与穿梭框选择用户" + }, + "rules": { + "departments": "部门", + "roles": "角色", + "tags": "标签", + "departmentsPlaceholder": "请选择部门", + "tagsPlaceholder": "请选择或输入标签", + "emptyDepartments": "暂无部门数据", + "emptyRoles": "暂无角色数据", + "estimateLabel": "预估人数", + "estimateLoading": "正在预估..." + }, + "users": { + "placeholder": "请输入姓名/手机号/邮箱搜索用户", + "unknownUser": "用户 {id}" + }, + "manual": { + "searchPlaceholder": "输入姓名/手机号/邮箱搜索", + "transferLeft": "候选用户", + "transferRight": "已选用户", + "filterPlaceholder": "输入关键词筛选", + "estimateLabel": "已选人数" + }, + "errors": { + "loadRolesFailed": "加载角色失败", + "loadUsersFailed": "加载用户失败" + } + } + }, + "store": { + "title": "门店管理", + "list": "门店列表", + "detail": "门店详情", + "fee": { + "title": "费用配置", + "minimumOrderAmount": "起送金额", + "deliveryFee": "基础配送费", + "packagingFeeMode": "打包费模式", + "fixedPackagingFee": "固定打包费", + "freeDeliveryThreshold": "免配送费门槛 (满免)", + "packagingMode": { + "title": "收取方式", + "fixed": "固定按单", + "perItem": "按商品累加", + "byItem": "按商品收费", + "byOrder": "按订单收费" + }, + "packagingHelp": { + "title": "收费说明", + "item1": "按订单统一设置打包费", + "item2": "一口价模式:每单按照一口价进行收取", + "item3": "阶梯价模式:每单按照订单内商品折后价总和收取对应的打包费 (折后价总和=折扣商品折后价+非折扣商品原价,不计算满减、红包、商品券等优惠)" + }, + "packagingRule": { + "title": "收费规则", + "tiered": "阶梯价", + "fixed": "一口价" + }, + "tier": { + "label": "折后价阶梯{index}", + "inputPlaceholder": "请输入", + "unit": "元", + "unitInclude": "元(含)", + "fee": "打包费", + "limitInfinity": "阶梯上限 - 无限", + "add": "新增阶梯{current}/{max}" + }, + "preview": { + "title": "费用计算预览", + "action": "计算", + "orderAmount": "订单金额", + "itemCount": "商品数量", + "itemsTitle": "商品明细 (模拟)", + "addItem": "添加商品", + "skuId": "SKU ID", + "quantity": "数量", + "packagingFee": "单品打包费", + "result": { + "totalAmount": "总金额", + "totalFee": "总费用", + "deliveryFee": "配送费", + "packagingFee": "打包费", + "meetsMinimum": "满足起送", + "shortfall": "起送差额" + } + } + }, + "deliveryZone": { + "tip": "配置门店的配送区域及其对应的起送价、配送费和预计送达时间。", + "action": { + "add": "新增区域", + "edit": "编辑区域", + "setup": "去设置" + }, + "table": { + "zoneName": "区域名称", + "minimumOrderAmount": "起送价", + "deliveryFee": "配送费", + "estimatedMinutes": "预计送达(分)" + }, + "status": { + "configured": "已配置配送区域", + "notConfigured": "未配置配送区域" + }, + "form": { + "basicInfo": "基础信息", + "costSettings": "费用与时效", + "zoneName": "区域名称", + "zoneNamePlaceholder": "给此区域起个名字,如:核心配送区", + "polygonGeoJson": "配送范围", + "polygonPlaceholder": "请点击下方按钮绘制", + "minimumOrderAmount": "起送金额", + "deliveryFee": "配送费", + "estimatedMinutes": "预计送达(分钟)", + "sortOrder": "排序 (越小越前)", + "drawPolygonTitle": "绘制配送范围", + "mapKeyMissing": "未配置地图密钥", + "mapKeyMissingHint": "请联系管理员配置 VITE_TENCENT_MAP_KEY", + "drawPolygon": "绘制范围", + "editPolygon": "编辑图形", + "clearPolygon": "清空图形", + "drawHint": "在地图上点击添加点,双击结束绘制", + "editHint": "拖动图形顶点进行调整", + "clickToEdit": "点击修改范围", + "clickToDraw": "点击设置范围" + }, + "check": { + "title": "配送范围校验", + "action": "检测坐标", + "longitude": "经度", + "latitude": "纬度", + "inRange": "在配送范围内", + "outOfRange": "超出配送范围", + "distance": "距离门店", + "zoneName": "所在区域" + }, + "rules": { + "zoneName": "请输入区域名称", + "polygonGeoJson": "请设置配送范围" + }, + "deleteConfirm": "确定要删除该配送区域配置吗?", + "deleteTitle": "删除确认" + } + }, + "platform": { + "title": "平台运营", + "storeAudits": "门店审核", + "qualificationAlerts": "资质预警" + } +} diff --git a/src/locales/zh-CN/announcement.ts b/src/locales/zh-CN/announcement.ts new file mode 100644 index 0000000..e343acc --- /dev/null +++ b/src/locales/zh-CN/announcement.ts @@ -0,0 +1,154 @@ +/** + * 公告模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `announcement.*` 命名空间下 + */ +export default { + common: { + empty: '-' + }, + create: { + title: '新建平台公告' + }, + edit: { + title: '编辑平台公告', + notEditable: '当前公告已发布或撤销,无法编辑', + readonlyFieldsHint: '当前接口仅支持编辑标题、内容与目标受众,其余字段仅展示' + }, + list: { + total: '共 {total} 条公告' + }, + action: { + create: '新建', + refresh: '刷新', + detail: '详情', + edit: '编辑', + publish: '发布', + revoke: '撤销', + delete: '删除', + save: '保存', + back: '返回' + }, + search: { + status: '状态', + statusPlaceholder: '全部状态', + keyword: '关键词', + keywordPlaceholder: '标题/内容', + dateRange: '日期范围', + dateRangeStart: '开始日期', + dateRangeEnd: '结束日期' + }, + table: { + title: '标题', + type: '类型', + priority: '优先级', + status: '状态', + effectiveRange: '有效期', + publishedAt: '发布时间', + actions: '操作', + effectiveOpenEnded: '长期有效' + }, + form: { + title: '标题', + titlePlaceholder: '请输入公告标题', + type: '类型', + typePlaceholder: '请选择公告类型', + priority: '优先级', + effectiveFrom: '生效开始时间', + effectiveFromPlaceholder: '请选择开始时间', + effectiveTo: '生效结束时间', + effectiveToPlaceholder: '请选择结束时间(可选)', + target: '目标受众', + targetTypePlaceholder: '请选择受众类型', + content: '内容' + }, + status: { + draft: '草稿', + published: '已发布', + revoked: '已撤销', + unknown: '未知' + }, + type: { + system: '系统公告', + billing: '账单/订阅相关提醒', + operation: '运营通知', + platformUpdate: '平台系统更新公告', + security: '系统安全公告', + compliance: '系统合规公告', + tenantInternal: '租户内部公告', + tenantFinance: '租户财务公告', + tenantOperation: '租户运营公告', + unknown: '未知类型' + }, + targetType: { + all: '全部用户', + rules: '规则筛选', + roles: '指定角色', + users: '指定用户', + manual: '手动选择' + }, + audience: { + placeholder: '受众选择器将在后续版本接入完整能力', + rulesPlaceholder: '请输入规则 JSON(示例:{"roles":["roleId"]})', + usersPlaceholder: '请输入用户ID,使用英文逗号分隔' + }, + draft: { + notSaved: '未保存', + savedAt: '已保存 · {time}', + readOnly: '只读状态,已停止自动保存' + }, + validation: { + titleRequired: '请输入公告标题', + titleMax: '标题不能超过 128 个字符', + contentRequired: '请输入公告内容', + typeRequired: '请选择公告类型', + priorityRequired: '请输入优先级', + priorityRange: '优先级范围为 1-5', + effectiveFromRequired: '请选择生效开始时间', + effectiveToAfter: '结束时间必须晚于开始时间', + targetTypeRequired: '请选择目标受众类型', + targetRulesRequired: '请完善规则受众条件', + targetUsersRequired: '请至少选择一个用户', + targetRulesInvalid: '规则 JSON 格式不正确' + }, + message: { + loadFailed: '加载公告失败', + createSuccess: '创建成功', + createFailed: '创建失败,请稍后重试', + updateSuccess: '更新成功', + updateFailed: '更新失败,请稍后重试', + publishConfirm: '确认发布该公告?', + publishSuccess: '发布成功', + publishFailed: '发布失败,请稍后重试', + revokeConfirm: '确认撤销该公告?', + revokeSuccess: '撤销成功', + revokeFailed: '撤销失败,请稍后重试', + deleteNotSupported: '平台公告暂不支持删除', + rowVersionMissing: '缺少并发控制版本,操作已取消', + validationFailed: '请完善表单必填项', + missingId: '缺少公告ID,无法加载详情', + targetParseFailed: '目标受众参数解析失败', + concurrencyConflict: '并发冲突: {message}', + refreshAndRetry: '数据已被其他用户修改,请刷新页面后重试', + cannotEditPublished: '当前公告状态为 {status},无法编辑', + invalidId: '无效的公告ID' + }, + detail: { + id: '公告ID', + status: '状态', + type: '类型', + priority: '优先级', + effectiveFrom: '生效开始时间', + effectiveTo: '生效结束时间', + publishedAt: '发布时间', + revokedAt: '撤销时间', + targetType: '目标受众', + targetParameters: '受众参数', + publisherScope: '发布范围', + content: '公告内容', + contentPlaceholder: '暂无公告内容', + contentHint: '富文本预览将接入安全渲染组件(DOMPurify)', + readStats: '已读统计', + readStatsPlaceholder: '已读统计接口尚未接入' + } +} diff --git a/src/locales/zh-CN/billing.json b/src/locales/zh-CN/billing.json new file mode 100644 index 0000000..000ad05 --- /dev/null +++ b/src/locales/zh-CN/billing.json @@ -0,0 +1,244 @@ +{ + "table": { + "column": { + "action": "操作" + } + }, + "billing": { + "quickFilter": { + "title": "快捷筛选", + "all": "全部", + "pending": "待支付", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "search": { + "tenant": "租户", + "tenantPlaceholder": "请选择租户(可选)", + "billingType": "账单类型", + "billingTypePlaceholder": "请选择账单类型", + "status": "状态", + "statusPlaceholder": "请选择状态", + "dateRange": "日期范围", + "dateRangePlaceholder": "请选择日期范围", + "amountRange": "金额范围", + "minAmountPlaceholder": "最小金额", + "maxAmountPlaceholder": "最大金额", + "keyword": "关键词", + "keywordPlaceholder": "账单号/租户名" + }, + "action": { + "export": "导出", + "exportPdf": "导出 PDF", + "create": "新建", + "detail": "详情", + "cancel": "作废", + "recordPayment": "确认收款", + "verifyPayment": "审核收款", + "addLineItem": "新增明细", + "uploadProof": "上传凭证" + }, + "dialog": { + "createTitle": "创建账单", + "recordPaymentTitle": "确认收款", + "proofPreviewTitle": "凭证预览" + }, + "field": { + "statementNo": "账单号", + "tenantName": "租户", + "billingType": "账单类型", + "amount": "金额", + "amountDue": "应收金额", + "amountPaid": "实收金额", + "status": "状态", + "dueDate": "到期日", + "createdAt": "创建时间", + "periodStart": "周期开始", + "periodEnd": "周期结束", + "notes": "备注" + }, + "drawer": { + "tabs": { + "basic": "基本信息", + "lineItems": "账单明细", + "payments": "支付记录", + "statusFlow": "状态流转" + }, + "title": "账单详情", + "basicInfo": "基本信息", + "paymentList": "支付记录", + "openProof": "打开凭证", + "noPayments": "暂无支付记录", + "noStatusFlow": "暂无状态流转记录" + }, + "view": { + "timeline": "时间线", + "table": "表格" + }, + "status": { + "draft": "草稿", + "pending": "待支付", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "statusFlow": { + "created": "创建", + "due": "到期", + "paid": "已支付", + "overdue": "已逾期", + "cancelled": "已取消" + }, + "lineItem": { + "itemType": "类型", + "description": "描述", + "quantity": "数量", + "unitPrice": "单价", + "amount": "金额" + }, + "billingType": { + "subscription": "订阅账单", + "quotaPurchase": "配额包购买", + "manual": "手动创建", + "renewal": "续费账单" + }, + "placeholder": { + "enterItemType": "请输入类型", + "enterDescription": "请输入描述", + "selectTenant": "请选择租户", + "selectBillingType": "请选择账单类型", + "selectDueDate": "请选择到期日", + "enterNotes": "请输入备注(可选)", + "selectPaymentMethod": "请选择支付方式", + "enterPaymentAmount": "请输入支付金额", + "enterTransactionNo": "请输入交易号(可选)", + "enterPaymentNotes": "请输入备注(可选)" + }, + "hint": { + "amountAutoSum": "金额由明细自动汇总" + }, + "validation": { + "tenantRequired": "请选择租户", + "billingTypeRequired": "请选择账单类型", + "dueDateRequired": "请选择到期日", + "lineItemsRequired": "请至少添加 1 条明细", + "lineItemDescriptionRequired": "第 {index} 条明细请填写描述", + "lineItemQuantityInvalid": "第 {index} 条明细数量不合法", + "lineItemUnitPriceInvalid": "第 {index} 条明细单价不合法", + "amountMin": "应收金额需大于 0", + "paymentAmountRequired": "请输入支付金额", + "paymentAmountMin": "支付金额需大于 0", + "paymentAmountExceedRemain": "支付金额不能超过剩余应付", + "paymentMethodRequired": "请选择支付方式" + }, + "message": { + "exportFailed": "导出失败,请重试", + "exportNoData": "暂无可导出的数据", + "exportSuccess": "导出成功", + "exportExcelSuccess": "已导出 {count} 条", + "exportPdfNeedSingleSelection": "导出 PDF 仅支持单条选择", + "exportPdfOpened": "已在新窗口打开 PDF", + "exportPdfPopupBlocked": "弹窗被浏览器拦截,请允许弹窗后重试", + "generatingPdf": "正在生成 PDF,请稍候...", + "exportCancelled": "导出已取消", + "batchMarkPaidSuccess": "批量标记已支付成功", + "batchMarkPaidNotes": "批量标记已支付", + "batchNoPending": "所选账单中没有待支付项", + "batchCancelNotes": "批量作废", + "batchCancelSuccess": "批量作废成功", + "cancelByAdmin": "管理员作废", + "cancelConfirm": "确定要作废该账单吗?", + "cancelSuccess": "作废成功", + "createSuccess": "创建成功", + "loadDetailFailed": "加载账单详情失败", + "recordPaymentSuccess": "确认收款成功", + "verifyPaymentSuccess": "审核收款成功", + "verifyPaymentConfirm": "确定要审核通过该笔收款吗?审核后将同步更新账单已收金额。", + "proofTypeNotAllowed": "仅支持 JPG/PNG 图片", + "proofTooLarge": "文件过大(最大 {size}MB)", + "proofUploadSuccess": "凭证上传成功", + "sortFieldNotSupported": "暂不支持该排序字段" + }, + "batch": { + "confirmPayment": "确认收款", + "confirmPaymentTip": "将所选待支付账单标记为已支付", + "cancel": "批量作废", + "cancelTip": "将所选账单作废(不可恢复)", + "export": "批量导出", + "selectedCount": "已选 {count} 条" + }, + "payment": { + "amount": "收款金额", + "remainAmount": "剩余应收", + "method": "支付方式", + "transactionNo": "交易号", + "proofUrl": "支付凭证", + "proofUploadHint": "支持 JPG/PNG,单张 ≤ 10MB", + "paidAt": "支付时间", + "notes": "备注", + "status": "状态" + }, + "paymentMethod": { + "online": "在线支付", + "bankTransfer": "银行转账", + "other": "其他" + }, + "paymentStatus": { + "pending": "待处理", + "success": "成功", + "failed": "失败", + "refunded": "已退款" + }, + "export": { + "title": "导出账单", + "format": "导出格式", + "formatExcel": "Excel", + "formatPdf": "PDF", + "formatCsv": "CSV", + "scope": "导出范围", + "currentPage": "当前页", + "selected": "已选", + "all": "全部", + "fields": "导出字段", + "dateRange": "日期范围", + "dateRangePlaceholder": "请选择日期范围", + "confirm": "开始导出", + "sheetName": "账单", + "fileName": "账单导出", + "noPayments": "暂无支付记录", + "formatRequired": "请选择导出格式", + "scopeRequired": "请选择导出范围", + "fieldsRequired": "请至少选择 1 个字段" + }, + "statistics": { + "title": "账单统计", + "startDate": "开始日期", + "endDate": "结束日期", + "groupBy": { + "day": "按天", + "week": "按周", + "month": "按月" + }, + "totalRevenue": "实收金额", + "pendingAmount": "未收金额", + "overdueAmount": "逾期金额", + "totalAmountDue": "应收金额", + "statusDistribution": "账单状态分布", + "paymentMethodDistribution": "支付方式占比", + "paymentMethodNoData": "暂无支付方式统计数据", + "revenueTrend": "收入趋势", + "topDebtors": "租户欠款排行榜", + "overdueDays": "逾期天数" + }, + "timeline": { + "created": "创建账单", + "dueDate": "到期日", + "overdueByDays": "已逾期 {days} 天", + "payment": "支付记录", + "paymentDesc": "{method} ¥{amount}", + "currentStatus": "当前状态:{status}", + "currentStatusDesc": "以当前状态为准" + } + } +} diff --git a/src/locales/zh-CN/dictionary.json b/src/locales/zh-CN/dictionary.json new file mode 100644 index 0000000..269aaf0 --- /dev/null +++ b/src/locales/zh-CN/dictionary.json @@ -0,0 +1,197 @@ +{ + "dictionary": { + "common": { + "system": "系统", + "business": "业务", + "refresh": "刷新", + "warning": "提示", + "confirm": "确认", + "cancel": "取消", + "edit": "编辑", + "detail": "详情", + "delete": "删除", + "actions": "操作", + "key": "键", + "value": "值", + "enabled": "启用", + "description": "描述", + "default": "默认", + "sortOrder": "排序", + "code": "编码", + "name": "名称", + "scope": "作用域", + "order": "顺序", + "hidden": "隐藏", + "source": "来源", + "tenant": "租户", + "systemLabel": "系统", + "selectGroupFirst": "请先选择分组" + }, + "group": { + "searchPlaceholder": "按编码或名称搜索", + "new": "新增分组", + "createTitle": "新增字典分组", + "editTitle": "编辑字典分组", + "codePlaceholder": "例如 ORDER_STATUS", + "namePlaceholder": "字典分组名称", + "scopePlaceholder": "选择作用域", + "allowOverride": "允许覆盖", + "enabled": "启用", + "description": "描述", + "empty": "暂无分组", + "deleteConfirm": "删除分组会同时删除其字典项,是否继续?", + "deleteSuccess": "已删除", + "rowVersionMissing": "行版本缺失,请刷新后重试。", + "codeRequired": "编码不能为空", + "codeLength": "长度 {min}-{max}", + "codePattern": "仅支持字母、数字和下划线", + "nameRequired": "名称不能为空", + "nameTooLong": "名称过长", + "descriptionTooLong": "描述过长" + }, + "item": { + "new": "新增字典项", + "createTitle": "新增字典项", + "editTitle": "编辑字典项", + "keyPlaceholder": "例如 PENDING", + "value": "字典值", + "default": "默认", + "enabled": "启用", + "sortOrder": "排序", + "deleteConfirm": "确定删除该字典项?", + "deleteSuccess": "已删除", + "import": "导入", + "export": "导出", + "importCompleted": "导入完成", + "rowVersionMissing": "行版本缺失,请刷新后重试。", + "groupNotSelected": "请先选择分组", + "keyRequired": "键不能为空", + "keyTooLong": "键过长", + "valueRequired": "至少填写一种语言", + "descriptionTooLong": "描述过长", + "sortUpdated": "排序已更新" + }, + "i18n": { + "zh": "中文", + "en": "英文", + "zhPlaceholder": "请输入中文内容", + "enPlaceholder": "请输入英文内容", + "hint": "至少填写一种语言,建议同时填写中英文。" + }, + "import": { + "title": "批量导入字典项", + "dropHere": "拖拽文件到此或点击上传", + "tip": "最大 10MB,仅支持 CSV 或 JSON", + "conflictMode": "冲突处理", + "skip": "跳过", + "overwrite": "覆盖", + "append": "追加", + "successSummary": "成功: {success},跳过: {skip},错误: {error}", + "row": "行号", + "field": "字段", + "message": "错误信息", + "start": "开始导入", + "fileTooLarge": "文件大小超过 10MB", + "selectFile": "请选择文件", + "unsupportedFormat": "不支持的文件格式" + }, + "override": { + "toggleEnabled": "覆盖已启用", + "toggleDisabled": "覆盖未启用", + "systemItems": "系统字典项", + "customView": "自定义视图", + "newCustomItem": "新增自定义项", + "saveSortOrder": "保存排序", + "tenantGroupNotReady": "租户分组未就绪", + "hiddenSaved": "隐藏项已保存", + "sortSaved": "排序已保存", + "selectSystemDictionary": "请选择系统字典", + "selectGroupHint": "请选择分组后继续", + "unsavedSortConfirm": "排序未保存,确定离开?" + }, + "metrics": { + "cacheHitRatio": "缓存命中率", + "totalQueries": "总查询数", + "hits": "命中", + "misses": "未命中", + "avgResponse": "平均响应", + "last1h": "最近 1 小时", + "hitRatioTrend": "命中率趋势(L1 vs L2)", + "timeRange1h": "1 小时", + "timeRange24h": "24 小时", + "timeRange7d": "7 天", + "invalidationEvents": "失效事件", + "rangeTo": "至", + "start": "开始", + "end": "结束", + "timestamp": "时间", + "dictionary": "字典", + "operation": "操作", + "keys": "键数量", + "operator": "操作人", + "create": "新增", + "update": "更新", + "delete": "删除", + "l1HitRatio": "L1 命中率", + "l2HitRatio": "L2 命中率", + "target": "目标" + }, + "labelOverride": { + "title": "标签覆盖管理", + "tenantTitle": "租户标签覆盖", + "platformTitle": "平台标签覆盖", + "selectTenant": "选择租户", + "selectTenantHint": "选择租户查看其标签覆盖配置", + "dictionaryItem": "字典项", + "dictionaryItemKey": "字典项键", + "originalValue": "原始值", + "overrideValue": "覆盖值", + "overrideType": "覆盖类型", + "tenantCustomization": "租户定制", + "platformEnforcement": "平台强制", + "reason": "覆盖原因", + "reasonPlaceholder": "请输入覆盖原因(可选)", + "reasonHint": "建议说明覆盖原因,便于后续审计", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "operator": "操作人", + "newOverride": "新增覆盖", + "editOverride": "编辑覆盖", + "deleteOverride": "删除覆盖", + "deleteConfirm": "确定删除该标签覆盖?删除后将恢复原始显示值。", + "noOverrides": "暂无标签覆盖配置", + "selectDictionaryItem": "请选择字典项", + "overrideValueRequired": "请输入覆盖值", + "onlySystemDictionary": "租户只能覆盖系统字典项", + "filterAll": "全部", + "filterTenantCustomization": "租户定制", + "filterPlatformEnforcement": "平台强制" + }, + "errors": { + "loadGroups": "加载字典分组失败。", + "loadGroup": "加载字典分组失败。", + "createGroup": "创建字典分组失败。", + "updateGroup": "更新字典分组失败。", + "deleteGroup": "删除字典分组失败。", + "loadItems": "加载字典项失败。", + "createItem": "创建字典项失败。", + "updateItem": "更新字典项失败。", + "deleteItem": "删除字典项失败。", + "loadOverrides": "加载覆盖配置失败。", + "loadOverride": "加载覆盖配置失败。", + "enableOverride": "启用覆盖失败。", + "disableOverride": "关闭覆盖失败。", + "updateHidden": "更新隐藏项失败。", + "updateSort": "更新排序失败。", + "dataConflict": "数据已被他人修改,请刷新后重试。", + "loadLabelOverrides": "加载标签覆盖失败。", + "saveLabelOverride": "保存标签覆盖失败。", + "deleteLabelOverride": "删除标签覆盖失败。" + }, + "messages": { + "deleted": "已删除", + "importCompleted": "导入完成", + "sortUpdated": "排序已更新" + } + } +} diff --git a/src/locales/zh-CN/merchant.ts b/src/locales/zh-CN/merchant.ts new file mode 100644 index 0000000..6c8369d --- /dev/null +++ b/src/locales/zh-CN/merchant.ts @@ -0,0 +1,135 @@ +/** + * 商户模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `merchant.*` 命名空间下 + */ +export default { + list: { + title: '商户列表', + search: { + keyword: '关键词', + keywordPlaceholder: '商户名称/营业执照号', + status: '审核状态', + operatingMode: '经营模式', + tenant: '租户', + tenantPlaceholder: '请输入租户ID' + }, + table: { + name: '商户名称', + tenantName: '所属租户', + operatingMode: '经营模式', + status: '状态', + frozen: '冻结状态', + storeCount: '门店数量', + createdAt: '创建时间' + } + }, + review: { + title: '商户审核', + claim: '领取', + release: '释放', + approve: '通过', + reject: '驳回', + revoke: '撤销审核', + reason: '原因', + comment: '备注', + result: '审核结果', + claimed: '已领取', + unclaimed: '未领取', + readonlyTip: '当前审核已被他人领取,您只能查看', + reasonPlaceholder: '请输入驳回原因', + selectResult: '请选择审核结果', + needClaim: '请先领取审核', + success: '审核提交成功', + pendingReApproval: '关键信息变更待审核', + empty: '暂无审核记录', + section: { + action: '审核操作', + history: '审核历史' + } + }, + detail: { + title: '商户详情', + basicInfo: '基本信息', + subjectInfo: '主体信息', + stores: '门店列表', + auditHistory: '审核历史', + changeHistory: '变更历史' + }, + fields: { + name: '商户名称', + tenant: '租户', + status: '审核状态', + operatingMode: '经营模式', + licenseNumber: '营业执照号', + legalRepresentative: '法人', + registeredAddress: '注册地址', + contactPhone: '联系电话', + contactEmail: '联系邮箱', + isFrozen: '冻结状态', + frozenReason: '冻结原因', + approvedAt: '审核通过时间', + approvedBy: '审核通过人', + createdAt: '创建时间', + updatedAt: '更新时间' + }, + status: { + pending: '待审核', + approved: '审核通过', + rejected: '已拒绝', + frozen: '冻结', + unknown: '未知' + }, + operatingMode: { + same: '同一主体', + different: '不同主体' + }, + frozen: { + yes: '已冻结', + no: '正常' + }, + action: { + edit: '编辑', + detail: '查看详情', + export: '导出PDF' + }, + placeholder: { + name: '请输入商户名称', + licenseNumber: '请输入营业执照号', + legalRepresentative: '请输入法人姓名', + registeredAddress: '请输入注册地址', + contactPhone: '请输入联系电话', + contactEmail: '请输入联系邮箱' + }, + rules: { + nameRequired: '请输入商户名称', + contactPhoneRequired: '请输入联系电话', + contactEmailInvalid: '邮箱格式不正确' + }, + message: { + rowVersionMissing: '缺少版本信息,请刷新后重试', + updateSuccess: '更新成功', + updateRequiresReview: '关键信息修改,商户已进入待审核状态' + }, + store: { + name: '门店名称', + status: '门店状态', + address: '门店地址', + contactPhone: '联系电话', + licenseNumber: '营业执照号', + empty: '暂无门店', + statusOperating: '营业中', + statusPreparing: '筹备中', + statusClosed: '已关闭', + statusSuspended: '已停业', + statusUnknown: '未知' + }, + change: { + field: '字段', + oldValue: '修改前', + newValue: '修改后', + changedBy: '操作人', + changedAt: '变更时间', + empty: '暂无变更记录' + } +} diff --git a/src/locales/zh-CN/store.ts b/src/locales/zh-CN/store.ts new file mode 100644 index 0000000..e521fd7 --- /dev/null +++ b/src/locales/zh-CN/store.ts @@ -0,0 +1,506 @@ +/** + * 门店模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `store.*` 命名空间下 + */ +export default { + list: { + search: { + keyword: '关键词', + keywordPlaceholder: '门店名称/门店编码', + merchantId: '商户ID', + merchantIdPlaceholder: '请输入商户ID', + auditStatus: '审核状态', + businessStatus: '经营状态', + ownershipType: '主体类型' + }, + table: { + name: '门店名称', + code: '门店编码', + ownershipType: '主体类型', + auditStatus: '审核状态', + businessStatus: '经营状态', + phone: '联系电话', + createdAt: '创建时间' + }, + action: { + create: '新增门店', + detail: '查看详情', + edit: '编辑', + toggleStatus: '切换状态', + submitAudit: '提交审核' + } + }, + form: { + createTitle: '新增门店', + editTitle: '编辑门店', + tabs: { + basic: '基础信息', + location: '位置信息', + settings: '经营设置', + services: '服务设施' + }, + merchantId: '商户ID', + merchantIdPlaceholder: '请输入商户ID', + code: '门店编码', + codePlaceholder: '请输入门店编码', + name: '门店名称', + namePlaceholder: '请输入门店名称', + phone: '联系电话', + phonePlaceholder: '请输入联系电话', + managerName: '负责人姓名', + managerNamePlaceholder: '请输入负责人姓名', + status: '门店状态', + signboardImageUrl: '门头招牌图', + signboardImageUrlPlaceholder: '请输入门头招牌图链接', + ownershipType: '主体类型', + categoryId: '类目ID', + categoryIdPlaceholder: '请输入类目ID', + deliveryRadiusKm: '配送半径(公里)', + locationTitle: '地址信息', + province: '省份', + provincePlaceholder: '请输入省份', + city: '城市', + cityPlaceholder: '请输入城市', + district: '区县', + districtPlaceholder: '请输入区县', + address: '详细地址', + addressPlaceholder: '请输入详细地址', + coordinate: '经纬度', + coordinatePlaceholder: '例如 39.695954,116.074058', + coordinatePasteAction: '一键粘贴', + coordinatePickerAction: '打开坐标拾取', + coordinateHint: '支持纬度,经度或经度,纬度;可使用腾讯坐标拾取工具复制', + coordinatePasteUnavailable: '当前环境不支持读取剪贴板', + coordinatePasteEmpty: '剪贴板为空,请先复制坐标', + coordinateInvalid: '坐标格式不正确,请检查', + settingsTitle: '服务设置', + announcement: '门店公告', + announcementPlaceholder: '请输入门店公告', + tags: '标签', + tagsPlaceholder: '多个标签用逗号分隔', + supportsDineIn: '支持堂食', + supportsPickup: '支持自取', + supportsDelivery: '支持外卖', + supportsReservation: '支持预订', + supportsQueueing: '支持排队' + }, + rules: { + merchantId: '请输入商户ID', + code: '请输入门店编码', + name: '请输入门店名称', + signboardImageUrl: '请填写门头招牌图', + ownershipType: '请选择主体类型', + coordinateText: '请输入经纬度', + coordinateTextFormat: '坐标格式应为 纬度,经度 或 经度,纬度' + }, + detail: { + title: '门店详情', + empty: '暂无门店信息', + back: '返回列表', + fields: { + name: '门店名称', + code: '门店编码', + auditStatus: '审核状态', + businessStatus: '经营状态', + ownershipType: '主体类型', + phone: '联系电话', + address: '地址', + createdAt: '创建时间' + }, + tabs: { + basic: '基本信息', + qualification: '资质管理', + businessHours: '营业时段', + temporaryHours: '临时调整', + deliveryZone: '配送区域', + fee: '费用配置' + } + }, + qualification: { + warningTitle: '资质预警', + complete: '资质齐全', + incomplete: '资质不完整', + expiringSoon: '即将过期 {count} 项', + expired: '已过期 {count} 项', + action: { + add: '新增资质', + edit: '编辑资质' + }, + table: { + type: '资质类型', + documentNumber: '证件编号', + expiresAt: '到期时间', + status: '状态' + }, + form: { + type: '资质类型', + fileUrl: '文件链接', + fileUrlPlaceholder: '请输入资质文件链接', + documentNumber: '证件编号', + documentNumberPlaceholder: '请输入证件编号', + issuedAt: '签发日期', + expiresAt: '到期日期', + sortOrder: '排序' + }, + rules: { + type: '请选择资质类型', + fileUrl: '请输入资质文件链接', + documentNumber: '请输入证件编号', + expiresAt: '请选择到期日期' + }, + status: { + valid: '有效', + expiringSoon: '即将过期', + expired: '已过期' + }, + type: { + businessLicense: '营业执照', + foodService: '食品经营许可证', + storefront: '门头实景照', + interior: '店内环境照' + }, + deleteTitle: '删除资质', + deleteConfirm: '确认删除该资质吗?' + }, + businessHours: { + tip: '支持跨天营业时间段,保存时会自动校验重叠与跨天情况。', + action: { + add: '新增时段', + save: '保存设置' + }, + table: { + dayOfWeek: '星期', + hourType: '营业类型', + startTime: '开始时间', + endTime: '结束时间', + capacityLimit: '接单上限', + notes: '备注', + notesPlaceholder: '备注信息' + }, + days: { + sunday: '周日', + monday: '周一', + tuesday: '周二', + wednesday: '周三', + thursday: '周四', + friday: '周五', + saturday: '周六' + }, + hourType: { + normal: '正常营业', + reservation: '仅预订', + pickup: '自取/配送', + closed: '暂停营业' + } + }, + temporaryHours: { + tip: '设置特定日期的歇业、临时营业或调整营业时间', + allDay: '全天', + action: { + add: '添加临时调整' + }, + table: { + dateRange: '日期范围', + timeRange: '时间段', + overrideType: '调整类型', + reason: '说明' + }, + overrideType: { + closed: '歇业', + temporaryOpen: '临时营业', + modifiedHours: '调整时间' + }, + dialog: { + addTitle: '添加临时调整', + editTitle: '编辑临时调整' + }, + form: { + dateRange: '日期范围', + isAllDay: '全天生效', + timeRange: '时间段', + overrideType: '调整类型', + reason: '说明', + reasonPlaceholder: '如:春节放假、情人节延长营业等' + }, + rules: { + dateRequired: '请选择日期范围', + typeRequired: '请选择调整类型', + timeRequired: '非全天模式下请选择时间段' + }, + deleteConfirm: '确定要删除这条临时调整吗?' + }, + deliveryZone: { + tip: '配送区域使用 GeoJSON 描述,可先维护多边形后再进行配送检测。', + action: { + add: '新增区域', + edit: '编辑区域' + }, + table: { + zoneName: '区域名称', + minimumOrderAmount: '起送金额', + deliveryFee: '配送费', + estimatedMinutes: '预计送达(分钟)' + }, + form: { + zoneName: '区域名称', + zoneNamePlaceholder: '请输入区域名称', + polygonGeoJson: 'GeoJSON', + polygonPlaceholder: '请输入 GeoJSON 多边形', + drawPolygon: '绘制区域', + drawPolygonTitle: '绘制配送区域', + editPolygon: '编辑', + clearPolygon: '清空', + drawHint: '绘制完成后点击确定应用到 GeoJSON', + mapKeyMissing: '未配置腾讯地图 Key,无法加载绘图组件', + mapKeyMissingHint: '请在环境变量 VITE_TENCENT_MAP_KEY 中配置腾讯地图 Key', + minimumOrderAmount: '起送金额', + deliveryFee: '配送费', + estimatedMinutes: '预计送达(分钟)', + sortOrder: '排序' + }, + rules: { + zoneName: '请输入区域名称', + polygonGeoJson: '请输入 GeoJSON' + }, + deleteTitle: '删除配送区域', + deleteConfirm: '确认删除该配送区域吗?', + check: { + title: '配送检测', + action: '检测', + longitude: '经度', + latitude: '纬度', + inRange: '配送范围内', + outOfRange: '配送范围外', + distance: '距离', + zoneName: '命中区域' + } + }, + fee: { + title: '费用配置', + minimumOrderAmount: '起送费', + deliveryFee: '配送费', + packagingFeeMode: '打包费模式', + fixedPackagingFee: '固定打包费', + freeDeliveryThreshold: '免配送门槛', + packagingMode: { + fixed: '固定金额', + perItem: '按商品' + }, + preview: { + title: '费用预览', + action: '计算预览', + orderAmount: '订单金额', + itemCount: '商品数量', + itemsTitle: '商品明细', + addItem: '新增商品', + skuId: 'SKU', + quantity: '数量', + packagingFee: '打包费', + result: { + totalAmount: '订单总额', + totalFee: '费用合计', + deliveryFee: '配送费', + packagingFee: '打包费', + meetsMinimum: '满足起送', + shortfall: '差额' + } + } + }, + auditStatus: { + draft: '草稿', + pending: '待审核', + activated: '已激活', + rejected: '已驳回', + unknown: '未知' + }, + businessStatus: { + title: '经营状态调整', + targetStatus: '目标状态', + closureReason: '歇业原因', + closureReasonText: '补充说明', + closureReasonTextPlaceholder: '请输入补充说明', + forceClosedTip: '当前门店已被平台强制关闭,无法由租户自行调整。', + rules: { + status: '请选择目标状态', + reason: '请选择歇业原因' + }, + open: '营业中', + resting: '休息中', + forceClosed: '强制关闭', + unknown: '未知' + }, + ownership: { + same: '同一主体', + different: '不同主体' + }, + closureReason: { + equipment: '设备维护', + vacation: '负责人休假', + outOfStock: '缺货', + temporarilyClosed: '临时歇业', + other: '其他' + }, + status: { + closed: '已关闭', + preparing: '筹备中', + operating: '营业中', + suspended: '已停业' + }, + action: { + save: '保存', + edit: '编辑', + delete: '删除', + refresh: '刷新' + }, + message: { + createSuccess: '创建成功', + updateSuccess: '更新成功', + deleteSuccess: '删除成功', + statusUpdated: '经营状态已更新', + submitAuditSuccess: '审核已提交' + }, + audit: { + stats: { + pending: '待审核', + overdue: '超时待审', + approved: '已通过', + rejected: '已驳回', + avgProcessing: '平均处理时长(小时)' + }, + search: { + keyword: '关键词', + keywordPlaceholder: '门店名称/商户名称', + tenantId: '租户ID', + tenantIdPlaceholder: '请输入租户ID', + dateRange: '提交时间', + dateRangeStart: '开始日期', + dateRangeEnd: '结束日期', + overdueOnly: '仅显示超时' + }, + table: { + storeName: '门店名称', + storeCode: '门店编码', + tenantName: '所属租户', + merchantName: '商户名称', + ownershipType: '主体类型', + submittedAt: '提交时间', + waitingDays: '等待天数', + qualificationCount: '资质数量', + overdue: '超时状态' + }, + action: { + detail: '查看详情', + approve: '审核通过', + reject: '审核驳回', + riskControl: '风控操作' + }, + overdue: '超时', + notOverdue: '正常', + detail: { + title: '审核详情', + empty: '暂无数据', + loadFailed: '加载审核详情失败', + viewFile: '查看文件', + tabs: { + basic: '基本信息', + qualification: '资质信息', + history: '审核记录' + }, + sections: { + tenant: '租户信息', + merchant: '商户信息' + }, + fields: { + storeName: '门店名称', + storeCode: '门店编码', + auditStatus: '审核状态', + ownershipType: '主体类型', + phone: '联系电话', + address: '门店地址', + submittedAt: '提交时间', + signboard: '门头招牌', + tenantName: '租户名称', + tenantContact: '联系人', + tenantPhone: '联系电话', + merchantName: '商户名称', + merchantLegal: '法人名称', + merchantCredit: '社会信用代码', + file: '资质文件' + } + }, + history: { + action: '动作', + operator: '操作人', + remark: '备注', + createdAt: '时间' + }, + approve: { + prompt: '可填写审核备注(可选)', + placeholder: '请输入审核备注', + success: '审核已通过' + }, + reject: { + title: '审核驳回', + reason: '驳回原因', + reasonText: '补充说明', + reasonTextPlaceholder: '请输入补充说明', + remark: '审核备注', + remarkPlaceholder: '请输入审核备注', + success: '审核已驳回', + rules: { + reason: '请选择驳回原因', + reasonText: '请输入驳回原因补充说明' + }, + reasonOptions: { + licenseMissing: '证照缺失', + photoBlur: '证照模糊', + inconsistent: '信息不一致', + other: '其他' + } + }, + riskControl: { + title: '风控操作', + storeName: '当前门店:{name}', + action: '操作类型', + forceClose: '强制关闭', + reopen: '解除关闭', + reason: '关闭原因', + reasonPlaceholder: '请输入关闭原因', + remark: '备注', + remarkPlaceholder: '请输入备注', + forceCloseSuccess: '门店已强制关闭', + reopenSuccess: '门店已解除强制关闭', + rules: { + action: '请选择操作类型', + reason: '请输入关闭原因' + } + } + }, + alerts: { + summary: { + expiringSoon: '即将过期', + expired: '已过期' + }, + search: { + tenantId: '租户ID', + tenantIdPlaceholder: '请输入租户ID', + daysThreshold: '预警阈值(天)', + expired: '过期状态' + }, + table: { + storeName: '门店名称', + storeCode: '门店编码', + tenantName: '所属租户', + qualificationType: '资质类型', + expiresAt: '到期时间', + daysUntilExpiry: '剩余天数', + status: '状态', + businessStatus: '经营状态' + }, + status: { + expiring: '即将过期', + expired: '已过期' + } + } +} diff --git a/src/locales/zh-CN/tenant.ts b/src/locales/zh-CN/tenant.ts new file mode 100644 index 0000000..d7e90d6 --- /dev/null +++ b/src/locales/zh-CN/tenant.ts @@ -0,0 +1,267 @@ +/** + * 租户模块国际化(zh-CN) + * + * 注意:该文件会在 i18n 初始化时合并到 `tenant.*` 命名空间下 + */ +export default { + // 详情 Drawer + detail: { + title: '租户详情', + basicInfo: '基本信息', + statusInfo: '状态信息', + subscriptionInfo: '订阅信息', + quotaInfo: '配额信息', + billingInfo: '账单信息', + + tenantId: '租户ID', + name: '租户名称', + code: '租户代码', + shortName: '租户简称', + industry: '所属行业', + contactName: '联系人', + contactPhone: '联系电话', + contactEmail: '联系邮箱', + + status: '租户状态', + verificationStatus: '认证状态', + autoRenew: '自动续费', + effectiveFrom: '生效时间', + effectiveTo: '到期时间', + currentPackage: '当前套餐' + }, + + // Tab 标签 + tabs: { + basicInfo: '基本信息', + statusInfo: '状态信息', + subscription: '订阅信息', + quotaOverview: '配额概览', + billing: '账单信息' + }, + + // 配额相关 + quota: { + title: '配额资源概览', + usageSummary: '配额使用情况', + usageHistory: '配额使用明细', + purchaseHistory: '配额购买记录', + + recordedAt: '记录时间', + changeType: '变更类型', + snapshot: '快照', + historyNotice: '当前为实时数据快照,历史记录功能待后端接口补充', + fullOrderId: '完整订单ID', + + orderNo: '订单号', + quotaType: '配额类型', + quotaPackage: '配额包', + limitValue: '配额上限', + usedValue: '已使用', + remainingValue: '剩余', + usagePercentage: '使用率', + resetCycle: '重置周期', + lastResetAt: '上次重置时间', + + purchaseValue: '购买数量', + price: '价格', + purchasedAt: '购买时间', + expiredAt: '过期时间', + notes: '备注', + + type: { + store: '门店数', + account: '账号数', + storageGb: '存储空间(GB)', + smsCredits: '短信额度', + deliveryOrders: '外卖订单数', + unknown: '未知类型({value})' + } + }, + + // 公告管理 + announcement: { + title: '租户公告', + listTitle: '租户公告列表', + createTitle: '新建公告', + editTitle: '编辑公告', + detailTitle: '公告详情', + + search: { + status: '状态', + statusPlaceholder: '全部状态', + keyword: '关键词', + keywordPlaceholder: '标题/内容关键词', + dateRange: '日期范围', + dateRangePlaceholder: '选择日期范围' + }, + + field: { + title: '标题', + content: '内容', + announcementType: '公告类型', + priority: '优先级', + status: '状态', + effectiveRange: '有效期', + effectiveFrom: '生效开始', + effectiveTo: '生效结束', + targetType: '目标受众', + targetParameters: '目标参数', + departmentIds: '部门ID', + roleIds: '角色ID', + tagIds: '标签ID', + publishedAt: '发布时间', + revokedAt: '撤销时间', + isActive: '是否生效', + rowVersion: '并发版本' + }, + + action: { + create: '新建公告', + edit: '编辑', + detail: '详情', + publish: '发布', + revoke: '撤销', + delete: '删除', + save: '保存', + back: '返回', + more: '更多' + }, + + status: { + draft: '草稿', + published: '已发布', + revoked: '已撤销' + }, + + type: { + system: '系统公告', + billing: '账单/订阅相关提醒', + operation: '运营通知', + systemPlatformUpdate: '平台系统更新公告', + systemSecurityNotice: '系统安全公告', + systemCompliance: '系统合规公告', + tenantInternal: '租户内部公告', + tenantFinance: '租户财务公告', + tenantOperation: '租户运营公告' + }, + + targetType: { + all: '全员', + roles: '角色', + users: '指定用户', + rules: '规则匹配', + manual: '手动选择' + }, + + placeholder: { + title: '请输入公告标题', + content: '请输入公告内容', + type: '请选择公告类型', + priority: '请输入优先级', + effectiveRange: '选择生效时间范围', + targetType: '请选择目标受众', + roleIds: '请输入角色ID,逗号分隔', + userIds: '请输入用户ID,逗号分隔', + departmentIds: '请输入部门ID,逗号分隔', + tagIds: '请输入标签ID,逗号分隔' + }, + + validation: { + titleRequired: '请输入公告标题', + titleLength: '标题长度需为 2-128 字符', + contentRequired: '请输入公告内容', + typeRequired: '请选择公告类型', + priorityRequired: '请输入优先级', + effectiveRangeRequired: '请选择有效期', + targetTypeRequired: '请选择目标受众', + roleIdsRequired: '请输入至少一个角色ID', + userIdsRequired: '请输入至少一个用户ID', + targetRuleRequired: '请至少填写一个规则字段' + }, + + message: { + loadListFailed: '加载公告列表失败', + loadDetailFailed: '加载公告详情失败', + detailNotFound: '未获取到公告详情,请返回列表重试', + tenantIdMissing: '未获取到租户ID,无法继续操作', + publishConfirm: '确认发布该公告吗?', + revokeConfirm: '确认撤销该公告吗?', + deleteConfirm: '确认删除该公告吗?', + publishSuccess: '公告发布成功', + revokeSuccess: '公告已撤销', + deleteSuccess: '公告已删除', + createSuccess: '公告创建成功', + updateSuccess: '公告更新成功', + actionNotAllowed: '当前状态不支持该操作', + empty: '暂无公告数据' + }, + + tip: { + tenantScope: '当前为租户范围公告,将仅发送给本租户用户。', + draftAutoSave: '草稿自动保存中', + draftSaved: '草稿已保存于 {time}', + draftLoaded: '已加载草稿', + draftNotEditable: '仅草稿状态可编辑' + }, + + section: { + content: '公告内容', + audience: '目标受众' + } + }, + + // 编辑/新增弹窗 + edit: { + titleEdit: '编辑租户', + titleCreate: '新增租户', + + placeholder: { + code: '请输入租户代码', + name: '请输入租户名称', + shortName: '请输入租户简称', + industry: '请输入所属行业', + contactName: '请输入联系人', + contactPhone: '请输入联系电话', + contactEmail: '请输入联系邮箱', + tenantPackageId: '请选择套餐', + effectiveFrom: '选择生效日期' + }, + + package: { + title: '套餐信息', + select: '选择套餐', + durationMonths: '购买时长' + }, + + packageType: { + free: '免费', + paid: '付费' + }, + + unit: { + month: '月' + }, + + validation: { + codeRequired: '请输入租户代码', + nameRequired: '请输入租户名称', + tenantPackageIdRequired: '请选择套餐' + } + }, + + // 提示信息 + warning: { + updateApiNotReady: '租户编辑接口开发中,请稍后再试' + }, + error: { + loadDetailFailed: '加载租户详情失败', + loadQuotaFailed: '加载配额信息失败', + loadBillingFailed: '加载账单信息失败', + loadSubscriptionFailed: '加载订阅信息失败', + updateFailed: '更新失败,请重试' + }, + success: { + registerSuccess: '注册成功', + updateSuccess: '更新成功' + } +} diff --git a/src/locales/zh-CN/tenantPackage.ts b/src/locales/zh-CN/tenantPackage.ts new file mode 100644 index 0000000..e0fd6fe --- /dev/null +++ b/src/locales/zh-CN/tenantPackage.ts @@ -0,0 +1,137 @@ +/** + * 租户套餐模块国际化(zh-CN) + */ +export default { + actions: { + more: '更多', + copy: '复制', + toggleActive: '启用/禁用', + toggleVisible: '显示/隐藏', + togglePurchasable: '上架/下架', + publish: '发布', + rollbackDraft: '撤回草稿', + quotaEdit: '调整配额', + viewTenants: '查看关联租户', + delete: '删除', + view: '查看', + edit: '编辑' + }, + drawer: { + title: '关联租户 - {name}', + expiringTitle: '关联租户 - {name} ({days}天内到期)', + searchPlaceholder: '搜索租户名称/代码', + tenantName: '租户名称', + tenantCode: '租户代码', + tenantStatus: '租户状态', + contact: '联系人', + phone: '联系电话', + effectiveTo: '到期时间' + }, + search: { + keyword: '关键词', + keywordPlaceholder: '搜索套餐名称', + status: '状态', + statusAll: '全部', + statusEnabled: '启用', + statusDisabled: '禁用' + }, + form: { + name: '套餐名称', + namePlaceholder: '请输入套餐名称', + packageType: '套餐类型', + packageTypePlaceholder: '请选择套餐类型', + monthlyPrice: '月付价格', + yearlyPrice: '年付价格', + pricePlaceholder: '请输入价格', + maxStoreCount: '最大门店数', + maxAccountCount: '最大账号数', + maxStorageGb: '存储空间(GB)', + maxSmsCredits: '短信额度', + maxDeliveryOrders: '外卖订单数', + quotaPlaceholder: '请输入数量 (-1为无限)', + featurePoliciesJson: '功能策略', + featurePlaceholder: '请输入JSON格式的策略配置', + isActive: '是否启用', + publicVisible: '公开可见', + allowNewPurchase: '允许新购', + publishStatus: '发布状态', + isRecommended: '是否推荐', + sortOrder: '排序值', + submit: '提交' + }, + status: { + enabled: '启用', + disabled: '禁用' + }, + publishStatus: { + published: '已发布', + draft: '草稿' + }, + type: { + free: '免费版', + standard: '标准版', + professional: '专业版', + enterprise: '企业版' + }, + tags: { + recommended: '推荐', + bestValue: '超值', + flagship: '旗舰' + }, + table: { + name: '套餐名称', + packageType: '类型', + publishStatus: '发布状态', + publicVisible: '公开', + allowNewPurchase: '可购', + monthlyPrice: '月付', + yearlyPrice: '年付', + quota: '配额概览', + usage: '使用情况', + status: '状态', + actions: '操作', + quotaEmpty: '无限制' + }, + message: { + updateSuccess: '更新成功', + + toggleConfirmEnable: '确认启用该套餐吗?', + toggleConfirmDisable: '确认禁用该套餐吗?禁用后不可新购。', + toggleTitleEnable: '启用确认', + toggleTitleDisable: '禁用确认', + toggleSuccessEnable: '已启用', + toggleSuccessDisable: '已禁用', + + toggleVisibleConfirmEnable: '确认设为公开可见吗?', + toggleVisibleConfirmDisable: '确认设为隐藏吗?', + toggleVisibleTitleEnable: '公开确认', + toggleVisibleTitleDisable: '隐藏确认', + toggleVisibleSuccessEnable: '已设为公开', + toggleVisibleSuccessDisable: '已设为隐藏', + + togglePurchasableConfirmEnable: '确认允许新用户购买吗?', + togglePurchasableConfirmDisable: '确认禁止新用户购买吗?', + togglePurchasableTitleEnable: '允许购买确认', + togglePurchasableTitleDisable: '禁止购买确认', + togglePurchasableSuccessEnable: '已允许购买', + togglePurchasableSuccessDisable: '已禁止购买', + + publishConfirm: '确认发布该套餐吗?发布后租户可见。', + publishTitle: '发布确认', + publishSuccess: '发布成功', + + rollbackDraftConfirm: '确认撤回为草稿吗?撤回后新用户不可见。', + rollbackDraftTitle: '撤回确认', + rollbackDraftSuccess: '已撤回为草稿', + + deleteConfirm: '确认删除该套餐吗?此操作不可恢复。', + deleteTitle: '删除确认', + deleteSuccess: '删除成功' + }, + dialog: { + quotaTitle: '配额管理 - {name}' + }, + featurePolicy: { + invalidJson: 'JSON格式不正确' + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b257649 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,34 @@ +import App from './App.vue' +import { createApp } from 'vue' +import { initStore } from './store' // Store +import { initRouter } from './router' // Router +import language from './locales' // 国际化 +import '@styles/core/tailwind.css' // tailwind +import '@styles/index.scss' // 样式 +import '@utils/sys/console.ts' // 控制台输出内容 +import { setupGlobDirectives } from './directives' +import { setupErrorHandle } from './utils/sys/error-handle' + +document.addEventListener( + 'touchstart', + function () {}, + { passive: false } +) + +const app = createApp(App) +const warnIgnores = [ + 'onUnmounted is called when there is no active component instance to be associated with.' +] +app.config.warnHandler = (msg, instance, trace) => { + if (warnIgnores.some((item) => msg.includes(item))) { + return + } + console.warn(`[Vue warn]: ${msg}${trace}`) +} +initStore(app) +initRouter(app) +setupGlobDirectives(app) +setupErrorHandle(app) + +app.use(language) +app.mount('#app') diff --git a/src/mock/temp/formData.ts b/src/mock/temp/formData.ts new file mode 100644 index 0000000..8aa7ae5 --- /dev/null +++ b/src/mock/temp/formData.ts @@ -0,0 +1,273 @@ +import avatar1 from '@/assets/images/avatar/avatar1.webp' +import avatar2 from '@/assets/images/avatar/avatar2.webp' +import avatar3 from '@/assets/images/avatar/avatar3.webp' +import avatar4 from '@/assets/images/avatar/avatar4.webp' +import avatar5 from '@/assets/images/avatar/avatar5.webp' +import avatar6 from '@/assets/images/avatar/avatar6.webp' +import avatar7 from '@/assets/images/avatar/avatar7.webp' +import avatar8 from '@/assets/images/avatar/avatar8.webp' +import avatar9 from '@/assets/images/avatar/avatar9.webp' +import avatar10 from '@/assets/images/avatar/avatar10.webp' + +export interface User { + id: number + username: string + gender: 1 | 0 + mobile: string + email: string + dep: string + status: string + create_time: string + avatar: string +} + +// 用户列表 +export const ACCOUNT_TABLE_DATA: User[] = [ + { + id: 1, + username: 'alexmorgan', + gender: 1, + mobile: '18670001591', + email: 'alexmorgan@company.com', + dep: '研发部', + status: '1', + create_time: '2020-09-09 10:01:10', + avatar: avatar1 + }, + { + id: 2, + username: 'sophiabaker', + gender: 1, + mobile: '17766664444', + email: 'sophiabaker@company.com', + dep: '电商部', + status: '1', + create_time: '2020-10-10 13:01:12', + avatar: avatar2 + }, + { + id: 3, + username: 'liampark', + gender: 1, + mobile: '18670001597', + email: 'liampark@company.com', + dep: '人事部', + status: '1', + create_time: '2020-11-14 12:01:45', + avatar: avatar3 + }, + { + id: 4, + username: 'oliviagrant', + gender: 0, + mobile: '18670001596', + email: 'oliviagrant@company.com', + dep: '产品部', + status: '1', + create_time: '2020-11-14 09:01:20', + avatar: avatar4 + }, + { + id: 5, + username: 'emmawilson', + gender: 0, + mobile: '18670001595', + email: 'emmawilson@company.com', + dep: '财务部', + status: '1', + create_time: '2020-11-13 11:01:05', + avatar: avatar5 + }, + { + id: 6, + username: 'noahevan', + gender: 1, + mobile: '18670001594', + email: 'noahevan@company.com', + dep: '运营部', + status: '1', + create_time: '2020-10-11 13:10:26', + avatar: avatar6 + }, + { + id: 7, + username: 'avamartin', + gender: 1, + mobile: '18123820191', + email: 'avamartin@company.com', + dep: '客服部', + status: '2', + create_time: '2020-05-14 12:05:10', + avatar: avatar7 + }, + { + id: 8, + username: 'jacoblee', + gender: 1, + mobile: '18670001592', + email: 'jacoblee@company.com', + dep: '总经办', + status: '3', + create_time: '2020-11-12 07:22:25', + avatar: avatar8 + }, + { + id: 9, + username: 'miaclark', + gender: 0, + mobile: '18670001581', + email: 'miaclark@company.com', + dep: '研发部', + status: '4', + create_time: '2020-06-12 05:04:20', + avatar: avatar9 + }, + { + id: 10, + username: 'ethanharris', + gender: 1, + mobile: '13755554444', + email: 'ethanharris@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-12 16:01:10', + avatar: avatar10 + }, + { + id: 11, + username: 'isabellamoore', + gender: 1, + mobile: '13766660000', + email: 'isabellamoore@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar6 + }, + { + id: 12, + username: 'masonwhite', + gender: 1, + mobile: '18670001502', + email: 'masonwhite@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar7 + }, + { + id: 13, + username: 'charlottehall', + gender: 1, + mobile: '13006644977', + email: 'charlottehall@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar8 + }, + { + id: 14, + username: 'benjaminscott', + gender: 0, + mobile: '13599998888', + email: 'benjaminscott@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar9 + }, + { + id: 15, + username: 'ameliaking', + gender: 1, + mobile: '13799998888', + email: 'ameliaking@company.com', + dep: '研发部', + status: '1', + create_time: '2020-11-14 12:01:20', + avatar: avatar10 + } +] + +export interface Role { + roleName: string + roleCode: string + des: string + date: string + enable: boolean +} + +// 角色列表 +export const ROLE_LIST_DATA: Role[] = [ + { + roleName: '超级管理员', + roleCode: 'R_SUPER', + des: '拥有系统全部权限', + date: '2025-05-15 12:30:45', + enable: true + }, + { + roleName: '管理员', + roleCode: 'R_ADMIN', + des: '拥有系统管理权限', + date: '2025-05-15 12:30:45', + enable: true + }, + { + roleName: '普通用户', + roleCode: 'R_USER', + des: '拥有系统普通权限', + date: '2025-05-15 12:30:45', + enable: true + }, + { + roleName: '财务管理员', + roleCode: 'R_FINANCE', + des: '管理财务相关权限', + date: '2025-05-16 09:15:30', + enable: true + }, + { + roleName: '数据分析师', + roleCode: 'R_ANALYST', + des: '拥有数据分析权限', + date: '2025-05-16 11:45:00', + enable: false + }, + { + roleName: '客服专员', + roleCode: 'R_SUPPORT', + des: '处理客户支持请求', + date: '2025-05-17 14:30:22', + enable: true + }, + { + roleName: '营销经理', + roleCode: 'R_MARKETING', + des: '管理营销活动权限', + date: '2025-05-17 15:10:50', + enable: true + }, + { + roleName: '访客用户', + roleCode: 'R_GUEST', + des: '仅限浏览权限', + date: '2025-05-18 08:25:40', + enable: false + }, + { + roleName: '系统维护员', + roleCode: 'R_MAINTAINER', + des: '负责系统维护和更新', + date: '2025-05-18 09:50:12', + enable: true + }, + { + roleName: '项目经理', + roleCode: 'R_PM', + des: '管理项目相关权限', + date: '2025-05-19 13:40:35', + enable: true + } +] diff --git a/src/mock/upgrade/changeLog.ts b/src/mock/upgrade/changeLog.ts new file mode 100644 index 0000000..dd6b772 --- /dev/null +++ b/src/mock/upgrade/changeLog.ts @@ -0,0 +1,12 @@ +import { ref } from 'vue' + +interface UpgradeLog { + version: string // 版本号 + title: string // 更新标题 + date: string // 更新日期 + detail?: string[] // 更新内容 + requireReLogin?: boolean // 是否需要重新登录 + remark?: string // 备注 +} + +export const upgradeLogList = ref([]) diff --git a/src/plugins/echarts.ts b/src/plugins/echarts.ts new file mode 100644 index 0000000..4f56d89 --- /dev/null +++ b/src/plugins/echarts.ts @@ -0,0 +1,76 @@ +/** + * ECharts 插件配置 + * + * 按需导入 ECharts 图表和组件,减小打包体积。 + * 只注册项目中实际使用的图表类型和组件。 + * + * @module plugins/echarts + * @author Art Design Pro Team + */ + +// ECharts 按需导入配置 +import * as echarts from 'echarts/core' + +// 导入图表类型 +import { + BarChart, + LineChart, + PieChart, + ScatterChart, + RadarChart, + MapChart, + CandlestickChart +} from 'echarts/charts' + +// 导入组件 +import { + TitleComponent, + TooltipComponent, + GridComponent, + LegendComponent, + DataZoomComponent, + MarkPointComponent, + MarkLineComponent, + ToolboxComponent, + BrushComponent, + GeoComponent, + VisualMapComponent +} from 'echarts/components' + +// 导入渲染器 +import { CanvasRenderer } from 'echarts/renderers' + +// 注册必要的组件 +echarts.use([ + // 图表类型 + BarChart, + LineChart, + PieChart, + ScatterChart, + RadarChart, + MapChart, + CandlestickChart, + + // 组件 + TitleComponent, + TooltipComponent, + GridComponent, + LegendComponent, + DataZoomComponent, + MarkPointComponent, + MarkLineComponent, + ToolboxComponent, + BrushComponent, + GeoComponent, + VisualMapComponent, + + // 渲染器 + CanvasRenderer +]) + +// 导出 echarts 实例和类型 +export { echarts } +export type { EChartsOption, BarSeriesOption } from 'echarts' + +// 导出常用的图形工具 +export const graphic = echarts.graphic diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..4536a86 --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,6 @@ +/** + * 插件统一导出 + * 集中管理第三方库的封装和配置 + */ + +export * from './echarts' diff --git a/src/router/core/ComponentLoader.ts b/src/router/core/ComponentLoader.ts new file mode 100644 index 0000000..9467934 --- /dev/null +++ b/src/router/core/ComponentLoader.ts @@ -0,0 +1,85 @@ +/** + * 组件加载器 + * + * 负责动态加载 Vue 组件 + * + * @module router/core/ComponentLoader + * @author Art Design Pro Team + */ + +import { h } from 'vue' + +export class ComponentLoader { + private modules: Record Promise> + + constructor() { + // 动态导入 views 目录下所有 .vue 组件 + this.modules = import.meta.glob('../../views/**/*.vue') + } + + /** + * 加载组件 + */ + load(componentPath: string): () => Promise { + if (!componentPath) { + return this.createEmptyComponent() + } + + // 规范化路径,确保以 / 开头 + const normalizedPath = componentPath.startsWith('/') ? componentPath : `/${componentPath}` + + // 构建可能的路径 + const fullPath = `../../views${normalizedPath}.vue` + const fullPathWithIndex = `../../views${normalizedPath}/index.vue` + + // 先尝试直接路径,再尝试添加/index的路径 + const module = this.modules[fullPath] || this.modules[fullPathWithIndex] + + if (!module) { + console.error( + `[ComponentLoader] 未找到组件: ${componentPath},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}` + ) + return this.createErrorComponent(componentPath) + } + + return module + } + + /** + * 加载布局组件 + */ + loadLayout(): () => Promise { + return () => import('@/views/index/index.vue') + } + + /** + * 加载 iframe 组件 + */ + loadIframe(): () => Promise { + return () => import('@/views/outside/Iframe.vue') + } + + /** + * 创建空组件 + */ + private createEmptyComponent(): () => Promise { + return () => + Promise.resolve({ + render() { + return h('div', {}) + } + }) + } + + /** + * 创建错误提示组件 + */ + private createErrorComponent(componentPath: string): () => Promise { + return () => + Promise.resolve({ + render() { + return h('div', { class: 'route-error' }, `组件未找到: ${componentPath}`) + } + }) + } +} diff --git a/src/router/core/IframeRouteManager.ts b/src/router/core/IframeRouteManager.ts new file mode 100644 index 0000000..c054ca1 --- /dev/null +++ b/src/router/core/IframeRouteManager.ts @@ -0,0 +1,78 @@ +/** + * Iframe 路由管理器 + * + * 负责管理 iframe 类型的路由 + * + * @module router/core/IframeRouteManager + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' + +export class IframeRouteManager { + private static instance: IframeRouteManager + private iframeRoutes: AppRouteRecord[] = [] + + private constructor() {} + + static getInstance(): IframeRouteManager { + if (!IframeRouteManager.instance) { + IframeRouteManager.instance = new IframeRouteManager() + } + return IframeRouteManager.instance + } + + /** + * 添加 iframe 路由 + */ + add(route: AppRouteRecord): void { + if (!this.iframeRoutes.find((r) => r.path === route.path)) { + this.iframeRoutes.push(route) + } + } + + /** + * 获取所有 iframe 路由 + */ + getAll(): AppRouteRecord[] { + return this.iframeRoutes + } + + /** + * 根据路径查找 iframe 路由 + */ + findByPath(path: string): AppRouteRecord | undefined { + return this.iframeRoutes.find((route) => route.path === path) + } + + /** + * 清空所有 iframe 路由 + */ + clear(): void { + this.iframeRoutes = [] + } + + /** + * 保存到 sessionStorage + */ + save(): void { + if (this.iframeRoutes.length > 0) { + sessionStorage.setItem('iframeRoutes', JSON.stringify(this.iframeRoutes)) + } + } + + /** + * 从 sessionStorage 加载 + */ + load(): void { + try { + const data = sessionStorage.getItem('iframeRoutes') + if (data) { + this.iframeRoutes = JSON.parse(data) + } + } catch (error) { + console.error('[IframeRouteManager] 加载 iframe 路由失败:', error) + this.iframeRoutes = [] + } + } +} diff --git a/src/router/core/MenuProcessor.ts b/src/router/core/MenuProcessor.ts new file mode 100644 index 0000000..c7b55cc --- /dev/null +++ b/src/router/core/MenuProcessor.ts @@ -0,0 +1,261 @@ +/** + * 菜单处理器 + * + * 负责菜单数据的获取、过滤和处理 + * + * @module router/core/MenuProcessor + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' +import { useUserStore } from '@/store/modules/user' +import { useAppMode } from '@/hooks/core/useAppMode' +import { fetchGetMenu } from '@/api/auth' +import { asyncRoutes } from '../routes/asyncRoutes' +import { RoutesAlias } from '../routesAlias' +import { formatMenuTitle } from '@/utils' + +export class MenuProcessor { + /** + * 获取菜单数据 + */ + async getMenuList(): Promise { + const { isFrontendMode } = useAppMode() + + let menuList: AppRouteRecord[] + if (isFrontendMode.value) { + menuList = await this.processFrontendMenu() + } else { + menuList = await this.processBackendMenu() + } + + // 在规范化路径之前,验证原始路径配置 + this.validateMenuPaths(menuList) + + // 规范化路径(将相对路径转换为完整路径) + const normalizedMenuList = this.normalizeMenuPaths(menuList) + + return normalizedMenuList + } + + /** + * 处理前端控制模式的菜单 + */ + private async processFrontendMenu(): Promise { + const userStore = useUserStore() + const roles = userStore.info?.roles + + let menuList = [...asyncRoutes] + + // 根据角色过滤菜单 + if (roles && roles.length > 0) { + menuList = this.filterMenuByRoles(menuList, roles) + } + + return this.filterEmptyMenus(menuList) + } + + /** + * 处理后端控制模式的菜单 + */ + private async processBackendMenu(): Promise { + const list = await fetchGetMenu() + return this.filterEmptyMenus(list as unknown as AppRouteRecord[]) + } + + /** + * 通过 name/path 匹配路由索引 + */ + private findRouteIndex(list: AppRouteRecord[], target: AppRouteRecord): number { + // 1. 优先按 name 匹配 + if (target.name) { + const index = list.findIndex((item) => item.name === target.name) + if (index >= 0) return index + } + + // 2. 回退按 path 匹配 + if (target.path) { + return list.findIndex((item) => item.path === target.path) + } + + return -1 + } + + /** + * 根据角色过滤菜单 + */ + private filterMenuByRoles(menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] { + return menu.reduce((acc: AppRouteRecord[], item) => { + const itemRoles = item.meta?.roles + const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role)) + + if (hasPermission) { + const filteredItem = { ...item } + if (filteredItem.children?.length) { + filteredItem.children = this.filterMenuByRoles(filteredItem.children, roles) + } + acc.push(filteredItem) + } + + return acc + }, []) + } + + /** + * 递归过滤空菜单项 + */ + private filterEmptyMenus(menuList: AppRouteRecord[]): AppRouteRecord[] { + return menuList + .map((item) => { + // 如果有子菜单,先递归过滤子菜单 + if (item.children && item.children.length > 0) { + const filteredChildren = this.filterEmptyMenus(item.children) + return { + ...item, + children: filteredChildren + } + } + return item + }) + .filter((item) => { + // 如果定义了 children 属性(即使是空数组),说明这是一个目录菜单,应该保留 + if ('children' in item) { + return true + } + + // 如果有外链或 iframe,保留 + if (item.meta?.isIframe === true || item.meta?.link) { + return true + } + + // 如果有有效的 component,保留 + if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) { + return true + } + + // 其他情况过滤掉 + return false + }) + } + + /** + * 验证菜单列表是否有效 + */ + validateMenuList(menuList: AppRouteRecord[]): boolean { + return Array.isArray(menuList) && menuList.length > 0 + } + + /** + * 规范化菜单路径 + * 将相对路径转换为完整路径,确保菜单跳转正确 + */ + private normalizeMenuPaths(menuList: AppRouteRecord[], parentPath = ''): AppRouteRecord[] { + return menuList.map((item) => { + // 构建完整路径 + const fullPath = this.buildFullPath(item.path || '', parentPath) + + // 递归处理子菜单 + const children = item.children?.length + ? this.normalizeMenuPaths(item.children, fullPath) + : item.children + + return { + ...item, + path: fullPath, + children + } + }) + } + + /** + * 验证菜单路径配置 + * 检测非一级菜单是否错误使用了 / 开头的路径 + */ + /** + * 验证菜单路径配置 + * 检测非一级菜单是否错误使用了 / 开头的路径 + */ + private validateMenuPaths(menuList: AppRouteRecord[], level = 1): void { + menuList.forEach((route) => { + if (!route.children?.length) return + + const parentName = String(route.name || route.path || '未知路由') + + route.children.forEach((child) => { + const childPath = child.path || '' + + // 跳过合法的绝对路径:外部链接和 iframe 路由 + if (this.isValidAbsolutePath(childPath)) return + + // 检测非法的绝对路径 + if (childPath.startsWith('/')) { + this.logPathError(child, childPath, parentName, level) + } + }) + + // 递归检查更深层级的子路由 + this.validateMenuPaths(route.children, level + 1) + }) + } + + /** + * 判断是否为合法的绝对路径 + */ + private isValidAbsolutePath(path: string): boolean { + return ( + path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('/outside/iframe/') + ) + } + + /** + * 输出路径配置错误日志 + */ + private logPathError( + route: AppRouteRecord, + path: string, + parentName: string, + level: number + ): void { + const routeName = String(route.name || path || '未知路由') + const menuTitle = route.meta?.title || routeName + const suggestedPath = path.split('/').pop() || path.slice(1) + + console.error( + `[路由配置错误] 菜单 "${formatMenuTitle(menuTitle)}" (name: ${routeName}, path: ${path}) 配置错误\n` + + ` 位置: ${parentName} > ${routeName}\n` + + ` 问题: ${level + 1}级菜单的 path 不能以 / 开头\n` + + ` 当前配置: path: '${path}'\n` + + ` 应该改为: path: '${suggestedPath}'` + ) + } + + /** + * 构建完整路径 + */ + private buildFullPath(path: string, parentPath: string): string { + if (!path) return '' + + // 外部链接直接返回 + if (path.startsWith('http://') || path.startsWith('https://')) { + return path + } + + // 如果已经是绝对路径,直接返回 + if (path.startsWith('/')) { + return path + } + + // 拼接父路径和当前路径 + if (parentPath) { + // 移除父路径末尾的斜杠,移除子路径开头的斜杠,然后拼接 + const cleanParent = parentPath.replace(/\/$/, '') + const cleanChild = path.replace(/^\//, '') + return `${cleanParent}/${cleanChild}` + } + + // 没有父路径,添加前导斜杠 + return `/${path}` + } +} diff --git a/src/router/core/RoutePermissionValidator.ts b/src/router/core/RoutePermissionValidator.ts new file mode 100644 index 0000000..72b9912 --- /dev/null +++ b/src/router/core/RoutePermissionValidator.ts @@ -0,0 +1,144 @@ +/** + * 路由权限验证模块 + * + * 提供路由权限验证和路径检查功能 + * + * ## 主要功能 + * + * - 验证路径是否在用户菜单权限中 + * - 构建菜单路径集合(扁平化处理) + * - 支持动态路由参数匹配 + * - 路径前缀匹配 + * + * ## 使用场景 + * + * - 路由守卫中验证用户权限 + * - 动态路由注册后的权限检查 + * - 防止用户访问无权限的页面 + * + * @module router/core/RoutePermissionValidator + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' + +/** + * 路由权限验证器 + */ +export class RoutePermissionValidator { + /** + * 验证路径是否在用户菜单权限中 + * @param targetPath 目标路径 + * @param menuList 菜单列表 + * @returns 是否有权限访问 + */ + static hasPermission(targetPath: string, menuList: AppRouteRecord[]): boolean { + // 根路径始终允许访问 + if (targetPath === '/') { + return true + } + + // 构建路径集合 + const pathSet = this.buildMenuPathSet(menuList) + + // 检查路径是否在集合中(精确匹配或前缀匹配) + return pathSet.has(targetPath) || this.checkPathPrefix(targetPath, pathSet) + } + + /** + * 构建菜单路径集合(扁平化处理) + * @param menuList 菜单列表 + * @param pathSet 路径集合 + * @returns 路径集合 + */ + static buildMenuPathSet( + menuList: AppRouteRecord[], + pathSet: Set = new Set() + ): Set { + if (!Array.isArray(menuList) || menuList.length === 0) { + return pathSet + } + + for (const menuItem of menuList) { + // 跳过隐藏的菜单项 + if (menuItem.meta?.isHide || !menuItem.path) { + continue + } + + // 标准化路径并添加到集合 + const menuPath = menuItem.path.startsWith('/') ? menuItem.path : `/${menuItem.path}` + pathSet.add(menuPath) + + // 递归处理子菜单 + if (menuItem.children?.length) { + this.buildMenuPathSet(menuItem.children, pathSet) + } + } + + return pathSet + } + + /** + * 检查目标路径是否匹配集合中的某个路径前缀 + * 用于支持动态路由参数匹配,如 /user/123 匹配 /user + * @param targetPath 目标路径 + * @param pathSet 路径集合 + * @returns 是否匹配 + */ + static checkPathPrefix(targetPath: string, pathSet: Set): boolean { + // 遍历路径集合,检查是否有前缀匹配 + for (const menuPath of pathSet) { + // 1. 动态路由参数匹配(:id → [^/]+) + if (this.isDynamicPath(menuPath)) { + const regex = new RegExp(`^${this.normalizeDynamicPath(menuPath)}(/|$)`) + if (regex.test(targetPath)) { + return true + } + continue + } + + // 2. 普通前缀匹配 + if (targetPath.startsWith(`${menuPath}/`)) { + return true + } + } + return false + } + + /** + * 判断是否包含动态路由参数 + */ + private static isDynamicPath(path: string): boolean { + return path.includes(':') + } + + /** + * 将动态路由转换为正则表达式片段 + * 例如:/tenant/:tenantId/announcements → /tenant/[^/]+/announcements + */ + private static normalizeDynamicPath(path: string): string { + return path.replace(/:[^/]+/g, '[^/]+') + } + + /** + * 验证并返回有效的路径 + * 如果目标路径无权限,返回首页路径 + * @param targetPath 目标路径 + * @param menuList 菜单列表 + * @param homePath 首页路径 + * @returns 验证后的路径 + */ + static validatePath( + targetPath: string, + menuList: AppRouteRecord[], + homePath: string = '/' + ): { path: string; hasPermission: boolean } { + const hasPermission = this.hasPermission(targetPath, menuList) + + if (hasPermission) { + return { path: targetPath, hasPermission: true } + } + + return { path: homePath, hasPermission: false } + } +} diff --git a/src/router/core/RouteRegistry.ts b/src/router/core/RouteRegistry.ts new file mode 100644 index 0000000..e1acb9e --- /dev/null +++ b/src/router/core/RouteRegistry.ts @@ -0,0 +1,90 @@ +/** + * 路由注册核心类 + * + * 负责动态路由的注册、验证和管理 + * + * @module router/core/RouteRegistry + * @author Art Design Pro Team + */ + +import type { Router, RouteRecordRaw } from 'vue-router' +import type { AppRouteRecord } from '@/types/router' +import { ComponentLoader } from './ComponentLoader' +import { RouteValidator } from './RouteValidator' +import { RouteTransformer } from './RouteTransformer' + +export class RouteRegistry { + private router: Router + private componentLoader: ComponentLoader + private validator: RouteValidator + private transformer: RouteTransformer + private removeRouteFns: (() => void)[] = [] + private registered = false + + constructor(router: Router) { + this.router = router + this.componentLoader = new ComponentLoader() + this.validator = new RouteValidator() + this.transformer = new RouteTransformer(this.componentLoader) + } + + /** + * 注册动态路由 + */ + register(menuList: AppRouteRecord[]): void { + if (this.registered) { + console.warn('[RouteRegistry] 路由已注册,跳过重复注册') + return + } + + // 验证路由配置 + const validationResult = this.validator.validate(menuList) + if (!validationResult.valid) { + throw new Error(`路由配置验证失败: ${validationResult.errors.join(', ')}`) + } + + // 转换并注册路由 + const removeRouteFns: (() => void)[] = [] + + menuList.forEach((route) => { + if (route.name && !this.router.hasRoute(route.name)) { + const routeConfig = this.transformer.transform(route) + const removeRouteFn = this.router.addRoute(routeConfig as RouteRecordRaw) + removeRouteFns.push(removeRouteFn) + } + }) + + this.removeRouteFns = removeRouteFns + this.registered = true + } + + /** + * 移除所有动态路由 + */ + unregister(): void { + this.removeRouteFns.forEach((fn) => fn()) + this.removeRouteFns = [] + this.registered = false + } + + /** + * 检查是否已注册 + */ + isRegistered(): boolean { + return this.registered + } + + /** + * 获取移除函数列表(用于 store 管理) + */ + getRemoveRouteFns(): (() => void)[] { + return this.removeRouteFns + } + + /** + * 标记为已注册(用于错误处理场景,避免重复请求) + */ + markAsRegistered(): void { + this.registered = true + } +} diff --git a/src/router/core/RouteTransformer.ts b/src/router/core/RouteTransformer.ts new file mode 100644 index 0000000..f00f27c --- /dev/null +++ b/src/router/core/RouteTransformer.ts @@ -0,0 +1,148 @@ +/** + * 路由转换器 + * + * 负责将菜单数据转换为 Vue Router 路由配置 + * + * @module router/core/RouteTransformer + * @author Art Design Pro Team + */ + +import type { RouteRecordRaw } from 'vue-router' +import type { AppRouteRecord } from '@/types/router' +import { ComponentLoader } from './ComponentLoader' +import { IframeRouteManager } from './IframeRouteManager' + +interface ConvertedRoute extends Omit { + id?: number + children?: ConvertedRoute[] + component?: RouteRecordRaw['component'] | (() => Promise) +} + +export class RouteTransformer { + private componentLoader: ComponentLoader + private iframeManager: IframeRouteManager + + constructor(componentLoader: ComponentLoader) { + this.componentLoader = componentLoader + this.iframeManager = IframeRouteManager.getInstance() + } + + /** + * 转换路由配置 + */ + transform(route: AppRouteRecord, depth = 0): ConvertedRoute { + const { component, children, ...routeConfig } = route + + // 基础路由配置 + const converted: ConvertedRoute = { + ...routeConfig, + component: undefined + } + + // 处理不同类型的路由 + if (route.meta.isIframe) { + this.handleIframeRoute(converted, route, depth) + } else if (this.isFirstLevelRoute(route, depth)) { + this.handleFirstLevelRoute(converted, route, component) + } else { + this.handleNormalRoute(converted, component) + } + + // 递归处理子路由 + if (children?.length) { + converted.children = children.map((child) => this.transform(child, depth + 1)) + } + + return converted + } + + /** + * 判断是否为一级路由(需要 Layout 包裹) + */ + private isFirstLevelRoute(route: AppRouteRecord, depth: number): boolean { + return depth === 0 && (!route.children || route.children.length === 0) + } + + /** + * 处理 iframe 类型路由 + */ + private handleIframeRoute( + targetRoute: ConvertedRoute, + sourceRoute: AppRouteRecord, + depth: number + ): void { + if (depth === 0) { + // 顶级 iframe:用 Layout 包裹 + targetRoute.component = this.componentLoader.loadLayout() + targetRoute.path = this.extractFirstSegment(sourceRoute.path || '') + targetRoute.name = '' + + targetRoute.children = [ + { + ...sourceRoute, + component: this.componentLoader.loadIframe() + } as ConvertedRoute + ] + } else { + // 非顶级(嵌套)iframe:直接使用 Iframe.vue + targetRoute.component = this.componentLoader.loadIframe() + } + + // 记录 iframe 路由 + this.iframeManager.add(sourceRoute) + } + + /** + * 处理一级菜单路由 + */ + private handleFirstLevelRoute( + converted: ConvertedRoute, + route: AppRouteRecord, + component: AppRouteRecord['component'] | undefined + ): void { + converted.component = this.componentLoader.loadLayout() + converted.path = this.extractFirstSegment(route.path || '') + converted.name = '' + route.meta.isFirstLevel = true + + converted.children = [ + { + ...route, + component: this.resolveComponent(component) + } as ConvertedRoute + ] + } + + /** + * 处理普通路由 + */ + private handleNormalRoute( + converted: ConvertedRoute, + component: AppRouteRecord['component'] | undefined + ): void { + converted.component = this.resolveComponent(component) + } + + /** + * 解析组件配置(支持字符串路径与动态导入函数) + */ + private resolveComponent( + component: AppRouteRecord['component'] | undefined + ): RouteRecordRaw['component'] | undefined { + if (!component) return undefined + + if (typeof component === 'function') { + return component + } + + return this.componentLoader.load(component) + } + + /** + * 提取路径的第一段 + */ + private extractFirstSegment(path: string): string { + const segments = path.split('/').filter(Boolean) + return segments.length > 0 ? `/${segments[0]}` : '/' + } +} diff --git a/src/router/core/RouteValidator.ts b/src/router/core/RouteValidator.ts new file mode 100644 index 0000000..f8e58fc --- /dev/null +++ b/src/router/core/RouteValidator.ts @@ -0,0 +1,187 @@ +/** + * 路由验证器 + * + * 负责验证路由配置的合法性 + * + * @module router/core/RouteValidator + * @author Art Design Pro Team + */ + +import type { AppRouteRecord } from '@/types/router' +import { RoutesAlias } from '../routesAlias' + +export interface ValidationResult { + valid: boolean + errors: string[] + warnings: string[] +} + +export class RouteValidator { + // 用于记录已经提示过的路由,避免重复提示 + private warnedRoutes = new Set() + + /** + * 验证路由配置 + */ + validate(routes: AppRouteRecord[]): ValidationResult { + const errors: string[] = [] + const warnings: string[] = [] + + // 检测重复路由 + this.checkDuplicates(routes, errors, warnings) + + // 检测组件配置 + this.checkComponents(routes, errors, warnings) + + // 检测嵌套菜单的 /index/index 配置 + this.checkNestedIndexComponent(routes) + + return { + valid: errors.length === 0, + errors, + warnings + } + } + + /** + * 检测重复路由 + */ + private checkDuplicates( + routes: AppRouteRecord[], + errors: string[], + warnings: string[], + parentPath = '' + ): void { + const routeNameMap = new Map() + const componentPathMap = new Map() + + const checkRoutes = (routes: AppRouteRecord[], parentPath = '') => { + routes.forEach((route) => { + const currentPath = route.path || '' + const fullPath = this.resolvePath(parentPath, currentPath) + + // 名称重复检测 + if (route.name) { + const routeName = String(route.name) + if (routeNameMap.has(routeName)) { + warnings.push(`路由名称重复: "${routeName}" (${fullPath})`) + } else { + routeNameMap.set(routeName, fullPath) + } + } + + // 组件路径重复检测 + if (route.component && typeof route.component === 'string') { + const componentPath = route.component + if (componentPath !== RoutesAlias.Layout) { + const componentKey = `${parentPath}:${componentPath}` + if (componentPathMap.has(componentKey)) { + warnings.push(`组件路径重复: "${componentPath}" (${fullPath})`) + } else { + componentPathMap.set(componentKey, fullPath) + } + } + } + + // 递归处理子路由 + if (route.children?.length) { + checkRoutes(route.children, fullPath) + } + }) + } + + checkRoutes(routes, parentPath) + } + + /** + * 检测组件配置 + */ + private checkComponents( + routes: AppRouteRecord[], + errors: string[], + warnings: string[], + parentPath = '' + ): void { + routes.forEach((route) => { + const hasExternalLink = !!route.meta?.link?.trim() + const hasChildren = Array.isArray(route.children) && route.children.length > 0 + const routePath = route.path || '[未定义路径]' + const isIframe = route.meta?.isIframe + + // 如果配置了 component,则无需校验 + if (route.component) { + // 递归检查子路由 + if (route.children?.length) { + const fullPath = this.resolvePath(parentPath, route.path || '') + this.checkComponents(route.children, errors, warnings, fullPath) + } + return + } + + // 一级菜单:必须指定 Layout,除非是外链或 iframe + if (parentPath === '' && !hasExternalLink && !isIframe) { + errors.push(`一级菜单(${routePath}) 缺少 component,必须指向 ${RoutesAlias.Layout}`) + return + } + + // 非一级菜单:如果既不是外链、iframe,也没有子路由,则必须配置 component + if (!hasExternalLink && !isIframe && !hasChildren) { + errors.push(`路由(${routePath}) 缺少 component 配置`) + } + + // 递归检查子路由 + if (route.children?.length) { + const fullPath = this.resolvePath(parentPath, route.path || '') + this.checkComponents(route.children, errors, warnings, fullPath) + } + }) + } + + /** + * 检测嵌套菜单的 Layout 组件配置 + * 只有一级菜单才能使用 Layout,二级及以下菜单不能使用 + */ + private checkNestedIndexComponent(routes: AppRouteRecord[], level = 1): void { + routes.forEach((route) => { + // 检查二级及以下菜单是否错误使用了 Layout + if (level > 1 && route.component === RoutesAlias.Layout) { + this.logLayoutError(route, level) + } + + // 递归检查子路由 + if (route.children?.length) { + this.checkNestedIndexComponent(route.children, level + 1) + } + }) + } + + /** + * 输出 Layout 组件配置错误日志 + */ + private logLayoutError(route: AppRouteRecord, level: number): void { + const routeName = String(route.name || route.path || '未知路由') + const routeKey = `${routeName}_${route.path}` + + // 避免重复提示 + if (this.warnedRoutes.has(routeKey)) return + this.warnedRoutes.add(routeKey) + + const menuTitle = route.meta?.title || routeName + const routePath = route.path || '/' + + console.error( + `[路由配置错误] 菜单 "${menuTitle}" (name: ${routeName}, path: ${routePath}) 配置错误\n` + + ` 问题: ${level}级菜单不能使用 ${RoutesAlias.Layout} 作为 component\n` + + ` 说明: 只有一级菜单才能使用 ${RoutesAlias.Layout},二级及以下菜单应该指向具体的组件路径\n` + + ` 当前配置: component: '${RoutesAlias.Layout}'\n` + + ` 应该改为: component: '/your/component/path' 或留空 ''(如果是目录菜单)` + ) + } + + /** + * 路径解析 + */ + private resolvePath(parent: string, child: string): string { + return [parent.replace(/\/$/, ''), child.replace(/^\//, '')].filter(Boolean).join('/') + } +} diff --git a/src/router/core/index.ts b/src/router/core/index.ts new file mode 100644 index 0000000..fcfecfc --- /dev/null +++ b/src/router/core/index.ts @@ -0,0 +1,14 @@ +/** + * 路由核心模块导出 + * + * @module router/core + * @author Art Design Pro Team + */ + +export { RouteRegistry } from './RouteRegistry' +export { ComponentLoader } from './ComponentLoader' +export { RouteValidator } from './RouteValidator' +export { RouteTransformer } from './RouteTransformer' +export { IframeRouteManager } from './IframeRouteManager' +export { MenuProcessor } from './MenuProcessor' +export { RoutePermissionValidator } from './RoutePermissionValidator' diff --git a/src/router/guards/afterEach.ts b/src/router/guards/afterEach.ts new file mode 100644 index 0000000..d60572d --- /dev/null +++ b/src/router/guards/afterEach.ts @@ -0,0 +1,34 @@ +import { nextTick } from 'vue' +import { useSettingStore } from '@/store/modules/setting' +import { Router } from 'vue-router' +import NProgress from 'nprogress' +import { useCommon } from '@/hooks/core/useCommon' +import { loadingService } from '@/utils/ui' +import { getPendingLoading, resetPendingLoading } from './beforeEach' + +/** 路由全局后置守卫 */ +export function setupAfterEachGuard(router: Router) { + const { scrollToTop } = useCommon() + + router.afterEach(() => { + scrollToTop() + + // 关闭进度条 + const settingStore = useSettingStore() + if (settingStore.showNprogress) { + NProgress.done() + // 确保进度条完全移除,避免残影 + setTimeout(() => { + NProgress.remove() + }, 600) + } + + // 关闭 loading 效果 + if (getPendingLoading()) { + nextTick(() => { + loadingService.hideLoading() + resetPendingLoading() + }) + } + }) +} diff --git a/src/router/guards/beforeEach.ts b/src/router/guards/beforeEach.ts new file mode 100644 index 0000000..07cbc17 --- /dev/null +++ b/src/router/guards/beforeEach.ts @@ -0,0 +1,430 @@ +/** + * 路由全局前置守卫模块 + * + * 提供完整的路由导航守卫功能 + * + * ## 主要功能 + * + * - 登录状态验证和重定向 + * - 动态路由注册和权限控制 + * - 菜单数据获取和处理(前端/后端模式) + * - 用户信息获取和缓存 + * - 页面标题设置 + * - 工作标签页管理 + * - 进度条和加载动画控制 + * - 静态路由识别和处理 + * - 错误处理和异常跳转 + * + * ## 使用场景 + * + * - 路由跳转前的权限验证 + * - 动态菜单加载和路由注册 + * - 用户登录状态管理 + * - 页面访问控制 + * - 路由级别的加载状态管理 + * + * ## 工作流程 + * + * 1. 检查登录状态,未登录跳转到登录页 + * 2. 首次访问时获取用户信息和菜单数据 + * 3. 根据权限动态注册路由 + * 4. 设置页面标题和工作标签页 + * 5. 处理根路径重定向到首页 + * 6. 未匹配路由跳转到 404 页面 + * + * @module router/guards/beforeEach + * @author Art Design Pro Team + */ +import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router' +import { nextTick } from 'vue' +import NProgress from 'nprogress' +import { useSettingStore } from '@/store/modules/setting' +import { useUserStore } from '@/store/modules/user' +import { useMenuStore } from '@/store/modules/menu' +import { setWorktab } from '@/utils/navigation' +import { setPageTitle } from '@/utils/router' +import { RoutesAlias } from '../routesAlias' +import { staticRoutes } from '../routes/staticRoutes' +import { loadingService } from '@/utils/ui' +import { useCommon } from '@/hooks/core/useCommon' +import { useWorktabStore } from '@/store/modules/worktab' +import { fetchGetUserInfo } from '@/api/auth' +import { ApiStatus } from '@/utils/http/status' +import { isHttpError } from '@/utils/http/error' +import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core' + +// 路由注册器实例 +let routeRegistry: RouteRegistry | null = null + +// 菜单处理器实例 +const menuProcessor = new MenuProcessor() + +// 跟踪是否需要关闭 loading +let pendingLoading = false + +// 路由初始化失败标记,防止死循环 +// 一旦设置为 true,只有刷新页面或重新登录才能重置 +let routeInitFailed = false + +// 路由初始化进行中标记,防止并发请求 +let routeInitInProgress = false + +/** + * 获取 pendingLoading 状态 + */ +export function getPendingLoading(): boolean { + return pendingLoading +} + +/** + * 重置 pendingLoading 状态 + */ +export function resetPendingLoading(): void { + pendingLoading = false +} + +/** + * 获取路由初始化失败状态 + */ +export function getRouteInitFailed(): boolean { + return routeInitFailed +} + +/** + * 重置路由初始化状态(用于重新登录场景) + */ +export function resetRouteInitState(): void { + routeInitFailed = false + routeInitInProgress = false +} + +/** + * 设置路由全局前置守卫 + */ +export function setupBeforeEachGuard(router: Router): void { + // 初始化路由注册器 + routeRegistry = new RouteRegistry(router) + + router.beforeEach( + async ( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext + ) => { + try { + await handleRouteGuard(to, from, next, router) + } catch (error) { + console.error('[RouteGuard] 路由守卫处理失败:', error) + closeLoading() + next({ name: 'Exception500' }) + } + } + ) +} + +/** + * 关闭 loading 效果 + */ +function closeLoading(): void { + if (pendingLoading) { + nextTick(() => { + loadingService.hideLoading() + pendingLoading = false + }) + } +} + +/** + * 处理路由守卫逻辑 + */ +async function handleRouteGuard( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext, + router: Router +): Promise { + const settingStore = useSettingStore() + const userStore = useUserStore() + + // 启动进度条 + if (settingStore.showNprogress) { + NProgress.start() + } + + // 1. 检查登录状态 + if (!handleLoginStatus(to, userStore, next)) { + return + } + + // 2. 检查路由初始化是否已失败(防止死循环) + if (routeInitFailed) { + // 已经失败过,直接放行到错误页面,不再重试 + if (to.matched.length > 0) { + next() + } else { + // 未匹配到路由,跳转到 500 页面 + next({ name: 'Exception500', replace: true }) + } + return + } + + // 3. 处理动态路由注册 + if (!routeRegistry?.isRegistered() && userStore.isLogin && to.path !== RoutesAlias.Login) { + // 静态页面(如入驻进度)不需要动态菜单,直接放行 + if (isStaticRoute(to.path)) { + setPageTitle(to) + next() + return + } + // 防止并发请求(快速连续导航场景) + if (routeInitInProgress) { + // 正在初始化中,等待完成后重新导航 + next(false) + return + } + await handleDynamicRoutes(to, next, router) + return + } + + // 4. 处理根路径重定向 + if (handleRootPathRedirect(to, next)) { + return + } + + // 5. 处理已匹配的路由 + if (to.matched.length > 0) { + setWorktab(to) + setPageTitle(to) + next() + return + } + + // 6. 未匹配到路由,跳转到 404 + next({ name: 'Exception404' }) +} + +/** + * 处理登录状态 + * @returns true 表示可以继续,false 表示已处理跳转 + */ +function handleLoginStatus( + to: RouteLocationNormalized, + userStore: ReturnType, + next: NavigationGuardNext +): boolean { + // 已登录或访问登录页或允许未登录访问的静态路由,直接放行 + if (userStore.isLogin || to.path === RoutesAlias.Login || isPublicStaticRoute(to.path)) { + return true + } + + // 未登录且访问需要权限的页面,跳转到登录页并携带 redirect 参数 + userStore.logOut() + next({ + name: 'Login', + query: { redirect: to.fullPath } + }) + return false +} + +/** + * 检查路由是否为无需登录的静态路由 + */ +function isPublicStaticRoute(path: string): boolean { + // 0. 入驻相关页面虽然是静态路由,但要求登录访问 + const loginRequiredStaticPaths = [ + '/onboarding/status', + '/onboarding/pricing', + '/onboarding/waiting', + '/onboarding/error' + ] + return isStaticRoute(path) && !loginRequiredStaticPaths.includes(path) +} + +/** + * 检查路由是否为静态路由 + */ +function isStaticRoute(path: string): boolean { + const checkRoute = (routes: any[], targetPath: string): boolean => { + return routes.some((route) => { + // 处理动态路由参数匹配 + const routePath = route.path + const pattern = routePath.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*') + const regex = new RegExp(`^${pattern}$`) + + if (regex.test(targetPath)) { + return true + } + if (route.children && route.children.length > 0) { + return checkRoute(route.children, targetPath) + } + return false + }) + } + + return checkRoute(staticRoutes, path) +} + +/** + * 处理动态路由注册 + */ +async function handleDynamicRoutes( + to: RouteLocationNormalized, + next: NavigationGuardNext, + router: Router +): Promise { + // 标记初始化进行中 + routeInitInProgress = true + + // 显示 loading + pendingLoading = true + loadingService.showLoading() + + try { + // 1. 获取用户信息 + await fetchUserInfo() + + // 2. 获取菜单数据 + const menuList = await menuProcessor.getMenuList() + + // 3. 验证菜单数据 + if (!menuProcessor.validateMenuList(menuList)) { + throw new Error('获取菜单列表失败,请重新登录') + } + + // 4. 注册动态路由 + routeRegistry?.register(menuList) + + // 5. 保存菜单数据到 store + const menuStore = useMenuStore() + menuStore.setMenuList(menuList) + menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || []) + + // 6. 保存 iframe 路由 + IframeRouteManager.getInstance().save() + + // 7. 验证工作标签页 + useWorktabStore().validateWorktabs(router) + + // 8. 验证目标路径权限 + const { homePath } = useCommon() + const { path: validatedPath, hasPermission } = RoutePermissionValidator.validatePath( + to.path, + menuList, + homePath.value || '/' + ) + + // 初始化成功,重置进行中标记 + routeInitInProgress = false + + // 9. 重新导航到目标路由 + if (!hasPermission) { + // 无权限访问,跳转到首页 + closeLoading() + + // 输出警告信息 + console.warn(`[RouteGuard] 用户无权限访问路径: ${to.path},已跳转到首页`) + + // 直接跳转到首页 + next({ + path: validatedPath, + replace: true + }) + } else { + // 有权限,正常导航 + next({ + path: to.path, + query: to.query, + hash: to.hash, + replace: true + }) + } + } catch (error) { + console.error('[RouteGuard] 动态路由注册失败:', error) + + // 关闭 loading + closeLoading() + + // 401 错误:axios 拦截器已处理退出登录,取消当前导航 + if (isUnauthorizedError(error)) { + // 重置状态,允许重新登录后再次初始化 + routeInitInProgress = false + next(false) + return + } + + // 处理菜单获取失败的情况(如接口异常或返回空数据),直接跳转登录页 + if (error instanceof Error && error.message === '获取菜单列表失败,请重新登录') { + const userStore = useUserStore() + userStore.logOut() + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + + // 标记初始化失败,防止死循环 + routeInitFailed = true + routeInitInProgress = false + + // 输出详细错误信息,便于排查 + if (isHttpError(error)) { + console.error(`[RouteGuard] 错误码: ${error.code}, 消息: ${error.message}`) + } + + // 跳转到 500 页面,使用 replace 避免产生历史记录 + next({ name: 'Exception500', replace: true }) + } +} + +/** + * 获取用户信息 + */ +async function fetchUserInfo(): Promise { + const userStore = useUserStore() + const data = await fetchGetUserInfo() + userStore.setUserInfo(data) + // 获取用户权限 + await userStore.getUserPermissions() + // 检查并清理工作台标签页(如果是不同用户登录) + userStore.checkAndClearWorktabs() +} + +/** + * 重置路由相关状态 + */ +export function resetRouterState(delay: number): void { + setTimeout(() => { + routeRegistry?.unregister() + IframeRouteManager.getInstance().clear() + + const menuStore = useMenuStore() + menuStore.removeAllDynamicRoutes() + menuStore.setMenuList([]) + + // 重置路由初始化状态,允许重新登录后再次初始化 + resetRouteInitState() + }, delay) +} + +/** + * 处理根路径重定向到首页 + * @returns true 表示已处理跳转,false 表示无需跳转 + */ +function handleRootPathRedirect(to: RouteLocationNormalized, next: NavigationGuardNext): boolean { + if (to.path !== '/') { + return false + } + + const { homePath } = useCommon() + if (homePath.value && homePath.value !== '/') { + next({ path: homePath.value, replace: true }) + return true + } + + return false +} + +/** + * 判断是否为未授权错误(401) + */ +function isUnauthorizedError(error: unknown): boolean { + return isHttpError(error) && error.code === ApiStatus.unauthorized +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..286ae58 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,23 @@ +import type { App } from 'vue' +import { createRouter, createWebHashHistory } from 'vue-router' +import { staticRoutes } from './routes/staticRoutes' +import { configureNProgress } from '@/utils/router' +import { setupBeforeEachGuard } from './guards/beforeEach' +import { setupAfterEachGuard } from './guards/afterEach' + +// 创建路由实例 +export const router = createRouter({ + history: createWebHashHistory(), + routes: staticRoutes // 静态路由 +}) + +// 初始化路由 +export function initRouter(app: App): void { + configureNProgress() // 顶部进度条 + setupBeforeEachGuard(router) // 路由前置守卫 + setupAfterEachGuard(router) // 路由后置守卫 + app.use(router) +} + +// 主页路径,默认使用菜单第一个有效路径,配置后使用此路径 +export const HOME_PAGE_PATH = '' diff --git a/src/router/modules/announcement.ts b/src/router/modules/announcement.ts new file mode 100644 index 0000000..ccd1abb --- /dev/null +++ b/src/router/modules/announcement.ts @@ -0,0 +1,144 @@ +import type { AppRouteRecord } from '@/types/router' + +/** + * 公告管理路由模块 + * + * 覆盖平台、租户、应用端和草稿中心的公告管理路由 + */ +const announcementRoutes: AppRouteRecord[] = [ + /** + * 平台公告管理 + * - 列表 /platform/announcements + * - 创建 /platform/announcements/create + * - 编辑 /platform/announcements/:announcementId/edit + * - 详情 /platform/announcements/:announcementId + */ + { + path: '/platform/announcements', + name: 'PlatformAnnouncementRoot', + component: '/index/index', + meta: { + title: 'menus.announcement.platform.title', + icon: 'ri:megaphone-line', + permission: ['platform-announcement:read', 'platform-announcement:create'] + }, + children: [ + { + path: '', + name: 'PlatformAnnouncementList', + component: () => import('@views/platform/announcements/index.vue'), + meta: { + title: 'menus.announcement.platform.list', + icon: 'ri:list-check-2', + permission: ['platform-announcement:read', 'platform-announcement:create'], + authList: [ + { title: '创建平台公告', authMark: 'platform-announcement:create' }, + { title: '查看平台公告', authMark: 'platform-announcement:read' }, + { title: '编辑平台公告', authMark: 'platform-announcement:update' }, + { title: '删除平台公告', authMark: 'platform-announcement:delete' }, + { title: '发布平台公告', authMark: 'platform-announcement:publish' }, + { title: '撤销平台公告', authMark: 'platform-announcement:revoke' } + ] + } + }, + { + path: 'create', + name: 'PlatformAnnouncementCreate', + component: () => import('@views/platform/announcements/create.vue'), + meta: { + title: 'menus.announcement.platform.create', + icon: 'ri:add-circle-line', + permission: ['platform-announcement:create'] + } + }, + { + path: ':announcementId/edit', + name: 'PlatformAnnouncementEdit', + component: () => import('@views/platform/announcements/edit.vue'), + meta: { + title: 'menus.announcement.platform.edit', + icon: 'ri:edit-line', + permission: ['platform-announcement:update'] + } + }, + { + path: ':announcementId', + name: 'PlatformAnnouncementDetail', + component: () => import('@views/platform/announcements/detail.vue'), + meta: { + title: 'menus.announcement.platform.detail', + icon: 'ri:file-text-line', + permission: ['platform-announcement:read', 'platform-announcement:create'] + } + } + ] + }, + + /** + * 应用端公告(仅读) + * - 列表 /app/announcements + * - 详情 /app/announcements/:announcementId + */ + { + path: '/app/announcements', + name: 'AppAnnouncementRoot', + component: '/index/index', + meta: { + title: 'menus.announcement.app.title', + icon: 'ri:notification-badge-line', + permission: ['app-announcement:read'] + }, + children: [ + { + path: '', + name: 'AppAnnouncementList', + component: () => import('@views/app/announcements/index.vue'), + meta: { + title: 'menus.announcement.app.list', + icon: 'ri:list-check-2', + permission: ['app-announcement:read'], + authList: [{ title: '查看应用端公告', authMark: 'app-announcement:read' }] + } + }, + { + path: ':announcementId', + name: 'AppAnnouncementDetail', + component: () => import('@views/app/announcements/detail.vue'), + meta: { + title: 'menus.announcement.app.detail', + icon: 'ri:file-text-line', + permission: ['app-announcement:read'] + } + } + ] + }, + + /** + * 草稿中心 + * - 列表 /announcement-drafts + */ + { + path: '/announcement-drafts', + name: 'AnnouncementDraftRoot', + component: '/index/index', + meta: { + title: 'menus.announcement.drafts.title', + icon: 'ri:draft-line', + permission: ['platform-announcement:create', 'tenant-announcement:create'] + }, + children: [ + { + path: '', + name: 'AnnouncementDraftList', + component: () => import('@views/announcement-drafts/index.vue'), + meta: { + title: 'menus.announcement.drafts.list', + icon: 'ri:draft-line', + permission: ['platform-announcement:create', 'tenant-announcement:create'] + } + } + ] + } +] + +export default announcementRoutes diff --git a/src/router/modules/dashboard.ts b/src/router/modules/dashboard.ts new file mode 100644 index 0000000..5f9c3e9 --- /dev/null +++ b/src/router/modules/dashboard.ts @@ -0,0 +1,24 @@ +import { AppRouteRecord } from '@/types/router' + +export const dashboardRoutes: AppRouteRecord = { + name: 'Dashboard', + path: '/dashboard', + component: '/index/index', + meta: { + title: 'menus.dashboard.title', + icon: 'ri:pie-chart-line', + roles: ['R_SUPER', 'R_ADMIN'] + }, + children: [ + { + path: 'console', + name: 'Console', + component: '/dashboard/console', + meta: { + title: 'menus.dashboard.console', + keepAlive: false, + fixedTab: true + } + } + ] +} diff --git a/src/router/modules/dictionary.ts b/src/router/modules/dictionary.ts new file mode 100644 index 0000000..5209a23 --- /dev/null +++ b/src/router/modules/dictionary.ts @@ -0,0 +1,76 @@ +import type { AppRouteRecord } from '@/types/router' + +/** + * 字典管理相关路由模块 + * 包含:系统字典管理、租户字典管理、字典覆盖配置、缓存监控 + */ +const dictionaryRoutes: AppRouteRecord = { + path: '/dictionary', + name: 'Dictionary', + component: '/index/index', + redirect: '/dictionary/system', + meta: { + title: 'menus.dictionary.title', + icon: 'ri:book-2-line', + order: 5 + }, + children: [ + { + path: 'system', + name: 'SystemDictionary', + component: () => import('@views/system/dictionary/index.vue'), + meta: { + title: 'menus.dictionary.system', + icon: 'ri:book-2-line', + roles: ['PlatformAdmin'], + permission: ['dictionary:group:read'] + } + }, + { + path: 'tenant', + name: 'TenantDictionary', + component: () => import('@views/tenant/dictionary/index.vue'), + meta: { + title: 'menus.dictionary.tenant', + icon: 'ri:bookmark-3-line', + roles: ['TenantAdmin'], + permission: ['dictionary:group:read'] + } + }, + { + path: 'override', + name: 'TenantDictionaryOverride', + component: () => import('@views/tenant/dictionary-override/index.vue'), + meta: { + title: 'menus.dictionary.override', + icon: 'ri:settings-3-line', + roles: ['TenantAdmin'], + permission: ['dictionary:override:read'] + } + }, + { + path: 'label-override', + name: 'PlatformLabelOverride', + component: () => import('@views/system/dictionary-label-override/index.vue'), + meta: { + title: 'menus.dictionary.labelOverride', + icon: 'ri:exchange-line', + roles: ['PlatformAdmin'], + permission: ['dictionary:override:platform:read'] + } + }, + { + path: 'metrics', + name: 'DictionaryMetrics', + component: () => import('@views/system/dictionary-metrics/index.vue'), + meta: { + title: 'menus.dictionary.metrics', + icon: 'ri:bar-chart-box-line', + roles: ['PlatformAdmin'], + permission: ['dictionary:group:read'] + } + } + ] +} + +export default dictionaryRoutes diff --git a/src/router/modules/exception.ts b/src/router/modules/exception.ts new file mode 100644 index 0000000..07c5604 --- /dev/null +++ b/src/router/modules/exception.ts @@ -0,0 +1,46 @@ +import { AppRouteRecord } from '@/types/router' + +export const exceptionRoutes: AppRouteRecord = { + path: '/exception', + name: 'Exception', + component: '/index/index', + meta: { + title: 'menus.exception.title', + icon: 'ri:error-warning-line' + }, + children: [ + { + path: '403', + name: 'Exception403', + component: '/exception/403', + meta: { + title: 'menus.exception.forbidden', + keepAlive: true, + isHideTab: true, + isFullPage: true + } + }, + { + path: '404', + name: 'Exception404', + component: '/exception/404', + meta: { + title: 'menus.exception.notFound', + keepAlive: true, + isHideTab: true, + isFullPage: true + } + }, + { + path: '500', + name: 'Exception500', + component: '/exception/500', + meta: { + title: 'menus.exception.serverError', + keepAlive: true, + isHideTab: true, + isFullPage: true + } + } + ] +} diff --git a/src/router/modules/index.ts b/src/router/modules/index.ts new file mode 100644 index 0000000..80d74d5 --- /dev/null +++ b/src/router/modules/index.ts @@ -0,0 +1,23 @@ +import { AppRouteRecord } from '@/types/router' +import { dashboardRoutes } from './dashboard' +import { resultRoutes } from './result' +import { exceptionRoutes } from './exception' +import tenantRoutes from './tenant' +import announcementRoutes from './announcement' +import merchantRoutes from './merchant' +import dictionaryRoutes from './dictionary' +import storeRoutes from './store' + +/** + * 导出所有模块化路由 + */ +export const routeModules: AppRouteRecord[] = [ + dashboardRoutes, + tenantRoutes, + merchantRoutes, + dictionaryRoutes, + ...storeRoutes, + ...announcementRoutes, + resultRoutes, + exceptionRoutes +] diff --git a/src/router/modules/merchant.ts b/src/router/modules/merchant.ts new file mode 100644 index 0000000..7c25be7 --- /dev/null +++ b/src/router/modules/merchant.ts @@ -0,0 +1,48 @@ +import { AppRouteRecord } from '@/types/router' + +/** + * 商户管理相关路由模块 + */ +const merchantRoutes: AppRouteRecord = { + path: '/merchant', + name: 'Merchant', + component: '/index/index', + redirect: '/merchant/list', + meta: { + title: 'menus.merchant.title', + icon: 'ri:store-2-line', + order: 4 + }, + children: [ + { + path: 'list', + name: 'MerchantList', + component: '/merchant/list', + meta: { + title: 'menus.merchant.list', + icon: 'ri:list-unordered' + } + }, + { + path: 'review', + name: 'MerchantReview', + component: '/merchant/review', + meta: { + title: 'menus.merchant.review', + icon: 'ri:shield-check-line' + } + }, + { + path: 'store', + name: 'StoreList', + component: '/store/store-list/index', + meta: { + title: 'menus.store.title', + icon: 'ri:store-2-line', + permission: ['store:read'] + } + } + ] +} + +export default merchantRoutes diff --git a/src/router/modules/result.ts b/src/router/modules/result.ts new file mode 100644 index 0000000..575a2f7 --- /dev/null +++ b/src/router/modules/result.ts @@ -0,0 +1,33 @@ +import { AppRouteRecord } from '@/types/router' + +export const resultRoutes: AppRouteRecord = { + path: '/result', + name: 'Result', + component: '/index/index', + meta: { + title: 'menus.result.title', + icon: 'ri:checkbox-circle-line' + }, + children: [ + { + path: 'success', + name: 'ResultSuccess', + component: '/result/success', + meta: { + title: 'menus.result.success', + icon: 'ri:checkbox-circle-line', + keepAlive: true + } + }, + { + path: 'fail', + name: 'ResultFail', + component: '/result/fail', + meta: { + title: 'menus.result.fail', + icon: 'ri:close-circle-line', + keepAlive: true + } + } + ] +} diff --git a/src/router/modules/store.ts b/src/router/modules/store.ts new file mode 100644 index 0000000..69b746d --- /dev/null +++ b/src/router/modules/store.ts @@ -0,0 +1,47 @@ +import type { AppRouteRecord } from '@/types/router' + +/** + * 门店管理路由模块 + */ +const storeRoutes: AppRouteRecord[] = [ + { + path: '/platform', + name: 'Platform', + component: '/index/index', + redirect: '/platform/store-audits', + meta: { + title: 'menus.platform.title', + icon: 'ri:shield-star-line', + permission: ['store-audit:read', 'store-qualification:read'] + }, + children: [ + { + path: 'store-audits', + name: 'PlatformStoreAudits', + component: '/platform/store-audits', + meta: { + title: 'menus.platform.storeAudits', + icon: 'ri:checkbox-multiple-line', + permission: ['store-audit:read'], + authList: [ + { title: '审核通过', authMark: 'store-audit:approve' }, + { title: '审核驳回', authMark: 'store-audit:reject' }, + { title: '强制关闭/解除', authMark: 'store-audit:force-close' } + ] + } + }, + { + path: 'qualification-alerts', + name: 'PlatformQualificationAlerts', + component: '/platform/qualification-alerts', + meta: { + title: 'menus.platform.qualificationAlerts', + icon: 'ri:alarm-warning-line', + permission: ['store-qualification:read'] + } + } + ] + } +] + +export default storeRoutes diff --git a/src/router/modules/tenant.ts b/src/router/modules/tenant.ts new file mode 100644 index 0000000..ddb1ffa --- /dev/null +++ b/src/router/modules/tenant.ts @@ -0,0 +1,88 @@ +import { AppRouteRecord } from '@/types/router' + +/** + * 租户管理相关路由模块 + */ +const tenantRoutes: AppRouteRecord = { + path: '/tenant', + name: 'Tenant', + component: '/index/index', + redirect: '/tenant/subscription', + meta: { + title: 'menus.tenant.title', + icon: 'ri:building-2-line', + order: 3 + }, + children: [ + { + path: 'subscription', + name: 'TenantSubscription', + component: '/tenant/subscription', + meta: { + title: 'menus.tenant.subscription', + icon: 'ri:calendar-check-line' + } + }, + { + path: 'billing', + name: 'TenantBilling', + component: '/tenant/billing', + meta: { + title: 'menus.tenant.billing', + icon: 'ri:bill-line' + } + }, + { + path: 'billing/statistics', + name: 'TenantBillingStatistics', + component: '/tenant/billing/statistics', + meta: { + title: 'menus.tenant.billingStatistics', + icon: 'ri:bar-chart-box-line', + isHideMenu: false + } + }, + { + path: 'announcements', + name: 'TenantAnnouncementList', + component: () => import('@views/tenant/announcements/index.vue'), + meta: { + title: 'menus.announcement.tenant.list', + icon: 'ri:notification-3-line', + permission: ['tenant-announcement:read'], + authList: [ + { title: '创建租户公告', authMark: 'tenant-announcement:create' }, + { title: '查看租户公告', authMark: 'tenant-announcement:read' }, + { title: '编辑租户公告', authMark: 'tenant-announcement:update' }, + { title: '删除租户公告', authMark: 'tenant-announcement:delete' }, + { title: '发布租户公告', authMark: 'tenant-announcement:publish' }, + { title: '撤销租户公告', authMark: 'tenant-announcement:revoke' } + ] + } + }, + { + path: 'announcements/create', + name: 'TenantAnnouncementCreate', + component: () => import('@views/tenant/announcements/create.vue'), + meta: { + title: 'menus.announcement.tenant.create', + icon: 'ri:add-circle-line', + permission: ['tenant-announcement:create'], + hideMenu: true + } + }, + { + path: 'announcements/:announcementId/edit', + name: 'TenantAnnouncementEdit', + component: () => import('@views/tenant/announcements/create.vue'), + meta: { + title: 'menus.announcement.tenant.edit', + icon: 'ri:edit-line', + permission: ['tenant-announcement:update'], + hideMenu: true + } + } + ] +} + +export default tenantRoutes diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts new file mode 100644 index 0000000..ccf1201 --- /dev/null +++ b/src/router/routes/asyncRoutes.ts @@ -0,0 +1,9 @@ +// 权限文档:https://www.artd.pro/docs/zh/guide/in-depth/permission.html +import { AppRouteRecord } from '@/types/router' +import { routeModules } from '../modules' + +/** + * 动态路由(需要权限才能访问的路由) + * 用于渲染菜单以及根据菜单权限动态加载路由,如果没有权限无法访问 + */ +export const asyncRoutes: AppRouteRecord[] = routeModules diff --git a/src/router/routes/staticRoutes.ts b/src/router/routes/staticRoutes.ts new file mode 100644 index 0000000..7af56d7 --- /dev/null +++ b/src/router/routes/staticRoutes.ts @@ -0,0 +1,108 @@ +import { AppRouteRecordRaw } from '@/utils/router' + +/** + * 静态路由配置(不需要权限就能访问的路由) + * + * 属性说明: + * isHideTab: true 表示不在标签页中显示 + * + * 注意事项: + * 1、path、name 不要和动态路由冲突,否则会导致路由冲突无法访问 + * 2、静态路由不管是否登录都可以访问 + */ +export const staticRoutes: AppRouteRecordRaw[] = [ + // 不需要登录就能访问的路由示例 + // { + // path: '/welcome', + // name: 'WelcomeStatic', + // component: () => import('@views/dashboard/console/index.vue'), + // meta: { title: 'menus.dashboard.title' } + // }, + { + path: '/auth/login', + name: 'Login', + component: () => import('@views/auth/login/index.vue'), + meta: { title: 'menus.login.title', isHideTab: true } + }, + { + path: '/auth/register', + name: 'Register', + component: () => import('@views/auth/register/index.vue'), + meta: { title: 'menus.register.title', isHideTab: true } + }, + { + path: '/onboarding/status', + name: 'TenantOnboardingStatus', + component: () => import('@views/onboarding/status/index.vue'), + meta: { title: 'menus.onboarding.status', isHideTab: true } + }, + { + path: '/onboarding/pricing', + name: 'TenantOnboardingPricing', + component: () => import('@views/onboarding/pricing/index.vue'), + meta: { title: 'menus.onboarding.pricing', isHideTab: true } + }, + { + path: '/onboarding/waiting', + name: 'TenantOnboardingWaiting', + component: () => import('@views/onboarding/waiting/index.vue'), + meta: { title: 'menus.onboarding.waiting', isHideTab: true } + }, + { + path: '/onboarding/error', + name: 'TenantOnboardingError', + component: () => import('@views/onboarding/error/index.vue'), + meta: { title: 'menus.onboarding.error', isHideTab: true } + }, + { + path: '/terms-of-service', + name: 'TermsOfService', + component: () => import('@views/onboarding/terms-of-service/index.vue'), + meta: { title: 'menus.termsOfService.title', isHideTab: true } + }, + { + path: '/auth/forget-password', + name: 'ForgetPassword', + component: () => import('@views/auth/forget-password/index.vue'), + meta: { title: 'menus.forgetPassword.title', isHideTab: true } + }, + { + path: '/auth/reset-password', + name: 'ResetPassword', + component: () => import('@views/auth/reset-password/index.vue'), + meta: { title: 'menus.resetPassword.title', isHideTab: true } + }, + { + path: '/403', + name: 'Exception403', + component: () => import('@views/exception/403/index.vue'), + meta: { title: '403', isHideTab: true } + }, + { + path: '/:pathMatch(.*)*', + name: 'Exception404', + component: () => import('@views/exception/404/index.vue'), + meta: { title: '404', isHideTab: true } + }, + { + path: '/500', + name: 'Exception500', + component: () => import('@views/exception/500/index.vue'), + meta: { title: '500', isHideTab: true } + }, + { + path: '/outside', + component: () => import('@views/index/index.vue'), + name: 'Outside', + meta: { title: 'menus.outside.title' }, + children: [ + // iframe 内嵌页面 + { + path: '/outside/iframe/:path', + name: 'Iframe', + component: () => import('@/views/outside/Iframe.vue'), + meta: { title: 'iframe' } + } + ] + } +] diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts new file mode 100644 index 0000000..2af1c68 --- /dev/null +++ b/src/router/routesAlias.ts @@ -0,0 +1,8 @@ +/** + * 公共路由别名 + # 存放系统级公共路由路径,如布局容器、登录页等 + */ +export enum RoutesAlias { + Layout = '/index/index', // 布局容器 + Login = '/auth/login' // 登录页 +} diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..b485999 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,52 @@ +/** + * Pinia Store 配置模块 + * + * 提供全局状态管理的初始化和配置 + * + * ## 主要功能 + * + * - Pinia Store 实例创建 + * - 持久化插件配置(pinia-plugin-persistedstate) + * - 版本化存储键管理 + * - 自动数据迁移(跨版本) + * - LocalStorage 序列化配置 + * - Store 初始化函数 + * + * ## 持久化策略 + * + * - 使用 StorageKeyManager 生成版本化的存储键 + * - 格式:sys-v{version}-{storeId} + * - 自动迁移旧版本数据到当前版本 + * - 使用 localStorage 作为存储介质 + * + * @module store/index + * @author Art Design Pro Team + */ +import type { App } from 'vue' +import { createPinia } from 'pinia' +import { createPersistedState } from 'pinia-plugin-persistedstate' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +export const store = createPinia() + +// 创建存储键管理器实例 +const storageKeyManager = new StorageKeyManager() + +// 配置持久化插件 +store.use( + createPersistedState({ + key: (storeId: string) => storageKeyManager.getStorageKey(storeId), + storage: localStorage, + serializer: { + serialize: JSON.stringify, + deserialize: JSON.parse + } + }) +) + +/** + * 初始化 Store + */ +export function initStore(app: App): void { + app.use(store) +} diff --git a/src/store/modules/__tests__/dictionaryGroup.spec.ts b/src/store/modules/__tests__/dictionaryGroup.spec.ts new file mode 100644 index 0000000..930c7d0 --- /dev/null +++ b/src/store/modules/__tests__/dictionaryGroup.spec.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { useDictionaryGroupStore } from '@/store/modules/dictionaryGroup' +import { DictionaryScope } from '@/enums/Dictionary' + +vi.mock('element-plus', () => ({ + ElMessage: { + error: vi.fn(), + success: vi.fn() + } +})) + +vi.mock('@/store/modules/user', () => ({ + useUserStore: () => ({ + info: { tenantId: '100' } + }) +})) + +const getGroupsMock = vi.fn() +const createGroupMock = vi.fn() +const deleteGroupMock = vi.fn() +const updateGroupMock = vi.fn() +const getGroupByIdMock = vi.fn() + +vi.mock('@/api/dictionary/group', () => ({ + getGroups: (...args: unknown[]) => getGroupsMock(...args), + createGroup: (...args: unknown[]) => createGroupMock(...args), + deleteGroup: (...args: unknown[]) => deleteGroupMock(...args), + updateGroup: (...args: unknown[]) => updateGroupMock(...args), + getGroupById: (...args: unknown[]) => getGroupByIdMock(...args) +})) + +const createDeferred = () => { + let resolve: (value: T) => void + let reject: (reason?: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve: resolve!, reject: reject! } +} + +describe('dictionaryGroup store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('fetchGroups updates state correctly', async () => { + const store = useDictionaryGroupStore() + const group: Api.Dictionary.DictionaryGroupDto = { + id: '1', + tenantId: '0', + code: 'order_status', + name: 'Order Status', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + getGroupsMock.mockResolvedValue({ + items: [group], + page: 1, + pageSize: 20, + totalCount: 1, + totalPages: 1 + }) + + await store.fetchGroups(DictionaryScope.System, 1, 20) + + expect(store.groups).toEqual([group]) + expect(store.pagination.totalCount).toBe(1) + }) + + it('createGroup optimistic update and rollback on error', async () => { + const store = useDictionaryGroupStore() + const existing: Api.Dictionary.DictionaryGroupDto = { + id: '2', + tenantId: '0', + code: 'payment_method', + name: 'Payment Method', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + store.groups = [existing] + + const deferred = createDeferred() + createGroupMock.mockReturnValue(deferred.promise) + + const actionPromise = store.createGroup({ + code: 'shipping_method', + name: 'Shipping Method', + scope: DictionaryScope.System, + allowOverride: false, + description: null + }) + + expect(store.groups.length).toBe(2) + expect(store.groups[0].code).toBe('shipping_method') + + deferred.reject(new Error('failed')) + await actionPromise + + expect(store.groups).toEqual([existing]) + }) + + it('deleteGroup removes from state', async () => { + const store = useDictionaryGroupStore() + const groupA: Api.Dictionary.DictionaryGroupDto = { + id: '3', + tenantId: '0', + code: 'order_status', + name: 'Order Status', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + const groupB: Api.Dictionary.DictionaryGroupDto = { + id: '4', + tenantId: '0', + code: 'payment_method', + name: 'Payment Method', + scope: DictionaryScope.System, + allowOverride: true, + isEnabled: true, + description: null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + store.groups = [groupA, groupB] + store.currentGroup = groupA + deleteGroupMock.mockResolvedValue(true) + + const result = await store.deleteGroup(groupA.id) + + expect(result).toBe(true) + expect(store.groups).toEqual([groupB]) + expect(store.currentGroup).toBeNull() + }) +}) diff --git a/src/store/modules/__tests__/dictionaryOverride.spec.ts b/src/store/modules/__tests__/dictionaryOverride.spec.ts new file mode 100644 index 0000000..55142d5 --- /dev/null +++ b/src/store/modules/__tests__/dictionaryOverride.spec.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { useDictionaryOverrideStore } from '@/store/modules/dictionaryOverride' + +vi.mock('element-plus', () => ({ + ElMessage: { + error: vi.fn(), + success: vi.fn() + } +})) + +const getOverridesMock = vi.fn() +const getOverrideMock = vi.fn() +const enableOverrideMock = vi.fn() +const disableOverrideMock = vi.fn() +const updateHiddenItemsMock = vi.fn() +const updateSortOrderMock = vi.fn() + +vi.mock('@/api/dictionary/override', () => ({ + getOverrides: (...args: unknown[]) => getOverridesMock(...args), + getOverride: (...args: unknown[]) => getOverrideMock(...args), + enableOverride: (...args: unknown[]) => enableOverrideMock(...args), + disableOverride: (...args: unknown[]) => disableOverrideMock(...args), + updateHiddenItems: (...args: unknown[]) => updateHiddenItemsMock(...args), + updateSortOrder: (...args: unknown[]) => updateSortOrderMock(...args) +})) + +describe('dictionaryOverride store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + vi.clearAllMocks() + }) + + it('enableOverride calls API and updates state', async () => { + const store = useDictionaryOverrideStore() + const config: Api.Dictionary.OverrideConfigDto = { + tenantId: '100', + systemDictionaryGroupCode: 'ORDER_STATUS', + overrideEnabled: true, + hiddenSystemItemIds: [], + customSortOrder: {} + } + enableOverrideMock.mockResolvedValue(config) + + const result = await store.enableOverride('ORDER_STATUS') + + expect(result).toEqual(config) + expect(store.overrides['order_status']).toEqual(config) + }) + + it('updateHiddenItems updates config', async () => { + const store = useDictionaryOverrideStore() + const config: Api.Dictionary.OverrideConfigDto = { + tenantId: '100', + systemDictionaryGroupCode: 'PAYMENT_METHOD', + overrideEnabled: true, + hiddenSystemItemIds: ['1', '2'], + customSortOrder: {} + } + updateHiddenItemsMock.mockResolvedValue(config) + + const result = await store.updateHiddenItems('PAYMENT_METHOD', ['1', '2']) + + expect(result).toEqual(config) + expect(store.overrides.payment_method).toEqual(config) + }) + + it('clearCache invalidates all cached overrides', async () => { + const store = useDictionaryOverrideStore() + const configs: Api.Dictionary.OverrideConfigDto[] = [ + { + tenantId: '100', + systemDictionaryGroupCode: 'ORDER_STATUS', + overrideEnabled: true, + hiddenSystemItemIds: [], + customSortOrder: {} + } + ] + getOverridesMock.mockResolvedValue(configs) + + await store.clearCache() + + expect(getOverridesMock).toHaveBeenCalled() + expect(store.overrides['order_status']).toEqual(configs[0]) + }) +}) diff --git a/src/store/modules/announcement.ts b/src/store/modules/announcement.ts new file mode 100644 index 0000000..7e4dc36 --- /dev/null +++ b/src/store/modules/announcement.ts @@ -0,0 +1,1004 @@ +/** + * 文件用途:公告模块状态管理 + * 作者:前端架构师(Codex) + * 日期:2025-12-20 + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' +import type { + AnnouncementDraft, + AnnouncementFormData, + AnnouncementQueryParams, + AnnouncementStatistics, + AudienceEstimate, + TargetRules, + TenantAnnouncementDto +} from '@/types/announcement' +import type { PagedResult } from '@/types/common/response' +import { + appAnnouncementApi, + platformAnnouncementApi, + tenantAnnouncementApi +} from '@/api/announcement' +import { normalizeAnnouncementStatus } from '@/utils/announcementStatus' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' +import { StorageConfig } from '@/utils/storage/storage-config' +import { useUserStore } from '@/store/modules/user' + +/** 公告作用域 */ +type AnnouncementScope = 'platform' | 'tenant' | 'app' + +/** 公告请求数据(允许传入 targetParameters 以便后端解析) */ +type AnnouncementRequestData = AnnouncementFormData & { targetParameters?: string | null } + +/** 公告操作可选项 */ +interface AnnouncementActionOptions { + scope?: AnnouncementScope + tenantId?: string + silent?: boolean + skipLoading?: boolean +} + +const storageKeyManager = new StorageKeyManager() +const DRAFT_STORE_ID = 'announcement-draft' +const DEFAULT_PAGE = 1 +const DEFAULT_PAGE_SIZE = 20 +const DEFAULT_DEBOUNCE_MS = 2000 +const DEFAULT_AUTO_SAVE_INTERVAL = 30000 + +/** + * 公告状态管理 Store + */ +export const useAnnouncementStore = defineStore( + 'announcementStore', + () => { + // 1. 公告列表数据 + const announcements = ref([]) + + // 2. 当前选中的公告 + const currentAnnouncement = ref(null) + + // 3. 未读公告数 + const unreadCount = ref(0) + + // 4. 统计数据 + const statistics = ref(null) + + // 5. 分页信息 + const pagination = ref({ + page: DEFAULT_PAGE, + pageSize: DEFAULT_PAGE_SIZE, + totalCount: 0, + totalPages: 0 + }) + + // 6. 加载状态 + const loading = ref(false) + + // 7. 草稿数据 + const currentDraft = ref(null) + + // 8. 草稿自动保存定时器 + let draftAutoSaveTimer: ReturnType | null = null + + // 9. 草稿防抖保存定时器 + let draftDebounceTimer: ReturnType | null = null + + /** + * 是否存在未读公告 + */ + const hasUnread = computed(() => unreadCount.value > 0) + + /** + * 草稿与已发布公告数量(合计) + */ + const draftPublishedAnnouncements = computed(() => { + // 1. 优先使用统计数据 + if (statistics.value) { + return statistics.value.draftCount + statistics.value.publishedCount + } + + // 2. 回退本地计算 + const draftCount = announcements.value.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Draft' + ).length + const publishedCount = announcements.value.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Published' + ).length + return draftCount + publishedCount + }) + + /** + * 有效期内公告(已发布且在有效期内) + */ + const effectiveAnnouncements = computed(() => { + // 1. 获取当前时间 + const now = Date.now() + + // 2. 过滤有效期公告 + return announcements.value.filter((item) => { + // 1. 解析时间范围 + const startAt = Date.parse(item.effectiveFrom) + const endAt = item.effectiveTo ? Date.parse(item.effectiveTo) : null + const startOk = !Number.isNaN(startAt) && startAt <= now + const endOk = !endAt || now <= endAt + + // 2. 判断有效状态 + return ( + item.isActive && + normalizeAnnouncementStatus(item.status) === 'Published' && + startOk && + endOk + ) + }) + }) + + /** + * 生成草稿存储键 + */ + const getDraftStorageKey = (draftId: string) => { + return `${storageKeyManager.getStorageKey(DRAFT_STORE_ID)}:${draftId}` + } + + /** + * 生成草稿ID + */ + const createDraftId = () => { + return `draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + } + + /** + * 解析作用域(优先使用显式 scope,其次根据参数判断) + */ + const resolveScope = ( + options?: AnnouncementActionOptions, + params?: AnnouncementQueryParams + ) => { + // 1. 优先使用显式 scope + if (options?.scope) { + return options.scope + } + + // 2. 根据租户参数推断为租户作用域 + if (params?.tenantId) { + return 'tenant' + } + + // 3. 根据当前租户信息判断 + const currentTenantId = resolveTenantId() + if (currentTenantId && currentTenantId !== '0') { + return 'tenant' + } + + // 4. 默认平台作用域 + return 'platform' + } + + /** + * 获取租户ID + */ + const resolveTenantId = (tenantId?: string) => { + // 1. 优先使用传入 tenantId + if (tenantId) { + return tenantId + } + + // 2. 回退用户信息中的 tenantId + const userStore = useUserStore() + if (userStore.info?.tenantId) { + return userStore.info.tenantId + } + + // 3. 回退本地存储的 tenantId + return localStorage.getItem(StorageConfig.TENANT_ID_KEY) || null + } + + /** + * 获取错误提示信息 + */ + const resolveErrorMessage = (error: unknown, fallback: string) => { + // 1. 使用异常信息作为提示 + if (error instanceof Error && error.message) { + return error.message + } + + // 2. 回退默认提示 + return fallback + } + + const handleActionError = ( + error: unknown, + fallback: string, + options?: AnnouncementActionOptions + ) => { + const message = resolveErrorMessage(error, fallback) + if (!options?.silent) { + ElMessage.error(message) + return null + } + throw error instanceof Error ? error : new Error(message) + } + + /** + * 构建目标受众参数 + */ + const buildTargetParameters = (data: AnnouncementRequestData): string | null => { + // 1. 直接使用调用方传入的 targetParameters + if (typeof data.targetParameters === 'string') { + return data.targetParameters + } + + // 2. 规则/角色模式使用规则参数 + if ((data.targetType === 'rules' || data.targetType === 'roles') && data.targetRules) { + return JSON.stringify(data.targetRules) + } + + // 3. 手选用户模式使用用户ID参数 + if ( + (data.targetType === 'users' || data.targetType === 'manual') && + data.targetUserIds?.length + ) { + return JSON.stringify({ userIds: data.targetUserIds }) + } + + // 4. 默认返回空 + return null + } + + /** + * 构建创建公告请求体 + */ + const buildCreatePayload = (data: AnnouncementRequestData) => { + // 1. 构建创建字段 + return { + title: data.title, + content: data.content, + announcementType: data.announcementType, + priority: data.priority, + effectiveFrom: data.effectiveFrom, + effectiveTo: data.effectiveTo ?? null, + targetType: data.targetType, + targetParameters: buildTargetParameters(data) + } + } + + /** + * 构建更新公告请求体 + */ + const buildUpdatePayload = (data: AnnouncementRequestData) => { + // 1. 构建更新字段 + return { + title: data.title, + content: data.content, + targetType: data.targetType, + targetParameters: buildTargetParameters(data), + rowVersion: data.rowVersion + } + } + + /** + * 更新分页状态 + */ + const applyPagedResult = (result: PagedResult) => { + // 1. 更新列表数据 + announcements.value = Array.isArray(result.items) ? result.items : [] + + // 2. 更新分页信息 + pagination.value = { + page: result.page, + pageSize: result.pageSize, + totalCount: result.totalCount, + totalPages: result.totalPages + } + } + + /** + * 刷新统计数据 + */ + const refreshStatistics = () => { + // 1. 统计列表中的状态数量 + const list = announcements.value + const draftCount = list.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Draft' + ).length + const publishedCount = list.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Published' + ).length + const revokedCount = list.filter( + (item) => normalizeAnnouncementStatus(item.status) === 'Revoked' + ).length + const totalCount = pagination.value.totalCount > 0 ? pagination.value.totalCount : list.length + + // 2. 生成统计结果 + statistics.value = { + totalCount, + draftCount, + publishedCount, + revokedCount, + unreadCount: unreadCount.value + } + } + + /** + * 同步公告到列表与当前详情 + */ + const syncAnnouncement = (announcement: TenantAnnouncementDto, insertIfMissing: boolean) => { + // 1. 同步公告列表 + const index = announcements.value.findIndex((item) => item.id === announcement.id) + if (index >= 0) { + announcements.value.splice(index, 1, announcement) + } else if (insertIfMissing) { + announcements.value = [announcement, ...announcements.value] + } + + // 2. 同步当前详情 + currentAnnouncement.value = announcement + } + + /** + * 获取公告列表 + */ + const fetchAnnouncements = async ( + params: AnnouncementQueryParams, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options, params) + const tenantId = + scope === 'tenant' ? resolveTenantId(options?.tenantId ?? params.tenantId) : null + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法加载租户公告') + return null + } + + // 2. 拉取公告列表数据 + let result: PagedResult + if (scope === 'app') { + result = await appAnnouncementApi.list(params) + } else if (scope === 'tenant' && tenantId) { + result = await tenantAnnouncementApi.list(tenantId, params) + } else { + result = await platformAnnouncementApi.list(params) + } + + // 3. 更新列表与分页状态 + applyPagedResult(result) + + // 4. 刷新统计数据 + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + return handleActionError(error, '获取公告列表失败', options) + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 获取公告详情 + */ + const fetchAnnouncementDetail = async ( + announcementId: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法加载公告详情') + return null + } + + if (scope === 'app') { + const cached = announcements.value.find((item) => item.id === announcementId) || null + currentAnnouncement.value = cached + return cached + } + + // 2. 拉取公告详情数据 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.detail(tenantId, announcementId) + : await platformAnnouncementApi.detail(announcementId) + + // 3. 同步详情数据 + syncAnnouncement(result, true) + + return result + } catch (error) { + // 1. 处理异常提示 + return handleActionError(error, '获取公告详情失败', options) + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 创建公告 + */ + const createAnnouncement = async ( + data: AnnouncementFormData, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持创建公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法创建公告') + return null + } + + // 2. 构建请求体并发起请求 + const payload = buildCreatePayload(data as AnnouncementRequestData) as AnnouncementFormData + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.create(tenantId, payload) + : await platformAnnouncementApi.create(payload) + + // 3. 同步列表与统计 + syncAnnouncement(result, true) + pagination.value.totalCount += 1 + pagination.value.totalPages = Math.max( + 1, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '创建公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 更新公告(仅草稿) + */ + const updateAnnouncement = async ( + announcementId: string, + data: AnnouncementFormData, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验并发控制版本 + if (!data.rowVersion) { + ElMessage.error('缺少并发控制版本,无法更新公告') + return null + } + + // 2. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持更新公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法更新公告') + return null + } + + // 3. 构建请求体并发起请求 + const payload = buildUpdatePayload(data as AnnouncementRequestData) as AnnouncementFormData + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.update(tenantId, announcementId, payload) + : await platformAnnouncementApi.update(announcementId, payload) + + // 4. 同步列表与统计 + syncAnnouncement(result, true) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '更新公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 发布公告 + */ + const publishAnnouncement = async ( + announcementId: string, + rowVersion: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验并发控制版本 + if (!rowVersion) { + ElMessage.error('缺少并发控制版本,无法发布公告') + return null + } + + // 2. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持发布公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法发布公告') + return null + } + + // 3. 发起发布请求 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.publish(tenantId, announcementId, { rowVersion }) + : await platformAnnouncementApi.publish(announcementId, { rowVersion }) + + // 4. 同步列表与统计 + syncAnnouncement(result, true) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '发布公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 撤销公告 + */ + const revokeAnnouncement = async ( + announcementId: string, + rowVersion: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验并发控制版本 + if (!rowVersion) { + ElMessage.error('缺少并发控制版本,无法撤销公告') + return null + } + + // 2. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'app') { + ElMessage.error('应用端不支持撤销公告') + return null + } + + if (scope === 'tenant' && !tenantId) { + ElMessage.error('未获取到租户ID,无法撤销公告') + return null + } + + // 3. 发起撤销请求 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.revoke(tenantId, announcementId, { rowVersion }) + : await platformAnnouncementApi.revoke(announcementId, { rowVersion }) + + // 4. 同步列表与统计 + syncAnnouncement(result, true) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '撤销公告失败')) + return null + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 删除公告 + */ + const deleteAnnouncement = async ( + announcementId: string, + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 解析作用域与租户上下文 + const scope = resolveScope(options) + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope !== 'tenant') { + ElMessage.error('仅租户公告支持删除操作') + return false + } + + if (!tenantId) { + ElMessage.error('未获取到租户ID,无法删除公告') + return false + } + + // 2. 发起删除请求 + const result = await tenantAnnouncementApi.delete(tenantId, announcementId) + + // 3. 同步列表与统计 + announcements.value = announcements.value.filter((item) => item.id !== announcementId) + if (currentAnnouncement.value?.id === announcementId) { + currentAnnouncement.value = null + } + pagination.value.totalCount = Math.max(0, pagination.value.totalCount - 1) + pagination.value.totalPages = Math.max( + 0, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '删除公告失败')) + return false + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 标记公告已读 + */ + const markAsRead = async (announcementId: string, options?: AnnouncementActionOptions) => { + if (!options?.skipLoading) { + loading.value = true + } + + try { + // 1. 解析作用域与租户上下文 + const scope = options?.scope ?? 'app' + const tenantId = scope === 'tenant' ? resolveTenantId(options?.tenantId) : null + + if (scope === 'platform') { + if (!options?.silent) { + ElMessage.error('平台公告不支持已读标记') + } + return null + } + + if (scope === 'tenant' && !tenantId) { + if (!options?.silent) { + ElMessage.error('未获取到租户ID,无法标记已读') + } + return null + } + + // 2. 发起标记请求 + const result = + scope === 'tenant' && tenantId + ? await tenantAnnouncementApi.markRead(tenantId, announcementId) + : await appAnnouncementApi.markRead(announcementId) + + // 3. 同步列表与统计 + const wasUnread = + announcements.value.find((item) => item.id === announcementId)?.isRead === false + syncAnnouncement(result, true) + if (wasUnread && unreadCount.value > 0) { + unreadCount.value -= 1 + } + refreshStatistics() + + return result + } catch (error) { + // 1. 处理异常提示 + if (!options?.silent) { + ElMessage.error(resolveErrorMessage(error, '标记已读失败')) + } + return null + } finally { + // 1. 关闭加载状态 + if (!options?.skipLoading) { + loading.value = false + } + } + } + + /** + * 批量标记已读 + */ + const batchMarkAsRead = async ( + announcementIds: string[], + options?: AnnouncementActionOptions + ) => { + loading.value = true + + try { + // 1. 校验参数 + if (!announcementIds.length) { + ElMessage.warning('未选择需要标记的公告') + return 0 + } + + // 2. 执行批量标记 + const tasks = announcementIds.map((id) => + markAsRead(id, { ...options, silent: true, skipLoading: true }) + ) + const results = await Promise.allSettled(tasks) + const successCount = results.filter( + (item) => item.status === 'fulfilled' && item.value + ).length + + // 3. 同步未读数量 + if (successCount < announcementIds.length) { + ElMessage.warning('部分公告标记失败,请检查网络或权限') + } + + return successCount + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '批量标记已读失败')) + return 0 + } finally { + // 1. 关闭加载状态 + loading.value = false + } + } + + /** + * 获取未读数量 + */ + const fetchUnreadCount = async () => { + try { + // 1. 拉取未读列表并读取总数 + const result = await appAnnouncementApi.unread({ page: 1, pageSize: 1 }) + unreadCount.value = Number.isNaN(result.totalCount) + ? result.items.length + : result.totalCount + + // 2. 刷新统计 + refreshStatistics() + + return unreadCount.value + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '获取未读数量失败')) + return 0 + } + } + + /** + * 获取统计数据 + */ + const fetchStatistics = async () => { + try { + // 1. 基于当前列表刷新统计 + refreshStatistics() + + return statistics.value + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '获取统计数据失败')) + return null + } + } + + /** + * 保存草稿 + */ + const saveDraft = (data: Partial) => { + try { + // 1. 准备草稿基础信息 + const draftId = currentDraft.value?.draftId ?? createDraftId() + const lastSaved = new Date().toISOString() + + // 2. 合并草稿数据 + const draft: AnnouncementDraft = { + ...(currentDraft.value || {}), + ...data, + draftId, + lastSaved + } + + // 3. 持久化到本地存储 + const storageKey = getDraftStorageKey(draftId) + localStorage.setItem(storageKey, JSON.stringify(draft)) + currentDraft.value = draft + + return draft + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '保存草稿失败')) + return null + } + } + + /** + * 加载草稿 + */ + const loadDraft = (draftId: string) => { + try { + // 1. 读取本地存储 + const storageKey = getDraftStorageKey(draftId) + const raw = localStorage.getItem(storageKey) + if (!raw) { + currentDraft.value = null + return null + } + + // 2. 解析草稿数据 + const draft = JSON.parse(raw) as AnnouncementDraft + currentDraft.value = draft + + return draft + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '加载草稿失败')) + return null + } + } + + /** + * 清除草稿 + */ + const clearDraft = (draftId: string) => { + try { + // 1. 删除本地存储 + const storageKey = getDraftStorageKey(draftId) + localStorage.removeItem(storageKey) + + // 2. 同步内存状态 + if (currentDraft.value?.draftId === draftId) { + currentDraft.value = null + } + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '清除草稿失败')) + } + } + + /** + * 自动保存草稿(定时 + 防抖) + */ + const autoSaveDraft = (data: Partial, interval: number) => { + // 1. 清理旧定时器 + if (draftAutoSaveTimer) { + clearInterval(draftAutoSaveTimer) + draftAutoSaveTimer = null + } + + if (draftDebounceTimer) { + clearTimeout(draftDebounceTimer) + draftDebounceTimer = null + } + + // 2. 触发防抖保存 + const scheduleSave = () => { + // 1. 清理旧防抖定时器 + if (draftDebounceTimer) { + clearTimeout(draftDebounceTimer) + } + + // 2. 设置新防抖定时器 + draftDebounceTimer = setTimeout(() => { + saveDraft(data) + }, DEFAULT_DEBOUNCE_MS) + } + + scheduleSave() + + // 3. 启动定时器 + const delay = + Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_AUTO_SAVE_INTERVAL + draftAutoSaveTimer = setInterval(() => scheduleSave(), delay) + + return () => { + if (draftAutoSaveTimer) { + clearInterval(draftAutoSaveTimer) + draftAutoSaveTimer = null + } + + if (draftDebounceTimer) { + clearTimeout(draftDebounceTimer) + draftDebounceTimer = null + } + } + } + + /** + * 预估受众人数 + */ + const estimateAudience = async (rules: TargetRules): Promise => { + try { + // 1. 受众预估接口尚未对接,返回空结果 + const estimate: AudienceEstimate = { + count: 0, + preview: [] + } + + if (Object.keys(rules || {}).length > 0) { + ElMessage.warning('受众预估接口暂未接入,请联系后端补齐') + } + + return estimate + } catch (error) { + // 1. 处理异常提示 + ElMessage.error(resolveErrorMessage(error, '预估受众失败')) + return { count: 0, preview: [] } + } + } + + return { + announcements, + currentAnnouncement, + unreadCount, + statistics, + pagination, + loading, + currentDraft, + hasUnread, + draftPublishedAnnouncements, + effectiveAnnouncements, + fetchAnnouncements, + fetchAnnouncementDetail, + createAnnouncement, + updateAnnouncement, + publishAnnouncement, + revokeAnnouncement, + deleteAnnouncement, + markAsRead, + batchMarkAsRead, + fetchUnreadCount, + fetchStatistics, + saveDraft, + loadDraft, + clearDraft, + autoSaveDraft, + estimateAudience + } + }, + { + // 使用 storeId 走全局 StorageKeyManager(避免硬编码 Key) + persist: { + paths: ['unreadCount', 'statistics'] + } + } +) diff --git a/src/store/modules/billingStore.ts b/src/store/modules/billingStore.ts new file mode 100644 index 0000000..299137a --- /dev/null +++ b/src/store/modules/billingStore.ts @@ -0,0 +1,408 @@ +/** + * 账单管理状态模块 + * + * 提供账单列表/详情/统计、筛选条件、批量选择等状态管理能力。 + * + * @module store/modules/billingStore + */ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { + batchUpdateStatus as apiBatchUpdateStatus, + cancelBilling as apiCancelBilling, + createBilling as apiCreateBilling, + fetchBillingDetail as apiFetchBillingDetail, + fetchBillingList as apiFetchBillingList, + fetchBillingPayments as apiFetchBillingPayments, + fetchBillingStatistics as apiFetchBillingStatistics, + fetchOverdueBillings as apiFetchOverdueBillings, + exportBillings as apiExportBillings, + recordPayment as apiRecordPayment, + updateBillingStatus as apiUpdateBillingStatus, + verifyPayment as apiVerifyPayment +} from '@/api/billing' +import { TenantBillingStatus } from '@/enums/Billing' + +interface BillingPagination { + page: number + pageSize: number + totalCount: number + totalPages: number +} + +/** + * 账单管理 Store + */ +export const useBillingStore = defineStore('billingStore', () => { + // ==================== State ==================== + + /** 账单列表 */ + const billings = ref([]) + + /** 当前账单详情 */ + const currentBilling = ref(null) + + /** 账单支付记录(可独立于 currentBilling 使用) */ + const payments = ref([]) + + /** 统计数据 */ + const statistics = ref(null) + + /** 逾期账单列表(用于逾期提醒/排行榜等,避免与 getter 同名冲突) */ + const overdueList = ref([]) + + /** 筛选条件(与后端 Query 保持一致) */ + const filters = ref>({ + PageNumber: 1, + PageSize: 20, + SortBy: 'CreatedAt', + SortDesc: true + }) + + /** 分页信息(来自后端 PageResult) */ + const pagination = ref({ + page: 1, + pageSize: 20, + totalCount: 0, + totalPages: 0 + }) + + /** 选中的账单 ID 集合 */ + const selectedIds = ref([]) + + /** 加载状态 */ + const loading = ref(false) + + // ==================== Getters ==================== + + /** 待支付账单 */ + const pendingBillings = computed(() => + billings.value.filter((b) => b.status === TenantBillingStatus.Pending) + ) + + /** 逾期账单 */ + const overdueBillings = computed(() => + billings.value.filter((b) => b.status === TenantBillingStatus.Overdue) + ) + + /** 当前列表合计金额(应付) */ + const totalAmount = computed(() => billings.value.reduce((sum, b) => sum + (b.amountDue || 0), 0)) + + /** 已选数量 */ + const selectedCount = computed(() => selectedIds.value.length) + + // ==================== Actions ==================== + + /** + * 获取账单列表 + * @param params 查询参数(可选) + */ + const fetchBillings = async (params?: Partial) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 合并参数(以传入 params 覆盖 filters) + const query: Partial = { + ...filters.value, + ...(params || {}) + } + + // 3. 请求列表数据 + const res = await apiFetchBillingList(query) + + // 4. 写入列表与分页 + billings.value = res.items || [] + pagination.value = { + page: res.page ?? query.PageNumber ?? 1, + pageSize: res.pageSize ?? query.PageSize ?? 20, + totalCount: res.totalCount ?? 0, + totalPages: res.totalPages ?? 0 + } + + // 5. 更新筛选条件(保持 UI 与 Store 一致) + filters.value = query + } finally { + // 6. 退出加载态 + loading.value = false + } + } + + /** + * 获取账单详情 + * @param id 账单 ID + */ + const fetchBillingDetail = async (id: string) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 请求详情 + currentBilling.value = await apiFetchBillingDetail(id) + + // 3. 同步支付记录(便于页面复用) + payments.value = currentBilling.value?.payments || [] + } finally { + // 4. 退出加载态 + loading.value = false + } + } + + /** + * 创建账单 + * @param data 创建账单参数 + */ + const createBilling = async (data: Api.Billing.CreateBillingCommand) => { + // 1. 创建 + const created = await apiCreateBilling(data) + + // 2. 刷新列表(保持 UI 一致) + await fetchBillings({ PageNumber: 1 }) + + return created + } + + /** + * 更新账单状态 + * @param id 账单 ID + * @param status 新状态 + * @param notes 备注(可选) + */ + const updateStatus = async ( + id: string, + status: Api.Billing.TenantBillingStatus, + notes?: string + ) => { + // 1. 更新状态 + const updated = await apiUpdateBillingStatus(id, { status, notes }) + + // 2. 同步列表项 + const index = billings.value.findIndex((b) => b.id === id) + if (index !== -1) { + billings.value[index] = { ...billings.value[index], ...updated } + } + + // 3. 同步详情 + if (currentBilling.value?.id === id) { + currentBilling.value = { ...currentBilling.value, ...updated } + } + + return updated + } + + /** + * 记录支付 + * @param billingId 账单 ID + * @param data 支付参数 + */ + const recordPayment = async (billingId: string, data: Api.Billing.RecordPaymentCommand) => { + // 1. 记录支付 + const payment = await apiRecordPayment(billingId, data) + + // 2. 刷新详情(保证支付列表最新) + await fetchBillingDetail(billingId) + + return payment + } + + /** + * 获取支付记录列表 + * @param billingId 账单 ID + */ + const fetchPayments = async (billingId: string) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 拉取支付记录 + payments.value = (await apiFetchBillingPayments(billingId)) || [] + + // 3. 若当前详情一致,则同步 + if (currentBilling.value?.id === billingId) { + currentBilling.value = { ...currentBilling.value, payments: payments.value } + } + } finally { + // 4. 退出加载态 + loading.value = false + } + } + + /** + * 审核支付记录 + * @param paymentId 支付记录 ID + * @param approved 是否通过 + * @param notes 备注(可选) + */ + const verifyPayment = async (paymentId: string, approved: boolean, notes?: string) => { + // 1. 请求审核 + const updated = await apiVerifyPayment(paymentId, { approved, notes }) + + // 2. 同步本地支付记录 + const index = payments.value.findIndex((p) => p.id === paymentId) + if (index !== -1) payments.value[index] = { ...payments.value[index], ...updated } + + // 3. 若当前详情存在,则同步详情 payments + if (currentBilling.value?.payments) { + const idx = currentBilling.value.payments.findIndex((p) => p.id === paymentId) + if (idx !== -1) { + currentBilling.value = { + ...currentBilling.value, + payments: currentBilling.value.payments.map((p) => + p.id === paymentId ? { ...p, ...updated } : p + ) + } + } + } + + return updated + } + + /** + * 作废账单(若后端支持 DELETE 作废) + * @param id 账单 ID + * @param reason 作废原因 + */ + const cancelBilling = async (id: string, reason: string) => { + // 1. 发起作废 + await apiCancelBilling(id, { reason }) + + // 2. 刷新列表/详情,保证状态一致 + await fetchBillings() + if (currentBilling.value?.id === id) { + await fetchBillingDetail(id) + } + } + + /** + * 批量更新账单状态 + * @param data 批量更新命令 + */ + const batchUpdateStatus = async (data: Api.Billing.BatchUpdateStatusCommand) => { + // 1. 发起更新 + await apiBatchUpdateStatus(data) + + // 2. 刷新列表 + await fetchBillings() + } + + /** + * 拉取逾期账单列表(用于排行榜/提醒) + * @param params 查询参数 + */ + const fetchOverdueList = async (params?: Partial) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 拉取逾期账单 + const res = await apiFetchOverdueBillings(params) + overdueList.value = res.items || [] + return res + } finally { + // 3. 退出加载态 + loading.value = false + } + } + + /** + * 导出账单(后端导出接口) + * @param data 导出参数 + */ + const exportBillings = async (data: Api.Billing.ExportParams) => { + // 1. 获取文件流 + return apiExportBillings(data) + } + + /** + * 获取统计数据 + * @param params 统计查询参数 + */ + const fetchStatistics = async (params: Api.Billing.BillingStatisticsParams) => { + // 1. 进入加载态 + loading.value = true + + try { + // 2. 请求统计数据 + statistics.value = await apiFetchBillingStatistics(params) + } finally { + // 3. 退出加载态 + loading.value = false + } + } + + /** + * 设置筛选条件(不自动请求) + * @param nextFilters 新筛选条件 + */ + const setFilters = (nextFilters: Partial) => { + // 1. 合并筛选条件 + filters.value = { ...filters.value, ...nextFilters } + } + + /** + * 选中/取消选中某条账单(切换) + * @param id 账单 ID + */ + const selectBilling = (id: string) => { + // 1. 切换选中状态 + if (selectedIds.value.includes(id)) { + selectedIds.value = selectedIds.value.filter((x) => x !== id) + return + } + + selectedIds.value = [...selectedIds.value, id] + } + + /** + * 全选当前列表 + */ + const selectAll = () => { + // 1. 全选当前页数据 + selectedIds.value = billings.value.map((b) => b.id) + } + + /** + * 清空选择 + */ + const clearSelection = () => { + // 1. 清空已选 + selectedIds.value = [] + } + + return { + // State + billings, + currentBilling, + payments, + statistics, + overdueList, + filters, + pagination, + selectedIds, + loading, + + // Getters + pendingBillings, + overdueBillings, + totalAmount, + selectedCount, + + // Actions + fetchBillings, + fetchBillingDetail, + createBilling, + updateStatus, + recordPayment, + fetchPayments, + verifyPayment, + cancelBilling, + batchUpdateStatus, + fetchOverdueList, + exportBillings, + fetchStatistics, + setFilters, + selectBilling, + selectAll, + clearSelection + } +}) diff --git a/src/store/modules/dictionaryCache.ts b/src/store/modules/dictionaryCache.ts new file mode 100644 index 0000000..5a09838 --- /dev/null +++ b/src/store/modules/dictionaryCache.ts @@ -0,0 +1,120 @@ +/** + * Dictionary cache store (client-side). + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { getDictionary, batchGetDictionaries } from '@/api/dictionary/query' + +const DEFAULT_TTL_MS = 30 * 60 * 1000 + +export const useDictionaryCacheStore = defineStore( + 'dictionaryCacheStore', + () => { + const cache = ref>({}) + const expiryMap = ref>({}) + const loading = ref(false) + + const normalizeCode = (code: string) => code.trim().toLowerCase() + + const isExpired = (code: string) => { + const key = normalizeCode(code) + const expiry = expiryMap.value[key] + return !expiry || Date.now() > expiry + } + + const isCached = (code: string) => { + const key = normalizeCode(code) + return Array.isArray(cache.value[key]) && !isExpired(key) + } + + const setCache = ( + code: string, + items: Api.Dictionary.DictionaryItemDto[], + ttl = DEFAULT_TTL_MS + ) => { + const key = normalizeCode(code) + cache.value[key] = items + expiryMap.value[key] = Date.now() + ttl + } + + const invalidate = (code: string) => { + const key = normalizeCode(code) + delete cache.value[key] + delete expiryMap.value[key] + } + + const invalidateAll = () => { + cache.value = {} + expiryMap.value = {} + } + + const getDictionaryAction = async (code: string, forceRefresh = false) => { + const key = normalizeCode(code) + if (!forceRefresh && isCached(key)) { + return cache.value[key] + } + + loading.value = true + try { + const result = await getDictionary(code) + setCache(code, result) + return result + } finally { + loading.value = false + } + } + + const batchGetDictionariesAction = async (codes: string[]) => { + if (!codes.length) return {} + + const cachedResult: Record = {} + const missing: string[] = [] + + codes.forEach((code) => { + const normalized = normalizeCode(code) + if (isCached(normalized)) { + cachedResult[code] = cache.value[normalized] + } else { + missing.push(code) + } + }) + + if (!missing.length) { + return cachedResult + } + + loading.value = true + try { + const fetched = await batchGetDictionaries(missing) + Object.keys(fetched).forEach((code) => { + setCache(code, fetched[code]) + cachedResult[code] = fetched[code] + }) + return cachedResult + } finally { + loading.value = false + } + } + + const cacheSize = computed(() => Object.keys(cache.value).length) + + return { + cache, + expiryMap, + loading, + cacheSize, + isCached, + isExpired, + getDictionary: getDictionaryAction, + batchGetDictionaries: batchGetDictionariesAction, + invalidate, + invalidateAll + } + }, + { + persist: { + paths: ['cache', 'expiryMap'] + } + } +) diff --git a/src/store/modules/dictionaryGroup.ts b/src/store/modules/dictionaryGroup.ts new file mode 100644 index 0000000..4771bad --- /dev/null +++ b/src/store/modules/dictionaryGroup.ts @@ -0,0 +1,227 @@ +/** + * Dictionary group state management. + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' +import { + createGroup, + deleteGroup, + getGroupById, + getGroups, + updateGroup +} from '@/api/dictionary/group' +import { useUserStore } from '@/store/modules/user' +import { $t } from '@/locales' +import { DictionaryScope } from '@/enums/Dictionary' + +const DEFAULT_PAGE = 1 +const DEFAULT_PAGE_SIZE = 20 + +export const useDictionaryGroupStore = defineStore('dictionaryGroupStore', () => { + const groups = ref([]) + const currentGroup = ref(null) + const loading = ref(false) + const error = ref(null) + const pagination = ref({ + page: DEFAULT_PAGE, + pageSize: DEFAULT_PAGE_SIZE, + totalCount: 0, + totalPages: 0 + }) + + const systemGroups = computed(() => + groups.value.filter((group) => group.scope === DictionaryScope.System) + ) + + const businessGroups = computed(() => + groups.value.filter((group) => group.scope === DictionaryScope.Business) + ) + + const resolveTenantId = () => { + const userStore = useUserStore() + return userStore.info?.tenantId || '0' + } + + const applyPageResult = (result: Api.Common.PageResult) => { + groups.value = Array.isArray(result.items) ? result.items : [] + pagination.value = { + page: result.page, + pageSize: result.pageSize, + totalCount: result.totalCount, + totalPages: result.totalPages + } + } + + const fetchGroups = async ( + scope?: Api.Dictionary.DictionaryScope, + page: number = DEFAULT_PAGE, + pageSize: number = DEFAULT_PAGE_SIZE + ) => { + loading.value = true + error.value = null + + try { + const result = await getGroups({ + Page: page, + PageSize: pageSize, + scope + }) + applyPageResult(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadGroups') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const fetchGroupById = async (groupId: string) => { + loading.value = true + error.value = null + + try { + const result = await getGroupById(groupId) + currentGroup.value = result + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadGroup') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const createGroupAction = async (data: Api.Dictionary.CreateDictionaryGroupRequest) => { + loading.value = true + error.value = null + + const snapshot = [...groups.value] + const previousCurrentGroup = currentGroup.value + const tempId = `temp-${Date.now()}` + const tempGroup: Api.Dictionary.DictionaryGroupDto = { + id: tempId, + tenantId: String(resolveTenantId()), + code: data.code, + name: data.name, + scope: data.scope, + allowOverride: data.allowOverride, + isEnabled: true, + description: data.description ?? null, + createdAt: new Date().toISOString(), + updatedAt: null, + rowVersion: '', + items: [] + } + + groups.value = [tempGroup, ...groups.value] + + try { + const result = await createGroup(data) + const index = groups.value.findIndex((group) => group.id === tempId) + if (index >= 0) { + groups.value.splice(index, 1, result) + } else { + groups.value = [result, ...groups.value] + } + currentGroup.value = result + pagination.value.totalCount += 1 + pagination.value.totalPages = Math.max( + 1, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + return result + } catch (err) { + groups.value = snapshot + currentGroup.value = previousCurrentGroup + error.value = err instanceof Error ? err.message : $t('dictionary.errors.createGroup') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const updateGroupAction = async ( + groupId: string, + data: Api.Dictionary.UpdateDictionaryGroupRequest + ) => { + loading.value = true + error.value = null + + const snapshot = [...groups.value] + const index = groups.value.findIndex((group) => group.id === groupId) + if (index >= 0) { + groups.value.splice(index, 1, { + ...groups.value[index], + ...data, + updatedAt: new Date().toISOString() + }) + } + + try { + const result = await updateGroup(groupId, data) + if (index >= 0) { + groups.value.splice(index, 1, result) + } + if (currentGroup.value?.id === groupId) { + currentGroup.value = result + } + return result + } catch (err) { + groups.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateGroup') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const deleteGroupAction = async (groupId: string) => { + loading.value = true + error.value = null + + const snapshot = [...groups.value] + groups.value = groups.value.filter((group) => group.id !== groupId) + if (currentGroup.value?.id === groupId) { + currentGroup.value = null + } + + try { + await deleteGroup(groupId) + pagination.value.totalCount = Math.max(0, pagination.value.totalCount - 1) + pagination.value.totalPages = Math.max( + 0, + Math.ceil(pagination.value.totalCount / pagination.value.pageSize) + ) + return true + } catch (err) { + groups.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteGroup') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + return { + groups, + currentGroup, + loading, + error, + pagination, + systemGroups, + businessGroups, + fetchGroups, + fetchGroupById, + createGroup: createGroupAction, + updateGroup: updateGroupAction, + deleteGroup: deleteGroupAction + } +}) diff --git a/src/store/modules/dictionaryItem.ts b/src/store/modules/dictionaryItem.ts new file mode 100644 index 0000000..8c41835 --- /dev/null +++ b/src/store/modules/dictionaryItem.ts @@ -0,0 +1,171 @@ +/** + * Dictionary item state management. + */ + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { ElMessage } from 'element-plus' +import { createItem, deleteItem, getItems, updateItem } from '@/api/dictionary/item' +import { HttpError } from '@/utils/http/error' +import { $t } from '@/locales' + +export const useDictionaryItemStore = defineStore('dictionaryItemStore', () => { + const items = ref([]) + const currentGroupId = ref(null) + const loading = ref(false) + const error = ref(null) + + const enabledItems = computed(() => items.value.filter((item) => item.isEnabled)) + const defaultItem = computed(() => items.value.find((item) => item.isDefault) ?? null) + + const reset = () => { + items.value = [] + currentGroupId.value = null + loading.value = false + error.value = null + } + + const fetchItems = async (groupId: string) => { + error.value = null + currentGroupId.value = groupId + const requestGroupId = groupId + + if (requestGroupId.startsWith('temp-')) { + items.value = [] + loading.value = false + return null + } + + loading.value = true + try { + const result = await getItems(groupId) + if (currentGroupId.value !== requestGroupId) { + return null + } + items.value = Array.isArray(result) ? result : [] + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadItems') + ElMessage.error(error.value) + if (currentGroupId.value === requestGroupId) { + items.value = [] + } + return null + } finally { + if (currentGroupId.value === requestGroupId) { + loading.value = false + } + } + } + + const createItemAction = async ( + groupId: string, + data: Api.Dictionary.CreateDictionaryItemRequest + ) => { + loading.value = true + error.value = null + + const snapshot = [...items.value] + const tempId = `temp-${Date.now()}` + const tempItem: Api.Dictionary.DictionaryItemDto = { + id: tempId, + groupId, + key: data.key, + value: data.value, + isDefault: data.isDefault ?? false, + isEnabled: data.isEnabled ?? true, + sortOrder: data.sortOrder ?? 0, + description: data.description ?? null, + source: 'tenant', + rowVersion: '' + } + + items.value = [tempItem, ...items.value] + + try { + const result = await createItem(groupId, data) + const index = items.value.findIndex((item) => item.id === tempId) + if (index >= 0) { + items.value.splice(index, 1, result) + } else { + items.value = [result, ...items.value] + } + return result + } catch (err) { + items.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.createItem') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const updateItemAction = async ( + groupId: string, + itemId: string, + data: Api.Dictionary.UpdateDictionaryItemRequest + ) => { + loading.value = true + error.value = null + + const snapshot = [...items.value] + const index = items.value.findIndex((item) => item.id === itemId) + if (index >= 0) { + items.value.splice(index, 1, { ...items.value[index], ...data }) + } + + try { + const result = await updateItem(groupId, itemId, data) + if (index >= 0) { + items.value.splice(index, 1, result) + } + return result + } catch (err) { + items.value = snapshot + if (err instanceof HttpError && err.code === 409) { + ElMessage.warning($t('dictionary.errors.dataConflict')) + } else { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateItem') + ElMessage.error(error.value) + } + return null + } finally { + loading.value = false + } + } + + const deleteItemAction = async (groupId: string, itemId: string) => { + loading.value = true + error.value = null + + const snapshot = [...items.value] + items.value = items.value.filter((item) => item.id !== itemId) + + try { + await deleteItem(groupId, itemId) + return true + } catch (err) { + items.value = snapshot + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteItem') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + return { + items, + currentGroupId, + loading, + error, + enabledItems, + defaultItem, + fetchItems, + reset, + createItem: createItemAction, + updateItem: updateItemAction, + deleteItem: deleteItemAction + } +}) diff --git a/src/store/modules/dictionaryOverride.ts b/src/store/modules/dictionaryOverride.ts new file mode 100644 index 0000000..e617612 --- /dev/null +++ b/src/store/modules/dictionaryOverride.ts @@ -0,0 +1,160 @@ +/** + * Dictionary override state management. + */ + +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { ElMessage } from 'element-plus' +import { + disableOverride, + enableOverride, + getOverride, + getOverrides, + updateHiddenItems, + updateSortOrder +} from '@/api/dictionary/override' +import { $t } from '@/locales' + +export const useDictionaryOverrideStore = defineStore('dictionaryOverrideStore', () => { + const overrides = ref>({}) + const loading = ref(false) + const error = ref(null) + + const normalizeCode = (code: string) => code.trim().toLowerCase() + + const applyOverride = (config: Api.Dictionary.OverrideConfigDto) => { + overrides.value[normalizeCode(config.systemDictionaryGroupCode)] = config + } + + const fetchOverrides = async () => { + loading.value = true + error.value = null + + try { + const result = await getOverrides() + const nextMap: Record = {} + result.forEach((item) => { + nextMap[normalizeCode(item.systemDictionaryGroupCode)] = item + }) + overrides.value = nextMap + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadOverrides') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const fetchOverride = async (groupCode: string) => { + loading.value = true + error.value = null + + try { + const result = await getOverride(groupCode) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const enableOverrideAction = async (groupCode: string) => { + loading.value = true + error.value = null + + try { + const result = await enableOverride(groupCode) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.enableOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const disableOverrideAction = async (groupCode: string) => { + loading.value = true + error.value = null + + try { + await disableOverride(groupCode) + const key = normalizeCode(groupCode) + if (overrides.value[key]) { + overrides.value[key] = { ...overrides.value[key], overrideEnabled: false } + } else { + await fetchOverride(groupCode) + } + return true + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.disableOverride') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + const updateHiddenItemsAction = async (groupCode: string, hiddenIds: string[]) => { + loading.value = true + error.value = null + + try { + const result = await updateHiddenItems(groupCode, hiddenIds) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateHidden') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const updateCustomSortOrderAction = async ( + groupCode: string, + sortOrder: Record + ) => { + loading.value = true + error.value = null + + try { + const result = await updateSortOrder(groupCode, sortOrder) + applyOverride(result) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.updateSort') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + const clearCache = async () => { + overrides.value = {} + return fetchOverrides() + } + + return { + overrides, + loading, + error, + fetchOverrides, + fetchOverride, + enableOverride: enableOverrideAction, + disableOverride: disableOverrideAction, + updateHiddenItems: updateHiddenItemsAction, + updateCustomSortOrder: updateCustomSortOrderAction, + clearCache + } +}) diff --git a/src/store/modules/impersonation.ts b/src/store/modules/impersonation.ts new file mode 100644 index 0000000..6b0e436 --- /dev/null +++ b/src/store/modules/impersonation.ts @@ -0,0 +1,119 @@ +/** + * 租户伪装登录状态管理模块 + * + * 负责管理“平台管理员伪装登录租户”的状态切换与退出恢复 + * + * @module store/modules/impersonation + */ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { router } from '@/router' +import { resetRouterState } from '@/router/guards/beforeEach' +import { useUserStore } from '@/store/modules/user' +import { StorageConfig } from '@/utils/storage/storage-config' + +interface ImpersonationSnapshot { + originalAccessToken: string + originalRefreshToken: string + originalTenantId: string + impersonatedTenantId: string + startedAt: number +} + +/** + * 租户伪装登录 Store + */ +export const useImpersonationStore = defineStore( + 'impersonationStore', + () => { + // 伪装快照(为空表示未处于伪装态) + const snapshot = ref(null) + + // 计算属性:是否处于伪装态 + const isImpersonating = computed(() => snapshot.value !== null) + + /** + * 开始伪装登录 + */ + const start = async (token: Api.Auth.TokenResponse) => { + const userStore = useUserStore() + + // 1. 校验当前不处于伪装态 + if (isImpersonating.value) { + throw new Error('当前已处于伪装态,请先退出后再继续') + } + + // 2. 记录原始会话快照 + const originalTenantId = localStorage.getItem(StorageConfig.TENANT_ID_KEY) || '' + snapshot.value = { + originalAccessToken: userStore.accessToken, + originalRefreshToken: userStore.refreshToken, + originalTenantId, + impersonatedTenantId: token.user?.tenantId || '', + startedAt: Date.now() + } + + // 3. 切换到目标租户会话(Token + TenantId) + userStore.setToken(token.accessToken, token.refreshToken) + if (token.user) { + userStore.setUserInfo(token.user) + } + userStore.setLoginStatus(true) + + if (token.user?.tenantId) { + localStorage.setItem(StorageConfig.TENANT_ID_KEY, token.user.tenantId) + } + + // 4. 重置动态路由并重新导航(触发重新拉取用户信息与菜单) + resetRouterState(0) + await new Promise((resolve) => setTimeout(resolve, 20)) + await router.replace({ path: '/' }) + } + + /** + * 退出伪装登录 + */ + const exit = async () => { + const userStore = useUserStore() + + // 1. 校验当前处于伪装态 + if (!snapshot.value) { + return + } + + // 2. 恢复原始 Token 与 TenantId + userStore.setToken(snapshot.value.originalAccessToken, snapshot.value.originalRefreshToken) + if (snapshot.value.originalTenantId) { + localStorage.setItem(StorageConfig.TENANT_ID_KEY, snapshot.value.originalTenantId) + } + userStore.setLoginStatus(true) + + // 3. 清空快照并重置动态路由 + snapshot.value = null + resetRouterState(0) + await new Promise((resolve) => setTimeout(resolve, 20)) + await router.replace({ path: '/' }) + } + + /** + * 清空伪装快照(不做会话恢复) + */ + const clear = () => { + snapshot.value = null + } + + return { + snapshot, + isImpersonating, + start, + exit, + clear + } + }, + { + persist: { + key: 'impersonation', + storage: localStorage + } + } +) diff --git a/src/store/modules/labelOverride.ts b/src/store/modules/labelOverride.ts new file mode 100644 index 0000000..2a420bf --- /dev/null +++ b/src/store/modules/labelOverride.ts @@ -0,0 +1,301 @@ +/** + * 字典标签覆盖状态管理 + */ + +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { + getTenantLabelOverrides, + upsertTenantLabelOverride, + deleteTenantLabelOverride, + getPlatformLabelOverrides, + upsertPlatformLabelOverride, + deletePlatformLabelOverride +} from '@/api/dictionary/labelOverride' +import { $t } from '@/locales' + +export const useLabelOverrideStore = defineStore('labelOverrideStore', () => { + // ==================== 状态 ==================== + + // 1. 租户端覆盖列表 + const tenantOverrides = ref([]) + + // 2. 平台端覆盖列表(按租户 ID 分组) + const platformOverrides = ref([]) + + // 3. 当前选中的目标租户(平台管理用) + const selectedTenantId = ref(null) + + // 4. 加载状态 + const loading = ref(false) + + // 5. 错误信息 + const error = ref(null) + + // ==================== 计算属性 ==================== + + // 1. 按字典项 ID 索引的覆盖配置(方便快速查找) + const tenantOverrideMap = computed(() => { + const map: Record = {} + tenantOverrides.value.forEach((item) => { + map[item.dictionaryItemId] = item + }) + return map + }) + + // 2. 平台覆盖按字典项 ID 索引 + const platformOverrideMap = computed(() => { + const map: Record = {} + platformOverrides.value.forEach((item) => { + map[item.dictionaryItemId] = item + }) + return map + }) + + // ==================== 租户端操作 ==================== + + /** + * 获取当前租户的所有标签覆盖 + */ + const fetchTenantOverrides = async () => { + loading.value = true + error.value = null + + try { + const result = await getTenantLabelOverrides() + tenantOverrides.value = result ?? [] + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadLabelOverrides') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 租户创建/更新标签覆盖 + */ + const upsertTenantOverrideAction = async (data: Api.Dictionary.UpsertLabelOverrideRequest) => { + loading.value = true + error.value = null + + try { + const result = await upsertTenantLabelOverride(data) + + // 1. 更新本地状态 + const idx = tenantOverrides.value.findIndex( + (o) => o.dictionaryItemId === data.dictionaryItemId + ) + if (idx >= 0) { + tenantOverrides.value[idx] = result + } else { + tenantOverrides.value.push(result) + } + + ElMessage.success($t('common.saveSuccess')) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.saveLabelOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 租户删除标签覆盖 + */ + const deleteTenantOverrideAction = async (dictionaryItemId: string) => { + loading.value = true + error.value = null + + try { + await deleteTenantLabelOverride(dictionaryItemId) + + // 1. 从本地移除 + tenantOverrides.value = tenantOverrides.value.filter( + (o) => o.dictionaryItemId !== dictionaryItemId + ) + + ElMessage.success($t('common.deleteSuccess')) + return true + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteLabelOverride') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + // ==================== 平台端操作 ==================== + + /** + * 设置当前选中的目标租户 + */ + const setSelectedTenant = (tenantId: string | null) => { + selectedTenantId.value = tenantId + if (!tenantId) { + platformOverrides.value = [] + } + } + + /** + * 获取指定租户的所有标签覆盖(平台管理员用) + */ + const fetchPlatformOverrides = async ( + targetTenantId: string, + overrideType?: Api.Dictionary.OverrideType + ) => { + loading.value = true + error.value = null + + try { + const result = await getPlatformLabelOverrides(targetTenantId, overrideType) + platformOverrides.value = result ?? [] + selectedTenantId.value = targetTenantId + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.loadLabelOverrides') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 平台强制覆盖租户字典项的标签 + */ + const upsertPlatformOverrideAction = async ( + targetTenantId: string, + data: Api.Dictionary.UpsertLabelOverrideRequest + ) => { + loading.value = true + error.value = null + + try { + const result = await upsertPlatformLabelOverride(targetTenantId, data) + + // 1. 更新本地状态 + const idx = platformOverrides.value.findIndex( + (o) => o.dictionaryItemId === data.dictionaryItemId + ) + if (idx >= 0) { + platformOverrides.value[idx] = result + } else { + platformOverrides.value.push(result) + } + + ElMessage.success($t('common.saveSuccess')) + return result + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.saveLabelOverride') + ElMessage.error(error.value) + return null + } finally { + loading.value = false + } + } + + /** + * 平台删除对租户的强制覆盖 + */ + const deletePlatformOverrideAction = async (targetTenantId: string, dictionaryItemId: string) => { + loading.value = true + error.value = null + + try { + await deletePlatformLabelOverride(targetTenantId, dictionaryItemId) + + // 1. 从本地移除 + platformOverrides.value = platformOverrides.value.filter( + (o) => o.dictionaryItemId !== dictionaryItemId + ) + + ElMessage.success($t('common.deleteSuccess')) + return true + } catch (err) { + error.value = err instanceof Error ? err.message : $t('dictionary.errors.deleteLabelOverride') + ElMessage.error(error.value) + return false + } finally { + loading.value = false + } + } + + // ==================== 辅助方法 ==================== + + /** + * 清空缓存 + */ + const clearCache = () => { + tenantOverrides.value = [] + platformOverrides.value = [] + selectedTenantId.value = null + error.value = null + } + + /** + * 检查某个字典项是否已有覆盖(租户端) + */ + const hasTenantOverride = (dictionaryItemId: string) => { + return !!tenantOverrideMap.value[dictionaryItemId] + } + + /** + * 检查某个字典项是否已有平台覆盖 + */ + const hasPlatformOverride = (dictionaryItemId: string) => { + return !!platformOverrideMap.value[dictionaryItemId] + } + + /** + * 获取字典项的覆盖配置(租户端) + */ + const getTenantOverride = (dictionaryItemId: string) => { + return tenantOverrideMap.value[dictionaryItemId] ?? null + } + + /** + * 获取字典项的平台覆盖配置 + */ + const getPlatformOverride = (dictionaryItemId: string) => { + return platformOverrideMap.value[dictionaryItemId] ?? null + } + + return { + // 状态 + tenantOverrides, + platformOverrides, + selectedTenantId, + loading, + error, + + // 计算属性 + tenantOverrideMap, + platformOverrideMap, + + // 租户端操作 + fetchTenantOverrides, + upsertTenantOverride: upsertTenantOverrideAction, + deleteTenantOverride: deleteTenantOverrideAction, + + // 平台端操作 + setSelectedTenant, + fetchPlatformOverrides, + upsertPlatformOverride: upsertPlatformOverrideAction, + deletePlatformOverride: deletePlatformOverrideAction, + + // 辅助方法 + clearCache, + hasTenantOverride, + hasPlatformOverride, + getTenantOverride, + getPlatformOverride + } +}) diff --git a/src/store/modules/menu.ts b/src/store/modules/menu.ts new file mode 100644 index 0000000..85d13da --- /dev/null +++ b/src/store/modules/menu.ts @@ -0,0 +1,109 @@ +/** + * 菜单状态管理模块 + * + * 提供菜单数据和动态路由的状态管理 + * + * ## 主要功能 + * + * - 菜单列表存储和管理 + * - 首页路径配置 + * - 动态路由注册和移除 + * - 路由移除函数管理 + * - 菜单宽度配置 + * + * ## 使用场景 + * + * - 动态菜单加载和渲染 + * - 路由权限控制 + * - 首页路径动态设置 + * - 登出时清理动态路由 + * + * ## 工作流程 + * + * 1. 获取菜单数据(前端/后端模式) + * 2. 设置菜单列表和首页路径 + * 3. 注册动态路由并保存移除函数 + * 4. 登出时调用移除函数清理路由 + * + * @module store/modules/menu + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { AppRouteRecord } from '@/types/router' +import { getFirstMenuPath } from '@/utils' +import { HOME_PAGE_PATH } from '@/router' + +/** + * 菜单状态管理 + * 管理应用的菜单列表、首页路径、菜单宽度和动态路由移除函数 + */ +export const useMenuStore = defineStore('menuStore', () => { + /** 首页路径 */ + const homePath = ref(HOME_PAGE_PATH) + /** 菜单列表 */ + const menuList = ref([]) + /** 菜单宽度 */ + const menuWidth = ref('') + /** 存储路由移除函数的数组 */ + const removeRouteFns = ref<(() => void)[]>([]) + + /** + * 设置菜单列表 + * @param list 菜单路由记录数组 + */ + const setMenuList = (list: AppRouteRecord[]) => { + menuList.value = list + setHomePath(HOME_PAGE_PATH || getFirstMenuPath(list)) + } + + /** + * 获取首页路径 + * @returns 首页路径字符串 + */ + const getHomePath = () => homePath.value + + /** + * 设置主页路径 + * @param path 主页路径 + */ + const setHomePath = (path: string) => { + homePath.value = path + } + + /** + * 添加路由移除函数 + * @param fns 要添加的路由移除函数数组 + */ + const addRemoveRouteFns = (fns: (() => void)[]) => { + removeRouteFns.value.push(...fns) + } + + /** + * 移除所有动态路由 + * 执行所有存储的路由移除函数并清空数组 + */ + const removeAllDynamicRoutes = () => { + removeRouteFns.value.forEach((fn) => fn()) + removeRouteFns.value = [] + } + + /** + * 清空路由移除函数数组 + */ + const clearRemoveRouteFns = () => { + removeRouteFns.value = [] + } + + return { + menuList, + menuWidth, + removeRouteFns, + setMenuList, + getHomePath, + setHomePath, + addRemoveRouteFns, + removeAllDynamicRoutes, + clearRemoveRouteFns + } +}) diff --git a/src/store/modules/merchant.ts b/src/store/modules/merchant.ts new file mode 100644 index 0000000..fc09dc2 --- /dev/null +++ b/src/store/modules/merchant.ts @@ -0,0 +1,31 @@ +/** + * 文件用途:商户模块状态管理 + */ + +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useMerchantStore = defineStore('merchantStore', () => { + const currentMerchant = ref(null) + const listFilters = ref>({}) + + const setCurrentMerchant = (merchant: Api.Merchant.MerchantDetail | null) => { + currentMerchant.value = merchant + } + + const setListFilters = (filters: Partial) => { + listFilters.value = { ...listFilters.value, ...filters } + } + + const resetListFilters = () => { + listFilters.value = {} + } + + return { + currentMerchant, + listFilters, + setCurrentMerchant, + setListFilters, + resetListFilters + } +}) diff --git a/src/store/modules/quota-alert.ts b/src/store/modules/quota-alert.ts new file mode 100644 index 0000000..b3b6b30 --- /dev/null +++ b/src/store/modules/quota-alert.ts @@ -0,0 +1,63 @@ +/** + * 配额告警配置状态管理模块 + * + * 用于维护“配额使用率阈值”配置,并持久化到 localStorage。 + * + * @module store/modules/quota-alert + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// 默认阈值(百分比) +const DEFAULT_ALERT_THRESHOLD = 80 + +// 默认各类型阈值(按后端 QuotaType 枚举值) +const DEFAULT_THRESHOLDS: Record = { + 0: DEFAULT_ALERT_THRESHOLD, // 门店 + 1: DEFAULT_ALERT_THRESHOLD, // 账号 + 2: DEFAULT_ALERT_THRESHOLD, // 存储 + 3: DEFAULT_ALERT_THRESHOLD, // 短信 + 4: DEFAULT_ALERT_THRESHOLD, // 配送订单 + 5: DEFAULT_ALERT_THRESHOLD // 促销位 +} + +/** + * 配额告警配置 Store + */ +export const useQuotaAlertStore = defineStore( + 'quotaAlertStore', + () => { + // 1. 阈值配置(百分比) + const thresholds = ref>({ ...DEFAULT_THRESHOLDS }) + + // 2. 获取指定类型阈值 + const getThreshold = (quotaType: number) => { + return thresholds.value[quotaType] ?? DEFAULT_ALERT_THRESHOLD + } + + // 3. 设置指定类型阈值(0~100) + const setThreshold = (quotaType: number, value: number) => { + const nextValue = Math.max(0, Math.min(100, Math.round(value))) + thresholds.value = { + ...thresholds.value, + [quotaType]: nextValue + } + } + + // 4. 重置为默认值 + const resetToDefault = () => { + thresholds.value = { ...DEFAULT_THRESHOLDS } + } + + return { + thresholds, + getThreshold, + setThreshold, + resetToDefault + } + }, + { + // 使用 storeId 走全局 StorageKeyManager(避免硬编码 Key) + persist: true + } +) diff --git a/src/store/modules/setting.ts b/src/store/modules/setting.ts new file mode 100644 index 0000000..2878259 --- /dev/null +++ b/src/store/modules/setting.ts @@ -0,0 +1,450 @@ +/** + * 系统设置状态管理模块 + * + * 提供完整的系统设置状态管理 + * + * ## 主要功能 + * + * - 菜单布局配置(左侧、顶部、混合、双栏) + * - 主题管理(亮色、暗色、自动) + * - 菜单主题样式配置 + * - 界面显示开关(面包屑、标签页、语言切换等) + * - 功能开关(手风琴模式、色弱模式、水印等) + * - 样式配置(边框、圆角、容器宽度、页面过渡) + * - 节日功能配置 + * - Element Plus 主题色动态设置 + * + * ## 使用场景 + * + * - 设置面板配置管理 + * - 主题切换和样式定制 + * - 界面功能开关控制 + * - 用户偏好设置持久化 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-setting + * - 支持跨版本数据迁移 + * + * @module store/modules/setting + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { MenuThemeType } from '@/types/store' +import AppConfig from '@/config' +import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from '@/enums/appEnum' +import { setElementThemeColor } from '@/utils/ui' +import { useCeremony } from '@/hooks/core/useCeremony' +import { StorageConfig } from '@/utils' +import { SETTING_DEFAULT_CONFIG } from '@/config/setting' + +/** + * 系统设置状态管理 + * 管理应用的菜单、主题、界面显示等各项设置 + */ +export const useSettingStore = defineStore( + 'settingStore', + () => { + // 菜单相关设置 + /** 菜单类型 */ + const menuType = ref(SETTING_DEFAULT_CONFIG.menuType) + /** 菜单展开宽度 */ + const menuOpenWidth = ref(SETTING_DEFAULT_CONFIG.menuOpenWidth) + /** 菜单是否展开 */ + const menuOpen = ref(SETTING_DEFAULT_CONFIG.menuOpen) + /** 双菜单是否显示文本 */ + const dualMenuShowText = ref(SETTING_DEFAULT_CONFIG.dualMenuShowText) + + // 主题相关设置 + /** 系统主题类型 */ + const systemThemeType = ref(SETTING_DEFAULT_CONFIG.systemThemeType) + /** 系统主题模式 */ + const systemThemeMode = ref(SETTING_DEFAULT_CONFIG.systemThemeMode) + /** 菜单主题类型 */ + const menuThemeType = ref(SETTING_DEFAULT_CONFIG.menuThemeType) + /** 系统主题颜色 */ + const systemThemeColor = ref(SETTING_DEFAULT_CONFIG.systemThemeColor) + + // 界面显示设置 + /** 是否显示菜单按钮 */ + const showMenuButton = ref(SETTING_DEFAULT_CONFIG.showMenuButton) + /** 是否显示快速入口 */ + const showFastEnter = ref(SETTING_DEFAULT_CONFIG.showFastEnter) + /** 是否显示刷新按钮 */ + const showRefreshButton = ref(SETTING_DEFAULT_CONFIG.showRefreshButton) + /** 是否显示面包屑 */ + const showCrumbs = ref(SETTING_DEFAULT_CONFIG.showCrumbs) + /** 是否显示工作台标签 */ + const showWorkTab = ref(SETTING_DEFAULT_CONFIG.showWorkTab) + /** 是否显示语言切换 */ + const showLanguage = ref(SETTING_DEFAULT_CONFIG.showLanguage) + /** 是否显示进度条 */ + const showNprogress = ref(SETTING_DEFAULT_CONFIG.showNprogress) + /** 是否显示设置引导 */ + const showSettingGuide = ref(SETTING_DEFAULT_CONFIG.showSettingGuide) + /** 是否显示节日文本 */ + const showFestivalText = ref(SETTING_DEFAULT_CONFIG.showFestivalText) + /** 是否显示水印 */ + const watermarkVisible = ref(SETTING_DEFAULT_CONFIG.watermarkVisible) + + // 功能设置 + /** 是否自动关闭 */ + const autoClose = ref(SETTING_DEFAULT_CONFIG.autoClose) + /** 是否唯一展开 */ + const uniqueOpened = ref(SETTING_DEFAULT_CONFIG.uniqueOpened) + /** 是否色弱模式 */ + const colorWeak = ref(SETTING_DEFAULT_CONFIG.colorWeak) + /** 是否刷新 */ + const refresh = ref(SETTING_DEFAULT_CONFIG.refresh) + /** 是否加载节日烟花 */ + const holidayFireworksLoaded = ref(SETTING_DEFAULT_CONFIG.holidayFireworksLoaded) + + // 样式设置 + /** 边框模式 */ + const boxBorderMode = ref(SETTING_DEFAULT_CONFIG.boxBorderMode) + /** 页面过渡效果 */ + const pageTransition = ref(SETTING_DEFAULT_CONFIG.pageTransition) + /** 标签页样式 */ + const tabStyle = ref(SETTING_DEFAULT_CONFIG.tabStyle) + /** 自定义圆角 */ + const customRadius = ref(SETTING_DEFAULT_CONFIG.customRadius) + /** 容器宽度 */ + const containerWidth = ref(SETTING_DEFAULT_CONFIG.containerWidth) + + // 节日相关 + /** 节日日期 */ + const festivalDate = ref('') + + /** + * 获取菜单主题 + * 根据当前主题类型和暗色模式返回对应的主题配置 + */ + const getMenuTheme = computed((): MenuThemeType => { + const list = AppConfig.themeList.filter((item) => item.theme === menuThemeType.value) + if (isDark.value) { + return AppConfig.darkMenuStyles[0] + } else { + return list[0] + } + }) + + /** + * 判断是否为暗色模式 + */ + const isDark = computed((): boolean => { + return systemThemeType.value === SystemThemeEnum.DARK + }) + + /** + * 获取菜单展开宽度 + */ + const getMenuOpenWidth = computed((): string => { + return menuOpenWidth.value + 'px' || SETTING_DEFAULT_CONFIG.menuOpenWidth + 'px' + }) + + /** + * 获取自定义圆角 + */ + const getCustomRadius = computed((): string => { + return customRadius.value + 'rem' || SETTING_DEFAULT_CONFIG.customRadius + 'rem' + }) + + /** + * 是否显示烟花 + * 根据当前日期和节日日期判断是否显示烟花效果 + */ + const isShowFireworks = computed((): boolean => { + return festivalDate.value === useCeremony().currentFestivalData.value?.date ? false : true + }) + + /** + * 切换菜单布局 + * @param type 菜单类型 + */ + const switchMenuLayouts = (type: MenuTypeEnum) => { + menuType.value = type + } + + /** + * 设置菜单展开宽度 + * @param width 宽度值 + */ + const setMenuOpenWidth = (width: number) => { + menuOpenWidth.value = width + } + + /** + * 设置全局主题 + * @param theme 主题类型 + * @param themeMode 主题模式 + */ + const setGlopTheme = (theme: SystemThemeEnum, themeMode: SystemThemeEnum) => { + systemThemeType.value = theme + systemThemeMode.value = themeMode + localStorage.setItem(StorageConfig.THEME_KEY, theme) + } + + /** + * 切换菜单样式 + * @param theme 菜单主题 + */ + const switchMenuStyles = (theme: MenuThemeEnum) => { + menuThemeType.value = theme + } + + /** + * 设置Element Plus主题颜色 + * @param theme 主题颜色 + */ + const setElementTheme = (theme: string) => { + systemThemeColor.value = theme + setElementThemeColor(theme) + } + + /** + * 切换边框模式 + */ + const setBorderMode = () => { + boxBorderMode.value = !boxBorderMode.value + } + + /** + * 设置容器宽度 + * @param width 容器宽度枚举值 + */ + const setContainerWidth = (width: ContainerWidthEnum) => { + containerWidth.value = width + } + + /** + * 切换唯一展开模式 + */ + const setUniqueOpened = () => { + uniqueOpened.value = !uniqueOpened.value + } + + /** + * 切换菜单按钮显示 + */ + const setButton = () => { + showMenuButton.value = !showMenuButton.value + } + + /** + * 切换快速入口显示 + */ + const setFastEnter = () => { + showFastEnter.value = !showFastEnter.value + } + + /** + * 切换自动关闭 + */ + const setAutoClose = () => { + autoClose.value = !autoClose.value + } + + /** + * 切换刷新按钮显示 + */ + const setShowRefreshButton = () => { + showRefreshButton.value = !showRefreshButton.value + } + + /** + * 切换面包屑显示 + */ + const setCrumbs = () => { + showCrumbs.value = !showCrumbs.value + } + + /** + * 设置工作台标签显示 + * @param show 是否显示 + */ + const setWorkTab = (show: boolean) => { + showWorkTab.value = show + } + + /** + * 切换语言切换显示 + */ + const setLanguage = () => { + showLanguage.value = !showLanguage.value + } + + /** + * 切换进度条显示 + */ + const setNprogress = () => { + showNprogress.value = !showNprogress.value + } + + /** + * 切换色弱模式 + */ + const setColorWeak = () => { + colorWeak.value = !colorWeak.value + } + + /** + * 隐藏设置引导 + */ + const hideSettingGuide = () => { + showSettingGuide.value = false + } + + /** + * 显示设置引导 + */ + const openSettingGuide = () => { + showSettingGuide.value = true + } + + /** + * 设置页面过渡效果 + * @param transition 过渡效果名称 + */ + const setPageTransition = (transition: string) => { + pageTransition.value = transition + } + + /** + * 设置标签页样式 + * @param style 样式名称 + */ + const setTabStyle = (style: string) => { + tabStyle.value = style + } + + /** + * 设置菜单展开状态 + * @param open 是否展开 + */ + const setMenuOpen = (open: boolean) => { + menuOpen.value = open + } + + /** + * 刷新页面 + */ + const reload = () => { + refresh.value = !refresh.value + } + + /** + * 设置水印显示 + * @param visible 是否显示 + */ + const setWatermarkVisible = (visible: boolean) => { + watermarkVisible.value = visible + } + + /** + * 设置自定义圆角 + * @param radius 圆角值 + */ + const setCustomRadius = (radius: string) => { + customRadius.value = radius + document.documentElement.style.setProperty('--custom-radius', `${radius}rem`) + } + + /** + * 设置节日烟花加载状态 + * @param isLoad 是否已加载 + */ + const setholidayFireworksLoaded = (isLoad: boolean) => { + holidayFireworksLoaded.value = isLoad + } + + /** + * 设置节日文本显示 + * @param show 是否显示 + */ + const setShowFestivalText = (show: boolean) => { + showFestivalText.value = show + } + + const setFestivalDate = (date: string) => { + festivalDate.value = date + } + + const setDualMenuShowText = (show: boolean) => { + dualMenuShowText.value = show + } + + return { + menuType, + menuOpenWidth, + systemThemeType, + systemThemeMode, + menuThemeType, + systemThemeColor, + boxBorderMode, + uniqueOpened, + showMenuButton, + showFastEnter, + showRefreshButton, + showCrumbs, + autoClose, + showWorkTab, + showLanguage, + showNprogress, + colorWeak, + showSettingGuide, + pageTransition, + tabStyle, + menuOpen, + refresh, + watermarkVisible, + customRadius, + holidayFireworksLoaded, + showFestivalText, + festivalDate, + dualMenuShowText, + containerWidth, + getMenuTheme, + isDark, + getMenuOpenWidth, + getCustomRadius, + isShowFireworks, + switchMenuLayouts, + setMenuOpenWidth, + setGlopTheme, + switchMenuStyles, + setElementTheme, + setBorderMode, + setContainerWidth, + setUniqueOpened, + setButton, + setFastEnter, + setAutoClose, + setShowRefreshButton, + setCrumbs, + setWorkTab, + setLanguage, + setNprogress, + setColorWeak, + hideSettingGuide, + openSettingGuide, + setPageTransition, + setTabStyle, + setMenuOpen, + reload, + setWatermarkVisible, + setCustomRadius, + setholidayFireworksLoaded, + setShowFestivalText, + setFestivalDate, + setDualMenuShowText + } + }, + { + persist: { + key: 'setting', + storage: localStorage + } + } +) diff --git a/src/store/modules/table.ts b/src/store/modules/table.ts new file mode 100644 index 0000000..094c310 --- /dev/null +++ b/src/store/modules/table.ts @@ -0,0 +1,97 @@ +/** + * 表格状态管理模块 + * + * 提供表格显示配置的状态管理 + * + * ## 主要功能 + * + * - 表格尺寸配置(紧凑、默认、宽松) + * - 斑马纹显示开关 + * - 边框显示开关 + * - 表头背景显示开关 + * - 全屏模式开关 + * + * ## 使用场景 + * - 表格组件样式配置 + * - 用户表格偏好设置 + * - 表格工具栏功能控制 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-table + * - 用户配置跨页面保持 + * + * @module store/modules/table + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { TableSizeEnum } from '@/enums/formEnum' + +// 表格 +export const useTableStore = defineStore( + 'tableStore', + () => { + // 表格大小 + const tableSize = ref(TableSizeEnum.DEFAULT) + // 斑马纹 + const isZebra = ref(false) + // 边框 + const isBorder = ref(false) + // 表头背景 + const isHeaderBackground = ref(false) + + // 是否全屏 + const isFullScreen = ref(false) + + /** + * 设置表格大小 + * @param size 表格大小枚举值 + */ + const setTableSize = (size: TableSizeEnum) => (tableSize.value = size) + + /** + * 设置斑马纹显示状态 + * @param value 是否显示斑马纹 + */ + const setIsZebra = (value: boolean) => (isZebra.value = value) + + /** + * 设置表格边框显示状态 + * @param value 是否显示边框 + */ + const setIsBorder = (value: boolean) => (isBorder.value = value) + + /** + * 设置表头背景显示状态 + * @param value 是否显示表头背景 + */ + const setIsHeaderBackground = (value: boolean) => (isHeaderBackground.value = value) + + /** + * 设置是否全屏 + * @param value 是否全屏 + */ + const setIsFullScreen = (value: boolean) => (isFullScreen.value = value) + + return { + tableSize, + isZebra, + isBorder, + isHeaderBackground, + setTableSize, + setIsZebra, + setIsBorder, + setIsHeaderBackground, + isFullScreen, + setIsFullScreen + } + }, + { + persist: { + key: 'table', + storage: localStorage + } + } +) diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts new file mode 100644 index 0000000..b5adfef --- /dev/null +++ b/src/store/modules/user.ts @@ -0,0 +1,257 @@ +/** + * 用户状态管理模块 + * + * 提供用户相关的状态管理 + * + * ## 主要功能 + * + * - 用户登录状态管理 + * - 用户信息存储 + * - 访问令牌和刷新令牌管理 + * - 语言设置 + * - 搜索历史记录 + * - 锁屏状态和密码管理 + * - 登出清理逻辑 + * + * ## 使用场景 + * + * - 用户登录和认证 + * - 权限验证 + * - 个人信息展示 + * - 多语言切换 + * - 锁屏功能 + * - 搜索历史管理 + * + * ## 持久化 + * + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-user + * - 登出时自动清理 + * + * @module store/modules/user + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { LanguageEnum } from '@/enums/appEnum' +import { router } from '@/router' +import { useSettingStore } from './setting' +import { useWorktabStore } from './worktab' +import { AppRouteRecord } from '@/types/router' +import { setPageTitle } from '@/utils/router' +import { resetRouterState } from '@/router/guards/beforeEach' +import { useMenuStore } from './menu' +import { StorageConfig } from '@/utils/storage/storage-config' +import { fetchGetUserPermissions } from '@/api/auth' + +/** + * 用户状态管理 + * 管理用户登录状态、个人信息、语言设置、搜索历史、锁屏状态等 + */ +export const useUserStore = defineStore( + 'userStore', + () => { + // 语言设置 + const language = ref(LanguageEnum.ZH) + // 登录状态 + const isLogin = ref(false) + // 锁屏状态 + const isLock = ref(false) + // 锁屏密码 + const lockPassword = ref('') + // 用户信息 + const info = ref>({}) + // 搜索历史记录 + const searchHistory = ref([]) + // 访问令牌 + const accessToken = ref('') + // 刷新令牌 + const refreshToken = ref('') + // 用户权限列表 + const permissions = ref([]) + + // 计算属性:获取用户信息 + const getUserInfo = computed(() => info.value) + // 计算属性:获取设置状态 + const getSettingState = computed(() => useSettingStore().$state) + // 计算属性:获取工作台状态 + const getWorktabState = computed(() => useWorktabStore().$state) + + /** + * 设置用户信息 + * @param newInfo 新的用户信息 + */ + const setUserInfo = (newInfo: Api.Auth.UserInfo) => { + info.value = newInfo + } + + /** + * 设置登录状态 + * @param status 登录状态 + */ + const setLoginStatus = (status: boolean) => { + isLogin.value = status + } + + /** + * 设置语言 + * @param lang 语言枚举值 + */ + const setLanguage = (lang: LanguageEnum) => { + setPageTitle(router.currentRoute.value) + language.value = lang + } + + /** + * 设置搜索历史 + * @param list 搜索历史列表 + */ + const setSearchHistory = (list: AppRouteRecord[]) => { + searchHistory.value = list + } + + /** + * 设置锁屏状态 + * @param status 锁屏状态 + */ + const setLockStatus = (status: boolean) => { + isLock.value = status + } + + /** + * 设置锁屏密码 + * @param password 锁屏密码 + */ + const setLockPassword = (password: string) => { + lockPassword.value = password + } + + /** + * 设置令牌 + * @param newAccessToken 访问令牌 + * @param newRefreshToken 刷新令牌(可选) + */ + const setToken = (newAccessToken: string, newRefreshToken?: string) => { + accessToken.value = newAccessToken + if (newRefreshToken) { + refreshToken.value = newRefreshToken + } + } + + /** + * 获取用户权限 + */ + const getUserPermissions = async () => { + if (!info.value.userId) return [] + try { + const res = await fetchGetUserPermissions(String(info.value.userId)) + permissions.value = res.permissions + return res.permissions + } catch (error) { + console.error('获取用户权限失败:', error) + return [] + } + } + + /** + * 退出登录 + * 清空所有用户相关状态并跳转到登录页 + * 如果是同一账号重新登录,保留工作台标签页 + */ + const logOut = () => { + // 保存当前用户 ID,用于下次登录时判断是否为同一用户 + const currentUserId = info.value.userId + if (currentUserId) { + localStorage.setItem(StorageConfig.LAST_USER_ID_KEY, String(currentUserId)) + } + + // 清空用户信息 + info.value = {} + // 重置登录状态 + isLogin.value = false + // 重置锁屏状态 + isLock.value = false + // 清空锁屏密码 + lockPassword.value = '' + // 清空访问令牌 + accessToken.value = '' + // 清空刷新令牌 + refreshToken.value = '' + // 清空权限列表 + permissions.value = [] + // 注意:不清空工作台标签页,等下次登录时根据用户判断 + // 移除iframe路由缓存 + sessionStorage.removeItem('iframeRoutes') + // 清空主页路径 + useMenuStore().setHomePath('') + // 重置路由状态 + resetRouterState(500) + // 跳转到登录页,携带当前路由作为 redirect 参数 + const currentRoute = router.currentRoute.value + const redirect = currentRoute.path !== '/login' ? currentRoute.fullPath : undefined + router.push({ + name: 'Login', + query: redirect ? { redirect } : undefined + }) + } + + /** + * 检查并清理工作台标签页 + * 如果不是同一用户登录,清空工作台标签页 + * 应在登录成功后调用 + */ + const checkAndClearWorktabs = () => { + const lastUserId = localStorage.getItem(StorageConfig.LAST_USER_ID_KEY) + const currentUserId = info.value.userId + + // 无法获取当前用户 ID,跳过检查 + if (!currentUserId) return + + // 首次登录或缓存已清除,保留现有标签页 + if (!lastUserId) { + return + } + + // 不同用户登录,清空工作台标签页 + if (String(currentUserId) !== lastUserId) { + const worktabStore = useWorktabStore() + worktabStore.opened = [] + worktabStore.keepAliveExclude = [] + } + + // 清除临时存储 + localStorage.removeItem(StorageConfig.LAST_USER_ID_KEY) + } + + return { + language, + isLogin, + isLock, + lockPassword, + info, + searchHistory, + accessToken, + refreshToken, + permissions, + getUserInfo, + getSettingState, + getWorktabState, + setUserInfo, + setLoginStatus, + setLanguage, + setSearchHistory, + setLockStatus, + setLockPassword, + setToken, + logOut, + checkAndClearWorktabs, + getUserPermissions + } + }, + { + persist: { + key: 'user', + storage: localStorage + } + } +) diff --git a/src/store/modules/worktab.ts b/src/store/modules/worktab.ts new file mode 100644 index 0000000..caa0d90 --- /dev/null +++ b/src/store/modules/worktab.ts @@ -0,0 +1,568 @@ +/** + * 工作标签页状态管理模块 + * + * 提供多标签页功能的完整状态管理 + * + * ## 主要功能 + * + * - 标签页打开和关闭 + * - 标签页固定和取消固定 + * - 批量关闭(左侧、右侧、其他、全部) + * - 标签页缓存管理(KeepAlive) + * - 标签页标题自定义 + * - 标签页路由验证 + * - 动态路由参数处理 + * + * ## 使用场景 + * + * - 多标签页导航 + * - 页面缓存控制 + * - 标签页右键菜单 + * - 固定常用页面 + * - 批量关闭标签 + * + * ## 核心特性 + * + * - 智能标签页复用(同路由名称复用) + * - 固定标签页保护(不可关闭) + * - KeepAlive 缓存排除管理 + * - 路由有效性验证 + * - 首页自动保留 + * + * ## 持久化 + * - 使用 localStorage 存储 + * - 存储键:sys-v{version}-worktab + * - 刷新页面保持标签状态 + * + * @module store/modules/worktab + * @author Art Design Pro Team + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { router } from '@/router' +import { LocationQueryRaw, Router } from 'vue-router' +import { WorkTab } from '@/types' +import { useCommon } from '@/hooks/core/useCommon' + +interface WorktabState { + current: Partial + opened: WorkTab[] + keepAliveExclude: string[] +} + +/** + * 工作台标签页管理 Store + */ +export const useWorktabStore = defineStore( + 'worktabStore', + () => { + // 状态定义 + const current = ref>({}) + const opened = ref([]) + const keepAliveExclude = ref([]) + + // 计算属性 + const hasOpenedTabs = computed(() => opened.value.length > 0) + const hasMultipleTabs = computed(() => opened.value.length > 1) + const currentTabIndex = computed(() => + current.value.path ? opened.value.findIndex((tab) => tab.path === current.value.path) : -1 + ) + + /** + * 查找标签页索引 + */ + const findTabIndex = (path: string): number => { + return opened.value.findIndex((tab) => tab.path === path) + } + + /** + * 获取标签页 + */ + const getTab = (path: string): WorkTab | undefined => { + return opened.value.find((tab) => tab.path === path) + } + + /** + * 检查标签页是否可关闭 + */ + const isTabClosable = (tab: WorkTab): boolean => { + return !tab.fixedTab + } + + /** + * 安全的路由跳转 + */ + const safeRouterPush = (tab: Partial): void => { + if (!tab.path) { + console.warn('尝试跳转到无效路径的标签页') + return + } + + try { + router.push({ + path: tab.path, + query: tab.query as LocationQueryRaw + }) + } catch (error) { + console.error('路由跳转失败:', error) + } + } + + /** + * 打开或激活一个选项卡 + */ + const openTab = (tab: WorkTab): void => { + if (!tab.path) { + console.warn('尝试打开无效的标签页') + return + } + + // 从 keepAlive 排除列表中移除 + if (tab.name) { + removeKeepAliveExclude(tab.name) + } + + // 先根据路由名称查找(应对动态路由参数导致的多开问题),找不到再根据路径查找 + let existingIndex = -1 + if (tab.name) { + existingIndex = opened.value.findIndex((t) => t.name === tab.name) + } + if (existingIndex === -1) { + existingIndex = findTabIndex(tab.path) + } + + if (existingIndex === -1) { + // 新增标签页 + const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length + const newTab = { ...tab } + + if (tab.fixedTab) { + opened.value.splice(insertIndex, 0, newTab) + } else { + opened.value.push(newTab) + } + + current.value = newTab + } else { + // 更新现有标签页(当动态路由参数或查询变更时,复用同一标签) + const existingTab = opened.value[existingIndex] + + opened.value[existingIndex] = { + ...existingTab, + path: tab.path, + params: tab.params, + query: tab.query, + title: tab.title || existingTab.title, + fixedTab: tab.fixedTab ?? existingTab.fixedTab, + keepAlive: tab.keepAlive ?? existingTab.keepAlive, + name: tab.name || existingTab.name, + icon: tab.icon || existingTab.icon + } + + current.value = opened.value[existingIndex] + } + } + + /** + * 查找固定标签页的插入位置 + */ + const findFixedTabInsertIndex = (): number => { + let insertIndex = 0 + for (let i = 0; i < opened.value.length; i++) { + if (opened.value[i].fixedTab) { + insertIndex = i + 1 + } else { + break + } + } + return insertIndex + } + + /** + * 关闭指定的选项卡 + */ + const removeTab = (path: string): void => { + const targetTab = getTab(path) + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭不存在的标签页: ${path}`) + return + } + + if (targetTab && !isTabClosable(targetTab)) { + console.warn(`尝试关闭固定标签页: ${path}`) + return + } + + // 从标签页列表中移除 + opened.value.splice(targetIndex, 1) + + // 处理缓存排除 + if (targetTab?.name) { + addKeepAliveExclude(targetTab) + } + + const { homePath } = useCommon() + + // 如果关闭后无标签页,跳转首页 + if (!hasOpenedTabs.value) { + if (path !== homePath.value) { + current.value = {} + safeRouterPush({ path: homePath.value }) + } + return + } + + // 如果关闭的是当前激活标签,需要激活其他标签 + if (current.value.path === path) { + const newIndex = targetIndex >= opened.value.length ? opened.value.length - 1 : targetIndex + current.value = opened.value[newIndex] + safeRouterPush(current.value) + } + } + + /** + * 关闭左侧选项卡 + */ + const removeLeft = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭左侧标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取左侧可关闭的标签页 + const leftTabs = opened.value.slice(0, targetIndex) + const closableLeftTabs = leftTabs.filter(isTabClosable) + + if (closableLeftTabs.length === 0) { + console.warn('左侧没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableLeftTabs) + + // 移除左侧可关闭的标签页 + opened.value = opened.value.filter( + (tab, index) => index >= targetIndex || !isTabClosable(tab) + ) + + // 确保当前标签是激活状态 + const targetTab = getTab(path) + if (targetTab) { + current.value = targetTab + } + } + + /** + * 关闭右侧选项卡 + */ + const removeRight = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试关闭右侧标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取右侧可关闭的标签页 + const rightTabs = opened.value.slice(targetIndex + 1) + const closableRightTabs = rightTabs.filter(isTabClosable) + + if (closableRightTabs.length === 0) { + console.warn('右侧没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableRightTabs) + + // 移除右侧可关闭的标签页 + opened.value = opened.value.filter( + (tab, index) => index <= targetIndex || !isTabClosable(tab) + ) + + // 确保当前标签是激活状态 + const targetTab = getTab(path) + if (targetTab) { + current.value = targetTab + } + } + + /** + * 关闭其他选项卡 + */ + const removeOthers = (path: string): void => { + const targetTab = getTab(path) + + if (!targetTab) { + console.warn(`尝试关闭其他标签页,但目标标签页不存在: ${path}`) + return + } + + // 获取其他可关闭的标签页 + const otherTabs = opened.value.filter((tab) => tab.path !== path) + const closableTabs = otherTabs.filter(isTabClosable) + + if (closableTabs.length === 0) { + console.warn('没有其他可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableTabs) + + // 只保留当前标签和固定标签 + opened.value = opened.value.filter((tab) => tab.path === path || !isTabClosable(tab)) + + // 确保当前标签是激活状态 + current.value = targetTab + } + + /** + * 关闭所有可关闭的标签页 + */ + const removeAll = (): void => { + const { homePath } = useCommon() + const hasFixedTabs = opened.value.some((tab) => tab.fixedTab) + + // 获取可关闭的标签页 + const closableTabs = opened.value.filter((tab) => { + if (!isTabClosable(tab)) return false + // 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页 + return hasFixedTabs || tab.path !== homePath.value + }) + + if (closableTabs.length === 0) { + console.warn('没有可关闭的标签页') + return + } + + // 标记为缓存排除 + markTabsToRemove(closableTabs) + + // 保留不可关闭的标签页和首页(当没有固定标签时) + opened.value = opened.value.filter((tab) => { + return !isTabClosable(tab) || (!hasFixedTabs && tab.path === homePath.value) + }) + + // 处理激活状态 + if (!hasOpenedTabs.value) { + current.value = {} + safeRouterPush({ path: homePath.value }) + return + } + + // 选择激活的标签页:优先首页,其次第一个可用标签 + const homeTab = opened.value.find((tab) => tab.path === homePath.value) + const targetTab = homeTab || opened.value[0] + + current.value = targetTab + safeRouterPush(targetTab) + } + + /** + * 将指定选项卡添加到 keepAlive 排除列表中 + */ + const addKeepAliveExclude = (tab: WorkTab): void => { + if (!tab.keepAlive || !tab.name) return + + if (!keepAliveExclude.value.includes(tab.name)) { + keepAliveExclude.value.push(tab.name) + } + } + + /** + * 从 keepAlive 排除列表中移除指定组件名称 + */ + const removeKeepAliveExclude = (name: string): void => { + if (!name) return + + keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name) + } + + /** + * 将传入的一组选项卡的组件名称标记为排除缓存 + */ + const markTabsToRemove = (tabs: WorkTab[]): void => { + tabs.forEach((tab) => { + if (tab.name) { + addKeepAliveExclude(tab) + } + }) + } + + /** + * 切换指定标签页的固定状态 + */ + const toggleFixedTab = (path: string): void => { + const targetIndex = findTabIndex(path) + + if (targetIndex === -1) { + console.warn(`尝试切换不存在标签页的固定状态: ${path}`) + return + } + + const tab = { ...opened.value[targetIndex] } + tab.fixedTab = !tab.fixedTab + + // 移除原位置 + opened.value.splice(targetIndex, 1) + + if (tab.fixedTab) { + // 固定标签插入到所有固定标签的末尾 + const firstNonFixedIndex = opened.value.findIndex((t) => !t.fixedTab) + const insertIndex = firstNonFixedIndex === -1 ? opened.value.length : firstNonFixedIndex + opened.value.splice(insertIndex, 0, tab) + } else { + // 非固定标签插入到所有固定标签后 + const fixedCount = opened.value.filter((t) => t.fixedTab).length + opened.value.splice(fixedCount, 0, tab) + } + + // 更新当前标签引用 + if (current.value.path === path) { + current.value = tab + } + } + + /** + * 验证工作台标签页的路由有效性 + */ + const validateWorktabs = (routerInstance: Router): void => { + try { + // 动态路由校验:优先使用路由 name 判断有效性;否则用 resolve 匹配参数化路径 + const isTabRouteValid = (tab: Partial): boolean => { + try { + if (tab.name) { + const routes = routerInstance.getRoutes() + if (routes.some((r) => r.name === tab.name)) return true + } + if (tab.path) { + const resolved = routerInstance.resolve({ + path: tab.path, + query: (tab.query as LocationQueryRaw) || undefined + }) + return resolved.matched.length > 0 + } + return false + } catch { + return false + } + } + + // 过滤出有效的标签页 + const validTabs = opened.value.filter((tab) => isTabRouteValid(tab)) + + if (validTabs.length !== opened.value.length) { + console.warn('发现无效的标签页路由,已自动清理') + opened.value = validTabs + } + + // 验证当前激活标签的有效性 + const isCurrentValid = current.value && isTabRouteValid(current.value) + + if (!isCurrentValid && validTabs.length > 0) { + console.warn('当前激活标签无效,已自动切换') + current.value = validTabs[0] + } else if (!isCurrentValid) { + current.value = {} + } + } catch (error) { + console.error('验证工作台标签页失败:', error) + } + } + + /** + * 清空所有状态(用于登出等场景) + */ + const clearAll = (): void => { + current.value = {} + opened.value = [] + keepAliveExclude.value = [] + } + + /** + * 获取状态快照(用于持久化存储) + */ + const getStateSnapshot = (): WorktabState => { + return { + current: { ...current.value }, + opened: [...opened.value], + keepAliveExclude: [...keepAliveExclude.value] + } + } + + /** + * 获取标签页标题 + */ + const getTabTitle = (path: string): WorkTab | undefined => { + const tab = getTab(path) + return tab + } + + /** + * 更新标签页标题 + */ + const updateTabTitle = (path: string, title: string): void => { + const tab = getTab(path) + if (tab) { + tab.customTitle = title + } + } + + /** + * 重置标签页标题 + */ + const resetTabTitle = (path: string): void => { + const tab = getTab(path) + if (tab) { + tab.customTitle = '' + } + } + + return { + // 状态 + current, + opened, + keepAliveExclude, + + // 计算属性 + hasOpenedTabs, + hasMultipleTabs, + currentTabIndex, + + // 方法 + openTab, + removeTab, + removeLeft, + removeRight, + removeOthers, + removeAll, + toggleFixedTab, + validateWorktabs, + clearAll, + getStateSnapshot, + + // 工具方法 + findTabIndex, + getTab, + isTabClosable, + addKeepAliveExclude, + removeKeepAliveExclude, + markTabsToRemove, + getTabTitle, + updateTabTitle, + resetTabTitle + } + }, + { + persist: { + key: 'worktab', + storage: localStorage + } + } +) diff --git a/src/types/announcement.ts b/src/types/announcement.ts new file mode 100644 index 0000000..94a0249 --- /dev/null +++ b/src/types/announcement.ts @@ -0,0 +1,140 @@ +/** + * 文件用途:公告模块 TypeScript 类型定义 + * 作者:前端架构师(Codex) + * 日期:2025-12-20 + */ + +/** 雪花ID(后端long序列化为字符串) */ +export type SnowflakeId = string + +/** UTC ISO 8601 时间字符串 */ +export type IsoDateTimeString = string + +/** 公告状态(字符串字面量) */ +export type AnnouncementStatus = 'Draft' | 'Published' | 'Revoked' + +/** 发布者范围(字符串字面量) */ +export type PublisherScope = 'Platform' | 'Tenant' + +/** 公告类型(数字枚举 0-8) */ +export enum TenantAnnouncementType { + System = 0, // 系统公告 + Billing = 1, // 账单/订阅相关提醒 + Operation = 2, // 运营通知 + SYSTEM_PLATFORM_UPDATE = 3, // 平台系统更新公告 + SYSTEM_SECURITY_NOTICE = 4, // 系统安全公告 + SYSTEM_COMPLIANCE = 5, // 系统合规公告 + TENANT_INTERNAL = 6, // 租户内部公告 + TENANT_FINANCE = 7, // 租户财务公告 + TENANT_OPERATION = 8 // 租户运营公告 +} + +/** 目标受众类型(字符串字面量) */ +export type AnnouncementTargetType = 'All' | 'Roles' | 'Users' | 'Rules' | 'Manual' + +/** 公告DTO(对齐后端 TenantAnnouncementDto) */ +export interface TenantAnnouncementDto { + id: SnowflakeId // 公告ID(雪花ID字符串) + tenantId: SnowflakeId // 租户ID(雪花ID字符串) + title: string // 标题 + content: string // 内容(富文本) + announcementType: TenantAnnouncementType // 公告类型 + priority: number // 优先级 + effectiveFrom: IsoDateTimeString // 生效开始时间(UTC) + effectiveTo?: IsoDateTimeString | null // 生效结束时间(UTC) + publisherScope: PublisherScope // 发布者范围 + publisherUserId?: SnowflakeId | null // 发布人用户ID(雪花ID字符串) + status: AnnouncementStatus // 公告状态 + publishedAt?: IsoDateTimeString | null // 发布时间(UTC) + revokedAt?: IsoDateTimeString | null // 撤销时间(UTC) + scheduledPublishAt?: IsoDateTimeString | null // 计划发布时间(UTC) + targetType: AnnouncementTargetType // 目标受众类型 + targetParameters?: string | null // 目标参数(JSON字符串) + rowVersion: string // 并发控制版本(Base64字符串) + isActive: boolean // 是否生效 + isRead: boolean // 当前用户是否已读 + readAt?: IsoDateTimeString | null // 已读时间(UTC) +} + +/** 目标受众规则(规则模式) */ +export interface TargetRules { + departments?: string[] // 部门ID列表 + roles?: string[] // 角色ID列表 + tags?: string[] // 标签ID列表 +} + +/** 公告表单数据(创建/编辑) */ +export interface AnnouncementFormData { + title: string // 标题 + content: string // 内容(富文本) + announcementType: TenantAnnouncementType // 公告类型 + priority: number // 优先级 + effectiveFrom: IsoDateTimeString // 生效开始时间(UTC) + effectiveTo?: IsoDateTimeString | null // 生效结束时间(UTC) + targetType: AnnouncementTargetType // 目标受众类型 + targetRules?: TargetRules // 规则模式参数 + targetUserIds?: SnowflakeId[] // 手选用户ID列表(雪花ID字符串) + rowVersion?: string // 并发控制版本(更新时必传) +} + +/** 公告查询参数 */ +export interface AnnouncementQueryParams { + page?: number // 页码 + pageSize?: number // 每页数量 + status?: AnnouncementStatus // 状态筛选 + keyword?: string // 关键词(标题/内容) + effectiveFrom?: IsoDateTimeString // 生效开始时间(UTC) + effectiveTo?: IsoDateTimeString // 生效结束时间(UTC) + dateFrom?: IsoDateTimeString // 起始时间(UTC) + dateTo?: IsoDateTimeString // 结束时间(UTC) + onlyEffective?: boolean // 仅查询有效公告 + tenantId?: SnowflakeId // 租户ID(雪花ID字符串) + isRead?: boolean // 已读状态筛选 +} + +/** 公告草稿数据 */ +export interface AnnouncementDraft extends Partial { + draftId: string // 草稿ID + lastSaved: IsoDateTimeString // 上次保存时间(UTC) +} + +/** 受众预估结果 */ +export interface AudienceEstimate { + count: number // 预估人数 + preview: SnowflakeId[] // 预览用户ID(前10个) +} + +/** 公告统计数据 */ +export interface AnnouncementStatistics { + totalCount: number // 总数 + draftCount: number // 草稿数量 + publishedCount: number // 已发布数量 + revokedCount: number // 已撤销数量 + unreadCount: number // 未读数量 +} + +/** 公告操作命令(发布/撤销) */ +export interface AnnouncementCommand { + announcementId: SnowflakeId // 公告ID(雪花ID字符串) + rowVersion: string // 并发控制版本(Base64字符串) +} + +/** 批量标记已读命令 */ +export interface BatchMarkReadCommand { + announcementIds: SnowflakeId[] // 公告ID列表(雪花ID字符串) +} + +/** 公告类型选项(下拉框) */ +export interface AnnouncementTypeOption { + label: string // 显示文本 + value: TenantAnnouncementType // 类型值 + icon?: string // 图标 + color?: string // 颜色 +} + +/** 公告状态选项(下拉框) */ +export interface AnnouncementStatusOption { + label: string // 显示文本 + value: AnnouncementStatus // 状态值 + color?: string // 颜色 +} diff --git a/src/types/api/auth.d.ts b/src/types/api/auth.d.ts new file mode 100644 index 0000000..6e40df5 --- /dev/null +++ b/src/types/api/auth.d.ts @@ -0,0 +1,82 @@ +declare namespace Api { + /** 认证类型 */ + namespace Auth { + /** 登录参数 */ + interface LoginParams { + account: string + password: string + } + + /** Token 响应 */ + interface TokenResponse { + accessToken: string + accessTokenExpiresAt: string + refreshToken: string + refreshTokenExpiresAt: string + user: UserInfo + isNewUser: boolean + } + + /** 登录响应 */ + type LoginResponse = TokenResponse + + /** 用户信息 */ + interface UserInfo { + userId: string + account: string + displayName: string + tenantId: string + merchantId?: string + roles: string[] + permissions: string[] + avatar?: string + } + + /** 菜单权限项 */ + interface MenuAuthItem { + title: string + authMark: string + } + + /** 菜单元数据 */ + interface MenuMeta { + title: string + icon?: string + keepAlive?: boolean + isIframe?: boolean + link?: string + roles?: string[] + authList?: MenuAuthItem[] + } + + /** 菜单节点 */ + interface MenuNode { + name: string + path: string + component: string + meta: MenuMeta + children?: MenuNode[] + } + + /** 菜单列表响应 */ + type MenuListResponse = MenuNode[] + + /** 用户权限响应 */ + interface UserPermissionsResponse { + userId: string + tenantId: string + merchantId: string + account: string + displayName: string + roles: string[] + permissions: string[] + createdAt: string + } + + /** 通过重置链接令牌重置管理员密码请求 */ + interface ResetAdminPasswordRequest { + token: string + newPassword: string + } + } +} diff --git a/src/types/api/billing.d.ts b/src/types/api/billing.d.ts new file mode 100644 index 0000000..fae9f99 --- /dev/null +++ b/src/types/api/billing.d.ts @@ -0,0 +1,369 @@ +declare namespace Api { + /** 账单管理类型 */ + namespace Billing { + // ==================== 枚举定义 ==================== + + /** 账单状态枚举 */ + enum TenantBillingStatus { + /** 待支付 */ + /** 已支付 */ + Pending = 0, + /** 已支付 */ + Paid = 1, + /** 已逾期 */ + Overdue = 2, + /** 已取消 */ + Cancelled = 3 + } + + /** 账单类型枚举 */ + enum BillingType { + /** 订阅账单 */ + Subscription = 0, + /** 配额包购买 */ + QuotaPurchase = 1, + /** 手动创建 */ + Manual = 2, + /** 续费账单 */ + Renewal = 3 + } + + /** 租户账单支付方式枚举(后端已从 PaymentMethod 重命名为 TenantPaymentMethod,以解决 Swagger schemaId 冲突) */ + enum TenantPaymentMethod { + /** 在线支付 */ + Online = 0, + /** 银行转账 */ + BankTransfer = 1, + /** 其他方式 */ + Other = 2 + } + + /** 租户账单支付状态枚举(后端已从 PaymentStatus 重命名为 TenantPaymentStatus,以解决 Swagger schemaId 冲突) */ + enum TenantPaymentStatus { + /** 待处理 */ + Pending = 0, + /** 成功 */ + Success = 1, + /** 失败 */ + Failed = 2, + /** 已退款 */ + Refunded = 3 + } + + /** 导出格式枚举 */ + enum ExportFormat { + /** Excel 格式 */ + Excel = 0, + /** PDF 格式 */ + Pdf = 1, + /** CSV 格式 */ + Csv = 2 + } + + // ==================== DTOs 定义 ==================== + + /** 账单列表项 DTO */ + interface BillingListDto { + id: string + tenantId: string + tenantName?: string + statementNo: string + /** 账单类型(部分接口可能不返回) */ + billingType?: BillingType + periodStart: string + periodEnd: string + amountDue: number + amountPaid: number + discountAmount?: number + taxAmount?: number + currency?: string + status: TenantBillingStatus + dueDate: string + overdueNotifiedAt?: string + createdAt: string + updatedAt?: string + } + + /** 账单详情 DTO */ + interface BillingDetailDto extends BillingListDto { + /** 账单明细 JSON(后端可能直接返回字符串) */ + lineItemsJson?: string + /** 账单明细列表(后端可能直接返回数组,或由前端从 lineItemsJson 解析) */ + lineItems?: BillingLineItemDto[] + /** 支付记录列表 */ + payments: PaymentRecordDto[] + notes?: string + reminderSentAt?: string + cancelledAt?: string + cancelReason?: string + } + + /** 账单明细项 DTO */ + interface BillingLineItemDto { + itemType: string + description: string + quantity: number + unitPrice: number + amount: number + discountRate?: number + } + + /** 支付记录 DTO */ + interface PaymentRecordDto { + id: string + /** 账单 ID(兼容不同接口字段命名) */ + billingStatementId?: string + /** 账单 ID(兼容不同接口字段命名) */ + billingId?: string + amount: number + method: TenantPaymentMethod + status: TenantPaymentStatus + transactionNo?: string + proofUrl?: string + paidAt?: string + notes?: string + /** 兼容部分后端返回的支付方式文本字段 */ + paymentMethod?: string + /** 兼容部分后端返回的审核状态字段 */ + verificationStatus?: string + verifiedBy?: string + verifiedAt?: string + refundReason?: string + refundedAt?: string + createdAt: string + } + + /** 账单统计数据 DTO */ + interface BillingStatisticsDto { + // 兼容“基础统计”返回(当前后端实现) + totalCount?: number + pendingCount?: number + paidCount?: number + overdueCount?: number + cancelledCount?: number + totalAmountDue?: number + totalAmountPaid?: number + totalAmountUnpaid?: number + totalOverdueAmount?: number + averageAmount?: number + paymentSuccessRate?: number + dailyStats?: Array<{ + date: string + count: number + totalAmount: number + paidAmount: number + }> + + // 兼容“增强统计”返回(用于仪表盘展示) + /** 总收入 */ + totalRevenue?: number + /** 本月收入 */ + monthlyRevenue?: number + /** 待收款金额 */ + pendingAmount?: number + /** 逾期金额 */ + overdueAmount?: number + /** 已支付账单数 */ + paidBillCount?: number + /** 待支付账单数 */ + pendingBillCount?: number + /** 已逾期账单数 */ + overdueBillCount?: number + /** 按状态分布 */ + statusDistribution?: Array<{ + status: TenantBillingStatus + count: number + amount: number + }> + /** 收入趋势(按日期) */ + revenueTrend?: Array<{ + date: string + amount: number + count: number + }> + /** 支付方式占比 */ + paymentMethodDistribution?: Array<{ + method: TenantPaymentMethod + count: number + amount: number + }> + /** 租户欠款排行(Top 10) */ + topDebtors?: Array<{ + tenantId: string + tenantName: string + overdueAmount: number + overdueDays: number + }> + } + + /** 账单导出数据 DTO */ + interface BillingExportDto { + statementNo: string + tenantName: string + billingType: string + periodStart: string + periodEnd: string + amountDue: number + amountPaid: number + status: string + dueDate: string + createdAt: string + } + + // ==================== 查询参数 ==================== + + /** 账单列表查询参数 */ + interface BillingListParams { + /** 页码(从 1 开始) */ + PageNumber?: number + /** 页大小 */ + PageSize?: number + /** 租户 ID */ + TenantId?: string + /** 账单状态 */ + Status?: TenantBillingStatus + /** 账单类型 */ + BillingType?: BillingType + /** 开始日期(账单创建时间或到期日) */ + StartDate?: string + /** 结束日期 */ + EndDate?: string + /** 最小金额 */ + MinAmount?: number + /** 最大金额 */ + MaxAmount?: number + /** 搜索关键词(账单号、租户名) */ + Keyword?: string + /** 排序字段(CreatedAt, DueDate, AmountDue) */ + SortBy?: string + /** 是否降序 */ + SortDesc?: boolean + } + + /** 账单统计查询参数 */ + interface BillingStatisticsParams { + /** 租户 ID(可选,不传则统计全部) */ + TenantId?: string + /** 开始日期 */ + StartDate: string + /** 结束日期 */ + EndDate: string + /** 分组方式(Day, Week, Month) */ + GroupBy?: 'Day' | 'Week' | 'Month' + } + + /** 导出参数 */ + interface ExportParams { + /** 导出格式 */ + Format: ExportFormat + /** 账单 ID 列表(可选:按选中导出) */ + BillingIds?: string[] + /** 租户 ID(可选:按筛选条件导出) */ + TenantId?: string + /** 状态(可选) */ + Status?: TenantBillingStatus + /** 开始日期(可选) */ + StartDate?: string + /** 结束日期(可选) */ + EndDate?: string + /** 关键词(可选) */ + Keyword?: string + } + + // ==================== 命令参数 ==================== + + /** 创建账单命令 */ + interface CreateBillingCommand { + /** 租户 ID */ + tenantId: string + /** 账单类型 */ + billingType: BillingType + /** 应付金额 */ + amountDue: number + /** 到期日期 */ + dueDate: string + /** 账单明细 */ + lineItems: Array<{ + itemType: string + description: string + quantity: number + unitPrice: number + amount: number + discountRate?: number + }> + /** 备注 */ + notes?: string + } + + /** 更新账单状态命令 */ + interface UpdateStatusCommand { + /** 新状态 */ + status: TenantBillingStatus + /** 备注 */ + notes?: string + } + + /** 记录支付命令 */ + interface RecordPaymentCommand { + /** 支付金额 */ + amount: number + /** 支付方式 */ + method: TenantPaymentMethod + /** 交易号 */ + transactionNo?: string + /** 支付凭证 URL */ + proofUrl?: string + /** 备注 */ + notes?: string + } + + /** 审核支付命令 */ + interface VerifyPaymentCommand { + /** 审核通过 */ + approved: boolean + /** 审核备注 */ + notes?: string + } + + /** 取消账单命令 */ + interface CancelBillingCommand { + /** 取消原因 */ + reason: string + } + + /** 批量更新状态命令 */ + interface BatchUpdateStatusCommand { + /** 账单 ID 列表 */ + billingIds: string[] + /** 新状态 */ + newStatus: TenantBillingStatus + /** 备注 */ + notes?: string + } + + // ==================== 响应类型 ==================== + + /** 账单列表响应 */ + type BillingListResponse = Common.PageResult + + /** 支付记录列表响应 */ + type PaymentRecordListResponse = PaymentRecordDto[] + + // ==================== 兼容别名(适配现有页面/历史命名) ==================== + + /** 账单列表项(兼容旧命名) */ + type BillDto = BillingListDto + /** 账单详情(兼容旧命名) */ + type BillDetailDto = BillingDetailDto + /** 账单列表查询参数(兼容旧命名) */ + type BillListParams = BillingListParams + /** 账单列表响应(兼容旧命名) */ + type BillListResponse = BillingListResponse + /** 支付记录(兼容旧命名) */ + type PaymentDto = PaymentRecordDto + /** 创建账单命令(兼容旧命名) */ + type CreateBillCommand = CreateBillingCommand + /** 更新账单状态命令(兼容旧命名) */ + type UpdateBillStatusCommand = UpdateStatusCommand + } +} diff --git a/src/types/api/common.d.ts b/src/types/api/common.d.ts new file mode 100644 index 0000000..d156d96 --- /dev/null +++ b/src/types/api/common.d.ts @@ -0,0 +1,43 @@ +declare namespace Api { + /** 通用类型 */ + namespace Common { + /** 分页参数 */ + interface PaginationParams { + /** 当前页码 */ + current: number + /** 每页条数 */ + size: number + /** 总条数 */ + total: number + } + + /** 分页查询参数 */ + interface PageParams { + Page?: number + PageSize?: number + } + + /** 通用搜索参数 */ + type CommonSearchParams = Pick + + /** 分页响应基础结构 */ + interface PaginatedResponse { + records: T[] + current: number + size: number + total: number + } + + /** 后端标准分页结果 */ + interface PageResult { + items: T[] + page: number + pageSize: number + totalCount: number + totalPages: number + } + + /** 启用状态 */ + type EnableStatus = '1' | '2' + } +} diff --git a/src/types/api/dictionary.d.ts b/src/types/api/dictionary.d.ts new file mode 100644 index 0000000..62d863d --- /dev/null +++ b/src/types/api/dictionary.d.ts @@ -0,0 +1,215 @@ +declare namespace Api { + /** 字典管理类型 */ + namespace Dictionary { + /** 字典作用域 */ + enum DictionaryScope { + System = 1, + Business = 2 + } + + /** 导入冲突处理模式 */ + enum ConflictResolutionMode { + Skip = 1, + Overwrite = 2, + Append = 3 + } + + /** 缓存失效操作类型 */ + enum CacheInvalidationOperation { + Create = 1, + Update = 2, + Delete = 3 + } + + /** 字典分组 */ + interface DictionaryGroupDto { + id: string + tenantId: string + code: string + name: string + scope: DictionaryScope + allowOverride: boolean + isEnabled: boolean + description?: string | null + createdAt: string + updatedAt?: string | null + rowVersion: string + items?: DictionaryItemDto[] + } + + /** 字典项 */ + interface DictionaryItemDto { + id: string + groupId: string + key: string + value: Record + isDefault: boolean + isEnabled: boolean + sortOrder: number + description?: string | null + source: 'system' | 'tenant' + rowVersion: string + } + + /** 覆盖配置 */ + interface OverrideConfigDto { + tenantId: string + systemDictionaryGroupCode: string + overrideEnabled: boolean + hiddenSystemItemIds: string[] + customSortOrder: Record + } + + /** 字典导入错误 */ + interface DictionaryImportError { + rowNumber: number + field: string + message: string + } + + /** 字典导入结果 */ + interface DictionaryImportResultDto { + successCount: number + skipCount: number + errorCount: number + errors: DictionaryImportError[] + duration: string + } + + /** 缓存失效日志 */ + interface CacheInvalidationLogDto { + id: string + tenantId: string + timestamp: string + dictionaryCode: string + scope: DictionaryScope + affectedCacheKeyCount: number + operatorId: string + operation: CacheInvalidationOperation + } + + /** 字典分组查询参数 */ + interface DictionaryGroupQueryParams extends Api.Common.PageParams { + scope?: DictionaryScope + keyword?: string + isEnabled?: boolean + sortBy?: string + sortOrder?: 'asc' | 'desc' + includeItems?: boolean + } + + /** 创建字典分组 */ + interface CreateDictionaryGroupRequest { + code: string + name: string + scope: DictionaryScope + allowOverride: boolean + description?: string | null + } + + /** 更新字典分组 */ + interface UpdateDictionaryGroupRequest { + name: string + description?: string | null + allowOverride: boolean + isEnabled: boolean + rowVersion: string + } + + /** 创建字典项 */ + interface CreateDictionaryItemRequest { + key: string + value: Record + isDefault?: boolean + isEnabled?: boolean + sortOrder?: number + description?: string | null + } + + /** 更新字典项 */ + interface UpdateDictionaryItemRequest { + key: string + value: Record + isDefault?: boolean + isEnabled?: boolean + sortOrder?: number + description?: string | null + rowVersion: string + } + + /** 导出请求 */ + interface DictionaryExportRequest { + format?: 'csv' | 'json' + } + + /** 覆盖隐藏项请求 */ + interface DictionaryOverrideHiddenItemsRequest { + hiddenItemIds: string[] + } + + /** 覆盖排序请求 */ + interface DictionaryOverrideSortOrderRequest { + sortOrder: Record + } + + /** 批量查询请求 */ + interface DictionaryBatchQueryRequest { + codes: string[] + } + + // ==================== 标签覆盖相关类型 ==================== + + /** 覆盖类型枚举 */ + enum OverrideType { + /** 租户定制(租户覆盖系统字典) */ + TenantCustomization = 1, + /** 平台强制(平台覆盖租户字典) */ + PlatformEnforcement = 2 + } + + /** 标签覆盖 DTO */ + interface LabelOverrideDto { + /** 覆盖记录 ID */ + id: string + /** 租户 ID */ + tenantId: string + /** 被覆盖的字典项 ID */ + dictionaryItemId: string + /** 字典项 Key */ + dictionaryItemKey: string + /** 原始显示值(多语言) */ + originalValue: Record + /** 覆盖后的显示值(多语言) */ + overrideValue: Record + /** 覆盖类型 */ + overrideType: OverrideType + /** 覆盖类型名称 */ + overrideTypeName: string + /** 覆盖原因/备注 */ + reason?: string | null + /** 创建时间 */ + createdAt: string + /** 更新时间 */ + updatedAt?: string | null + /** 创建人 ID */ + createdBy?: string | null + /** 更新人 ID */ + updatedBy?: string | null + } + + /** 创建/更新标签覆盖请求 */ + interface UpsertLabelOverrideRequest { + /** 被覆盖的字典项 ID */ + dictionaryItemId: string + /** 覆盖后的显示值(多语言) */ + overrideValue: Record + /** 覆盖原因/备注(平台强制覆盖时建议填写) */ + reason?: string | null + } + + /** 批量标签覆盖请求 */ + interface BatchLabelOverrideRequest { + items: UpsertLabelOverrideRequest[] + } + } +} diff --git a/src/types/api/files.d.ts b/src/types/api/files.d.ts new file mode 100644 index 0000000..f7a10ea --- /dev/null +++ b/src/types/api/files.d.ts @@ -0,0 +1,23 @@ +declare namespace Api { + /** 文件相关类型 */ + namespace Files { + /** 上传类型(后端 UploadFileType 枚举字符串) */ + type UploadFileType = + | 'dish_image' + | 'merchant_logo' + | 'user_avatar' + | 'review_image' + | 'business_license' + | 'other' + + /** 上传文件结果 */ + interface FileUploadResponse { + /** 上传后的可访问地址 */ + url?: string | null + /** 文件名 */ + fileName?: string | null + /** 文件大小(字节) */ + fileSize: number + } + } +} diff --git a/src/types/api/merchant.d.ts b/src/types/api/merchant.d.ts new file mode 100644 index 0000000..2829d60 --- /dev/null +++ b/src/types/api/merchant.d.ts @@ -0,0 +1,187 @@ +declare namespace Api { + /** 商户管理类型 */ + namespace Merchant { + /** 商户状态枚举 */ + enum MerchantStatus { + Pending = 0, + Approved = 1, + Rejected = 2, + Frozen = 3 + } + + /** 经营模式枚举 */ + enum OperatingMode { + SameEntity = 1, + DifferentEntity = 2 + } + + /** 门店状态枚举 */ + enum StoreStatus { + Closed = 0, + Preparing = 1, + Operating = 2, + Suspended = 3 + } + + /** 审核动作枚举 */ + enum ReviewAction { + ApplicationSubmitted = 0, + DocumentUploaded = 1, + DocumentReviewed = 2, + ContractUpdated = 3, + ContractStatusChanged = 4, + MerchantReviewed = 5, + ReviewClaimed = 6, + ReviewReleased = 7, + ReviewApproved = 8, + ReviewRejected = 9, + ReviewRevoked = 10, + ReviewPendingReApproval = 11, + ReviewForceClaimed = 12 + } + + /** 商户列表项 */ + interface MerchantListItem { + id: string // long -> string + tenantId: string // long -> string + tenantName?: string + name: string + operatingMode?: OperatingMode + licenseNumber?: string + status: MerchantStatus + isFrozen: boolean + storeCount?: number + createdAt: string + updatedAt?: string + } + + /** 商户审核列表项 */ + interface MerchantReviewListItem { + id: string // long -> string + tenantId: string // long -> string + tenantName?: string + name: string + operatingMode?: OperatingMode + licenseNumber?: string + status: MerchantStatus + claimedByName?: string + claimedAt?: string | null + claimExpiresAt?: string | null + createdAt: string + } + + /** 商户列表查询参数 */ + interface MerchantListParams extends Common.PageParams { + keyword?: string + status?: MerchantStatus + operatingMode?: OperatingMode + tenantId?: string // long -> string + sortBy?: string + sortOrder?: 'asc' | 'desc' + } + + /** 商户列表响应 */ + type MerchantListResponse = Common.PageResult + + /** 商户审核列表响应 */ + type MerchantReviewListResponse = Common.PageResult + + /** 门店列表项 */ + interface StoreListItem { + id: string // long -> string + name: string + licenseNumber?: string | null + contactPhone?: string + address?: string + status: StoreStatus + } + + /** 商户详情 */ + interface MerchantDetail { + id: string // long -> string + tenantId: string // long -> string + tenantName?: string + name: string + operatingMode?: OperatingMode + licenseNumber?: string + legalRepresentative?: string + registeredAddress?: string + contactPhone?: string + contactEmail?: string + status: MerchantStatus + isFrozen: boolean + frozenReason?: string | null + frozenAt?: string | null + approvedBy?: string | null // long -> string + approvedAt?: string | null + stores?: StoreListItem[] + rowVersion?: string + createdAt?: string + createdBy?: string | null // long -> string + updatedAt?: string | null + updatedBy?: string | null // long -> string + } + + /** 更新商户请求 */ + interface UpdateMerchantRequest { + name?: string + licenseNumber?: string + legalRepresentative?: string + registeredAddress?: string + contactPhone?: string + contactEmail?: string + rowVersion: string + } + + /** 更新商户响应 */ + interface UpdateMerchantResult { + merchant: MerchantDetail + requiresReview: boolean + } + + /** 审核领取信息 */ + interface ClaimInfo { + merchantId: string // long -> string + claimedBy?: string // long -> string + claimedByName?: string + claimedAt?: string | null + claimExpiresAt?: string | null + } + + /** 审核记录 */ + interface MerchantAuditRecord { + id: string // long -> string + merchantId?: string // long -> string + action: ReviewAction + operatorId?: string | null // long -> string + operatorName?: string | null + ipAddress?: string | null + title?: string + description?: string | null + createdAt: string + } + + /** 变更记录 */ + interface MerchantChangeLogRecord { + id: string // long -> string + fieldName: string + oldValue?: string | null + newValue?: string | null + changedBy?: string | null // long -> string + changedByName?: string | null + changedAt: string + changeReason?: string | null + } + + /** 审核请求 */ + interface ReviewMerchantRequest { + approve: boolean + remarks?: string + } + + /** 撤销审核请求 */ + interface RevokeMerchantRequest { + reason: string + } + } +} diff --git a/src/types/api/permission.d.ts b/src/types/api/permission.d.ts new file mode 100644 index 0000000..061ba02 --- /dev/null +++ b/src/types/api/permission.d.ts @@ -0,0 +1,25 @@ +declare namespace Api { + namespace Permission { + /** 权限 DTO */ + interface PermissionDto { + id: string + parentId?: string | null + tenantId: string + name: string | null + code: string | null + type?: 'group' | 'leaf' | string + description: string | null + children?: PermissionDto[] + } + + /** 查询参数 */ + interface PermissionQueryParams extends Api.Common.PageParams { + Keyword?: string + SortBy?: string + SortDescending?: boolean + } + + /** 分页响应 */ + type PermissionPageResult = Api.Common.PageResult + } +} diff --git a/src/types/api/role-template.d.ts b/src/types/api/role-template.d.ts new file mode 100644 index 0000000..d0c70e8 --- /dev/null +++ b/src/types/api/role-template.d.ts @@ -0,0 +1,53 @@ +declare namespace Api { + /** 角色模板类型 */ + namespace RoleTemplate { + /** 权限模板 DTO */ + interface PermissionTemplateDto { + code: string + name: string + description?: string + } + + /** 角色模板 DTO */ + interface RoleTemplateDto { + templateCode: string + name: string + description?: string + isActive: boolean + permissions: PermissionTemplateDto[] + } + + /** 创建角色模板参数 */ + interface CreateRoleTemplateCommand { + templateCode: string + name: string + description?: string + isActive: boolean + permissionCodes?: string[] + } + + /** 更新角色模板参数 */ + interface UpdateRoleTemplateCommand { + templateCode?: string + name?: string + description?: string + isActive?: boolean + permissionCodes?: string[] + } + + /** 克隆角色模板参数 */ + interface CloneRoleTemplateCommand { + sourceTemplateCode?: string + newTemplateCode?: string + name?: string + description?: string + isActive?: boolean + permissionCodes?: string[] + } + + /** 初始化角色模板参数 */ + interface InitializeRoleTemplatesCommand { + templateCodes?: string[] + } + } +} diff --git a/src/types/api/statistics.d.ts b/src/types/api/statistics.d.ts new file mode 100644 index 0000000..28e5c52 --- /dev/null +++ b/src/types/api/statistics.d.ts @@ -0,0 +1,144 @@ +/** + * 统计相关类型定义 + */ +declare namespace Api.Statistics { + /** + * 订阅概览统计 + */ + interface SubscriptionOverview { + /** 总订阅数 */ + totalSubscriptions: number + /** 活跃订阅数 */ + activeSubscriptions: number + /** 7天内到期 */ + expiringIn7Days: number + /** 3天内到期 */ + expiringIn3Days: number + /** 明天到期 */ + expiringIn1Day: number + /** 已过期 */ + expiredSubscriptions: number + /** 待激活 */ + pendingSubscriptions: number + /** 已暂停 */ + suspendedSubscriptions: number + } + + /** + * 配额使用排行项 + */ + interface QuotaUsageRankingItem { + /** 租户ID */ + tenantId: string + /** 租户名称 */ + tenantName: string + /** 配额类型 */ + quotaType: number + /** 配额名称 */ + quotaName: string + /** 当前使用量 */ + currentUsage: number + /** 配额上限 */ + quotaLimit: number + /** 使用率(百分比) */ + usagePercentage: number + } + + /** + * 配额使用排行请求参数 + */ + interface QuotaUsageRankingParams { + /** 配额类型(可选) */ + quotaType?: number + /** 返回数量限制 */ + limit?: number + } + + /** + * 配额使用排行响应 + */ + interface QuotaUsageRankingResponse { + /** 排行列表 */ + items: QuotaUsageRankingItem[] + } + + /** + * 月度收入统计项 + */ + interface MonthlyRevenueItem { + /** 年份 */ + year: number + /** 月份 */ + month: number + /** 月度收入 */ + revenue: number + } + + /** + * 收入统计请求参数 + */ + interface RevenueStatisticsParams { + /** 年份(可选,默认当前年) */ + year?: number + } + + /** + * 收入统计响应 + */ + interface RevenueStatisticsResponse { + /** 总收入 */ + totalRevenue: number + /** 本月收入 */ + currentMonthRevenue: number + /** 本季度收入 */ + currentQuarterRevenue: number + /** 月度收入列表(最近6个月) */ + monthlyRevenues: MonthlyRevenueItem[] + } + + /** + * 即将到期订阅项 + */ + interface ExpiringSubscriptionItem { + /** 订阅ID */ + subscriptionId: string + /** 租户ID */ + tenantId: string + /** 租户名称 */ + tenantName: string + /** 套餐名称 */ + packageName: string + /** 到期日期 */ + expireDate: string + /** 距离到期天数 */ + daysUntilExpiry: number + /** 订阅状态 */ + status: number + } + + /** + * 即将到期订阅请求参数 + */ + interface ExpiringSubscriptionsParams { + /** 天数范围(默认7天) */ + days?: number + /** 页码 */ + page?: number + /** 每页数量 */ + pageSize?: number + } + + /** + * 即将到期订阅响应 + */ + interface ExpiringSubscriptionsResponse { + /** 订阅列表 */ + items: ExpiringSubscriptionItem[] + /** 总数 */ + totalCount: number + /** 当前页 */ + page: number + /** 每页数量 */ + pageSize: number + } +} diff --git a/src/types/api/store.d.ts b/src/types/api/store.d.ts new file mode 100644 index 0000000..0bfcb8a --- /dev/null +++ b/src/types/api/store.d.ts @@ -0,0 +1,609 @@ +declare namespace Api { + /** 门店管理类型 */ + namespace Store { + /** 门店状态枚举 */ + enum StoreStatus { + Closed = 0, + Preparing = 1, + Operating = 2, + Suspended = 3 + } + + /** 门店审核状态枚举 */ + enum StoreAuditStatus { + Draft = 0, + Pending = 1, + Activated = 2, + Rejected = 3 + } + + /** 门店经营状态枚举 */ + enum StoreBusinessStatus { + Open = 0, + Resting = 1, + ForceClosed = 2 + } + + /** 门店主体类型枚举 */ + enum StoreOwnershipType { + SameEntity = 0, + DifferentEntity = 1 + } + + /** 门店歇业原因枚举 */ + enum StoreClosureReason { + OutOfBusinessHours = 0, + EquipmentMaintenance = 1, + OwnerVacation = 2, + OutOfStock = 3, + TemporarilyClosed = 4, + LicenseExpired = 5, + Other = 99 + } + + /** 打包费模式枚举 */ + enum PackagingFeeMode { + Fixed = 0, + PerItem = 1 + } + + /** 门店资质类型枚举 */ + enum StoreQualificationType { + BusinessLicense = 0, + FoodServiceLicense = 1, + StorefrontPhoto = 2, + InteriorPhoto = 3 + } + + /** 门店审核动作枚举 */ + enum StoreAuditAction { + Submit = 0, + Resubmit = 1, + Approve = 2, + Reject = 3, + ForceClose = 4, + Reopen = 5, + AutoActivate = 6 + } + + /** 营业时段类型枚举 */ + enum BusinessHourType { + Normal = 0, + ReservationOnly = 1, + PickupOrDelivery = 2, + Closed = 3 + } + + /** 临时时段覆盖类型枚举 */ + enum OverrideType { + Closed = 0, + TemporaryOpen = 1, + ModifiedHours = 2 + } + + /** 门店信息 */ + interface StoreDto { + id: string + tenantId: string + merchantId: string + code: string + name: string + phone?: string | null + managerName?: string | null + status: StoreStatus + signboardImageUrl?: string | null + ownershipType: StoreOwnershipType + auditStatus: StoreAuditStatus + businessStatus: StoreBusinessStatus + closureReason?: StoreClosureReason | null + closureReasonText?: string | null + categoryId?: string | null + rejectionReason?: string | null + submittedAt?: string | null + activatedAt?: string | null + forceClosedAt?: string | null + forceCloseReason?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + announcement?: string | null + tags?: string | null + deliveryRadiusKm: number + supportsDineIn: boolean + supportsPickup: boolean + supportsDelivery: boolean + supportsReservation: boolean + supportsQueueing: boolean + createdAt: string + } + + /** 门店列表查询参数 */ + interface StoreListParams extends Api.Common.PageParams { + merchantId?: string + status?: StoreStatus + auditStatus?: StoreAuditStatus + businessStatus?: StoreBusinessStatus + ownershipType?: StoreOwnershipType + keyword?: string + sortBy?: string + sortDesc?: boolean + } + + /** 门店列表响应 */ + type StoreListResponse = Api.Common.PageResult + + /** 创建门店请求 */ + interface CreateStoreRequest { + merchantId: string + code: string + name: string + phone?: string | null + managerName?: string | null + status?: StoreStatus + signboardImageUrl?: string | null + ownershipType: StoreOwnershipType + categoryId?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + announcement?: string | null + tags?: string | null + deliveryRadiusKm: number + supportsDineIn?: boolean + supportsPickup?: boolean + supportsDelivery?: boolean + supportsReservation?: boolean + supportsQueueing?: boolean + } + + /** 更新门店请求 */ + interface UpdateStoreRequest { + merchantId: string + code: string + name: string + phone?: string | null + managerName?: string | null + status?: StoreStatus + signboardImageUrl?: string | null + categoryId?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + announcement?: string | null + tags?: string | null + deliveryRadiusKm: number + supportsDineIn?: boolean + supportsPickup?: boolean + supportsDelivery?: boolean + supportsReservation?: boolean + supportsQueueing?: boolean + } + + /** 切换经营状态请求 */ + interface ToggleBusinessStatusRequest { + businessStatus: StoreBusinessStatus + closureReason?: StoreClosureReason | null + closureReasonText?: string | null + } + + /** 门店资质 */ + interface StoreQualificationDto { + id: string + storeId: string + qualificationType: StoreQualificationType + fileUrl: string + documentNumber?: string | null + issuedAt?: string | null + expiresAt?: string | null + isExpired: boolean + isExpiringSoon: boolean + daysUntilExpiry?: number | null + sortOrder: number + createdAt: string + updatedAt?: string | null + } + + /** 资质完整性项 */ + interface StoreQualificationRequirementDto { + qualificationType: StoreQualificationType + isRequired: boolean + isUploaded: boolean + isValid: boolean + uploadedCount: number + } + + /** 资质完整性检查结果 */ + interface StoreQualificationCheckResultDto { + isComplete: boolean + canSubmitAudit: boolean + requiredTypes: StoreQualificationRequirementDto[] + expiringSoonCount: number + expiredCount: number + missingTypes: string[] + warnings: string[] + } + + /** 创建资质请求 */ + interface CreateStoreQualificationRequest { + qualificationType: StoreQualificationType + fileUrl: string + documentNumber?: string | null + issuedAt?: string | null + expiresAt?: string | null + sortOrder?: number + } + + /** 更新资质请求 */ + interface UpdateStoreQualificationRequest { + fileUrl?: string | null + documentNumber?: string | null + issuedAt?: string | null + expiresAt?: string | null + sortOrder?: number | null + } + + /** 营业时段 */ + interface StoreBusinessHourDto { + id: string + tenantId: string + storeId: string + dayOfWeek: number + hourType: BusinessHourType + startTime: string + endTime: string + capacityLimit?: number | null + notes?: string | null + createdAt: string + } + + /** 批量更新营业时段输入项 */ + interface StoreBusinessHourInputDto { + dayOfWeek: number + hourType: BusinessHourType + startTime: string + endTime: string + capacityLimit?: number | null + notes?: string | null + } + + /** 批量更新营业时段请求 */ + interface BatchUpdateBusinessHoursRequest { + items: StoreBusinessHourInputDto[] + } + + /** 门店临时时段 */ + interface StoreHolidayDto { + id: string + tenantId: string + storeId: string + date: string + endDate?: string + isAllDay: boolean + startTime?: string + endTime?: string + overrideType: OverrideType + isClosed: boolean + reason?: string + createdAt: string + } + + /** 创建临时时段请求 */ + interface CreateStoreHolidayRequest { + date: string + endDate?: string + isAllDay: boolean + startTime?: string + endTime?: string + overrideType: OverrideType + reason?: string + } + + /** 更新临时时段请求 */ + type UpdateStoreHolidayRequest = CreateStoreHolidayRequest + + /** 配送区域 */ + interface StoreDeliveryZoneDto { + id: string + tenantId: string + storeId: string + zoneName: string + polygonGeoJson: string + minimumOrderAmount?: number | null + deliveryFee?: number | null + estimatedMinutes?: number | null + sortOrder: number + createdAt: string + } + + /** 创建配送区域请求 */ + interface CreateStoreDeliveryZoneRequest { + zoneName: string + polygonGeoJson: string + minimumOrderAmount?: number | null + deliveryFee?: number | null + estimatedMinutes?: number | null + sortOrder?: number + } + + /** 更新配送区域请求 */ + interface UpdateStoreDeliveryZoneRequest { + zoneName: string + polygonGeoJson: string + minimumOrderAmount?: number | null + deliveryFee?: number | null + estimatedMinutes?: number | null + sortOrder?: number + } + + /** 配送范围检测请求 */ + interface StoreDeliveryCheckRequest { + longitude: number + latitude: number + } + + /** 配送范围检测结果 */ + interface StoreDeliveryCheckResultDto { + inRange: boolean + distance?: number | null + deliveryZoneId?: string | null + deliveryZoneName?: string | null + } + + /** 门店费用配置 */ + interface StoreFeeDto { + id: string + storeId: string + minimumOrderAmount: number + deliveryFee: number + packagingFeeMode: PackagingFeeMode + fixedPackagingFee: number + freeDeliveryThreshold?: number | null + createdAt: string + updatedAt?: string | null + } + + /** 更新费用请求 */ + interface UpdateStoreFeeRequest { + minimumOrderAmount: number + deliveryFee: number + packagingFeeMode: PackagingFeeMode + fixedPackagingFee?: number | null + freeDeliveryThreshold?: number | null + } + + /** 费用计算请求 */ + interface CalculateStoreFeeRequest { + orderAmount: number + itemCount?: number | null + items?: StoreFeeCalculationItemDto[] + } + + /** 费用计算商品项 */ + interface StoreFeeCalculationItemDto { + skuId: string + quantity: number + packagingFee: number + } + + /** 打包费拆分明细 */ + interface StoreFeeCalculationBreakdownDto { + skuId: string + quantity: number + unitFee: number + subtotal: number + } + + /** 费用计算结果 */ + interface StoreFeeCalculationResultDto { + orderAmount: number + minimumOrderAmount: number + meetsMinimum: boolean + shortfall?: number | null + deliveryFee: number + packagingFee: number + packagingFeeMode: PackagingFeeMode + packagingFeeBreakdown?: StoreFeeCalculationBreakdownDto[] | null + totalFee: number + totalAmount: number + message?: string | null + } + + /** 资质预警查询参数 */ + interface StoreQualificationAlertQueryParams extends Api.Common.PageParams { + daysThreshold?: number + tenantId?: string + expired?: boolean + } + + /** 资质预警项 */ + interface StoreQualificationAlertDto { + qualificationId: string + storeId: string + storeName: string + storeCode: string + tenantId: string + tenantName: string + qualificationType: StoreQualificationType + expiresAt?: string | null + daysUntilExpiry?: number | null + isExpired: boolean + storeBusinessStatus: StoreBusinessStatus + } + + /** 资质预警汇总 */ + interface StoreQualificationAlertSummaryDto { + expiringSoonCount: number + expiredCount: number + } + + /** 资质预警分页结果 */ + interface StoreQualificationAlertResultDto + extends Api.Common.PageResult { + summary: StoreQualificationAlertSummaryDto + } + } + + /** 门店审核管理类型 */ + namespace StoreAudit { + /** 待审核门店 */ + interface PendingStoreAuditDto { + storeId: string + storeName: string + storeCode: string + tenantId: string + tenantName: string + merchantId: string + merchantName: string + signboardImageUrl?: string | null + fullAddress: string + ownershipType: Api.Store.StoreOwnershipType + submittedAt?: string | null + waitingDays: number + isOverdue: boolean + qualificationCount: number + } + + /** 门店审核详情 - 门店信息 */ + interface StoreAuditStoreDto { + id: string + name: string + code: string + phone?: string | null + signboardImageUrl?: string | null + province?: string | null + city?: string | null + district?: string | null + address?: string | null + longitude?: number | null + latitude?: number | null + ownershipType: Api.Store.StoreOwnershipType + auditStatus: Api.Store.StoreAuditStatus + submittedAt?: string | null + } + + /** 门店审核详情 - 租户信息 */ + interface StoreAuditTenantDto { + id: string + name: string + contactName?: string | null + contactPhone?: string | null + } + + /** 门店审核详情 - 商户信息 */ + interface StoreAuditMerchantDto { + id: string + name: string + legalName?: string | null + creditCode?: string | null + } + + /** 审核记录 */ + interface StoreAuditRecordDto { + id: string + action: Api.Store.StoreAuditAction + actionName: string + operatorId?: string | null + operatorName: string + previousStatus?: Api.Store.StoreAuditStatus | null + newStatus: Api.Store.StoreAuditStatus + rejectionReasonId?: string | null + rejectionReasonText?: string | null + remark?: string | null + createdAt: string + } + + /** 门店审核详情 */ + interface StoreAuditDetailDto { + store: StoreAuditStoreDto + tenant: StoreAuditTenantDto + merchant: StoreAuditMerchantDto + qualifications: Api.Store.StoreQualificationDto[] + auditHistory: StoreAuditRecordDto[] + } + + /** 审核统计趋势项 */ + interface StoreAuditDailyTrendDto { + date: string + submitted: number + approved: number + rejected: number + } + + /** 审核统计 */ + interface StoreAuditStatisticsDto { + pendingCount: number + overdueCount: number + approvedCount: number + rejectedCount: number + avgProcessingHours: number + dailyTrend: StoreAuditDailyTrendDto[] + } + + /** 审核/风控操作结果 */ + interface StoreAuditActionResultDto { + storeId: string + auditStatus: Api.Store.StoreAuditStatus + businessStatus: Api.Store.StoreBusinessStatus + rejectionReason?: string | null + message?: string | null + } + + /** 待审核列表查询参数 */ + interface ListPendingStoreAuditsParams extends Api.Common.PageParams { + tenantId?: string + keyword?: string + submittedFrom?: string + submittedTo?: string + overdueOnly?: boolean + sortBy?: string + sortDesc?: boolean + } + + /** 待审核门店分页响应 */ + type PendingStoreAuditResponse = Api.Common.PageResult + + /** 审核记录查询参数 */ + interface ListStoreAuditRecordsParams { + page?: number + pageSize?: number + } + + /** 审核统计查询参数 */ + interface StoreAuditStatisticsParams { + dateFrom?: string + dateTo?: string + } + + /** 审核通过请求 */ + interface ApproveStoreAuditRequest { + remark?: string | null + } + + /** 审核驳回请求 */ + interface RejectStoreAuditRequest { + rejectionReasonId: number + rejectionReasonText?: string | null + remark?: string | null + } + + /** 强制关闭请求 */ + interface ForceCloseStoreRequest { + reason: string + remark?: string | null + } + + /** 解除关闭请求 */ + interface ReopenStoreRequest { + remark?: string | null + } + } +} diff --git a/src/types/api/subscription.d.ts b/src/types/api/subscription.d.ts new file mode 100644 index 0000000..7f857ab --- /dev/null +++ b/src/types/api/subscription.d.ts @@ -0,0 +1,194 @@ +/** + * 订阅管理相关类型定义 + */ +declare namespace Api { + namespace Subscription { + /** 订阅状态枚举 */ + enum SubscriptionStatus { + /** 待激活 */ + Pending = 0, + /** 生效中 */ + Active = 1, + /** 宽限期 */ + GracePeriod = 2, + /** 已取消 */ + Cancelled = 3, + /** 已暂停 */ + Suspended = 4 + } + + /** 订阅列表查询参数 */ + interface SubscriptionListParams extends Common.PageParams { + /** 订阅状态 */ + Status?: SubscriptionStatus + /** 套餐 ID */ + TenantPackageId?: string + /** 租户 ID */ + TenantId?: string + /** 租户关键词(名称或编码模糊匹配) */ + TenantKeyword?: string + /** 到期时间筛选:未来 N 天内到期 */ + ExpiringWithinDays?: number + /** 是否自动续费筛选 */ + AutoRenew?: boolean + /** 到期时间范围开始 */ + ExpireFrom?: string + /** 到期时间范围结束 */ + ExpireTo?: string + } + + /** 订阅列表响应 */ + type SubscriptionListResponse = Common.PageResult + + /** 订阅列表项 DTO */ + interface SubscriptionListDto { + /** 订阅 ID */ + id: string + /** 租户 ID */ + tenantId: string + /** 租户名称 */ + tenantName: string + /** 租户编码 */ + tenantCode: string + /** 当前套餐 ID */ + tenantPackageId: string + /** 当前套餐名称 */ + packageName: string + /** 当前套餐名称(别名,兼容旧代码) */ + tenantPackageName?: string + /** 排期套餐 ID(下周期生效) */ + scheduledPackageId?: string + /** 排期套餐名称 */ + scheduledPackageName?: string + /** 订阅状态 */ + status: SubscriptionStatus + /** 生效时间(UTC) */ + effectiveFrom: string + /** 到期时间(UTC) */ + effectiveTo: string + /** 下次计费时间 */ + nextBillingDate?: string + /** 是否自动续费 */ + autoRenew: boolean + /** 备注信息 */ + notes?: string + /** 创建时间 */ + createdAt: string + /** 更新时间 */ + updatedAt?: string + } + + /** 简化订阅 DTO(用于操作后返回) */ + type SubscriptionDto = SubscriptionListDto + + /** 订阅详情 DTO */ + interface SubscriptionDetailDto extends SubscriptionListDto { + /** 当前套餐信息 */ + package?: TenantPackageDto + /** 排期套餐信息 */ + scheduledPackage?: TenantPackageDto + /** 配额使用情况列表 */ + quotaUsages: QuotaUsageDto[] + /** 配额使用情况列表(别名,兼容旧代码) */ + quotaUsage?: QuotaUsageDto[] + /** 订阅变更历史列表 */ + changeHistory: SubscriptionHistoryDto[] + } + + /** 租户套餐 DTO */ + interface TenantPackageDto { + id: string + name: string + description?: string + packageType: number + monthlyPrice?: number + yearlyPrice?: number + maxStoreCount?: number + maxAccountCount?: number + maxStorageGb?: number + maxSmsCredits?: number + maxDeliveryOrders?: number + featurePoliciesJson?: string + isActive: boolean + isPublicVisible: boolean + isAllowNewTenantPurchase: boolean + publishStatus: number + isRecommended: boolean + tags: string[] + sortOrder: number + } + + /** 配额使用情况 DTO */ + interface QuotaUsageDto { + quotaType: number + quotaName: string + limit?: number + used: number + remaining?: number + usagePercentage?: number + } + + /** 订阅历史 DTO */ + interface SubscriptionHistoryDto { + id: string + subscriptionId: string + changeType: string + previousPackageId?: string + previousPackageName?: string + newPackageId?: string + newPackageName?: string + previousEffectiveTo?: string + newEffectiveTo?: string + notes?: string + /** 描述(兼容旧代码) */ + description?: string + createdAt: string + /** 时间戳(兼容旧代码,等同于 createdAt) */ + timestamp?: string + createdBy?: string + } + + /** 更新订阅命令 */ + interface UpdateSubscriptionCommand { + subscriptionId: string + autoRenew?: boolean + notes?: string + } + + /** 延期订阅命令 */ + interface ExtendSubscriptionCommand { + subscriptionId: string + durationMonths: number + notes?: string + } + + /** 变更套餐命令 */ + interface ChangePlanCommand { + subscriptionId: string + targetPackageId: string + immediate: boolean + notes?: string + } + + /** 更新订阅状态命令 */ + interface UpdateStatusCommand { + subscriptionId: string + status: SubscriptionStatus + notes?: string + } + + /** 批量操作结果 */ + interface BatchOperationResult { + successCount: number + failedCount: number + results: BatchOperationItem[] + } + + /** 批量操作项结果 */ + interface BatchOperationItem { + subscriptionId: string + success: boolean + message?: string + } + } +} diff --git a/src/types/api/system-manage.d.ts b/src/types/api/system-manage.d.ts new file mode 100644 index 0000000..cde880b --- /dev/null +++ b/src/types/api/system-manage.d.ts @@ -0,0 +1,112 @@ +declare namespace Api { + /** 系统管理类型 */ + namespace SystemManage { + /** 用户状态 */ + type IdentityUserStatus = 1 | 2 | 3 + /** 批量用户操作类型 */ + type IdentityUserBatchOperation = 1 | 2 | 3 | 4 | 5 + /** 用户列表项 */ + interface UserListItem { + userId: string + tenantId: string + account: string + displayName: string + avatar?: string + phone?: string + email?: string + status: IdentityUserStatus + isLocked: boolean + isDeleted: boolean + roles: string[] + createdAt: string + lastLoginAt?: string + } + /** 用户详情 */ + interface UserDetailDto { + userId: string + tenantId: string + merchantId?: string + account: string + displayName: string + phone?: string + email?: string + status: IdentityUserStatus + isLocked: boolean + roles: string[] + roleIds: string[] + permissions: string[] + createdAt: string + lastLoginAt?: string + avatar?: string + rowVersion: string + } + /** 用户列表响应 */ + type UserListResponse = Api.Common.PageResult + /** 用户搜索参数 */ + interface UserSearchParams extends Api.Common.PageParams { + TenantId?: string + Keyword?: string + Status?: IdentityUserStatus + RoleId?: string + CreatedAtFrom?: string + CreatedAtTo?: string + LastLoginFrom?: string + LastLoginTo?: string + IncludeDeleted?: boolean + SortBy?: string + SortDescending?: boolean + } + /** 创建用户命令 */ + interface CreateIdentityUserCommand { + tenantId?: string + account: string + displayName: string + password: string + phone?: string + email?: string + avatar?: string + roleIds?: string[] + status?: IdentityUserStatus + } + /** 更新用户命令 */ + interface UpdateIdentityUserCommand { + userId?: string + tenantId?: string + displayName: string + phone?: string + email?: string + avatar?: string + roleIds?: string[] + rowVersion: string + } + /** 更新用户状态命令 */ + interface ChangeIdentityUserStatusCommand { + userId?: string + tenantId?: string + status: IdentityUserStatus + } + /** 重置密码结果 */ + interface ResetIdentityUserPasswordResult { + token: string + expiresAt: string + } + /** 批量操作失败项 */ + interface BatchIdentityUserFailureItem { + userId: string + reason: string + } + /** 批量用户操作命令 */ + interface BatchIdentityUserOperationCommand { + tenantId?: string + operation: IdentityUserBatchOperation + userIds: string[] + } + /** 批量用户操作结果 */ + interface BatchIdentityUserOperationResult { + successCount: number + failureCount: number + failures: BatchIdentityUserFailureItem[] + exportItems: UserListItem[] + } + } +} diff --git a/src/types/api/tenant-role.d.ts b/src/types/api/tenant-role.d.ts new file mode 100644 index 0000000..3e6538c --- /dev/null +++ b/src/types/api/tenant-role.d.ts @@ -0,0 +1,44 @@ +declare namespace Api { + /** 租户角色类型 */ + namespace TenantRole { + /** 角色 DTO */ + interface RoleDto { + id: string // long -> string + tenantId: string // long -> string + name: string + code: string + description?: string + } + + /** 角色分页结果 */ + interface RoleDtoPagedResult { + items: RoleDto[] + totalCount: number // Swagger usually returns totalCount or total? Need to check PagedResult schema + } + + /** 角色分页查询参数 */ + interface RoleQueryParams { + TenantId?: string + Keyword?: string + Page?: number + PageSize?: number + SortBy?: string + SortDescending?: boolean + } + + /** 创建角色参数 */ + interface CreateRoleCommand { + name: string + code: string + description?: string + copyFromTemplateId?: string + } + + /** 更新角色参数 */ + interface UpdateRoleCommand { + roleId: string // long -> string + name?: string + description?: string + } + } +} diff --git a/src/types/api/tenant.d.ts b/src/types/api/tenant.d.ts new file mode 100644 index 0000000..da0d8e6 --- /dev/null +++ b/src/types/api/tenant.d.ts @@ -0,0 +1,412 @@ +declare namespace Api { + /** 租户管理类型 */ + namespace Tenant { + /** 租户状态枚举 */ + enum TenantStatus { + PendingReview = 0, + Active = 1, + Suspended = 2, + Expired = 3, + Closed = 4 + } + + /** 租户认证状态枚举 */ + enum TenantVerificationStatus { + Draft = 0, + Pending = 1, + Approved = 2, + Rejected = 3 + } + + /** 经营模式枚举 */ + enum OperatingMode { + SameEntity = 1, + DifferentEntity = 2 + } + + /** 租户信息 */ + interface TenantDto { + id: string // long -> string + code: string + name: string + shortName: string + industry?: string + contactName: string + contactPhone: string + contactEmail: string + status: TenantStatus + verificationStatus: TenantVerificationStatus + operatingMode?: OperatingMode + currentPackageId?: string // long -> string + effectiveFrom?: string + effectiveTo?: string + autoRenew: boolean + } + + /** 租户列表查询参数 */ + interface TenantListParams extends Common.PageParams { + Status?: TenantStatus + VerificationStatus?: TenantVerificationStatus + Name?: string + ContactName?: string + ContactPhone?: string + Keyword?: string + } + + /** 租户列表响应 */ + type TenantListResponse = Common.PageResult + + /** 注册租户命令 */ + interface RegisterTenantCommand { + code: string + name: string + shortName?: string + industry?: string + contactName?: string + contactPhone?: string + contactEmail?: string + tenantPackageId: string // long -> string + durationMonths: number + autoRenew: boolean + effectiveFrom?: string + } + + /** 后台手动新增租户命令(直接入驻) */ + interface CreateTenantManuallyCommand { + // 1. 租户信息(public.tenants) + code: string + name: string + shortName?: string + legalEntityName?: string + industry?: string + logoUrl?: string + coverImageUrl?: string + website?: string + country?: string + province?: string + city?: string + address?: string + contactName?: string + contactPhone?: string + contactEmail?: string + tags?: string + remarks?: string + suspendedAt?: string | Date + suspensionReason?: string + tenantStatus: TenantStatus + + // 2. 订阅信息(public.tenant_subscriptions) + tenantPackageId: string // long -> string + durationMonths: number + autoRenew: boolean + subscriptionEffectiveFrom?: string | Date + nextBillingDate?: string | Date + subscriptionStatus?: number + scheduledPackageId?: string // long -> string + subscriptionNotes?: string + + // 3. 认证信息(public.tenant_verification_profiles) + verificationStatus?: TenantVerificationStatus + businessLicenseNumber?: string + businessLicenseUrl?: string + legalPersonName?: string + legalPersonIdNumber?: string + legalPersonIdFrontUrl?: string + legalPersonIdBackUrl?: string + bankAccountName?: string + bankAccountNumber?: string + bankName?: string + additionalDataJson?: string + submittedAt?: string | Date + reviewedAt?: string | Date + reviewedByName?: string + reviewRemarks?: string + + // 4. 管理员账号(identity.identity_users) + adminAccount: string + adminDisplayName: string + adminPassword: string + adminAvatar?: string + adminMerchantId?: string // long -> string + } + + /** 自助注册租户命令 */ + interface SelfRegisterTenantCommand { + adminAccount: string + adminDisplayName?: string + adminEmail?: string + adminPhone: string + adminPassword: string + } + + /** 自助注册结果 */ + interface SelfRegisterResult { + tenantId: string // long -> string + code?: string + status: TenantStatus + verificationStatus: TenantVerificationStatus + effectiveFrom?: string + effectiveTo?: string + adminAccount?: string + } + + /** 入驻进度信息 */ + interface TenantProgress { + tenantId: string // long -> string + code?: string + status: TenantStatus + verificationStatus: TenantVerificationStatus + effectiveFrom?: string + effectiveTo?: string + } + + /** 自助实名提交命令 */ + interface SubmitTenantVerificationCommand { + tenantId: string // long -> string + businessLicenseNumber?: string + businessLicenseUrl?: string + legalPersonName?: string + legalPersonIdNumber?: string + legalPersonIdFrontUrl?: string + legalPersonIdBackUrl?: string + bankAccountName?: string + bankAccountNumber?: string + bankName?: string + additionalDataJson?: string + } + + /** 租户套餐类型枚举 */ + enum TenantPackageType { + Free = 0, + Standard = 1, + Professional = 2, + Enterprise = 3 + } + + /** 套餐发布状态 */ + enum TenantPackagePublishStatus { + Draft = 0, + Published = 1 + } + + /** 租户套餐信息 */ + interface TenantPackageDto { + id: string // long -> string + name: string + description?: string + packageType: TenantPackageType + monthlyPrice?: number + yearlyPrice?: number + maxStoreCount?: number + maxAccountCount?: number + maxStorageGb?: number + maxSmsCredits?: number + maxDeliveryOrders?: number + featurePoliciesJson?: string + isActive: boolean + isPublicVisible: boolean + isAllowNewTenantPurchase: boolean + publishStatus: TenantPackagePublishStatus + isRecommended: boolean + tags: string[] + sortOrder: number + } + + /** 租户套餐列表查询参数 */ + interface TenantPackageQueryParams extends Common.PageParams { + Keyword?: string + IsActive?: boolean + } + + /** 租户套餐列表响应 */ + type TenantPackageListResponse = Common.PageResult + + /** 公共租户套餐列表响应(匿名) */ + interface PublicTenantPackageListResponse { + pageIndex: number + pageSize: number + totalCount: number + items: TenantPackageDto[] + } + + /** 创建租户套餐参数 */ + interface CreateTenantPackageCommand { + name: string + description?: string + packageType: TenantPackageType + monthlyPrice?: number + yearlyPrice?: number + maxStoreCount?: number + maxAccountCount?: number + maxStorageGb?: number + maxSmsCredits?: number + maxDeliveryOrders?: number + featurePoliciesJson?: string + isActive: boolean + isPublicVisible: boolean + isAllowNewTenantPurchase: boolean + publishStatus: TenantPackagePublishStatus + isRecommended: boolean + tags: string[] + sortOrder: number + } + + /** 更新租户套餐参数 */ + interface UpdateTenantPackageCommand extends CreateTenantPackageCommand { + tenantPackageId: string // long -> string + } + + /** 套餐使用统计 */ + interface TenantPackageUsageDto { + tenantPackageId: string // long -> string + activeSubscriptionCount: number + activeTenantCount: number + totalSubscriptionCount: number + mrr: number + arr: number + expiringTenantCount7Days: number + expiringTenantCount15Days: number + expiringTenantCount30Days: number + } + + /** 套餐当前使用租户 */ + interface TenantPackageTenantDto { + tenantId: string // long -> string + code: string + name: string + status: TenantStatus + contactName?: string + contactPhone?: string + subscriptionEffectiveFrom: string + subscriptionEffectiveTo: string + } + + /** 订阅状态枚举 */ + enum SubscriptionStatus { + Active = 0, + Expired = 1, + Cancelled = 2, + Suspended = 3 + } + + /** 租户订阅信息 */ + interface TenantSubscriptionDto { + id: string // long -> string + tenantId: string // long -> string + tenantPackageId: string // long -> string + status: SubscriptionStatus + effectiveFrom: string + effectiveTo: string + nextBillingDate?: string + autoRenew: boolean + } + + /** 创建订阅命令 */ + interface CreateTenantSubscriptionCommand { + tenantId: string // long -> string + tenantPackageId: string // long -> string + durationMonths?: number + autoRenew?: boolean + notes?: string + } + + /** 延期/赠送订阅命令(按当前订阅续费) */ + interface ExtendTenantSubscriptionCommand { + tenantId: string // long -> string + durationMonths: number + notes?: string + } + + /** 初次绑定订阅命令(自助入驻) */ + interface BindInitialTenantSubscriptionCommand { + tenantId: string // long -> string + tenantPackageId: string // long -> string + autoRenew?: boolean + } + + /** 订阅升降配命令 */ + interface ChangeTenantSubscriptionPlanCommand { + tenantId: string // long -> string + tenantSubscriptionId: string // long -> string + targetPackageId: string // long -> string + immediate?: boolean + notes?: string + } + + /** 租户认证信息 */ + interface TenantVerificationDto { + id: string // long -> string + tenantId: string // long -> string + status: TenantVerificationStatus + businessLicenseNumber?: string + businessLicenseUrl?: string + legalPersonName?: string + legalPersonIdNumber?: string + legalPersonIdFrontUrl?: string + legalPersonIdBackUrl?: string + bankAccountName?: string + bankAccountNumber?: string + bankName?: string + additionalDataJson?: string + submittedAt?: string + reviewedBy?: string // long -> string + reviewRemarks?: string + reviewedByName?: string + reviewedAt?: string + } + + /** 租户详情信息 */ + interface TenantDetailDto { + tenant: TenantDto + verification: TenantVerificationDto + subscription: TenantSubscriptionDto + package: TenantPackageDto + } + + /** 租户审核日志 */ + interface TenantAuditLogDto { + id: string // long -> string + tenantId: string // long -> string + action: number + title: string + description?: string + operatorName?: string + previousStatus?: TenantStatus + currentStatus?: TenantStatus + createdAt: string + } + + /** 租户审核日志分页 */ + type TenantAuditLogListResponse = Common.PageResult + + /** 审核租户命令 */ + interface ReviewTenantCommand { + tenantId: string // long -> string + approve: boolean + reason?: string + renewMonths?: number + operatingMode?: OperatingMode + } + + /** 冻结租户命令 */ + interface FreezeTenantCommand { + tenantId: string // long -> string + reason: string + } + + /** 解冻租户命令 */ + interface UnfreezeTenantCommand { + tenantId: string // long -> string + reason?: string + } + + /** 租户审核领取信息 */ + interface TenantReviewClaimDto { + id: string // long -> string + tenantId: string // long -> string + claimedBy: string // long -> string + claimedByName: string + claimedAt: string + } + } +} diff --git a/src/types/common/index.ts b/src/types/common/index.ts new file mode 100644 index 0000000..7e751d1 --- /dev/null +++ b/src/types/common/index.ts @@ -0,0 +1,95 @@ +/** + * 通用类型定义模块 + * + * 提供项目中常用的通用类型定义 + * + * ## 主要功能 + * + * - 状态类型(启用/禁用) + * - 性别类型 + * - 排序方向类型 + * - 操作类型(增删改查) + * - 记录类型(键值对) + * - 时间范围类型 + * - 文件信息类型 + * - 坐标和尺寸类型 + * - 响应式断点类型 + * - 主题和语言类型 + * - 环境和弹窗类型 + * + * ## 使用场景 + * + * - 通用数据结构定义 + * - 类型约束和提示 + * - 减少重复类型定义 + * + * @module types/common/index + * @author Art Design Pro Team + */ + +// 导出响应类型 +export * from './response' + +// 状态类型 +export type Status = 0 | 1 // 0: 禁用, 1: 启用 + +// 性别类型 +export type Gender = 'male' | 'female' | 'unknown' + +// 排序方向 +export type SortOrder = 'ascending' | 'descending' + +// 操作类型 +export type ActionType = 'create' | 'update' | 'delete' | 'view' + +// 可选的记录类型 +export type Recordable = Record + +// 键值对类型 +export type KeyValue = { + key: string + value: T + label?: string +} + +// 时间范围类型 +export interface TimeRange { + startTime: string + endTime: string +} + +// 文件类型 +export interface FileInfo { + name: string + url: string + size: number + type: string + lastModified?: number +} + +// 坐标类型 +export interface Position { + x: number + y: number +} + +// 尺寸类型 +export interface Size { + width: number + height: number +} + +// 响应式断点类型 +export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +// 主题类型 +export type ThemeMode = 'light' | 'dark' | 'auto' + +// 语言类型 +export type Language = 'zh-CN' | 'en-US' + +// 环境类型 +export type Environment = 'development' | 'production' | 'test' + +// 弹窗类型 +export type DialogType = 'add' | 'edit' diff --git a/src/types/common/response.ts b/src/types/common/response.ts new file mode 100644 index 0000000..11dd274 --- /dev/null +++ b/src/types/common/response.ts @@ -0,0 +1,54 @@ +/** + * API 响应类型定义模块 + * + * 提供统一的 API 响应结构类型定义 + * + * ## 主要功能 + * + * - 基础响应结构定义 + * - 泛型支持(适配不同数据类型) + * - 统一的响应格式约束 + * + * ## 使用场景 + * + * - API 请求响应类型约束 + * - 接口数据类型定义 + * - 响应数据解析 + * + * @module types/common/response + * @author Art Design Pro Team + */ + +/** 基础 API 响应结构 */ +export interface BaseResponse { + /** 是否成功 */ + success?: boolean + /** 状态码 */ + code?: number + /** 提示消息 */ + message?: string + /** 兼容旧字段 */ + msg?: string + /** 数据 */ + data?: T + /** 错误详情(如字段错误) */ + errors?: unknown + /** TraceId,便于链路追踪 */ + traceId?: string + /** 时间戳 */ + timestamp?: string +} + +/** 后端标准分页结果 */ +export interface PagedResult { + /** 数据列表 */ + items: T[] + /** 当前页码(从 1 开始) */ + page: number + /** 每页条数 */ + pageSize: number + /** 总条数 */ + totalCount: number + /** 总页数 */ + totalPages: number +} diff --git a/src/types/component/chart.ts b/src/types/component/chart.ts new file mode 100644 index 0000000..c3225c9 --- /dev/null +++ b/src/types/component/chart.ts @@ -0,0 +1,324 @@ +/** + * 图表组件类型定义模块 + * + * 提供 ECharts 图表组件的完整类型定义 + * + * ## 主要功能 + * + * - 基础图表配置类型 + * - 柱状图类型定义 + * - 折线图类型定义 + * - 饼图/环形图类型定义 + * - 雷达图类型定义 + * - K线图类型定义 + * - 散点图类型定义 + * - 地图图表类型定义 + * - 双向堆叠柱状图类型定义 + * - 图表主题配置类型 + * - 图表事件回调类型 + * + * ## 使用场景 + * + * - 图表组件 Props 类型约束 + * - 图表配置类型定义 + * - 图表数据结构定义 + * - 图表事件处理 + * + * @module types/component/chart + * @author Art Design Pro Team + */ +import type { EChartsOption } from '@/plugins/echarts' + +// 图例位置类型 +export type LegendPosition = 'bottom' | 'top' | 'left' | 'right' + +export type SymbolType = + | 'circle' + | 'rect' + | 'roundRect' + | 'triangle' + | 'diamond' + | 'pin' + | 'arrow' + | 'none' + +// 图表主题配置 +export interface ChartThemeConfig { + /** 图表高度 */ + chartHeight: string + /** 字体大小 */ + fontSize: number + /** 字体颜色 */ + fontColor: string + /** 主题颜色 */ + themeColor: string + /** 颜色组 */ + colors: string[] +} + +// 图表初始化选项 +export interface UseChartOptions { + /** 初始化选项 */ + initOptions?: EChartsOption + /** 延迟初始化时间(ms) */ + initDelay?: number + /** IntersectionObserver阈值 */ + threshold?: number + /** 是否自动响应主题变化 */ + autoTheme?: boolean +} + +// 基础图表 Props 接口 - 统一所有图表的基础属性 +export interface BaseChartProps { + /** 图表高度 */ + height?: string + /** 是否加载中 */ + loading?: boolean + isEmpty?: boolean + /** 颜色配置 */ + colors?: string[] +} + +// 轴线显示控制接口 - 统一轴线相关配置 +export interface AxisDisplayProps { + /** 是否显示坐标轴标签 */ + showAxisLabel?: boolean + /** 是否显示坐标轴线 */ + showAxisLine?: boolean + /** 是否显示分割线 */ + showSplitLine?: boolean +} + +// 交互显示控制接口 - 统一交互相关配置 +export interface InteractionProps { + /** 是否显示提示框 */ + showTooltip?: boolean + /** 是否显示图例 */ + showLegend?: boolean + /** 图例位置 */ + legendPosition?: LegendPosition +} + +// 柱状图数据项接口 +export interface BarDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + data: number[] + /** 柱状图宽度 */ + barWidth?: string | number + /** 堆叠分组名称 */ + stack?: string +} + +// 柱状图 Props 接口 - 统一柱状图配置 +export interface BarChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 - 支持单组数据或多组数据 */ + data: number[] | BarDataItem[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 柱状图宽度 */ + barWidth?: string | number + /** 是否堆叠显示 */ + stack?: boolean + /** 圆角 */ + borderRadius?: number | number[] +} + +// 折线图数据项接口 +export interface LineDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + data: number[] + /** 线条宽度 */ + lineWidth?: number + /** 是否显示区域填充 */ + showAreaColor?: boolean + /** 区域样式配置 */ + areaStyle?: { + /** 渐变开始透明度 */ + startOpacity?: number + /** 渐变结束透明度 */ + endOpacity?: number + /** 自定义 ECharts areaStyle 配置 */ + custom?: any + } + /** 是否平滑曲线 */ + smooth?: boolean + /** 数据点符号 */ + symbol?: SymbolType + /** 数据点大小 */ + symbolSize?: number +} + +// 折线图 Props 接口 - 统一折线图配置 +export interface LineChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 - 支持单组数据或多组数据 */ + data: number[] | LineDataItem[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 线条宽度 */ + lineWidth?: number + /** 是否显示区域填充 */ + showAreaColor?: boolean + /** 是否平滑曲线 */ + smooth?: boolean + /** 数据点符号 */ + symbol?: SymbolType + /** 数据点大小 */ + symbolSize?: number + /** 多数据动画延迟间隔(毫秒) */ + animationDelay?: number +} + +// 雷达图数据项接口 +export interface RadarDataItem { + /** 系列名称 */ + name: string + /** 数据值 */ + value: number[] +} + +// 雷达图 Props 接口 - 统一雷达图配置 +export interface RadarChartProps extends BaseChartProps, InteractionProps { + /** 雷达图指标配置 */ + indicator?: Array<{ name: string; max: number }> + /** 图表数据 */ + data?: RadarDataItem[] +} + +// 饼图/环形图数据项接口 +export interface PieDataItem { + /** 数据值 */ + value: number + /** 数据名称 */ + name: string +} + +// 环形图 Props 接口 - 统一环形图配置 +export interface RingChartProps extends BaseChartProps, InteractionProps { + /** 图表数据 */ + data: PieDataItem[] + /** 内外半径 */ + radius?: string[] + /** 边框圆角 */ + borderRadius?: number + /** 中心文本 */ + centerText?: string + /** 是否显示标签 */ + showLabel?: boolean +} + +// K线图数据项接口 +export interface KLineDataItem { + /** 时间标签 */ + time: string + /** 开盘价 */ + open: number + /** 收盘价 */ + close: number + /** 最高价 */ + high: number + /** 最低价 */ + low: number +} + +// K线图 Props 接口 - 统一K线图配置 +export interface KLineChartProps extends BaseChartProps { + /** 图表数据 */ + data?: KLineDataItem[] + /** 是否显示数据缩放控件 */ + showDataZoom?: boolean + /** 数据缩放初始开始位置 */ + dataZoomStart?: number + /** 数据缩放初始结束位置 */ + dataZoomEnd?: number +} + +// 散点图数据项接口 +export interface ScatterDataItem { + /** 坐标值 [x, y] */ + value: number[] +} + +// 散点图 Props 接口 - 统一散点图配置 +export interface ScatterChartProps extends BaseChartProps, AxisDisplayProps, InteractionProps { + /** 图表数据 */ + data?: ScatterDataItem[] + /** 散点大小 */ + symbolSize?: number +} + +// 双柱对比图 Props 接口 - 统一双柱对比图配置 +export interface DualBarCompareChartProps extends BaseChartProps { + /** 上方数据 */ + topData: number[] + /** 下方数据 */ + bottomData: number[] + /** X轴标签数据 */ + xAxisData: string[] + /** 上方柱子颜色 */ + topColor?: string + /** 下方柱子颜色 */ + bottomColor?: string + /** 柱状图宽度 */ + barWidth?: number +} + +// 地图图表 Props 接口 - 统一地图图表配置 +export interface MapChartProps extends BaseChartProps { + /** 地图数据 */ + mapData?: any[] + /** 选中区域 */ + selectedRegion?: string + /** 是否显示标签 */ + showLabels?: boolean + /** 是否显示散点 */ + showScatter?: boolean +} + +// 双向堆叠柱状图 Props 接口(人口金字塔样式) +export interface BidirectionalBarChartProps + extends BaseChartProps, + AxisDisplayProps, + InteractionProps { + /** 正向数据(向上显示) */ + positiveData: number[] + /** 负向数据(向下显示) */ + negativeData: number[] + /** X轴标签数据 */ + xAxisData?: string[] + /** 正向数据名称 */ + positiveName?: string + /** 负向数据名称 */ + negativeName?: string + /** 柱状图宽度 */ + barWidth?: string | number + /** Y轴最小值 */ + yAxisMin?: number + /** Y轴最大值 */ + yAxisMax?: number + /** 是否显示数据标签 */ + showDataLabel?: boolean + /** 正向数据圆角配置 */ + positiveBorderRadius?: number | number[] + /** 负向数据圆角配置 */ + negativeBorderRadius?: number | number[] +} + +// 图表配置生成器函数类型 +export type ChartOptionGenerator = () => EChartsOption + +// 图表事件回调类型 +export type ChartEventCallback = (params: any) => void + +// 图表错误信息接口 +export interface ChartError { + /** 错误码 */ + code: string + /** 错误信息 */ + message: string + /** 错误详情 */ + details?: any +} diff --git a/src/types/component/index.ts b/src/types/component/index.ts new file mode 100644 index 0000000..cd89bce --- /dev/null +++ b/src/types/component/index.ts @@ -0,0 +1,145 @@ +/** + * 组件类型定义模块 + * + * 提供项目组件的类型定义 + * + * ## 主要功能 + * + * - 搜索组件类型定义 + * - 表格列配置类型 + * - 分页配置类型 + * - 表单规则类型 + * - 对话框配置类型 + * + * ## 使用场景 + * + * - 组件 Props 类型约束 + * - 组件配置类型定义 + * - 组件事件参数类型 + * + * @module types/component/index + * @author Art Design Pro Team + */ + +// 搜索组件类型 +export type SearchComponentType = + | 'input' + | 'select' + | 'radio' + | 'checkbox' + | 'date' + | 'datetime' + | 'daterange' + | 'datetimerange' + | 'month' + | 'monthrange' + | 'year' + | 'yearrange' + | 'week' + | 'time' + | 'timerange' + +// 搜索框值变化参数 +export interface SearchChangeParams { + prop: string + val: unknown +} + +// 表格列配置接口 +export interface ColumnOption { + // 列类型 + type?: 'selection' | 'expand' | 'index' | 'globalIndex' + // 列属性名 + prop?: string + // 列标题 + label?: string + // 列宽度 + width?: string | number + // 最小列宽度 + minWidth?: string | number + // 固定列 + fixed?: boolean | 'left' | 'right' + // 是否可排序 + sortable?: boolean + // 过滤器选项 + filters?: any[] + // 过滤方法 + filterMethod?: (value: any, row: any) => boolean + // 过滤器位置 + filterPlacement?: string + // 是否禁用 + disabled?: boolean + // 是否显示列 + visible?: boolean + // 是否选中显示 + checked?: boolean + // 自定义渲染函数 + formatter?: (row: T) => any + // 插槽相关配置 + // 是否使用插槽渲染内容 + useSlot?: boolean + // 插槽名称(默认为 prop 值) + slotName?: string + // 是否使用表头插槽 + useHeaderSlot?: boolean + // 表头插槽名称(默认为 `${prop}-header`) + headerSlotName?: string + // 其他属性 + [key: string]: any +} + +// 分页配置 +export interface PaginationConfig { + // 当前页 + currentPage: number + // 每页条数 + pageSize: number + // 总条数 + total: number + // 每页显示个数选择器的选项 + pageSizes?: number[] + // 组件布局 + layout?: string + // 是否为小型分页 + small?: boolean +} + +// 表单规则 +export interface FormRule { + // 是否必填 + required?: boolean + // 错误提示信息 + message?: string + // 触发方式 + trigger?: string | string[] + // 最小长度 + min?: number + // 最大长度 + max?: number + // 正则表达式 + pattern?: RegExp + // 自定义验证函数 + validator?: (rule: any, value: any, callback: any) => void +} + +// 对话框配置 +export interface DialogConfig { + // 标题 + title: string + // 是否显示 + visible: boolean + // 宽度 + width?: string | number + // 是否可以通过点击 modal 关闭 + closeOnClickModal?: boolean + // 是否可以通过按下 ESC 关闭 + closeOnPressEscape?: boolean + // 是否显示关闭按钮 + showClose?: boolean + // 是否在 Dialog 出现时将 body 滚动锁定 + lockScroll?: boolean + // 是否显示遮罩层 + modal?: boolean + // 自定义类名 + customClass?: string +} diff --git a/src/types/config/index.ts b/src/types/config/index.ts new file mode 100644 index 0000000..dd144de --- /dev/null +++ b/src/types/config/index.ts @@ -0,0 +1,211 @@ +/** + * 配置类型定义模块 + * + * 提供系统配置相关的类型定义 + * + * ## 主要功能 + * + * - 主题设置类型 + * - 菜单布局类型 + * - 节日配置类型 + * - 系统基础配置类型 + * - 快速入口配置类型 + * - 顶部栏功能配置类型 + * - 环境配置类型 + * - 应用配置类型 + * + * ## 使用场景 + * + * - 系统配置文件类型约束 + * - 配置项类型定义 + * - 配置数据验证 + * + * @module types/config/index + * @author Art Design Pro Team + */ + +import { MenuTypeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { MenuThemeType, SystemThemeTypes } from '@/types/store' + +// 主题设置 +export interface ThemeSetting { + /** 主题名称 */ + name: string + /** 系统主题类型 */ + theme: SystemThemeEnum + /** 主题颜色数组 */ + color: string[] + /** 左侧线条颜色 */ + leftLineColor: string + /** 右侧线条颜色 */ + rightLineColor: string + /** 主题图片 */ + img: string +} + +// 菜单布局 +export interface MenuLayout { + /** 布局名称 */ + name: string + /** 菜单类型值 */ + value: MenuTypeEnum + /** 布局预览图 */ + img: string + /** 布局描述 */ + description?: string +} + +// 节日配置 +export interface FestivalConfig { + /** 节日日期(单日)或开始日期(日期范围) */ + date: string + /** 节日结束日期(可选,用于跨日期节日) */ + endDate?: string + /** 节日名称 */ + name: string + /** 烟花图片 */ + image: string + /** 滚动文本 */ + scrollText: string + /** 是否激活 */ + isActive?: boolean + /** 烟花播放次数(可选,默认为 3 次) */ + count?: number +} + +// 系统基础配置 +export interface SystemBasicConfig { + // 系统名称 + name: string + // 系统描述 + description?: string + // 系统logo + logo?: string + // 系统favicon + favicon?: string + // 版权信息 + copyright?: string +} + +// 快速入口基础项 +export interface FastEnterBaseItem { + /** 名称 */ + name: string + /** 是否启用 */ + enabled?: boolean + /** 排序权重 */ + order?: number + /** 路由名称 */ + routeName?: string + /** 外部链接 */ + link?: string +} + +// 快速入口应用项 +export interface FastEnterApplication extends FastEnterBaseItem { + /** 应用描述 */ + description: string + /** 图标代码 */ + icon: string + /** 图标颜色 */ + iconColor: string +} + +// 快速链接项 +export type FastEnterQuickLink = FastEnterBaseItem + +// 快速入口配置 +export interface FastEnterConfig { + /** 应用列表 */ + applications: FastEnterApplication[] + /** 快速链接 */ + quickLinks: FastEnterQuickLink[] + /** 显示条件(屏幕宽度) */ + minWidth?: number +} + +// 系统配置 +export interface SystemConfig { + // 系统基础信息 + systemInfo: SystemBasicConfig + // 系统主题样式 + systemThemeStyles: SystemThemeTypes + // 设置主题列表 + settingThemeList: ThemeSetting[] + // 菜单布局列表 + menuLayoutList: MenuLayout[] + // 主题列表 + themeList: MenuThemeType[] + // 暗色菜单样式 + darkMenuStyles: MenuThemeType[] + // 系统主色调 + systemMainColor: readonly string[] + // 快速入口配置 + fastEnter?: FastEnterConfig + // 顶部栏功能配置 + headerBar?: HeaderBarFeatureConfig +} + +// 环境配置 +export interface EnvConfig { + // 环境名称 + NODE_ENV: string + // 应用版本 + VITE_VERSION: string + // 应用端口 + VITE_PORT: string + // 应用基础路径 + VITE_BASE_URL: string + // API 地址 + VITE_API_URL: string + // 是否开启 Mock + VITE_USE_MOCK?: string + // 是否开启压缩 + VITE_USE_GZIP?: string + // 是否开启 CDN + VITE_USE_CDN?: string +} + +// 应用配置 +export interface AppConfig extends SystemConfig { + // 环境配置 + env: EnvConfig + // 开发模式 + isDev: boolean + // 生产模式 + isProd: boolean + // 测试模式 + isTest: boolean +} + +// 功能配置项基础接口 +export interface FeatureConfigItem { + enabled: boolean + description: string +} + +// 顶部栏功能配置接口 +export interface HeaderBarFeatureConfig { + /** 菜单按钮 */ + menuButton: FeatureConfigItem + /** 刷新按钮 */ + refreshButton: FeatureConfigItem + /** 快速入口 */ + fastEnter: FeatureConfigItem + /** 面包屑导航 */ + breadcrumb: FeatureConfigItem + /** 全局搜索 */ + globalSearch: FeatureConfigItem + /** 全屏功能 */ + fullscreen: FeatureConfigItem + /** 通知功能 */ + notification: FeatureConfigItem + /** 聊天功能 */ + chat: FeatureConfigItem + /** 多语言切换 */ + language: FeatureConfigItem + /** 设置面板 */ + settings: FeatureConfigItem + /** 主题切换 */ + themeToggle: FeatureConfigItem +} diff --git a/src/types/dictionary.ts b/src/types/dictionary.ts new file mode 100644 index 0000000..da7c507 --- /dev/null +++ b/src/types/dictionary.ts @@ -0,0 +1,25 @@ +export const ValidationConstraints = { + codeMinLength: 2, + codeMaxLength: 64, + nameMaxLength: 128, + keyMaxLength: 128, + descriptionMaxLength: 512 +} + +export function extractI18nText( + value: Record | undefined, + locale?: string +): string { + if (!value) return '' + + const normalizedLocale = locale?.trim() + if (normalizedLocale && value[normalizedLocale]) { + return value[normalizedLocale] + } + + if (value['zh-CN']) return value['zh-CN'] + if (value.en) return value.en + + const first = Object.values(value)[0] + return first ?? '' +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..9032fd2 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,22 @@ +/** + * 类型定义统一导出模块 + * 提供全局类型定义的统一导出入口 + * + * @module types/index + * @author Art Design Pro Team + */ + +/** 通用类型定义(基础类型、工具类型等) */ +export * from './common' + +/** 组件相关类型定义 */ +export * from './component' + +/** 状态管理相关类型定义 */ +export * from './store' + +/** 路由相关类型定义 */ +export * from './router' + +/** 配置相关类型定义 */ +export * from './config' diff --git a/src/types/json-bigint.d.ts b/src/types/json-bigint.d.ts new file mode 100644 index 0000000..b30735b --- /dev/null +++ b/src/types/json-bigint.d.ts @@ -0,0 +1 @@ +declare module 'json-bigint' diff --git a/src/types/modules/quotaPackage.d.ts b/src/types/modules/quotaPackage.d.ts new file mode 100644 index 0000000..5094d61 --- /dev/null +++ b/src/types/modules/quotaPackage.d.ts @@ -0,0 +1,98 @@ +/** + * 配额包相关类型定义 + */ +declare namespace Api.QuotaPackage { + /** + * 配额包列表项 DTO + */ + interface QuotaPackageListDto { + id: string + name: string + quotaType: number + quotaValue: number + price: number + isActive: boolean + sortOrder: number + description?: string + } + + /** + * 配额包详情 DTO + */ + interface QuotaPackageDto { + id: string + name: string + quotaType: number + quotaValue: number + price: number + isActive: boolean + sortOrder: number + description?: string + createdAt: string + updatedAt?: string + } + + /** + * 创建配额包命令 + */ + interface CreateQuotaPackageCommand { + name: string + quotaType: number + quotaValue: number + price: number + isActive?: boolean + sortOrder?: number + description?: string + } + + /** + * 更新配额包命令 + */ + interface UpdateQuotaPackageCommand { + name: string + quotaType: number + quotaValue: number + price: number + isActive?: boolean + sortOrder?: number + description?: string + } + + /** + * 购买配额包命令 + */ + interface PurchaseQuotaPackageCommand { + quotaPackageId: string + expiredAt?: string + notes?: string + } + + /** + * 租户配额购买记录 DTO + */ + interface TenantQuotaPurchaseDto { + id: string + tenantId: string + quotaPackageId: string + quotaPackageName: string + quotaType: number + quotaValue: number + price: number + purchasedAt: string + expiredAt?: string + notes?: string + } + + /** + * 租户配额使用情况 DTO + */ + interface TenantQuotaUsageDto { + tenantId: string + quotaType: number + limitValue: number + usedValue: number + remainingValue: number + resetCycle?: string + lastResetAt?: string + } +} diff --git a/src/types/router/index.ts b/src/types/router/index.ts new file mode 100644 index 0000000..d9ef012 --- /dev/null +++ b/src/types/router/index.ts @@ -0,0 +1,80 @@ +/** + * 路由类型定义模块 + * + * 提供路由相关的类型定义 + * + * ## 主要功能 + * + * - 路由元数据类型(标题、图标、权限等) + * - 应用路由记录类型 + * - 路由配置扩展 + * + * ## 使用场景 + * + * - 路由配置类型约束 + * - 路由元数据定义 + * - 菜单生成 + * - 权限控制 + * + * @module types/router/index + * @author Art Design Pro Team + */ + +import { RouteRecordRaw } from 'vue-router' + +/** + * 路由元数据接口 + * 定义路由的各种配置属性 + */ +export interface RouteMeta extends Record { + /** 路由标题 */ + title: string + /** 路由图标 */ + icon?: string + /** 是否显示徽章 */ + showBadge?: boolean + /** 文本徽章 */ + showTextBadge?: string + /** 是否在菜单中隐藏 */ + isHide?: boolean + /** 是否在标签页中隐藏 */ + isHideTab?: boolean + /** 外部链接 */ + link?: string + /** 是否为iframe */ + isIframe?: boolean + /** 是否缓存 */ + keepAlive?: boolean + /** 操作权限 */ + authList?: Array<{ + title: string + authMark: string + }> + /** 是否为一级菜单 */ + isFirstLevel?: boolean + /** 角色权限 */ + roles?: string[] + /** 是否固定标签页 */ + fixedTab?: boolean + /** 激活菜单路径 */ + activePath?: string + /** 是否为全屏页面 */ + isFullPage?: boolean + /** 是否为权限按钮行 */ + isAuthButton?: boolean + /** 权限标识 */ + authMark?: string + /** 父级路径 */ + parentPath?: string +} + +/** + * 应用路由记录接口 + * 扩展 Vue Router 的路由记录类型 + */ +export interface AppRouteRecord extends Omit { + id?: number + meta: RouteMeta + children?: AppRouteRecord[] + component?: string | (() => Promise) +} diff --git a/src/types/store/index.ts b/src/types/store/index.ts new file mode 100644 index 0000000..019801e --- /dev/null +++ b/src/types/store/index.ts @@ -0,0 +1,157 @@ +/** + * Store 状态类型定义模块 + * + * 提供 Pinia Store 的状态类型定义 + * + * ## 主要功能 + * + * - 系统主题类型 + * - 菜单主题类型 + * - 设置状态类型 + * - 工作标签页类型 + * - 用户状态类型 + * - 菜单状态类型 + * - 根状态类型 + * + * ## 使用场景 + * + * - Store 状态类型约束 + * - 状态数据结构定义 + * - 类型提示和自动补全 + * + * @module types/store/index + * @author Art Design Pro Team + */ + +import { MenuThemeEnum, SystemThemeEnum } from '@/enums/appEnum' +import { LocationQueryRaw } from 'vue-router' + +// 系统主题样式(light | dark) +export interface SystemThemeType { + /** 主题类名 */ + className: string +} + +// 定义包含多个主题的类型 +export type SystemThemeTypes = { + [key in Exclude]: SystemThemeType +} + +// 菜单主题样式 +export interface MenuThemeType { + /** 主题类型 */ + theme: MenuThemeEnum + /** 背景颜色 */ + background: string + /** 系统名称颜色 */ + systemNameColor: string + /** 文本颜色 */ + textColor: string + /** 图标颜色 */ + iconColor: string + /** 背景图片 */ + img?: string +} + +// 设置中心 +export interface SettingState { + /** 主题 */ + theme: string + /** 是否只保持一个子菜单的展开 */ + uniqueOpened: boolean + /** 是否显示菜单按钮 */ + menuButton: boolean + /** 是否显示刷新按钮 */ + showRefreshButton: boolean + /** 是否显示面包屑 */ + showCrumbs: boolean + /** 是否自动关闭 */ + autoClose: boolean + /** 是否显示工作标签页 */ + showWorkTab: boolean + /** 是否显示语言切换 */ + showLanguage: boolean + /** 是否显示进度条 */ + showNprogress: boolean + /** 主题模式 */ + themeModel: string +} + +// 多标签 +export interface WorkTab { + /** 标签标题 */ + title: string + /** 自定义标题 */ + customTitle?: string + /** 路由路径 */ + path: string + /** 路由名称 */ + name: string + /** 是否缓存 */ + keepAlive: boolean + /** 是否固定标签 */ + fixedTab?: boolean + /** 路由参数 */ + params?: object + /** 路由查询参数 */ + query?: LocationQueryRaw + /** 图标 */ + icon?: string + /** 是否激活 */ + isActive?: boolean +} + +// 用户Store状态 +export interface UserState { + /** 用户信息 */ + userInfo: Api.Auth.UserInfo | null + /** 认证令牌 */ + token: string | null + /** 用户角色列表 */ + roles: string[] + /** 用户权限列表 */ + permissions: string[] +} + +// 设置Store状态 +export interface SettingStoreState extends SettingState { + // 额外的设置状态 + /** 菜单是否折叠 */ + collapsed: boolean + /** 设备类型 */ + device: 'desktop' | 'mobile' + /** 当前语言 */ + language: string +} + +// 工作标签页Store状态 +export interface WorkTabState { + /** 标签页列表 */ + tabs: WorkTab[] + /** 当前激活的标签页 */ + activeTab: string + /** 缓存的标签页列表 */ + cachedTabs: string[] +} + +// 菜单Store状态 +export interface MenuState { + /** 菜单列表 */ + menuList: any[] + /** 菜单是否已加载 */ + isLoaded: boolean + /** 菜单是否折叠 */ + collapsed: boolean +} + +// 根Store状态类型 +export interface RootState { + /** 用户状态 */ + user: UserState + /** 设置状态 */ + setting: SettingStoreState + /** 工作标签页状态 */ + workTab: WorkTabState + /** 菜单状态 */ + menu: MenuState +} diff --git a/src/types/tenant/index.ts b/src/types/tenant/index.ts new file mode 100644 index 0000000..cf18986 --- /dev/null +++ b/src/types/tenant/index.ts @@ -0,0 +1,105 @@ +/** + * 租户详情/配额/订阅/账单相关类型(Milestone 1 预置) + * + * 说明: + * - 遵循零 Any 原则 + * - 所有 Snowflake ID 一律使用 string + * - 已存在的全局类型(Api.*)优先复用,避免重复定义与漂移 + */ + +/** + * Snowflake ID(后端 long -> 前端 string) + */ +export type SnowflakeId = string + +/** + * 租户详情(来自 Swagger: TenantDetailDto) + */ +export type TenantDetailDto = Api.Tenant.TenantDetailDto + +/** + * 更新租户命令(TD-001) + * + * 说明: + * - Swagger 当前未提供 UpdateTenantCommand,按现有页面字段 + 合理推断补齐 + * - Snowflake ID 一律使用 string + * - 待后端补齐 PUT /api/admin/v1/tenants/{tenantId} 后,可按 Swagger 对齐字段 + */ +export interface UpdateTenantCommand { + tenantId: SnowflakeId + + name: string + shortName?: string + industry?: string + contactName?: string + contactPhone?: string + contactEmail?: string + + logoUrl?: string + coverImageUrl?: string + website?: string + + country?: string + province?: string + city?: string + address?: string + + tags?: string + remarks?: string +} + +/** + * 配额汇总(来自 Swagger: TenantQuotaUsageDto) + */ +export type QuotaSummaryDto = Api.QuotaPackage.TenantQuotaUsageDto + +/** + * 配额使用明细(来自 Swagger: QuotaUsageDto) + */ +export interface QuotaUsageDto { + id: SnowflakeId + quotaType: number + limitValue: number + usedValue: number + usagePercentage: number + remainingValue: number + resetCycle?: string | null + lastResetAt?: string | null +} + +/** + * 配额购买记录(来自 Swagger: TenantQuotaPurchaseDto) + */ +export type QuotaPurchaseDto = Api.QuotaPackage.TenantQuotaPurchaseDto + +/** + * 订阅信息(来自 Swagger: TenantSubscriptionDto) + */ +export type SubscriptionDto = Api.Tenant.TenantSubscriptionDto + +/** + * 账单信息(来自 Swagger: TenantBillingDto) + */ +export interface BillingDto { + id: SnowflakeId + tenantId: SnowflakeId + statementNo: string | null + periodStart: string + periodEnd: string + amountDue: number + amountPaid: number + status: Api.Billing.TenantBillingStatus + dueDate: string + lineItemsJson: string | null +} + +/** + * 配额使用历史记录 DTO(TD-002 待后端接口补充) + */ +export interface QuotaUsageHistoryDto { + quotaType: number + usedValue: number + limitValue: number + recordedAt: string // ISO 8601 + changeType: 'increase' | 'decrease' | 'init' | 'snapshot' +} diff --git a/src/utils/announcementStatus.ts b/src/utils/announcementStatus.ts new file mode 100644 index 0000000..f7609b8 --- /dev/null +++ b/src/utils/announcementStatus.ts @@ -0,0 +1,36 @@ +import type { AnnouncementStatus } from '@/types/announcement' + +const statusNumberMap: Record = { + 0: 'Draft', + 1: 'Published', + 2: 'Revoked' +} + +const statusStringMap: Record = { + DRAFT: 'Draft', + PUBLISHED: 'Published', + REVOKED: 'Revoked' +} + +export const normalizeAnnouncementStatus = (status: unknown): AnnouncementStatus | null => { + // 1. 数字状态直接映射 + if (typeof status === 'number') { + return statusNumberMap[status] ?? null + } + + // 2. 字符串状态兼容大小写与数字 + if (typeof status === 'string') { + const normalized = status.trim().toUpperCase() + if (normalized in statusStringMap) { + return statusStringMap[normalized] + } + + const numeric = Number(normalized) + if (Number.isFinite(numeric)) { + return statusNumberMap[numeric] ?? null + } + } + + // 3. 无法识别时返回空 + return null +} diff --git a/src/utils/billing.ts b/src/utils/billing.ts new file mode 100644 index 0000000..5b73853 --- /dev/null +++ b/src/utils/billing.ts @@ -0,0 +1,308 @@ +/** + * 账单管理工具函数 + * + * 提供账单相关的通用工具函数 + * + * @module utils/billing + */ + +import { $t } from '@/locales' +import { + BillingType, + ExportFormat, + TenantPaymentMethod, + TenantPaymentStatus +} from '@/enums/Billing' + +/** + * 格式化账单状态为文本 + * @param status 账单状态 + * @returns 状态文本 + */ +export function formatBillingStatus(status: number): string { + const statusMap: Record = { + 0: $t('billing.status.pending'), + 1: $t('billing.status.paid'), + 2: $t('billing.status.overdue'), + 3: $t('billing.status.cancelled'), + // 兼容:若后端未来扩展状态值(或历史数据脏值) + 4: $t('billing.status.cancelled') + } + return statusMap[status] || '未知' +} + +/** + * 获取账单状态对应的 Element Plus Tag 类型 + * @param status 账单状态 + * @returns Tag 类型 + */ +export function getBillingStatusType(status: number): 'success' | 'warning' | 'danger' | 'info' { + const typeMap: Record = { + 0: 'warning', + 1: 'success', + 2: 'danger', + 3: 'info', + 4: 'info' + } + return typeMap[status] || 'info' +} + +/** + * 获取账单状态颜色 + * @param status 账单状态 + * @returns 颜色值 + */ +export function getBillingStatusColor(status: number): string { + const colorMap: Record = { + 0: 'var(--el-color-warning)', + 1: 'var(--el-color-success)', + 2: 'var(--el-color-danger)', + 3: 'var(--el-color-info)', + 4: 'var(--el-color-info)' + } + return colorMap[status] || 'var(--el-color-info)' +} + +/** + * 格式化账单类型为文本 + * @param type 账单类型 + * @returns 类型文本 + */ +export function formatBillingType(type?: BillingType | number | null): string { + if (type === undefined || type === null) return '-' + const typeMap: Record = { + [BillingType.Subscription]: $t('billing.billingType.subscription'), + [BillingType.QuotaPurchase]: $t('billing.billingType.quotaPurchase'), + [BillingType.Manual]: $t('billing.billingType.manual'), + [BillingType.Renewal]: $t('billing.billingType.renewal') + } + return typeMap[type as number] || '未知' +} + +/** + * 格式化支付方式为文本 + * @param method 支付方式 + * @returns 方式文本 + */ +export function formatPaymentMethod(method: TenantPaymentMethod | number): string { + const methodMap: Record = { + [TenantPaymentMethod.Online]: $t('billing.paymentMethod.online'), + [TenantPaymentMethod.BankTransfer]: $t('billing.paymentMethod.bankTransfer'), + [TenantPaymentMethod.Other]: $t('billing.paymentMethod.other') + } + return methodMap[method as number] || '未知' +} + +/** + * 获取支付方式图标名称 + * @param method 支付方式 + * @returns 图标名称(Element Plus Icon) + */ +export function getPaymentMethodIcon(method: TenantPaymentMethod | number): string { + const iconMap: Record = { + [TenantPaymentMethod.Online]: 'CreditCard', + [TenantPaymentMethod.BankTransfer]: 'Wallet', + [TenantPaymentMethod.Other]: 'Money' + } + return iconMap[method as number] || 'Money' +} + +/** + * 格式化支付状态为文本 + * @param status 支付状态 + * @returns 状态文本 + */ +export function formatPaymentStatus(status: TenantPaymentStatus | number): string { + const statusMap: Record = { + [TenantPaymentStatus.Pending]: $t('billing.paymentStatus.pending'), + [TenantPaymentStatus.Success]: $t('billing.paymentStatus.success'), + [TenantPaymentStatus.Failed]: $t('billing.paymentStatus.failed'), + [TenantPaymentStatus.Refunded]: $t('billing.paymentStatus.refunded') + } + return statusMap[status as number] || '未知' +} + +/** + * 获取支付状态对应的 Tag 类型 + * @param status 支付状态 + * @returns Tag 类型 + */ +export function getPaymentStatusType( + status: TenantPaymentStatus | number +): 'success' | 'warning' | 'danger' | 'info' { + const typeMap: Record = { + [TenantPaymentStatus.Pending]: 'warning' as const, + [TenantPaymentStatus.Success]: 'success' as const, + [TenantPaymentStatus.Failed]: 'danger' as const, + [TenantPaymentStatus.Refunded]: 'info' as const + } + return typeMap[status as number] || 'info' +} + +/** + * 计算逾期天数 + * @param dueDate 到期日期(ISO 8601 字符串) + * @returns 逾期天数(负数表示未到期) + */ +export function calculateDaysOverdue(dueDate: string): number { + const due = new Date(dueDate) + const now = new Date() + const diff = now.getTime() - due.getTime() + return Math.floor(diff / (1000 * 60 * 60 * 24)) +} + +/** + * 判断账单是否即将到期(7天内) + * @param dueDate 到期日期 + * @returns 是否即将到期 + */ +export function isBillingDueSoon(dueDate: string): boolean { + const days = calculateDaysOverdue(dueDate) + return days >= -7 && days < 0 +} + +function pad2(n: number): string { + return String(n).padStart(2, '0') +} + +/** + * 格式化日期时间(YYYY-MM-DD HH:mm) + * @param value ISO 字符串 / Date 可解析值 + */ +export function formatDateTime(value?: string | null): string { + if (!value) return '-' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return '-' + return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}` +} + +/** + * 格式化日期(YYYY-MM-DD) + * @param value ISO 字符串 / Date 可解析值 + */ +export function formatDate(value?: string | null): string { + if (!value) return '-' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return '-' + return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}` +} + +/** + * 格式化金额(带货币符号) + * @param amount 金额 + * @param currency 货币类型(默认 CNY) + * @returns 格式化后的金额 + */ +export function formatAmount(amount?: number | null, currency: string = 'CNY'): string { + const safeAmount = Number.isFinite(amount as number) ? (amount as number) : 0 + const currencySymbol = currency === 'CNY' ? '¥' : currency === 'USD' ? '$' : '' + if (currencySymbol) { + return `${currencySymbol}${safeAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + } + return `${currency} ${safeAmount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` +} + +/** + * 生成导出文件名 + * @param format 导出格式 + * @param dateRange 日期范围(可选) + * @returns 文件名 + */ +export function generateExportFileName( + format: ExportFormat, + dateRange?: { start?: string; end?: string } +): string { + const timestamp = new Date().toISOString().split('T')[0] // YYYY-MM-DD + const formatExt = + format === ExportFormat.Excel ? 'xlsx' : format === ExportFormat.Pdf ? 'pdf' : 'csv' + + const baseName = $t('billing.export.fileName') || 'billing_export' + let fileName = `${baseName}_${timestamp}` + if (dateRange?.start && dateRange?.end) { + fileName = `${baseName}_${dateRange.start}_to_${dateRange.end}` + } + + // Windows 文件名非法字符处理 + const safeName = fileName.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_') + + return `${safeName}.${formatExt}` +} + +/** + * 解析账单明细 JSON + * @param lineItemsJson 明细 JSON 字符串 + * @returns 明细数组 + */ +export function parseLineItems(lineItemsJson?: string): Api.Billing.BillingLineItemDto[] { + if (!lineItemsJson) return [] + try { + return JSON.parse(lineItemsJson) + } catch { + return [] + } +} + +/** + * 计算账单总金额(包含税费和折扣) + * @param billing 账单数据 + * @returns 总金额 + */ +export function calculateTotalAmount( + billing: Partial +): number { + const { amountDue = 0, discountAmount = 0, taxAmount = 0 } = billing + return amountDue - discountAmount + taxAmount +} + +/** + * 计算未付款金额 + * @param billing 账单数据 + * @returns 未付款金额 + */ +export function calculateRemainingAmount( + billing: Partial +): number { + const total = calculateTotalAmount(billing) + const paid = billing.amountPaid || 0 + return Math.max(total - paid, 0) +} + +/** + * 格式化导出格式为文本 + * @param format 导出格式 + * @returns 格式文本 + */ +export function formatExportFormat(format: ExportFormat): string { + const formatMap = { + [ExportFormat.Excel]: 'Excel', + [ExportFormat.Pdf]: 'PDF', + [ExportFormat.Csv]: 'CSV' + } + return formatMap[format] || '未知' +} + +/** + * 验证金额格式 + * @param amount 金额字符串 + * @returns 是否有效 + */ +export function validateAmount(amount: string): boolean { + const regex = /^\d+(\.\d{1,2})?$/ + return regex.test(amount) && parseFloat(amount) > 0 +} + +/** + * 格式化日期范围为查询参数 + * @param dateRange 日期范围(数组格式 [start, end]) + * @returns 查询参数 + */ +export function formatDateRangeParams(dateRange?: [string, string] | null): { + StartDate?: string + EndDate?: string +} { + if (!dateRange || dateRange.length !== 2) return {} + return { + StartDate: dateRange[0], + EndDate: dateRange[1] + } +} diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts new file mode 100644 index 0000000..831be29 --- /dev/null +++ b/src/utils/constants/index.ts @@ -0,0 +1,8 @@ +/** + * 常量定义相关工具函数统一导出 + * + * @module utils/constants/index + * @author Art Design Pro Team + */ + +export * from './links' diff --git a/src/utils/constants/links.ts b/src/utils/constants/links.ts new file mode 100644 index 0000000..06d297e --- /dev/null +++ b/src/utils/constants/links.ts @@ -0,0 +1,35 @@ +/** + * 网站链接常量配置 + * 集中管理便于维护和更新链接地址 + * + * @module utils/constants/links + * @author Art Design Pro Team + */ +export const WEB_LINKS = { + // Github 主页 + GITHUB_HOME: 'https://github.com/Daymychen/art-design-pro', + + // 项目 Github 主页 + GITHUB: 'https://github.com/Daymychen/art-design-pro', + + // 个人博客 + BLOG: 'https://www.artd.pro', + + // 项目文档 + DOCS: 'https://www.artd.pro/docs/zh/', + + // 精简版本 + LiteVersion: 'https://www.artd.pro/docs/zh/guide/lite-version.html', + + // v2.6.1版本 + OldVersion: 'https://www.artd.pro/v2/', + + // 项目社区 + COMMUNITY: 'https://www.artd.pro/docs/zh/community/communicate.html', + + // 个人 Bilibili 主页 + BILIBILI: 'https://space.bilibili.com/425500936?spm_id_from=333.1007.0.0', + + // 项目介绍 + INTRODUCE: 'https://www.artd.pro/docs/zh/guide/introduce.html' +} diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts new file mode 100644 index 0000000..a398ea0 --- /dev/null +++ b/src/utils/errorHandler.ts @@ -0,0 +1,70 @@ +import { ElMessage } from 'element-plus' +import { router } from '@/router' +import { $t } from '@/locales' +import { ApiStatus } from '@/utils/http/status' +import type { HttpError } from '@/utils/http/error' + +const collectValidationMessages = (payload: unknown): string[] => { + if (!payload) return [] + if (typeof payload === 'string') return [payload] + if (Array.isArray(payload)) return payload.map((item) => String(item)) + + if (typeof payload === 'object') { + const messages: string[] = [] + Object.entries(payload as Record).forEach(([field, value]) => { + if (Array.isArray(value)) { + value.forEach((item) => messages.push(`${field}: ${String(item)}`)) + return + } + + if (typeof value === 'string') { + messages.push(`${field}: ${value}`) + return + } + + if (value) { + messages.push(`${field}: ${String(value)}`) + } + }) + + return messages + } + + return [] +} + +const redirectForbidden = () => { + router.replace('/403').catch(() => {}) +} + +export const handleHttpError = (error: HttpError, showMessage: boolean = true) => { + if (error.code === ApiStatus.forbidden) { + redirectForbidden() + if (showMessage) { + ElMessage.error($t('httpMsg.forbidden')) + } + return + } + + if (!showMessage) { + return + } + + if (error.code === ApiStatus.conflict) { + ElMessage.warning($t('httpMsg.conflict')) + return + } + + if (error.code === ApiStatus.error || error.code === ApiStatus.unprocessableEntity) { + const messages = collectValidationMessages(error.data) + ElMessage.error(messages.length > 0 ? messages.join('; ') : error.message) + return + } + + if (error.code >= ApiStatus.internalServerError) { + ElMessage.error($t('httpMsg.internalServerError')) + return + } + + ElMessage.error(error.message) +} diff --git a/src/utils/form/index.ts b/src/utils/form/index.ts new file mode 100644 index 0000000..ed23a46 --- /dev/null +++ b/src/utils/form/index.ts @@ -0,0 +1,12 @@ +/** + * 表单工具函数统一导出 + * + * @module utils/form + * @author Art Design Pro Team + */ + +// 表单验证器 +export * from './validator' + +// 响应式布局 +export * from './responsive' diff --git a/src/utils/form/responsive.ts b/src/utils/form/responsive.ts new file mode 100644 index 0000000..c11df92 --- /dev/null +++ b/src/utils/form/responsive.ts @@ -0,0 +1,122 @@ +/** + * 表单响应式布局工具模块 + * + * 提供表单项在不同屏幕尺寸下的智能布局计算 + * + * ## 主要功能 + * + * - 响应式断点管理(xs/sm/md/lg/xl) + * - 表单列宽自动降级(避免小屏幕压缩) + * - 基于阈值的智能 span 计算 + * - 响应式计算器工厂函数 + * - 可配置的断点规则 + * + * ## 使用场景 + * + * - 表单组件响应式布局 + * - 搜索表单自适应 + * - 移动端表单优化 + * - 多列表单布局 + * + * ## 断点说明(基于 Element Plus Grid 24 栅格系统): + * - xs (手机): < 768px,小于 12 时降级为 24(满宽) + * - sm (平板): ≥ 768px,小于 12 时降级为 12(半宽) + * - md (中等屏幕): ≥ 992px,小于 8 时降级为 8(三分之一宽) + * - lg (大屏幕): ≥ 1200px,直接使用设置的 span + * - xl (超大屏幕): ≥ 1920px,直接使用设置的 span + * + * ## 核心功能 + * + * - calculateResponsiveSpan: 计算响应式列宽 + * - createResponsiveSpanCalculator: 创建 span 计算器(柯里化) + * + * @module utils/form/responsive + * @author Art Design Pro Team + */ + +/** + * 响应式断点类型 + */ +export type ResponsiveBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' + +/** + * 断点配置映射 + */ +interface BreakpointConfig { + /** 最小 span 阈值 */ + threshold: number + /** 降级后的 span 值 */ + fallback: number +} + +/** + * 响应式断点配置 + */ +const BREAKPOINT_CONFIG: Record = { + xs: { threshold: 12, fallback: 24 }, // 手机:小于 12 时使用满宽 + sm: { threshold: 12, fallback: 12 }, // 平板:小于 12 时使用半宽 + md: { threshold: 8, fallback: 8 }, // 中等屏幕:小于 8 时使用三分之一宽 + lg: null, // 大屏幕:直接使用设置的 span + xl: null // 超大屏幕:直接使用设置的 span +} + +/** + * 计算响应式列宽 + * + * 根据屏幕尺寸智能降级,避免小屏幕上表单项被压缩过小 + * + * @param itemSpan 表单项自定义的 span 值 + * @param defaultSpan 默认的 span 值 + * @param breakpoint 当前断点 + * @returns 计算后的 span 值 + * + * @example + * ```ts + * // 在 xs 断点下,span 为 6 会降级为 24(满宽) + * calculateResponsiveSpan(6, 6, 'xs') // 24 + * + * // 在 md 断点下,span 为 6 会降级为 8(三分之一宽) + * calculateResponsiveSpan(6, 6, 'md') // 8 + * + * // 在 lg 断点下,直接使用原始 span + * calculateResponsiveSpan(6, 6, 'lg') // 6 + * ``` + */ +export function calculateResponsiveSpan( + itemSpan: number | undefined, + defaultSpan: number, + breakpoint: ResponsiveBreakpoint +): number { + const finalSpan = itemSpan ?? defaultSpan + const config = BREAKPOINT_CONFIG[breakpoint] + + // 如果没有配置(lg/xl),直接返回原始 span + if (!config) { + return finalSpan + } + + // 如果 span 小于阈值,使用降级值 + return finalSpan >= config.threshold ? finalSpan : config.fallback +} + +/** + * 创建响应式 span 计算器 + * + * 返回一个函数,用于计算指定断点下的 span 值 + * + * @param defaultSpan 默认的 span 值 + * @returns span 计算函数 + * + * @example + * ```ts + * const getColSpan = createResponsiveSpanCalculator(6) + * getColSpan(undefined, 'xs') // 24 + * getColSpan(8, 'md') // 8 + * getColSpan(12, 'lg') // 12 + * ``` + */ +export function createResponsiveSpanCalculator(defaultSpan: number) { + return (itemSpan: number | undefined, breakpoint: ResponsiveBreakpoint): number => { + return calculateResponsiveSpan(itemSpan, defaultSpan, breakpoint) + } +} diff --git a/src/utils/form/validator.ts b/src/utils/form/validator.ts new file mode 100644 index 0000000..3670763 --- /dev/null +++ b/src/utils/form/validator.ts @@ -0,0 +1,316 @@ +/** + * 表单验证工具模块 + * + * 提供全面的表单字段验证功能 + * + * ## 主要功能 + * + * - 手机号码验证(中国大陆格式) + * - 固定电话验证(支持区号格式) + * - 用户账号验证(字母开头,支持数字和下划线) + * - 密码强度验证(普通密码、强密码) + * - 密码强度评估(弱、中、强) + * - IPv4 地址验证 + * - 邮箱地址验证(RFC 5322 标准) + * - URL 地址验证 + * - 身份证号码验证(18位,含校验码验证) + * - 银行卡号验证(Luhn 算法) + * - 字符串空格处理 + * + * ## 验证规则 + * + * - 手机号:1开头,第二位3-9,共11位 + * - 账号:字母开头,5-20位,支持字母数字下划线 + * - 普通密码:6-20位,必须包含字母和数字 + * - 强密码:8-20位,必须包含大小写字母、数字和特殊字符 + * - 身份证:18位,含出生日期和校验码验证 + * - 银行卡:13-19位,通过 Luhn 算法验证 + * + * @module utils/validation/formValidator + * @author Art Design Pro Team + */ + +/** + * 密码强度级别枚举 + */ +export enum PasswordStrength { + WEAK = '弱', + MEDIUM = '中', + STRONG = '强' +} + +/** + * 去除字符串首尾空格 + * @param value 待处理的字符串 + * @returns 返回去除首尾空格后的字符串 + */ +export function trimSpaces(value: string): string { + if (typeof value !== 'string') { + return '' + } + return value.trim() +} + +/** + * 验证手机号码(中国大陆) + * @param value 手机号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validatePhone(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 中国大陆手机号码:1开头,第二位为3-9,共11位数字 + const phoneRegex = /^1[3-9]\d{9}$/ + return phoneRegex.test(value.trim()) +} + +/** + * 验证固定电话号码(中国大陆) + * @param value 电话号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateTelPhone(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 支持格式:区号-号码,如:010-12345678、0755-1234567 + const telRegex = /^0\d{2,3}-?\d{7,8}$/ + return telRegex.test(value.trim().replace(/\s+/g, '')) +} + +/** + * 验证用户账号 + * @param value 账号字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:字母开头,5-20位,支持字母、数字、下划线 + */ +export function validateAccount(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + // 字母开头,5-20位,支持字母、数字、下划线 + const accountRegex = /^[a-zA-Z][a-zA-Z0-9_]{4,19}$/ + return accountRegex.test(value.trim()) +} + +/** + * 验证密码 + * @param value 密码字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:6-20位,必须包含字母和数字 + */ +export function validatePassword(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 长度检查 + if (trimmedValue.length < 6 || trimmedValue.length > 20) { + return false + } + + // 必须包含字母和数字 + const hasLetter = /[a-zA-Z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + + return hasLetter && hasNumber +} + +/** + * 验证强密码 + * @param value 密码字符串 + * @returns 返回验证结果,true表示格式正确 + * @description 规则:8-20位,必须包含大写字母、小写字母、数字和特殊字符 + */ +export function validateStrongPassword(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 长度检查 + if (trimmedValue.length < 8 || trimmedValue.length > 20) { + return false + } + + // 必须包含:大写字母、小写字母、数字、特殊字符 + const hasUpperCase = /[A-Z]/.test(trimmedValue) + const hasLowerCase = /[a-z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue) + + return hasUpperCase && hasLowerCase && hasNumber && hasSpecialChar +} + +/** + * 获取密码强度 + * @param value 密码字符串 + * @returns 返回密码强度:弱、中、强 + * @description 弱:纯数字/纯字母/纯特殊字符;中:两种组合;强:三种或以上组合 + */ +export function getPasswordStrength(value: string): PasswordStrength { + if (!value || typeof value !== 'string') { + return PasswordStrength.WEAK + } + + const trimmedValue = value.trim() + + if (trimmedValue.length < 6) { + return PasswordStrength.WEAK + } + + const hasUpperCase = /[A-Z]/.test(trimmedValue) + const hasLowerCase = /[a-z]/.test(trimmedValue) + const hasNumber = /\d/.test(trimmedValue) + const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(trimmedValue) + + const typeCount = [hasUpperCase, hasLowerCase, hasNumber, hasSpecialChar].filter(Boolean).length + + if (typeCount >= 3) { + return PasswordStrength.STRONG + } else if (typeCount >= 2) { + return PasswordStrength.MEDIUM + } else { + return PasswordStrength.WEAK + } +} + +/** + * 验证IPv4地址 + * @param value IP地址字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateIPv4Address(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + const ipRegex = /^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$/ + + if (!ipRegex.test(trimmedValue)) { + return false + } + + // 额外检查每个段是否在有效范围内 + const segments = trimmedValue.split('.') + return segments.every((segment) => { + const num = parseInt(segment, 10) + return num >= 0 && num <= 255 + }) +} + +/** + * 验证邮箱地址 + * @param value 邮箱地址字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateEmail(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // RFC 5322 标准的简化版邮箱正则 + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + + return emailRegex.test(trimmedValue) && trimmedValue.length <= 254 +} + +/** + * 验证URL地址 + * @param value URL字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateURL(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + try { + new URL(value.trim()) + return true + } catch { + return false + } +} + +/** + * 验证身份证号码(中国大陆) + * @param value 身份证号码字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateChineseIDCard(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim() + + // 18位身份证号码正则 + const idCardRegex = + /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/ + + if (!idCardRegex.test(trimmedValue)) { + return false + } + + // 验证校验码 + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + + let sum = 0 + for (let i = 0; i < 17; i++) { + sum += parseInt(trimmedValue[i]) * weights[i] + } + + const checkCode = checkCodes[sum % 11] + return trimmedValue[17].toUpperCase() === checkCode +} + +/** + * 验证银行卡号 + * @param value 银行卡号字符串 + * @returns 返回验证结果,true表示格式正确 + */ +export function validateBankCard(value: string): boolean { + if (!value || typeof value !== 'string') { + return false + } + + const trimmedValue = value.trim().replace(/\s+/g, '') + + // 银行卡号通常为13-19位数字 + if (!/^\d{13,19}$/.test(trimmedValue)) { + return false + } + + // Luhn算法验证 + let sum = 0 + let shouldDouble = false + + for (let i = trimmedValue.length - 1; i >= 0; i--) { + let digit = parseInt(trimmedValue[i]) + + if (shouldDouble) { + digit *= 2 + if (digit > 9) { + digit = (digit % 10) + 1 + } + } + + sum += digit + shouldDouble = !shouldDouble + } + + return sum % 10 === 0 +} diff --git a/src/utils/http/error.ts b/src/utils/http/error.ts new file mode 100644 index 0000000..7c80da9 --- /dev/null +++ b/src/utils/http/error.ts @@ -0,0 +1,191 @@ +/** + * HTTP 错误处理模块 + * + * 提供统一的 HTTP 请求错误处理机制 + * + * ## 主要功能 + * + * - 自定义 HttpError 错误类,封装错误信息、状态码、时间戳等 + * - 错误拦截和转换,将 Axios 错误转换为标准的 HttpError + * - 错误消息国际化处理,根据状态码返回对应的多语言错误提示 + * - 错误日志记录,便于问题追踪和调试 + * - 错误和成功消息的统一展示 + * - 类型守卫函数,用于判断错误类型 + * + * ## 使用场景 + * + * - HTTP 请求拦截器中统一处理错误 + * - 业务代码中捕获和处理特定错误 + * - 错误日志收集和上报 + * + * @module utils/http/error + * @author Art Design Pro Team + */ +import { AxiosError } from 'axios' +import { ApiStatus } from './status' +import { $t } from '@/locales' + +// 错误响应接口 +export interface ErrorResponse { + /** 错误状态码 */ + code?: number + /** 错误消息 */ + message?: string + /** 兼容旧字段 */ + msg?: string + /** 错误附加数据 */ + data?: unknown + /** 后端错误详情 */ + errors?: unknown +} + +// 错误日志数据接口 +export interface ErrorLogData { + /** 错误状态码 */ + code: number + /** 错误消息 */ + message: string + /** 错误附加数据 */ + data?: unknown + /** 错误发生时间戳 */ + timestamp: string + /** 请求 URL */ + url?: string + /** 请求方法 */ + method?: string + /** 错误堆栈信息 */ + stack?: string +} + +// 自定义 HttpError 类 +export class HttpError extends Error { + public readonly code: number + public readonly data?: unknown + public readonly timestamp: string + public readonly url?: string + public readonly method?: string + + constructor( + message: string, + code: number, + options?: { + data?: unknown + url?: string + method?: string + } + ) { + super(message) + this.name = 'HttpError' + this.code = code + this.data = options?.data + this.timestamp = new Date().toISOString() + this.url = options?.url + this.method = options?.method + } + + public toLogData(): ErrorLogData { + return { + code: this.code, + message: this.message, + data: this.data, + timestamp: this.timestamp, + url: this.url, + method: this.method, + stack: this.stack + } + } +} + +/** + * 获取错误消息 + * @param status 错误状态码 + * @returns 错误消息 + */ +const getErrorMessage = (status: number): string => { + const errorMap: Record = { + [ApiStatus.unauthorized]: 'httpMsg.unauthorized', + [ApiStatus.forbidden]: 'httpMsg.forbidden', + [ApiStatus.notFound]: 'httpMsg.notFound', + [ApiStatus.methodNotAllowed]: 'httpMsg.methodNotAllowed', + [ApiStatus.conflict]: 'httpMsg.conflict', + [ApiStatus.unprocessableEntity]: 'httpMsg.validationFailed', + [ApiStatus.requestTimeout]: 'httpMsg.requestTimeout', + [ApiStatus.internalServerError]: 'httpMsg.internalServerError', + [ApiStatus.badGateway]: 'httpMsg.badGateway', + [ApiStatus.serviceUnavailable]: 'httpMsg.serviceUnavailable', + [ApiStatus.gatewayTimeout]: 'httpMsg.gatewayTimeout' + } + + return $t(errorMap[status] || 'httpMsg.internalServerError') +} + +/** + * 处理错误 + * @param error 错误对象 + * @returns 错误对象 + */ +export function handleError(error: AxiosError): never { + // 处理取消的请求 + if (error.code === 'ERR_CANCELED') { + console.warn('Request cancelled:', error.message) + throw new HttpError($t('httpMsg.requestCancelled'), ApiStatus.error) + } + + const statusCode = error.response?.status + const responseData = error.response?.data + const serverMessage = responseData?.message || responseData?.msg + const errorMessage = serverMessage || error.message + const requestConfig = error.config + const errorPayload = responseData?.errors ?? responseData?.data + + // 处理网络错误 + if (!error.response) { + throw new HttpError($t('httpMsg.networkError'), ApiStatus.error, { + url: requestConfig?.url, + method: requestConfig?.method?.toUpperCase() + }) + } + + // 处理 HTTP 状态码错误,优先使用后端返回的 message + const message = + serverMessage || + (statusCode ? getErrorMessage(statusCode) : errorMessage || $t('httpMsg.requestFailed')) + throw new HttpError(message, statusCode || ApiStatus.error, { + data: errorPayload, + url: requestConfig?.url, + method: requestConfig?.method?.toUpperCase() + }) +} + +/** + * 显示错误消息 + * @param error 错误对象 + * @param showMessage 是否显示错误消息 + */ +export function showError(error: HttpError, showMessage: boolean = true): void { + if (showMessage) { + ElMessage.error(error.message) + } + // 记录错误日志 + console.error('[HTTP Error]', error.toLogData()) +} + +/** + * 显示成功消息 + * @param message 成功消息 + * @param showMessage 是否显示消息 + */ +export function showSuccess(message: string, showMessage: boolean = true): void { + if (showMessage) { + ElMessage.success(message) + } +} + +/** + * 判断是否为 HttpError 类型 + * @param error 错误对象 + * @returns 是否为 HttpError 类型 + */ +export const isHttpError = (error: unknown): error is HttpError => { + return error instanceof HttpError +} diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts new file mode 100644 index 0000000..f75387a --- /dev/null +++ b/src/utils/http/index.ts @@ -0,0 +1,267 @@ +/** + * HTTP 请求封装模块 + * 基于 Axios 封装的 HTTP 请求工具,提供统一的请求/响应处理 + * + * ## 主要功能 + * + * - 请求/响应拦截器(自动添加 Token、统一错误处理) + * - 401 未授权自动登出(带防抖机制) + * - 请求失败自动重试(可配置) + * - 统一的成功/错误消息提示 + * - 支持 GET/POST/PUT/DELETE 等常用方法 + * + * @module utils/http + * @author Art Design Pro Team + */ + +import axios, { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import JSONBig from 'json-bigint' +import { useUserStore } from '@/store/modules/user' +import { ApiStatus } from './status' +import { ErrorResponse, HttpError, handleError, showSuccess } from './error' +import { handleHttpError } from '@/utils/errorHandler' +import { $t } from '@/locales' +import { BaseResponse } from '@/types' +import { StorageConfig } from '@/utils/storage' + +/** 请求配置常量 */ +const REQUEST_TIMEOUT = 15000 +const LOGOUT_DELAY = 500 +const MAX_RETRIES = 0 +const RETRY_DELAY = 1000 +const UNAUTHORIZED_DEBOUNCE_TIME = 3000 + +/** 401防抖状态 */ +let isUnauthorizedErrorShown = false +let unauthorizedTimer: NodeJS.Timeout | null = null + +/** 扩展 AxiosRequestConfig */ +interface ExtendedAxiosRequestConfig extends AxiosRequestConfig { + showErrorMessage?: boolean + showSuccessMessage?: boolean + skipTenantHeader?: boolean +} + +const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env +const jsonBigParser = JSONBig({ storeAsString: true }) + +/** Axios实例 */ +const axiosInstance = axios.create({ + timeout: REQUEST_TIMEOUT, + baseURL: VITE_API_URL, + withCredentials: VITE_WITH_CREDENTIALS === 'true', + validateStatus: (status) => status >= 200 && status < 300, + transformResponse: [ + (data, headers) => { + const contentType = headers['content-type'] + if (contentType?.includes('application/json')) { + try { + // 1. 使用 json-bigint 解析,避免 Snowflake 等长整型精度丢失 + return jsonBigParser.parse(data) + } catch { + return data + } + } + return data + } + ] +}) + +/** 请求拦截器 */ +axiosInstance.interceptors.request.use( + (request: InternalAxiosRequestConfig) => { + const userStore = useUserStore() + const { accessToken } = userStore + + // 1. 追加鉴权 Token + if (accessToken) { + const hasBearer = accessToken.toLowerCase().startsWith('bearer ') + request.headers.set('Authorization', hasBearer ? accessToken : `Bearer ${accessToken}`) + } + + // 2. 处理租户 Header(允许业务显式跳过) + const { skipTenantHeader } = request as ExtendedAxiosRequestConfig + if (skipTenantHeader) { + request.headers.delete('X-Tenant-Id') + request.headers.delete('x-tenant-id') + } else { + const headerTenantId = + request.headers?.get?.('X-Tenant-Id') ?? + request.headers?.get?.('x-tenant-id') ?? + (request.headers as Record | undefined)?.['X-Tenant-Id'] ?? + (request.headers as Record | undefined)?.['x-tenant-id'] + if (!headerTenantId) { + const tenantId = + localStorage.getItem(StorageConfig.TENANT_ID_KEY) || userStore.info?.tenantId + if (tenantId) { + request.headers.set('X-Tenant-Id', String(tenantId)) + } + } + } + + // 3. 自动补齐 Content-Type 并序列化 JSON + if (request.data && !(request.data instanceof FormData) && !request.headers['Content-Type']) { + request.headers.set('Content-Type', 'application/json') + request.data = JSON.stringify(request.data) + } + + return request + }, + (error) => { + handleHttpError(createHttpError($t('httpMsg.requestConfigError'), ApiStatus.error), true) + return Promise.reject(error) + } +) + +/** 响应拦截器 */ +axiosInstance.interceptors.response.use( + (response: AxiosResponse) => { + const { success, code, message, msg, data, errors } = response.data || {} + const bizCode = code ?? response.status ?? ApiStatus.error + const isSuccess = typeof success === 'boolean' ? success : bizCode === ApiStatus.success + + if (isSuccess) return response + if (bizCode === ApiStatus.unauthorized) handleUnauthorizedError(message || msg) + + throw createHttpError(message || msg || $t('httpMsg.requestFailed'), bizCode, { + data: errors ?? data, + url: response.config.url, + method: response.config.method?.toUpperCase() + }) + }, + (error) => { + if (error.response?.status === ApiStatus.unauthorized) { + const resData = error.response.data as ErrorResponse | undefined + handleUnauthorizedError(resData?.message || resData?.msg) + } + return Promise.reject(handleError(error)) + } +) + +/** 统一创建HttpError */ +function createHttpError( + message: string, + code: number, + options?: { data?: unknown; url?: string; method?: string } +) { + return new HttpError(message, code, options) +} + +/** 处理401错误(带防抖) */ +function handleUnauthorizedError(message?: string): never { + const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized) + + if (!isUnauthorizedErrorShown) { + isUnauthorizedErrorShown = true + logOut() + + unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME) + + handleHttpError(error, true) + throw error + } + + throw error +} + +/** 重置401防抖状态 */ +function resetUnauthorizedError() { + isUnauthorizedErrorShown = false + if (unauthorizedTimer) clearTimeout(unauthorizedTimer) + unauthorizedTimer = null +} + +/** 退出登录函数 */ +function logOut() { + setTimeout(() => { + useUserStore().logOut() + }, LOGOUT_DELAY) +} + +/** 是否需要重试 */ +function shouldRetry(statusCode: number) { + return [ + ApiStatus.requestTimeout, + ApiStatus.internalServerError, + ApiStatus.badGateway, + ApiStatus.serviceUnavailable, + ApiStatus.gatewayTimeout + ].includes(statusCode) +} + +/** 请求重试逻辑 */ +async function retryRequest( + config: ExtendedAxiosRequestConfig, + retries: number = MAX_RETRIES +): Promise { + try { + return await request(config) + } catch (error) { + if (retries > 0 && error instanceof HttpError && shouldRetry(error.code)) { + await delay(RETRY_DELAY) + return retryRequest(config, retries - 1) + } + throw error + } +} + +/** 延迟函数 */ +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** 请求函数 */ +async function request(config: ExtendedAxiosRequestConfig): Promise { + // POST | PUT 参数自动填充 + if ( + ['POST', 'PUT'].includes(config.method?.toUpperCase() || '') && + config.params && + !config.data + ) { + config.data = config.params + config.params = undefined + } + + try { + const res = await axiosInstance.request>(config) + + // 显示成功消息 + if (config.showSuccessMessage && (res.data.message || res.data.msg)) { + showSuccess((res.data.message || res.data.msg) as string) + } + + // 非 JSON 响应(如文件流)直接返回原始数据 + if (config.responseType && config.responseType !== 'json') { + return res.data as unknown as T + } + + return res.data.data as T + } catch (error) { + if (error instanceof HttpError && error.code !== ApiStatus.unauthorized) { + const showMsg = config.showErrorMessage !== false + handleHttpError(error, showMsg) + } + return Promise.reject(error) + } +} + +/** API方法集合 */ +const api = { + get(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'GET' }) + }, + post(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'POST' }) + }, + put(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'PUT' }) + }, + del(config: ExtendedAxiosRequestConfig) { + return retryRequest({ ...config, method: 'DELETE' }) + }, + request(config: ExtendedAxiosRequestConfig) { + return retryRequest(config) + } +} + +export default api diff --git a/src/utils/http/status.ts b/src/utils/http/status.ts new file mode 100644 index 0000000..8692d72 --- /dev/null +++ b/src/utils/http/status.ts @@ -0,0 +1,20 @@ +/** + * 接口状态码 + */ +export enum ApiStatus { + success = 200, // 成功 + error = 400, // 错误 + unauthorized = 401, // 未授权 + forbidden = 403, // 禁止访问 + notFound = 404, // 未找到 + methodNotAllowed = 405, // 方法不允许 + conflict = 409, // 资源冲突 + unprocessableEntity = 422, // 参数验证失败 + requestTimeout = 408, // 请求超时 + internalServerError = 500, // 服务器错误 + notImplemented = 501, // 未实现 + badGateway = 502, // 网关错误 + serviceUnavailable = 503, // 服务不可用 + gatewayTimeout = 504, // 网关超时 + httpVersionNotSupported = 505 // HTTP版本不支持 +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..f1e1b77 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,34 @@ +/** + * Utils 工具函数统一导出 + * 提供向后兼容性和便捷导入 + * + * @module utils/index + * @author Art Design Pro Team + */ + +// UI 相关 +export * from './ui' + +// 路由相关 +export * from './router' + +// 路由导航相关 +export * from './navigation' + +// 系统管理相关 +export * from './sys' + +// 常量定义相关 +export * from './constants' + +// 存储相关 +export * from './storage' + +// HTTP 相关 +export * from './http' + +// 表单相关 +export * from './form' + +// socket 相关 +export * from './socket' diff --git a/src/utils/navigation/index.ts b/src/utils/navigation/index.ts new file mode 100644 index 0000000..0b84e78 --- /dev/null +++ b/src/utils/navigation/index.ts @@ -0,0 +1,10 @@ +/** + * 路由和导航相关工具函数统一导出 + * + * @module utils/navigation/index + * @author Art Design Pro Team + */ + +export * from './jump' +export * from './worktab' +export * from './route' diff --git a/src/utils/navigation/jump.ts b/src/utils/navigation/jump.ts new file mode 100644 index 0000000..6391298 --- /dev/null +++ b/src/utils/navigation/jump.ts @@ -0,0 +1,74 @@ +/** + * 导航跳转工具模块 + * + * 提供统一的页面跳转和导航功能 + * + * ## 主要功能 + * + * - 外部链接打开(新窗口) + * - 菜单项跳转处理(支持内部路由和外部链接) + * - iframe 页面跳转支持 + * - 递归查找并跳转到第一个可见的子菜单 + * - 智能判断跳转目标类型(外部链接/内部路由) + * + * @module utils/navigation/jump + * @author Art Design Pro Team + */ +import { AppRouteRecord } from '@/types/router' +import { router } from '@/router' +import { useSettingStore } from '@/store/modules/setting' + +// 打开外部链接 +export const openExternalLink = (link: string) => { + window.open(link, '_blank') +} + +/** + * 菜单跳转 + * @param item 菜单项 + * @param jumpToFirst 是否跳转到第一个子菜单 + * @returns + */ +export const handleMenuJump = (item: AppRouteRecord, jumpToFirst: boolean = false) => { + // 处理外部链接 + const { link, isIframe } = item.meta + if (link && !isIframe) { + return openExternalLink(link) + } + + const navigateWithRefresh = (targetPath?: string) => { + if (!targetPath) return + const currentPath = router.currentRoute.value.path + if (targetPath === currentPath) return + return router.push(targetPath).then(() => { + if (router.currentRoute.value.path === targetPath) { + useSettingStore().reload() + } + }) + } + + // 如果不需要跳转到第一个子菜单,或者没有子菜单,直接跳转当前路径 + if (!jumpToFirst || !item.children?.length) { + return navigateWithRefresh(item.path) + } + + // 递归查找第一个可见的叶子节点菜单 + const findFirstLeafMenu = (items: AppRouteRecord[]): AppRouteRecord => { + for (const child of items) { + if (!child.meta.isHide) { + return child.children?.length ? findFirstLeafMenu(child.children) : child + } + } + return items[0] + } + + const firstChild = findFirstLeafMenu(item.children) + + // 如果第一个子菜单是外部链接则打开新窗口 + if (firstChild.meta?.link) { + return openExternalLink(firstChild.meta.link) + } + + // 跳转到子菜单路径 + return navigateWithRefresh(firstChild.path) +} diff --git a/src/utils/navigation/route.ts b/src/utils/navigation/route.ts new file mode 100644 index 0000000..9ca4f29 --- /dev/null +++ b/src/utils/navigation/route.ts @@ -0,0 +1,78 @@ +/** + * 路由工具模块 + * + * 提供路由处理和菜单路径相关的工具函数 + * + * ## 主要功能 + * + * - iframe 路由检测,判断是否为外部嵌入页面 + * - 菜单项有效性验证,过滤隐藏和无效菜单 + * - 路径标准化处理,统一路径格式 + * - 递归查找菜单树中第一个有效路径 + * - 支持多级嵌套菜单的路径解析 + * + * ## 使用场景 + * + * - 系统初始化时获取默认跳转路径 + * - 菜单权限过滤后获取首个可访问页面 + * - 路由重定向逻辑处理 + * - iframe 页面特殊处理 + * + * @module utils/navigation/route + * @author Art Design Pro Team + */ + +import { AppRouteRecord } from '@/types' + +// 检查是否为 iframe 路由 +export function isIframe(url: string): boolean { + return url.startsWith('/outside/iframe/') +} + +/** + * 验证菜单项是否有效 + * @param menuItem 菜单项 + * @returns 是否为有效菜单项 + */ +const isValidMenuItem = (menuItem: AppRouteRecord): boolean => { + return !!(menuItem.path && menuItem.path.trim() && !menuItem.meta?.isHide) +} + +/** + * 标准化路径格式 + * @param path 路径 + * @returns 标准化后的路径 + */ +const normalizePath = (path: string): string => { + return path.startsWith('/') ? path : `/${path}` +} + +/** + * 递归获取菜单的第一个有效路径 + * @param menuList 菜单列表 + * @returns 第一个有效路径,如果没有找到则返回空字符串 + */ +export const getFirstMenuPath = (menuList: AppRouteRecord[]): string => { + if (!Array.isArray(menuList) || menuList.length === 0) { + return '' + } + + for (const menuItem of menuList) { + if (!isValidMenuItem(menuItem)) { + continue + } + + // 如果有子菜单,优先查找子菜单 + if (menuItem.children?.length) { + const childPath = getFirstMenuPath(menuItem.children) + if (childPath) { + return childPath + } + } + + // 返回当前菜单项的标准化路径 + return normalizePath(menuItem.path!) + } + + return '' +} diff --git a/src/utils/navigation/worktab.ts b/src/utils/navigation/worktab.ts new file mode 100644 index 0000000..6db6a77 --- /dev/null +++ b/src/utils/navigation/worktab.ts @@ -0,0 +1,67 @@ +/** + * 工作标签页管理模块 + * + * 提供工作标签页(Worktab)的自动管理功能 + * + * ## 主要功能 + * + * - 根据路由导航自动创建和更新工作标签页 + * - iframe 页面标签页特殊处理 + * - 标签页信息提取(标题、路径、缓存状态等) + * - 固定标签页支持 + * - 根据系统设置控制标签页显示 + * - 首页标签页特殊处理 + * + * ## 使用场景 + * + * - 路由守卫中自动创建标签页 + * - 页面切换时更新标签页状态 + * - 多标签页导航系统 + * + * @module utils/navigation/worktab + * @author Art Design Pro Team + */ +import { useWorktabStore } from '@/store/modules/worktab' +import { RouteLocationNormalized } from 'vue-router' +import { isIframe } from './route' +import { useSettingStore } from '@/store/modules/setting' +import { IframeRouteManager } from '@/router/core' +import { useCommon } from '@/hooks/core/useCommon' + +/** + * 根据当前路由信息设置工作标签页(worktab) + * @param to 当前路由对象 + */ +export const setWorktab = (to: RouteLocationNormalized): void => { + const worktabStore = useWorktabStore() + const { meta, path, name, params, query } = to + if (!meta.isHideTab) { + // 如果是 iframe 页面,则特殊处理工作标签页 + if (isIframe(path)) { + const iframeRoute = IframeRouteManager.getInstance().findByPath(to.path) + + if (iframeRoute?.meta) { + worktabStore.openTab({ + title: iframeRoute.meta.title, + icon: meta.icon as string, + path, + name: name as string, + keepAlive: meta.keepAlive as boolean, + params, + query + }) + } + } else if (useSettingStore().showWorkTab || path === useCommon().homePath.value) { + worktabStore.openTab({ + title: meta.title as string, + icon: meta.icon as string, + path, + name: name as string, + keepAlive: meta.keepAlive as boolean, + params, + query, + fixedTab: meta.fixedTab as boolean + }) + } + } +} diff --git a/src/utils/router.ts b/src/utils/router.ts new file mode 100644 index 0000000..8c838ff --- /dev/null +++ b/src/utils/router.ts @@ -0,0 +1,61 @@ +/** + * 路由工具函数 + * + * 提供路由相关的工具函数 + * + * @module utils/router + */ +import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router' +import AppConfig from '@/config' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import i18n, { $t } from '@/locales' + +/** 扩展的路由配置类型 */ +export type AppRouteRecordRaw = RouteRecordRaw & { + hidden?: boolean +} + +/** 顶部进度条配置 */ +export const configureNProgress = () => { + NProgress.configure({ + easing: 'ease', + speed: 600, + showSpinner: false, + parent: 'body' + }) +} + +/** + * 设置页面标题,根据路由元信息和系统信息拼接标题 + * @param to 当前路由对象 + */ +export const setPageTitle = (to: RouteLocationNormalized): void => { + const { title } = to.meta + if (title) { + setTimeout(() => { + document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}` + }, 150) + } +} + +/** + * 格式化菜单标题 + * @param title 菜单标题,可以是 i18n 的 key,也可以是字符串 + * @returns 格式化后的菜单标题 + */ +export const formatMenuTitle = (title: string): string => { + if (title) { + if (title.startsWith('menus.')) { + // 使用 te() 方法检查翻译键值是否存在,避免控制台警告 + if (i18n.global.te(title)) { + return $t(title) + } else { + // 如果翻译不存在,返回键值的最后部分作为fallback + return title.split('.').pop() || title + } + } + return title + } + return '' +} diff --git a/src/utils/socket/index.ts b/src/utils/socket/index.ts new file mode 100644 index 0000000..77d46ad --- /dev/null +++ b/src/utils/socket/index.ts @@ -0,0 +1,388 @@ +interface WebSocketOptions { + url?: string + messageHandler: (event: MessageEvent) => void + reconnectInterval?: number // 重连间隔(ms) + heartbeatInterval?: number // 心跳检测间隔(ms) + pingInterval?: number // 发送ping间隔(ms) + reconnectTimeout?: number // 重连超时时间(ms) + maxReconnectAttempts?: number // 最大重连次数 + connectionTimeout?: number // 连接建立超时时间(ms) +} + +export default class WebSocketClient { + private static instance: WebSocketClient | null = null + private ws: WebSocket | null = null + private url: string + private messageHandler: (event: MessageEvent) => void + private reconnectInterval: number + private heartbeatInterval: number + private pingInterval: number + private reconnectTimeout: number + private maxReconnectAttempts: number + private connectionTimeout: number + private reconnectAttempts: number = 0 // 当前重连次数 + + // 消息队列 - 缓存连接建立前的消息 + private messageQueue: Array = [] + + // 定时器 + private detectionTimer: NodeJS.Timeout | null = null + private timeoutTimer: NodeJS.Timeout | null = null + private reconnectTimer: NodeJS.Timeout | null = null + private pingTimer: NodeJS.Timeout | null = null + private connectionTimer: NodeJS.Timeout | null = null // 连接超时定时器 + + // 状态标识 + private isConnected: boolean = false + private isConnecting: boolean = false // 是否正在连接中 + private stopReconnect: boolean = false + + private constructor(options: WebSocketOptions) { + this.url = options.url || (process.env.VUE_APP_LOGIN_WEBSOCKET as string) + this.messageHandler = options.messageHandler + this.reconnectInterval = options.reconnectInterval || 20 * 1000 // 默认20秒 + this.heartbeatInterval = options.heartbeatInterval || 5 * 1000 // 默认5秒 + this.pingInterval = options.pingInterval || 10 * 1000 // 默认10秒 + this.reconnectTimeout = options.reconnectTimeout || 30 * 1000 // 默认30秒 + this.maxReconnectAttempts = options.maxReconnectAttempts || 10 // 默认最多重连10次 + this.connectionTimeout = options.connectionTimeout || 10 * 1000 // 连接超时10秒 + } + + // 单例模式获取实例 + static getInstance(options: WebSocketOptions): WebSocketClient { + if (!WebSocketClient.instance) { + WebSocketClient.instance = new WebSocketClient(options) + } else { + // 更新消息处理器 + WebSocketClient.instance.messageHandler = options.messageHandler + // 如果提供了新的URL,则更新并重新连接 + if (options.url && WebSocketClient.instance.url !== options.url) { + WebSocketClient.instance.url = options.url + WebSocketClient.instance.reconnectAttempts = 0 + WebSocketClient.instance.init() + } + } + return WebSocketClient.instance + } + + // 初始化连接 + init(): void { + // 如果正在连接中,不重复连接 + if (this.isConnecting) { + console.log('正在建立WebSocket连接中...') + return + } + + // 如果已连接,不重复连接 + if (this.ws?.readyState === WebSocket.OPEN) { + console.warn('WebSocket连接已存在') + this.flushMessageQueue() // 确保队列中的消息被发送 + return + } + + try { + this.isConnecting = true + this.reconnectAttempts = 0 // 重置重连次数 + this.ws = new WebSocket(this.url) + + // 设置连接超时检测 + this.clearTimer('connectionTimer') + this.connectionTimer = setTimeout(() => { + console.error(`WebSocket连接超时 (${this.connectionTimeout}ms):${this.url}`) + this.handleConnectionTimeout() + }, this.connectionTimeout) + + this.ws.onopen = (event) => this.handleOpen(event) + this.ws.onmessage = (event) => this.handleMessage(event) + this.ws.onclose = (event) => this.handleClose(event) + this.ws.onerror = (event) => this.handleError(event) + } catch (error) { + console.error('WebSocket初始化失败:', error) + this.isConnecting = false + this.reconnect() + } + } + + // 处理连接超时 + private handleConnectionTimeout(): void { + if (this.ws?.readyState !== WebSocket.OPEN) { + console.error('WebSocket连接超时,强制关闭连接') + this.ws?.close(1000, 'Connection timeout') + this.isConnecting = false + this.reconnect() + } + } + + // 关闭连接 + close(force?: boolean): void { + this.clearAllTimers() + this.stopReconnect = true + this.isConnecting = false + + if (this.ws) { + // 1000 表示正常关闭 + this.ws.close(force ? 1001 : 1000, force ? 'Force closed' : 'Normal close') + this.ws = null + } + + this.isConnected = false + } + + // 发送消息 - 增加消息队列 + send(data: string | ArrayBufferLike | Blob | ArrayBufferView, immediate: boolean = false): void { + // 如果要求立即发送且未连接,则直接报错 + if (immediate && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) { + console.error('WebSocket未连接,无法立即发送消息') + return + } + + // 如果未连接且不要求立即发送,则加入消息队列 + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + console.log('WebSocket未连接,消息已加入队列等待发送') + this.messageQueue.push(data) + // 如果未在重连中,则尝试重连 + if (!this.isConnecting && !this.stopReconnect) { + this.init() + } + return + } + + try { + this.ws.send(data) + } catch (error) { + console.error('WebSocket发送消息失败:', error) + // 发送失败时将消息加入队列,等待重连后重试 + this.messageQueue.push(data) + this.reconnect() + } + } + + // 发送队列中的消息 + private flushMessageQueue(): void { + if (this.messageQueue.length > 0 && this.ws?.readyState === WebSocket.OPEN) { + console.log(`发送队列中的${this.messageQueue.length}条消息`) + while (this.messageQueue.length > 0) { + const data = this.messageQueue.shift() + if (data) { + try { + this.ws?.send(data) + } catch (error) { + console.error('发送队列消息失败:', error) + // 如果发送失败,将消息放回队列头部 + if (data) this.messageQueue.unshift(data) + break + } + } + } + } + } + + // 处理连接打开 + private handleOpen(event: Event): void { + console.log('WebSocket连接成功', event) + this.clearTimer('connectionTimer') // 清除连接超时定时器 + this.isConnected = true + this.isConnecting = false + this.stopReconnect = false + this.reconnectAttempts = 0 // 重置重连次数 + this.startHeartbeat() + this.startPing() + this.flushMessageQueue() // 发送队列中的消息 + } + + // 处理收到的消息 + private handleMessage(event: MessageEvent): void { + console.log('收到WebSocket消息:', event) + this.resetHeartbeat() + this.messageHandler(event) + } + + // 处理连接关闭 + private handleClose(event: CloseEvent): void { + console.log( + `WebSocket断开: 代码=${event.code}, 原因=${event.reason}, 干净关闭=${event.wasClean}` + ) + + // 1000 是正常关闭代码 + const isNormalClose = event.code === 1000 + + this.isConnected = false + this.isConnecting = false + this.clearAllTimers() + + if (!this.stopReconnect && !isNormalClose) { + this.reconnect() + } + } + + // 处理错误 - 增加详细错误信息 + private handleError(event: Event): void { + console.error('WebSocket连接错误:') + console.error('错误事件:', event) + console.error( + '当前连接状态:', + this.ws?.readyState ? this.getReadyStateText(this.ws.readyState) : '未初始化' + ) + + this.isConnected = false + this.isConnecting = false + + // 只有在未停止重连的情况下才尝试重连 + if (!this.stopReconnect) { + this.reconnect() + } + } + + // 转换连接状态为文本描述 + private getReadyStateText(state: number): string { + switch (state) { + case WebSocket.CONNECTING: + return 'CONNECTING (0) - 正在连接' + case WebSocket.OPEN: + return 'OPEN (1) - 已连接' + case WebSocket.CLOSING: + return 'CLOSING (2) - 正在关闭' + case WebSocket.CLOSED: + return 'CLOSED (3) - 已关闭' + default: + return `未知状态 (${state})` + } + } + + // 开始心跳检测 + private startHeartbeat(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + + this.detectionTimer = setTimeout(() => { + this.isConnected = this.ws?.readyState === WebSocket.OPEN + + if (!this.isConnected) { + console.warn('WebSocket心跳检测失败,尝试重连') + this.reconnect() + + this.timeoutTimer = setTimeout(() => { + console.warn('WebSocket重连超时') + this.close() + }, this.reconnectTimeout) + } + }, this.heartbeatInterval) + } + + // 重置心跳检测 + private resetHeartbeat(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + this.startHeartbeat() + } + + // 开始发送ping消息 + private startPing(): void { + this.clearTimer('pingTimer') + + this.pingTimer = setInterval(() => { + if (this.ws?.readyState !== WebSocket.OPEN) { + console.warn('WebSocket未连接,停止发送ping') + this.clearTimer('pingTimer') + this.reconnect() + return + } + + try { + this.ws.send('ping') + console.log('发送ping消息') + } catch (error) { + console.error('发送ping消息失败:', error) + this.clearTimer('pingTimer') + this.reconnect() + } + }, this.pingInterval) + } + + // 重连 - 增加重连次数限制 + private reconnect(): void { + if (this.stopReconnect || this.isConnecting) { + return + } + + // 检查是否超过最大重连次数 + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.error(`已达到最大重连次数(${this.maxReconnectAttempts}),停止重连`) + this.close(true) + return + } + + this.reconnectAttempts++ + this.stopReconnect = true + this.close(true) + + const delay = this.calculateReconnectDelay() + console.log( + `将在${delay / 1000}秒后尝试重新连接(第${this.reconnectAttempts}/${this.maxReconnectAttempts}次)` + ) + + this.clearTimer('reconnectTimer') + this.reconnectTimer = setTimeout(() => { + console.log(`尝试重新连接WebSocket(第${this.reconnectAttempts}次)`) + this.init() + this.stopReconnect = false + }, delay) + } + + // 计算重连延迟 - 指数退避策略 + private calculateReconnectDelay(): number { + // 基础延迟 + 随机值,避免多个客户端同时重连 + const jitter = Math.random() * 1000 // 0-1秒的随机延迟 + const baseDelay = Math.min( + this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), + this.reconnectInterval * 5 + ) + return baseDelay + jitter + } + + // 清除指定定时器 + private clearTimer( + timerName: + | 'detectionTimer' + | 'timeoutTimer' + | 'reconnectTimer' + | 'pingTimer' + | 'connectionTimer' + ): void { + if (this[timerName]) { + clearTimeout(this[timerName] as NodeJS.Timeout) + this[timerName] = null + } + } + + // 清除所有定时器 + private clearAllTimers(): void { + this.clearTimer('detectionTimer') + this.clearTimer('timeoutTimer') + this.clearTimer('reconnectTimer') + this.clearTimer('pingTimer') + this.clearTimer('connectionTimer') + } + + // 获取当前连接状态 + get isWebSocketConnected(): boolean { + return this.isConnected + } + + // 获取当前连接状态文本 + get connectionStatusText(): string { + if (this.isConnecting) return '正在连接' + if (this.isConnected) return '已连接' + if (this.reconnectAttempts > 0 && !this.stopReconnect) + return `重连中(${this.reconnectAttempts}/${this.maxReconnectAttempts})` + return '已断开' + } + + // 销毁实例 + static destroyInstance(): void { + if (WebSocketClient.instance) { + WebSocketClient.instance.close() + WebSocketClient.instance = null + } + } +} diff --git a/src/utils/storage/index.ts b/src/utils/storage/index.ts new file mode 100644 index 0000000..a4366f0 --- /dev/null +++ b/src/utils/storage/index.ts @@ -0,0 +1,7 @@ +/** + * 存储相关工具函数统一导出 + */ + +export * from './storage' +export * from './storage-config' +export * from './storage-key-manager' diff --git a/src/utils/storage/remember-login.ts b/src/utils/storage/remember-login.ts new file mode 100644 index 0000000..0a55466 --- /dev/null +++ b/src/utils/storage/remember-login.ts @@ -0,0 +1,77 @@ +/** + * 登录记住密码存储工具 + * + * 负责对登录账号与密码进行加密存储与读取 + * + * @module utils/storage/remember-login + */ +import CryptoJS from 'crypto-js' +import { StorageKeyManager } from '@/utils/storage/storage-key-manager' + +const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY +const REMEMBER_LOGIN_STORE_ID = 'remember-login' + +// 使用 StorageKeyManager 生成带版本的存储键,避免硬编码 +const storageKeyManager = new StorageKeyManager() +const REMEMBER_LOGIN_KEY = storageKeyManager.getStorageKey(REMEMBER_LOGIN_STORE_ID) + +export interface RememberLoginPayload { + account: string + phone?: string + password: string +} + +/** + * 保存加密后的登录信息 + */ +export const saveRememberLogin = (payload: RememberLoginPayload) => { + if (!payload.account || !payload.password) return + + const encryptedPassword = CryptoJS.AES.encrypt(payload.password, ENCRYPT_KEY).toString() + const data = { + account: payload.account, + phone: payload.phone, + password: encryptedPassword, + updatedAt: Date.now() + } + + localStorage.setItem(REMEMBER_LOGIN_KEY, JSON.stringify(data)) +} + +/** + * 读取并解密已保存的登录信息 + */ +export const loadRememberLogin = (): RememberLoginPayload | null => { + const storedValue = localStorage.getItem(REMEMBER_LOGIN_KEY) + if (!storedValue) return null + + try { + const parsed = JSON.parse(storedValue) as { + account?: string + phone?: string + password?: string + } + if (!parsed.account || !parsed.password) return null + + const decryptedPassword = CryptoJS.AES.decrypt(parsed.password, ENCRYPT_KEY).toString( + CryptoJS.enc.Utf8 + ) + + return { + account: parsed.account, + phone: parsed.phone, + password: decryptedPassword + } + } catch (error) { + console.warn('[Login] 解析记住密码数据失败:', error) + clearRememberLogin() + return null + } +} + +/** + * 清除记住的登录信息 + */ +export const clearRememberLogin = () => { + localStorage.removeItem(REMEMBER_LOGIN_KEY) +} diff --git a/src/utils/storage/storage-config.ts b/src/utils/storage/storage-config.ts new file mode 100644 index 0000000..2571f6d --- /dev/null +++ b/src/utils/storage/storage-config.ts @@ -0,0 +1,125 @@ +/** + * 存储配置管理模块 + * + * 提供统一的本地存储配置和工具方法 + * + * ## 主要功能 + * + * - 版本化存储键管理,支持多版本数据隔离 + * - 存储键名生成和解析(带版本前缀) + * - 版本号提取和验证 + * - 存储键匹配的正则表达式生成 + * - 旧版本存储键兼容处理 + * - 升级和登出延迟配置 + * - 主题存储键配置 + * + * ## 使用场景 + * + * - Pinia Store 持久化存储 + * - 应用版本升级时的数据迁移 + * - 多版本数据清理 + * - 存储键的统一管理和规范 + * + * 存储键格式:sys-v{version}-{storeId} + * 例如:sys-v1.0.0-user, sys-v1.0.0-setting + * + * @module utils/storage/storage-config + * @author Art Design Pro Team + */ +export class StorageConfig { + /** 当前应用版本 */ + static readonly CURRENT_VERSION = __APP_VERSION__ + + /** 存储键前缀 */ + static readonly STORAGE_PREFIX = 'sys-v' + + /** 版本键名 */ + static readonly VERSION_KEY = 'sys-version' + + /** 主题键名(index.html中使用了,如果修改,需要同步修改) */ + static readonly THEME_KEY = 'sys-theme' + + /** 上次登录用户ID键名(用于判断是否为同一用户登录) */ + static readonly LAST_USER_ID_KEY = 'sys-last-user-id' + + /** 租户ID键名 */ + static readonly TENANT_ID_KEY = 'sys-tenant-id' + + /** 跳过升级检查的版本 */ + static readonly SKIP_UPGRADE_VERSION = '1.0.0' + + /** 升级处理延迟时间(毫秒) */ + static readonly UPGRADE_DELAY = 1000 + + /** 登出延迟时间(毫秒) */ + static readonly LOGOUT_DELAY = 1000 + + /** + * 生成版本化的存储键名 + * @param storeId 存储ID + * @param version 版本号,默认使用当前版本 + */ + static generateStorageKey(storeId: string, version: string = this.CURRENT_VERSION): string { + return `${this.STORAGE_PREFIX}${version}-${storeId}` + } + + /** + * 生成旧版本的存储键名(不带分隔符) + * @param version 版本号,默认使用当前版本 + */ + static generateLegacyKey(version: string = this.CURRENT_VERSION): string { + return `${this.STORAGE_PREFIX}${version}` + } + + /** + * 创建存储键匹配的正则表达式 + * @param storeId 存储ID + */ + static createKeyPattern(storeId: string): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}[^-]+-${storeId}$`) + } + + /** + * 创建当前版本存储键匹配的正则表达式 + */ + static createCurrentVersionPattern(): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}${this.CURRENT_VERSION}-`) + } + + /** + * 创建任意版本存储键匹配的正则表达式 + */ + static createVersionPattern(): RegExp { + return new RegExp(`^${this.STORAGE_PREFIX}`) + } + + /** + * 检查是否为当前版本的键 + */ + static isCurrentVersionKey(key: string): boolean { + return key.startsWith(`${this.STORAGE_PREFIX}${this.CURRENT_VERSION}`) + } + + /** + * 检查是否为版本化的键 + */ + static isVersionedKey(key: string): boolean { + return key.startsWith(this.STORAGE_PREFIX) + } + + /** + * 从存储键中提取版本号 + */ + static extractVersionFromKey(key: string): string | null { + const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}([^-]+)`)) + return match ? match[1] : null + } + + /** + * 从存储键中提取存储ID + */ + static extractStoreIdFromKey(key: string): string | null { + const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}[^-]+-(.+)$`)) + return match ? match[1] : null + } +} diff --git a/src/utils/storage/storage-key-manager.ts b/src/utils/storage/storage-key-manager.ts new file mode 100644 index 0000000..ba14f65 --- /dev/null +++ b/src/utils/storage/storage-key-manager.ts @@ -0,0 +1,97 @@ +/** + * 存储键名管理器模块 + * + * 提供智能的版本化存储键管理和数据迁移功能 + * + * ## 主要功能 + * + * - 自动生成当前版本的存储键名 + * - 检测当前版本数据是否存在 + * - 查找其他版本的同名存储数据 + * - 自动将旧版本数据迁移到当前版本 + * - 数据迁移日志记录 + * - 迁移失败的错误处理 + * + * ## 使用场景 + * + * - Pinia Store 持久化插件中获取存储键 + * - 应用版本升级时自动迁移用户数据 + * - 避免版本升级导致的数据丢失 + * - 实现平滑的版本过渡 + * + * ## 工作流程 + * + * 1. 优先使用当前版本的存储键 + * 2. 如果当前版本无数据,查找其他版本的同名数据 + * 3. 找到旧版本数据后自动迁移到当前版本 + * 4. 返回当前版本的存储键供使用 + * + * @module utils/storage/storage-key-manager + * @author Art Design Pro Team + */ +import { StorageConfig } from '@/utils/storage' + +/** + * 存储键名管理器 + * 负责处理版本化的存储键名生成和数据迁移 + */ +export class StorageKeyManager { + /** + * 获取当前版本的存储键名 + */ + private getCurrentVersionKey(storeId: string): string { + return StorageConfig.generateStorageKey(storeId) + } + + /** + * 检查当前版本的数据是否存在 + */ + private hasCurrentVersionData(key: string): boolean { + return localStorage.getItem(key) !== null + } + + /** + * 查找其他版本的同名存储键 + */ + private findExistingKey(storeId: string): string | null { + const storageKeys = Object.keys(localStorage) + const pattern = StorageConfig.createKeyPattern(storeId) + + return storageKeys.find((key) => pattern.test(key) && localStorage.getItem(key)) || null + } + + /** + * 将数据从旧版本迁移到当前版本 + */ + private migrateData(fromKey: string, toKey: string): void { + try { + const existingData = localStorage.getItem(fromKey) + if (existingData) { + localStorage.setItem(toKey, existingData) + console.info(`[Storage] 已迁移数据: ${fromKey} → ${toKey}`) + } + } catch (error) { + console.warn(`[Storage] 数据迁移失败: ${fromKey}`, error) + } + } + + /** + * 获取持久化存储的键名(支持自动数据迁移) + */ + getStorageKey(storeId: string): string { + const currentKey = this.getCurrentVersionKey(storeId) + + // 优先使用当前版本的数据 + if (this.hasCurrentVersionData(currentKey)) { + return currentKey + } + + // 查找并迁移其他版本的数据 + const existingKey = this.findExistingKey(storeId) + if (existingKey) { + this.migrateData(existingKey, currentKey) + } + + return currentKey + } +} diff --git a/src/utils/storage/storage.ts b/src/utils/storage/storage.ts new file mode 100644 index 0000000..67b9e9e --- /dev/null +++ b/src/utils/storage/storage.ts @@ -0,0 +1,250 @@ +/** + * 存储兼容性管理模块 + * + * 提供完整的本地存储兼容性检查和数据验证功能 + * + * 主要功能 + * + * - 多版本存储数据检测和验证 + * - 新旧存储格式兼容处理 + * - 存储数据完整性校验 + * - 存储异常自动恢复(清理+登出) + * - 登录状态验证 + * - 存储为空检测 + * - 版本号管理 + * + * ## 使用场景 + * + * - 应用启动时检查存储数据有效性 + * - 路由守卫中验证登录状态 + * - 版本升级时的数据兼容性检查 + * - 存储异常时的自动恢复 + * - 防止因存储数据损坏导致的系统异常 + * + * ## 工作流程 + * + * 1. 优先检查当前版本的存储数据 + * 2. 检查其他版本的存储数据 + * 3. 兼容旧格式的存储数据 + * 4. 验证数据完整性 + * 5. 异常时提示用户并执行登出 + * + * @module utils/storage/storage + * @author Art Design Pro Team + */ +import { router } from '@/router' +import { useUserStore } from '@/store/modules/user' +import { StorageConfig } from '@/utils/storage/storage-config' + +/** + * 存储兼容性管理器 + * 负责处理不同版本间的存储兼容性检查和数据验证 + */ +class StorageCompatibilityManager { + /** + * 获取系统版本号 + */ + getSystemVersion(): string | null { + return localStorage.getItem(StorageConfig.VERSION_KEY) + } + + /** + * 获取系统存储数据(兼容旧格式) + */ + getSystemStorage(): any { + const version = this.getSystemVersion() || StorageConfig.CURRENT_VERSION + const legacyKey = StorageConfig.generateLegacyKey(version) + const data = localStorage.getItem(legacyKey) + return data ? JSON.parse(data) : null + } + + /** + * 检查当前版本是否有存储数据 + */ + private hasCurrentVersionStorage(): boolean { + const storageKeys = Object.keys(localStorage) + const currentVersionPattern = StorageConfig.createCurrentVersionPattern() + + return storageKeys.some( + (key) => currentVersionPattern.test(key) && localStorage.getItem(key) !== null + ) + } + + /** + * 检查是否存在任何版本的存储数据 + */ + private hasAnyVersionStorage(): boolean { + const storageKeys = Object.keys(localStorage) + const versionPattern = StorageConfig.createVersionPattern() + + return storageKeys.some((key) => versionPattern.test(key) && localStorage.getItem(key) !== null) + } + + /** + * 获取旧格式的本地存储数据 + */ + private getLegacyStorageData(): Record { + try { + const systemStorage = this.getSystemStorage() + return systemStorage || {} + } catch (error) { + console.warn('[Storage] 解析旧格式存储数据失败:', error) + return {} + } + } + + /** + * 显示存储错误消息 + */ + private showStorageError(): void { + ElMessage({ + type: 'error', + offset: 40, + duration: 5000, + message: '系统检测到本地数据异常,请重新登录系统恢复使用!' + }) + } + + /** + * 执行系统登出 + */ + private performSystemLogout(): void { + setTimeout(() => { + try { + localStorage.clear() + useUserStore().logOut() + router.push({ name: 'Login' }) + console.info('[Storage] 已执行系统登出') + } catch (error) { + console.error('[Storage] 系统登出失败:', error) + } + }, StorageConfig.LOGOUT_DELAY) + } + + /** + * 处理存储异常 + */ + private handleStorageError(): void { + this.showStorageError() + this.performSystemLogout() + } + + /** + * 验证存储数据完整性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ + validateStorageData(requireAuth: boolean = false): boolean { + try { + // 优先检查新版本存储结构 + if (this.hasCurrentVersionStorage()) { + // console.debug('[Storage] 发现当前版本存储数据') + return true + } + + // 检查是否有任何版本的存储数据 + if (this.hasAnyVersionStorage()) { + // console.debug('[Storage] 发现其他版本存储数据,可能需要迁移') + return true + } + + // 检查旧版本存储结构 + const legacyData = this.getLegacyStorageData() + if (Object.keys(legacyData).length === 0) { + // 只有在需要验证登录状态时才执行登出操作 + if (requireAuth) { + console.warn('[Storage] 未发现任何存储数据,需要重新登录') + this.performSystemLogout() + return false + } + // 首次访问或访问静态路由,不需要登出 + // console.debug('[Storage] 未发现存储数据,首次访问或访问静态路由') + return true + } + + console.debug('[Storage] 发现旧版本存储数据') + return true + } catch (error) { + console.error('[Storage] 存储数据验证失败:', error) + // 只有在需要验证登录状态时才处理错误 + if (requireAuth) { + this.handleStorageError() + return false + } + return true + } + } + + /** + * 检查存储是否为空 + */ + isStorageEmpty(): boolean { + // 检查新版本存储结构 + if (this.hasCurrentVersionStorage()) { + return false + } + + // 检查是否有任何版本的存储数据 + if (this.hasAnyVersionStorage()) { + return false + } + + // 检查旧版本存储结构 + const legacyData = this.getLegacyStorageData() + return Object.keys(legacyData).length === 0 + } + + /** + * 检查存储兼容性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ + checkCompatibility(requireAuth: boolean = false): boolean { + try { + const isValid = this.validateStorageData(requireAuth) + const isEmpty = this.isStorageEmpty() + + if (isValid || isEmpty) { + // console.debug('[Storage] 存储兼容性检查通过') + return true + } + + console.warn('[Storage] 存储兼容性检查失败') + return false + } catch (error) { + console.error('[Storage] 兼容性检查异常:', error) + return false + } + } +} + +// 创建存储兼容性管理器实例 +const storageManager = new StorageCompatibilityManager() + +/** + * 获取系统存储数据 + */ +export function getSystemStorage(): any { + return storageManager.getSystemStorage() +} + +/** + * 获取系统版本号 + */ +export function getSysVersion(): string | null { + return storageManager.getSystemVersion() +} + +/** + * 验证本地存储数据 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ +export function validateStorageData(requireAuth: boolean = false): boolean { + return storageManager.validateStorageData(requireAuth) +} + +/** + * 检查存储兼容性 + * @param requireAuth 是否需要验证登录状态(默认 false) + */ +export function checkStorageCompatibility(requireAuth: boolean = false): boolean { + return storageManager.checkCompatibility(requireAuth) +} diff --git a/src/utils/sys/console.ts b/src/utils/sys/console.ts new file mode 100644 index 0000000..d631087 --- /dev/null +++ b/src/utils/sys/console.ts @@ -0,0 +1,13 @@ +// ANSI 转义码生成网站 https://patorjk.com/software/taag/#p=display&f=Big&t=ABB%0A +const asciiArt = ` +\x1b[32m欢迎使用 Art Design Pro! +\x1b[0m +\x1b[36m哇!你居然在用我的项目~ 好用的话别忘了去 GitHub 点个 ★Star 呀,你的支持就是我更新的超强动力!祝使用体验满分💯 +\x1b[0m +\x1b[33mGitHub: https://github.com/Daymychen/art-design-pro +\x1b[0m +\x1b[31m技术支持(QQ群): 1038930070,和开发者一起交流~ 群里有小伙伴实时答疑,遇到问题不用慌! +\x1b[0m +` + +console.log(asciiArt) diff --git a/src/utils/sys/error-handle.ts b/src/utils/sys/error-handle.ts new file mode 100644 index 0000000..22109c2 --- /dev/null +++ b/src/utils/sys/error-handle.ts @@ -0,0 +1,102 @@ +/** + * 全局错误处理模块 + * + * 提供统一的错误捕获和处理机制 + * + * ## 主要功能 + * + * - Vue 运行时错误捕获(组件错误、生命周期错误等) + * - 全局脚本错误捕获(语法错误、运行时错误等) + * - Promise 未捕获错误处理(unhandledrejection) + * - 静态资源加载错误监控(图片、脚本、样式等) + * - 错误日志记录和上报 + * - 统一的错误处理入口 + * + * ## 使用场景 + * - 应用启动时安装全局错误处理器 + * - 捕获和记录所有类型的错误 + * - 错误上报到监控平台 + * - 提升应用稳定性和可维护性 + * - 问题排查和调试 + * + * ## 错误类型 + * + * - VueError: Vue 组件相关错误 + * - ScriptError: JavaScript 脚本错误 + * - PromiseError: Promise 未捕获的 rejection + * - ResourceError: 静态资源加载失败 + * + * @module utils/sys/error-handle + * @author Art Design Pro Team + */ +import type { App } from 'vue' + +/** + * Vue 运行时错误处理 + */ +export function vueErrorHandler(err: unknown, instance: any, info: string) { + console.error('[VueError]', err, info, instance) + // 这里可以上报到服务端,比如: + // reportError({ type: 'vue', err, info }) +} + +/** + * 全局脚本错误处理 + */ +export function scriptErrorHandler( + message: Event | string, + source?: string, + lineno?: number, + colno?: number, + error?: Error +): boolean { + console.error('[ScriptError]', { message, source, lineno, colno, error }) + // reportError({ type: 'script', message, source, lineno, colno, error }) + return true // 阻止默认控制台报错,可根据需求改 +} + +/** + * Promise 未捕获错误处理 + */ +export function registerPromiseErrorHandler() { + window.addEventListener('unhandledrejection', (event) => { + console.error('[PromiseError]', event.reason) + // reportError({ type: 'promise', reason: event.reason }) + }) +} + +/** + * 资源加载错误处理 (img, script, css...) + */ +export function registerResourceErrorHandler() { + window.addEventListener( + 'error', + (event: Event) => { + const target = event.target as HTMLElement + if ( + target && + (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK') + ) { + console.error('[ResourceError]', { + tagName: target.tagName, + src: + (target as HTMLImageElement).src || + (target as HTMLScriptElement).src || + (target as HTMLLinkElement).href + }) + // reportError({ type: 'resource', target }) + } + }, + true // 捕获阶段才能监听到资源错误 + ) +} + +/** + * 安装统一错误处理 + */ +export function setupErrorHandle(app: App) { + app.config.errorHandler = vueErrorHandler + window.onerror = scriptErrorHandler + registerPromiseErrorHandler() + registerResourceErrorHandler() +} diff --git a/src/utils/sys/index.ts b/src/utils/sys/index.ts new file mode 100644 index 0000000..a2e0729 --- /dev/null +++ b/src/utils/sys/index.ts @@ -0,0 +1,6 @@ +/** + * 系统管理相关工具函数统一导出 + */ + +export * from './upgrade' +export { default as mittBus } from './mittBus' diff --git a/src/utils/sys/mittBus.ts b/src/utils/sys/mittBus.ts new file mode 100644 index 0000000..22f0108 --- /dev/null +++ b/src/utils/sys/mittBus.ts @@ -0,0 +1,63 @@ +/** + * 全局事件总线模块 + * + * 基于 mitt 库实现的类型安全的事件总线 + * + * ## 主要功能 + * + * - 跨组件通信(发布/订阅模式) + * - 类型安全的事件定义和调用 + * - 全局事件管理(烟花效果、设置面板、搜索对话框等) + * - 解耦组件间的直接依赖 + * + * ## 使用场景 + * + * - 跨层级组件通信 + * - 全局功能触发(设置、搜索、聊天、锁屏等) + * - 特效触发(烟花效果) + * - 避免 props 层层传递 + * + * ## 用法示例 + * + * ```typescript + * // 订阅事件 + * mittBus.on('openSetting', () => { ... }) + * + * // 发布事件 + * mittBus.emit('openSetting') + * + * // 带参数的事件 + * mittBus.emit('triggerFireworks', 'image-url') + * ``` + * + * ## 已定义的事件 + * + * - triggerFireworks: 触发烟花效果(可选图片URL) + * - openSetting: 打开设置面板 + * - openSearchDialog: 打开搜索对话框 + * - openChat: 打开聊天窗口 + * - openLockScreen: 打开锁屏 + * + * @module utils/sys/mittBus + * @author Art Design Pro Team + */ +import mitt, { type Emitter } from 'mitt' + +// 定义事件类型映射 +type Events = { + // 烟花效果事件 - 可选的图片URL参数 + triggerFireworks: string | undefined + // 打开设置面板事件 - 无参数 + openSetting: void + // 打开搜索对话框事件 - 无参数 + openSearchDialog: void + // 打开聊天窗口事件 - 无参数 + openChat: void + // 打开锁屏事件 - 无参数 + openLockScreen: void +} + +// 创建类型安全的事件总线实例 +const mittBus: Emitter = mitt() + +export default mittBus diff --git a/src/utils/sys/upgrade.ts b/src/utils/sys/upgrade.ts new file mode 100644 index 0000000..53d3465 --- /dev/null +++ b/src/utils/sys/upgrade.ts @@ -0,0 +1,277 @@ +/** + * 系统版本升级管理模块 + * + * 提供完整的应用版本升级检测和处理功能 + * + * ## 主要功能 + * + * - 版本号比较和升级检测 + * - 首次访问识别和处理 + * - 旧版本数据自动清理 + * - 升级日志展示和通知 + * - 强制重新登录控制(根据升级日志配置) + * - 版本号规范化处理 + * - 旧存储结构迁移和清理 + * - 升级流程延迟执行(确保应用完全加载) + * + * ## 使用场景 + * + * - 应用启动时自动检测版本升级 + * - 版本更新后清理旧数据 + * - 向用户展示版本更新内容 + * - 重大更新时要求用户重新登录 + * - 防止旧版本数据污染新版本 + * + * ## 工作流程 + * + * 1. 检查本地存储的版本号 + * 2. 与当前应用版本对比 + * 3. 查找并清理旧版本数据 + * 4. 展示升级通知(包含更新日志) + * 5. 根据配置决定是否强制重新登录 + * 6. 更新本地版本号 + * + * @module utils/sys/upgrade + * @author Art Design Pro Team + */ +import { upgradeLogList } from '@/mock/upgrade/changeLog' +import { ElNotification } from 'element-plus' +import { useUserStore } from '@/store/modules/user' +import { StorageConfig } from '@/utils/storage/storage-config' + +/** + * 版本管理器 + * 负责处理版本比较、升级检测和数据清理 + */ +class VersionManager { + /** + * 规范化版本号字符串,移除前缀 'v' + */ + private normalizeVersion(version: string): string { + return version.replace(/^v/, '') + } + + /** + * 获取存储的版本号 + */ + private getStoredVersion(): string | null { + return localStorage.getItem(StorageConfig.VERSION_KEY) + } + + /** + * 设置版本号到存储 + */ + private setStoredVersion(version: string): void { + localStorage.setItem(StorageConfig.VERSION_KEY, version) + } + + /** + * 检查是否应该跳过升级处理 + */ + private shouldSkipUpgrade(): boolean { + return StorageConfig.CURRENT_VERSION === StorageConfig.SKIP_UPGRADE_VERSION + } + + /** + * 检查是否为首次访问 + */ + private isFirstVisit(storedVersion: string | null): boolean { + return !storedVersion + } + + /** + * 检查版本是否相同 + */ + private isSameVersion(storedVersion: string): boolean { + return storedVersion === StorageConfig.CURRENT_VERSION + } + + /** + * 查找旧的存储结构 + */ + private findLegacyStorage(): { oldSysKey: string | null; oldVersionKeys: string[] } { + const storageKeys = Object.keys(localStorage) + const currentVersionPrefix = StorageConfig.generateStorageKey('').slice(0, -1) // 移除末尾的 '-' + + // 查找旧的单一存储结构 + const oldSysKey = + storageKeys.find( + (key) => + StorageConfig.isVersionedKey(key) && key !== currentVersionPrefix && !key.includes('-') + ) || null + + // 查找旧版本的分离存储键 + const oldVersionKeys = storageKeys.filter( + (key) => + StorageConfig.isVersionedKey(key) && + !StorageConfig.isCurrentVersionKey(key) && + key.includes('-') + ) + + return { oldSysKey, oldVersionKeys } + } + + /** + * 检查是否需要重新登录 + */ + private shouldRequireReLogin(storedVersion: string): boolean { + const normalizedCurrent = this.normalizeVersion(StorageConfig.CURRENT_VERSION) + const normalizedStored = this.normalizeVersion(storedVersion) + + return upgradeLogList.value.some((item) => { + const itemVersion = this.normalizeVersion(item.version) + return ( + item.requireReLogin && itemVersion > normalizedStored && itemVersion <= normalizedCurrent + ) + }) + } + + /** + * 构建升级通知消息 + */ + private buildUpgradeMessage(requireReLogin: boolean): string { + const { title: content } = upgradeLogList.value[0] + + const messageParts = [ + `

`, + `系统已升级到 ${StorageConfig.CURRENT_VERSION} 版本,此次更新带来了以下改进:`, + `

`, + content + ] + + if (requireReLogin) { + messageParts.push( + `

升级完成,请重新登录后继续使用。

` + ) + } + + return messageParts.join('') + } + + /** + * 显示升级通知 + */ + private showUpgradeNotification(message: string): void { + ElNotification({ + title: '系统升级公告', + message, + duration: 0, + type: 'success', + dangerouslyUseHTMLString: true + }) + } + + /** + * 清理旧版本数据 + */ + private cleanupLegacyData(oldSysKey: string | null, oldVersionKeys: string[]): void { + // 清理旧的单一存储结构 + if (oldSysKey) { + localStorage.removeItem(oldSysKey) + console.info(`[Upgrade] 已清理旧存储: ${oldSysKey}`) + } + + // 清理旧版本的分离存储 + oldVersionKeys.forEach((key) => { + localStorage.removeItem(key) + console.info(`[Upgrade] 已清理旧存储: ${key}`) + }) + } + + /** + * 执行升级后的登出操作 + */ + private performLogout(): void { + try { + useUserStore().logOut() + console.info('[Upgrade] 已执行升级后登出') + } catch (error) { + console.error('[Upgrade] 升级后登出失败:', error) + } + } + + /** + * 执行升级流程 + */ + private async executeUpgrade( + storedVersion: string, + legacyStorage: ReturnType + ): Promise { + try { + if (!upgradeLogList.value.length) { + console.warn('[Upgrade] 升级日志列表为空') + return + } + + const requireReLogin = this.shouldRequireReLogin(storedVersion) + const message = this.buildUpgradeMessage(requireReLogin) + + // 显示升级通知 + this.showUpgradeNotification(message) + + // 更新版本号 + this.setStoredVersion(StorageConfig.CURRENT_VERSION) + + // 清理旧数据 + this.cleanupLegacyData(legacyStorage.oldSysKey, legacyStorage.oldVersionKeys) + + // 执行登出(如果需要) + if (requireReLogin) { + this.performLogout() + } + + console.info(`[Upgrade] 升级完成: ${storedVersion} → ${StorageConfig.CURRENT_VERSION}`) + } catch (error) { + console.error('[Upgrade] 系统升级处理失败:', error) + } + } + + /** + * 系统升级处理主流程 + */ + async processUpgrade(): Promise { + // 跳过特定版本 + if (this.shouldSkipUpgrade()) { + console.debug('[Upgrade] 跳过版本升级检查') + return + } + + const storedVersion = this.getStoredVersion() + + // 首次访问处理 + if (this.isFirstVisit(storedVersion)) { + this.setStoredVersion(StorageConfig.CURRENT_VERSION) + // console.info('[Upgrade] 首次访问,已设置当前版本') + return + } + + // 版本相同,无需升级 + if (this.isSameVersion(storedVersion!)) { + // console.debug('[Upgrade] 版本相同,无需升级') + return + } + + // 检查是否有需要升级的旧数据 + const legacyStorage = this.findLegacyStorage() + if (!legacyStorage.oldSysKey && legacyStorage.oldVersionKeys.length === 0) { + this.setStoredVersion(StorageConfig.CURRENT_VERSION) + console.info('[Upgrade] 无旧数据,已更新版本号') + return + } + + // 延迟执行升级流程,确保应用已完全加载 + setTimeout(() => { + this.executeUpgrade(storedVersion!, legacyStorage) + }, StorageConfig.UPGRADE_DELAY) + } +} + +// 创建版本管理器实例 +const versionManager = new VersionManager() + +/** + * 系统升级处理入口函数 + */ +export async function systemUpgrade(): Promise { + await versionManager.processUpgrade() +} diff --git a/src/utils/table/tableCache.ts b/src/utils/table/tableCache.ts new file mode 100644 index 0000000..045a7ce --- /dev/null +++ b/src/utils/table/tableCache.ts @@ -0,0 +1,266 @@ +/** + * 表格缓存管理模块 + * + * 提供高性能的表格数据缓存机制 + * + * ## 主要功能 + * + * - 基于参数的智能缓存键生成(使用 ohash) + * - LRU(最近最少使用)缓存淘汰策略 + * - 缓存过期时间管理 + * - 缓存大小限制和自动清理 + * - 基于标签的缓存分组管理 + * - 多种缓存失效策略(清空所有、清空当前、清空分页等) + * - 缓存访问统计和命中率分析 + * - 缓存大小估算 + * + * ## 使用场景 + * + * - 表格数据的分页缓存 + * - 减少重复的 API 请求 + * - 提升表格切换和返回的响应速度 + * - 搜索条件变化时的智能缓存管理 + * - 数据更新后的缓存失效处理 + * + * ## 缓存策略 + * + * - CLEAR_ALL: 清空所有缓存(适用于全局数据更新) + * - CLEAR_CURRENT: 仅清空当前查询条件的缓存(适用于单条数据更新) + * - CLEAR_PAGINATION: 清空所有分页缓存但保留不同搜索条件(适用于批量操作) + * - KEEP_ALL: 不清除缓存(适用于只读操作) + * + * @module utils/table/tableCache + * @author Art Design Pro Team + */ +import { hash } from 'ohash' + +// 缓存失效策略枚举 +export enum CacheInvalidationStrategy { + /** 清空所有缓存 */ + CLEAR_ALL = 'clear_all', + /** 仅清空当前查询条件的缓存 */ + CLEAR_CURRENT = 'clear_current', + /** 清空所有分页缓存(保留不同搜索条件的缓存) */ + CLEAR_PAGINATION = 'clear_pagination', + /** 不清除缓存 */ + KEEP_ALL = 'keep_all' +} + +// 通用 API 响应接口(兼容不同的后端响应格式) +export interface ApiResponse { + records?: T[] + data?: T[] + total?: number + current?: number + size?: number + [key: string]: unknown +} + +// 缓存存储接口 +export interface CacheItem { + data: T[] + response: ApiResponse + timestamp: number + params: string + // 缓存标签,用于分组管理 + tags: Set + // 访问次数(用于 LRU 算法) + accessCount: number + // 最后访问时间 + lastAccessTime: number +} + +// 增强的缓存管理类 +export class TableCache { + private cache = new Map>() + private cacheTime: number + private maxSize: number + private enableLog: boolean + + constructor(cacheTime = 5 * 60 * 1000, maxSize = 50, enableLog = false) { + // 默认5分钟,最多50条缓存 + this.cacheTime = cacheTime + this.maxSize = maxSize + this.enableLog = enableLog + } + + // 内部日志工具 + private log(message: string, ...args: any[]) { + if (this.enableLog) { + console.log(`[TableCache] ${message}`, ...args) + } + } + + // 生成稳定的缓存键 + private generateKey(params: unknown): string { + return hash(params) + } + + // 🔧 优化:增强类型安全性 + private generateTags(params: Record): Set { + const tags = new Set() + + // 添加搜索条件标签 + const searchKeys = Object.keys(params).filter( + (key) => + !['current', 'size', 'total'].includes(key) && + params[key] !== undefined && + params[key] !== '' && + params[key] !== null + ) + + if (searchKeys.length > 0) { + const searchTag = searchKeys.map((key) => `${key}:${String(params[key])}`).join('|') + tags.add(`search:${searchTag}`) + } else { + tags.add('search:default') + } + + // 添加分页标签 + tags.add(`pagination:${params.size || 10}`) + // 添加通用分页标签,用于清理所有分页缓存 + tags.add('pagination') + + return tags + } + + // 🔧 优化:LRU 缓存清理 + private evictLRU(): void { + if (this.cache.size <= this.maxSize) return + + // 找到最少使用的缓存项 + let lruKey = '' + let minAccessCount = Infinity + let oldestTime = Infinity + + for (const [key, item] of this.cache.entries()) { + if ( + item.accessCount < minAccessCount || + (item.accessCount === minAccessCount && item.lastAccessTime < oldestTime) + ) { + lruKey = key + minAccessCount = item.accessCount + oldestTime = item.lastAccessTime + } + } + + if (lruKey) { + this.cache.delete(lruKey) + this.log(`LRU 清理缓存: ${lruKey}`) + } + } + + // 设置缓存 + set(params: unknown, data: T[], response: ApiResponse): void { + const key = this.generateKey(params) + const tags = this.generateTags(params as Record) + const now = Date.now() + + // 检查是否需要清理 + this.evictLRU() + + this.cache.set(key, { + data, + response, + timestamp: now, + params: key, + tags, + accessCount: 1, + lastAccessTime: now + }) + } + + // 获取缓存 + get(params: unknown): CacheItem | null { + const key = this.generateKey(params) + const item = this.cache.get(key) + + if (!item) return null + + // 检查是否过期 + if (Date.now() - item.timestamp > this.cacheTime) { + this.cache.delete(key) + return null + } + + // 更新访问统计 + item.accessCount++ + item.lastAccessTime = Date.now() + + return item + } + + // 根据标签清除缓存 + clearByTags(tags: string[]): number { + let clearedCount = 0 + + for (const [key, item] of this.cache.entries()) { + // 检查是否包含任意一个标签 + const hasMatchingTag = tags.some((tag) => + Array.from(item.tags).some((itemTag) => itemTag.includes(tag)) + ) + + if (hasMatchingTag) { + this.cache.delete(key) + clearedCount++ + } + } + + return clearedCount + } + + // 清除当前搜索条件的缓存 + clearCurrentSearch(params: unknown): number { + const key = this.generateKey(params) + const deleted = this.cache.delete(key) + return deleted ? 1 : 0 + } + + // 清除分页缓存 + clearPagination(): number { + return this.clearByTags(['pagination']) + } + + // 清空所有缓存 + clear(): void { + this.cache.clear() + } + + // 获取缓存统计信息 + getStats(): { total: number; size: string; hitRate: string } { + const total = this.cache.size + let totalSize = 0 + let totalAccess = 0 + + for (const item of this.cache.values()) { + // 粗略估算大小(JSON字符串长度) + totalSize += JSON.stringify(item.data).length + totalAccess += item.accessCount + } + + // 转换为人类可读的大小 + const sizeInKB = (totalSize / 1024).toFixed(2) + const avgHits = total > 0 ? (totalAccess / total).toFixed(1) : '0' + + return { + total, + size: `${sizeInKB}KB`, + hitRate: `${avgHits} avg hits` + } + } + + // 清理过期缓存 + cleanupExpired(): number { + let cleanedCount = 0 + const now = Date.now() + + for (const [key, item] of this.cache.entries()) { + if (now - item.timestamp > this.cacheTime) { + this.cache.delete(key) + cleanedCount++ + } + } + + return cleanedCount + } +} diff --git a/src/utils/table/tableConfig.ts b/src/utils/table/tableConfig.ts new file mode 100644 index 0000000..9a71175 --- /dev/null +++ b/src/utils/table/tableConfig.ts @@ -0,0 +1,55 @@ +/** + * 表格全局配置模块 + * + * 提供表格与后端接口的字段映射配置 + * + * ## 主要功能 + * + * - 响应数据字段自动识别和映射 + * - 支持多种常见的后端响应格式 + * - 请求参数字段映射配置 + * - 可扩展的字段配置机制 + * + * ## 使用场景 + * + * - 适配不同后端的分页接口格式 + * - 统一前端表格组件的数据处理 + * - 减少重复的数据转换代码 + * - 支持多个后端服务的接口对接 + * + * ## 配置说明 + * + * - recordFields: 列表数据字段名(按优先级顺序查找) + * - totalFields: 总条数字段名 + * - currentFields: 当前页码字段名 + * - sizeFields: 每页大小字段名 + * - paginationKey: 前端发送请求时使用的分页参数名 + * + * ## 扩展方式 + * + * 如果后端使用其他字段名,可以在对应数组中添加新的字段名 + * 例如:recordFields: ['list', 'data', 'records', 'items', 'yourCustomField'] + * + * @module utils/table/tableConfig + * @author Art Design Pro Team + */ +export const tableConfig = { + // 响应数据字段映射配置,系统会从接口返回数据中按顺序查找这些字段 + // 列表数据 + recordFields: ['list', 'data', 'records', 'items', 'result', 'rows'], + // 总条数 + totalFields: ['total', 'count', 'totalCount'], + // 当前页码 + currentFields: ['current', 'page', 'pageNum'], + // 每页大小 + sizeFields: ['size', 'pageSize', 'limit'], + + // 请求参数映射配置,前端发送请求时使用的分页参数名 + // useTable 组合式函数传递分页参数的时候 用 current 跟 size + paginationKey: { + // 当前页码 + current: 'current', + // 每页大小 + size: 'size' + } +} diff --git a/src/utils/table/tableUtils.ts b/src/utils/table/tableUtils.ts new file mode 100644 index 0000000..3ca9db1 --- /dev/null +++ b/src/utils/table/tableUtils.ts @@ -0,0 +1,297 @@ +/** + * 表格工具函数模块 + * + * 提供表格数据处理和请求管理的核心工具函数 + * + * ## 主要功能 + * + * - 多格式 API 响应自动适配和标准化 + * - 表格数据提取和转换 + * - 分页信息自动更新和校验 + * - 智能防抖函数(支持取消和立即执行) + * - 统一的错误处理机制 + * - 嵌套数据结构解析 + * + * ## 使用场景 + * + * - useTable 组合式函数的底层工具 + * - 适配各种后端接口响应格式 + * - 表格数据的标准化处理 + * - 请求防抖和性能优化 + * - 错误统一处理和日志记录 + * + * ## 支持的响应格式 + * + * 1. 直接数组: [item1, item2, ...] + * 2. 标准对象: { records: [], total: 100 } + * 3. 嵌套data: { data: { list: [], total: 100 } } + * 4. 多种字段名: list/data/records/items/result/rows + * + * ## 核心功能 + * + * - defaultResponseAdapter: 智能识别和转换响应格式 + * - extractTableData: 提取表格数据数组 + * - updatePaginationFromResponse: 更新分页信息 + * - createSmartDebounce: 创建可控的防抖函数 + * - createErrorHandler: 生成错误处理器 + * + * @module utils/table/tableUtils + * @author Art Design Pro Team + */ + +import type { ApiResponse } from './tableCache' +import { tableConfig } from './tableConfig' + +// 请求参数基础接口,扩展分页参数 +export interface BaseRequestParams extends Api.Common.PaginationParams { + [key: string]: unknown +} + +// 错误处理接口 +export interface TableError { + code: string + message: string + details?: unknown +} + +// 辅助函数:从对象中提取记录数组 +function extractRecords(obj: Record, fields: string[]): T[] { + for (const field of fields) { + if (field in obj && Array.isArray(obj[field])) { + return obj[field] as T[] + } + } + return [] +} + +// 辅助函数:从对象中提取总数 +function extractTotal(obj: Record, records: unknown[], fields: string[]): number { + for (const field of fields) { + if (field in obj && typeof obj[field] === 'number') { + return obj[field] as number + } + } + return records.length +} + +// 辅助函数:提取分页参数 +function extractPagination( + obj: Record, + data?: Record +): Pick, 'current' | 'size'> | undefined { + const result: Partial, 'current' | 'size'>> = {} + const sources = [obj, data ?? {}] + + const currentFields = tableConfig.currentFields + for (const src of sources) { + for (const field of currentFields) { + if (field in src && typeof src[field] === 'number') { + result.current = src[field] as number + break + } + } + if (result.current !== undefined) break + } + + const sizeFields = tableConfig.sizeFields + for (const src of sources) { + for (const field of sizeFields) { + if (field in src && typeof src[field] === 'number') { + result.size = src[field] as number + break + } + } + if (result.size !== undefined) break + } + + if (result.current === undefined && result.size === undefined) return undefined + return result +} + +/** + * 默认响应适配器 - 支持多种常见的API响应格式 + */ +export const defaultResponseAdapter = (response: unknown): ApiResponse => { + // 定义支持的字段 + const recordFields = tableConfig.recordFields + + if (!response) { + return { records: [], total: 0 } + } + + if (Array.isArray(response)) { + return { records: response, total: response.length } + } + + if (typeof response !== 'object') { + console.warn( + '[tableUtils] 无法识别的响应格式,支持的格式包括: 数组、包含' + + recordFields.join('/') + + '字段的对象、嵌套data对象。当前格式:', + response + ) + return { records: [], total: 0 } + } + + const res = response as Record + let records: T[] = [] + let total = 0 + let pagination: Pick, 'current' | 'size'> | undefined + + // 处理标准格式或直接列表 + records = extractRecords(res, recordFields) + total = extractTotal(res, records, tableConfig.totalFields) + pagination = extractPagination(res) + + // 如果没有找到,检查嵌套data + if (records.length === 0 && 'data' in res && typeof res.data === 'object') { + const data = res.data as Record + records = extractRecords(data, ['list', 'records', 'items']) + total = extractTotal(data, records, tableConfig.totalFields) + pagination = extractPagination(res, data) + + if (Array.isArray(res.data)) { + records = res.data as T[] + total = records.length + } + } + + if (!recordFields.some((field) => field in res) && records.length === 0) { + console.warn('[tableUtils] 无法识别的响应格式') + console.warn('支持的字段包括: ' + recordFields.join('、'), response) + console.warn('扩展字段请到 utils/table/tableConfig 文件配置') + } + + const result: ApiResponse = { records, total } + if (pagination) { + Object.assign(result, pagination) + } + return result +} + +/** + * 从标准化的API响应中提取表格数据 + */ +export const extractTableData = (response: ApiResponse): T[] => { + const data = response.records || response.data || [] + return Array.isArray(data) ? data : [] +} + +/** + * 根据API响应更新分页信息 + */ +export const updatePaginationFromResponse = ( + pagination: Api.Common.PaginationParams, + response: ApiResponse +): void => { + pagination.total = response.total ?? pagination.total ?? 0 + + if (response.current !== undefined) { + pagination.current = response.current + } + + const maxPage = Math.max(1, Math.ceil(pagination.total / (pagination.size || 1))) + if (pagination.current > maxPage) { + pagination.current = maxPage + } +} + +/** + * 创建智能防抖函数 - 支持取消和立即执行 + */ +export const createSmartDebounce = Promise>( + fn: T, + delay: number +): T & { cancel: () => void; flush: () => Promise } => { + let timeoutId: NodeJS.Timeout | null = null + let lastArgs: Parameters | null = null + let lastResolve: ((value: any) => void) | null = null + let lastReject: ((reason: any) => void) | null = null + + const debouncedFn = (...args: Parameters): Promise => { + return new Promise((resolve, reject) => { + if (timeoutId) clearTimeout(timeoutId) + lastArgs = args + lastResolve = resolve + lastReject = reject + timeoutId = setTimeout(async () => { + try { + const result = await fn(...args) + resolve(result) + } catch (error) { + reject(error) + } finally { + timeoutId = null + lastArgs = null + lastResolve = null + lastReject = null + } + }, delay) + }) + } + + debouncedFn.cancel = () => { + if (timeoutId) clearTimeout(timeoutId) + timeoutId = null + lastArgs = null + lastResolve = null + lastReject = null + } + + debouncedFn.flush = async () => { + if (timeoutId && lastArgs && lastResolve && lastReject) { + clearTimeout(timeoutId) + timeoutId = null + const args = lastArgs + const resolve = lastResolve + const reject = lastReject + lastArgs = null + lastResolve = null + lastReject = null + try { + const result = await fn(...args) + resolve(result) + return result + } catch (error) { + reject(error) + throw error + } + } + return Promise.resolve() + } + + return debouncedFn as any +} + +/** + * 生成错误处理函数 + */ +export const createErrorHandler = ( + onError?: (error: TableError) => void, + enableLog: boolean = false +) => { + const logger = { + error: (message: string, ...args: any[]) => { + if (enableLog) console.error(`[useTable] ${message}`, ...args) + } + } + + return (err: unknown, context: string): TableError => { + const tableError: TableError = { + code: 'UNKNOWN_ERROR', + message: '未知错误', + details: err + } + + if (err instanceof Error) { + tableError.message = err.message + tableError.code = err.name + } else if (typeof err === 'string') { + tableError.message = err + } + + logger.error(`${context}:`, err) + onError?.(tableError) + return tableError + } +} diff --git a/src/utils/tencent-map.ts b/src/utils/tencent-map.ts new file mode 100644 index 0000000..809e9ab --- /dev/null +++ b/src/utils/tencent-map.ts @@ -0,0 +1,42 @@ +const DEFAULT_TENCENT_MAP_LIBRARIES = 'visualization,geometry,vector,tools,service' + +let mapScriptPromise: Promise | null = null +let scriptLoading = false +const pendingResolvers: Array<() => void> = [] + +export const loadTencentMapScript = (apiKey: string, libraries = DEFAULT_TENCENT_MAP_LIBRARIES) => { + const w = window as unknown as { TMap?: unknown } & Record + if (w.TMap) return Promise.resolve() + if (mapScriptPromise) return mapScriptPromise + mapScriptPromise = new Promise((resolve, reject) => { + pendingResolvers.push(resolve) + if (scriptLoading) return + scriptLoading = true + const callbackName = '__tencentMapInit' + if (!w[callbackName]) { + w[callbackName] = () => { + scriptLoading = false + const queue = pendingResolvers.splice(0, pendingResolvers.length) + queue.forEach((fn) => fn()) + } + } + const script = document.createElement('script') + const libParam = libraries?.trim() + script.src = libParam + ? `https://map.qq.com/api/gljs?v=1.exp&key=${apiKey}&libraries=${libParam}&callback=${callbackName}` + : `https://map.qq.com/api/gljs?v=1.exp&key=${apiKey}&callback=${callbackName}` + script.type = 'text/javascript' + script.async = true + script.defer = true + script.onerror = () => { + scriptLoading = false + mapScriptPromise = null + pendingResolvers.length = 0 + reject(new Error('Tencent map script load failed')) + } + document.body.appendChild(script) + }) + return mapScriptPromise +} + +export { DEFAULT_TENCENT_MAP_LIBRARIES } diff --git a/src/utils/ui/animation.ts b/src/utils/ui/animation.ts new file mode 100644 index 0000000..5efd02a --- /dev/null +++ b/src/utils/ui/animation.ts @@ -0,0 +1,80 @@ +/** + * 主题动画工具模块 + * + * 提供主题切换的视觉动画效果 + * + * ## 主要功能 + * + * - 基于鼠标点击位置的圆形扩散动画 + * - View Transition API 支持(现代浏览器) + * - 降级处理(不支持动画的浏览器) + * - 暗黑主题切换过渡效果 + * - 页面刷新时的主题过渡优化 + * + * ## 使用场景 + * + * - 明暗主题切换 + * - 提升用户体验的视觉反馈 + * - 页面刷新时的平滑过渡 + * + * ## 技术实现 + * + * - 使用 CSS 变量存储点击位置和半径 + * - 利用 View Transition API 实现流畅动画 + * - 通过 CSS class 控制过渡效果 + * - 自动计算最大扩散半径 + * + * @module utils/theme/animation + * @author Art Design Pro Team + */ +import { useCommon } from '@/hooks/core/useCommon' +import { useTheme } from '@/hooks/core/useTheme' +import { SystemThemeEnum } from '@/enums/appEnum' +import { useSettingStore } from '@/store/modules/setting' +const { LIGHT, DARK } = SystemThemeEnum + +/** + * 主题切换动画 + * @param e 鼠标点击事件 + */ +export const themeAnimation = (e: any) => { + const x = e.clientX + const y = e.clientY + // 计算鼠标点击位置距离视窗的最大圆半径 + const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)) + + // 设置CSS变量 + document.documentElement.style.setProperty('--x', x + 'px') + document.documentElement.style.setProperty('--y', y + 'px') + document.documentElement.style.setProperty('--r', endRadius + 'px') + + if (document.startViewTransition) { + document.startViewTransition(() => toggleTheme()) + } else { + toggleTheme() + } +} + +/** + * 切换主题 + */ +const toggleTheme = () => { + useTheme().switchThemeStyles(useSettingStore().systemThemeType === LIGHT ? DARK : LIGHT) + useCommon().refresh() +} + +/** + * 切换主题过渡效果 + * @param enable 是否启用过渡效果 + */ +export const toggleTransition = (enable: boolean) => { + const body = document.body + + if (enable) { + body.classList.add('theme-change') + } else { + setTimeout(() => { + body.classList.remove('theme-change') + }, 300) + } +} diff --git a/src/utils/ui/colors.ts b/src/utils/ui/colors.ts new file mode 100644 index 0000000..b4f6b77 --- /dev/null +++ b/src/utils/ui/colors.ts @@ -0,0 +1,273 @@ +/** + * 颜色处理工具模块 + * + * 提供完整的颜色格式转换和处理功能 + * + * ## 主要功能 + * + * - Hex 与 RGB/RGBA 格式互转 + * - 颜色混合计算 + * - 颜色变浅/变深处理 + * - Element Plus 主题色自动生成 + * - 颜色格式验证 + * - CSS 变量读取 + * - 暗黑模式颜色适配 + * + * ## 使用场景 + * + * - 主题色动态切换 + * - Element Plus 组件主题定制 + * - 颜色渐变生成 + * - 明暗主题颜色计算 + * - 颜色格式标准化 + * + * ## 核心功能 + * + * - hexToRgba: Hex 转 RGBA(支持透明度) + * - hexToRgb: Hex 转 RGB 数组 + * - rgbToHex: RGB 转 Hex + * - colourBlend: 两种颜色混合 + * - getLightColor: 生成变浅的颜色 + * - getDarkColor: 生成变深的颜色 + * - handleElementThemeColor: 处理 Element Plus 主题色 + * - setElementThemeColor: 设置完整的主题色系统 + * + * ## 支持格式 + * + * - Hex: #FFF, #FFFFFF + * - RGB: rgb(255, 255, 255) + * - RGBA: rgba(255, 255, 255, 0.5) + * + * @module utils/ui/colors + * @author Art Design Pro Team + */ +import { useSettingStore } from '@/store/modules/setting' + +/** + * 颜色转换结果接口 + */ +interface RgbaResult { + red: number + green: number + blue: number + rgba: string +} + +/** + * 获取CSS变量值(别名函数) + * @param name CSS变量名 + * @returns CSS变量值 + */ +export function getCssVar(name: string): string { + return getComputedStyle(document.documentElement).getPropertyValue(name) +} + +/** + * 验证hex颜色格式 + * @param hex hex颜色值 + * @returns 是否为有效的hex颜色 + */ +function isValidHexColor(hex: string): boolean { + const cleanHex = hex.trim().replace(/^#/, '') + return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex) +} + +/** + * 验证RGB颜色值 + * @param r 红色值 + * @param g 绿色值 + * @param b 蓝色值 + * @returns 是否为有效的RGB值 + */ +function isValidRgbValue(r: number, g: number, b: number): boolean { + const isValid = (value: number) => Number.isInteger(value) && value >= 0 && value <= 255 + return isValid(r) && isValid(g) && isValid(b) +} + +/** + * 将hex颜色转换为RGBA + * @param hex hex颜色值 (支持 #FFF 或 #FFFFFF 格式) + * @param opacity 透明度 (0-1) + * @returns 包含RGB值和RGBA字符串的对象 + */ +export function hexToRgba(hex: string, opacity: number): RgbaResult { + if (!isValidHexColor(hex)) { + throw new Error('Invalid hex color format') + } + + // 移除可能存在的 # 前缀并转换为大写 + let cleanHex = hex.trim().replace(/^#/, '').toUpperCase() + + // 如果是缩写形式(如 FFF),转换为完整形式 + if (cleanHex.length === 3) { + cleanHex = cleanHex + .split('') + .map((char) => char.repeat(2)) + .join('') + } + + // 解析 RGB 值 + const [red, green, blue] = cleanHex.match(/\w\w/g)!.map((x) => parseInt(x, 16)) + + // 确保 opacity 在有效范围内 + const validOpacity = Math.max(0, Math.min(1, opacity)) + + // 构建 RGBA 字符串 + const rgba = `rgba(${red}, ${green}, ${blue}, ${validOpacity.toFixed(2)})` + + return { red, green, blue, rgba } +} + +/** + * 将hex颜色转换为RGB数组 + * @param hexColor hex颜色值 + * @returns RGB数组 [r, g, b] + */ +export function hexToRgb(hexColor: string): number[] { + if (!isValidHexColor(hexColor)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + const cleanHex = hexColor.replace(/^#/, '') + let hex = cleanHex + + // 处理缩写形式 + if (hex.length === 3) { + hex = hex + .split('') + .map((char) => char.repeat(2)) + .join('') + } + + const hexPairs = hex.match(/../g) + if (!hexPairs) { + throw new Error('Invalid hex color format') + } + + return hexPairs.map((hexPair) => parseInt(hexPair, 16)) +} + +/** + * 将RGB颜色转换为hex + * @param r 红色值 (0-255) + * @param g 绿色值 (0-255) + * @param b 蓝色值 (0-255) + * @returns hex颜色值 + */ +export function rgbToHex(r: number, g: number, b: number): string { + if (!isValidRgbValue(r, g, b)) { + ElMessage.warning('输入错误的RGB颜色值') + throw new Error('Invalid RGB color values') + } + + const toHex = (value: number) => { + const hex = value.toString(16) + return hex.length === 1 ? `0${hex}` : hex + } + + return `#${toHex(r)}${toHex(g)}${toHex(b)}` +} + +/** + * 颜色混合 + * @param color1 第一个颜色 + * @param color2 第二个颜色 + * @param ratio 混合比例 (0-1) + * @returns 混合后的颜色 + */ +export function colourBlend(color1: string, color2: string, ratio: number): string { + const validRatio = Math.max(0, Math.min(1, Number(ratio))) + + const rgb1 = hexToRgb(color1) + const rgb2 = hexToRgb(color2) + + const blendedRgb = rgb1.map((value1, index) => { + const value2 = rgb2[index] + return Math.round(value1 * (1 - validRatio) + value2 * validRatio) + }) + + return rgbToHex(blendedRgb[0], blendedRgb[1], blendedRgb[2]) +} + +/** + * 获取变浅的颜色 + * @param color 原始颜色 + * @param level 变浅程度 (0-1) + * @param isDark 是否为暗色主题 + * @returns 变浅后的颜色 + */ +export function getLightColor(color: string, level: number, isDark: boolean = false): string { + if (!isValidHexColor(color)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + if (isDark) { + return getDarkColor(color, level) + } + + const rgb = hexToRgb(color) + const lightRgb = rgb.map((value) => Math.floor((255 - value) * level + value)) + + return rgbToHex(lightRgb[0], lightRgb[1], lightRgb[2]) +} + +/** + * 获取变深的颜色 + * @param color 原始颜色 + * @param level 变深程度 (0-1) + * @returns 变深后的颜色 + */ +export function getDarkColor(color: string, level: number): string { + if (!isValidHexColor(color)) { + ElMessage.warning('输入错误的hex颜色值') + throw new Error('Invalid hex color format') + } + + const rgb = hexToRgb(color) + const darkRgb = rgb.map((value) => Math.floor(value * (1 - level))) + + return rgbToHex(darkRgb[0], darkRgb[1], darkRgb[2]) +} + +/** + * 处理 Element Plus 主题颜色 + * @param theme 主题颜色 + * @param isDark 是否为暗色主题 + */ +export function handleElementThemeColor(theme: string, isDark: boolean = false): void { + document.documentElement.style.setProperty('--el-color-primary', theme) + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-light-${i}`, + getLightColor(theme, i / 10, isDark) + ) + } + + for (let i = 1; i <= 9; i++) { + document.documentElement.style.setProperty( + `--el-color-primary-dark-${i}`, + getDarkColor(theme, i / 10) + ) + } +} + +/** + * 设置 Element Plus 主题颜色 + * @param color 主题颜色 + */ +export function setElementThemeColor(color: string): void { + const mixColor = '#ffffff' + const elStyle = document.documentElement.style + + elStyle.setProperty('--el-color-primary', color) + handleElementThemeColor(color, useSettingStore().isDark) + + // 生成更淡一点的颜色 + for (let i = 1; i < 16; i++) { + const itemColor = colourBlend(color, mixColor, i / 16) + elStyle.setProperty(`--el-color-primary-custom-${i}`, itemColor) + } +} diff --git a/src/utils/ui/emojo.ts b/src/utils/ui/emojo.ts new file mode 100644 index 0000000..cabad7d --- /dev/null +++ b/src/utils/ui/emojo.ts @@ -0,0 +1,24 @@ +/** + * 表情 + * 用于在消息提示的时候显示对应的表情 + * + * 用法 + * ElMessage.success(`${EmojiText[200]} 图片上传成功`) + * ElMessage.error(`${EmojiText[400]} 图片上传失败`) + * ElMessage.error(`${EmojiText[500]} 图片上传失败`) + * + * @module utils/ui/emojo + * @author Art Design Pro Team + */ + +// macos 用户 按 shift + 6 可以唤出更多表情…… +const EmojiText: { [key: string]: string } = { + '0': 'O_O', // 空 + '200': '^_^', // 成功 + '400': 'T_T', // 错误请求 + '500': 'X_X' // 服务器内部错误,无法完成请求 +} + +// const EmojiIcon = ['🟢', '🔴', '🟡 ', '🚀', '✨', '💡', '🛠️', '🔥', '🎉', '🌟', '🌈'] + +export default EmojiText diff --git a/src/utils/ui/iconify-loader.ts b/src/utils/ui/iconify-loader.ts new file mode 100644 index 0000000..035de16 --- /dev/null +++ b/src/utils/ui/iconify-loader.ts @@ -0,0 +1,31 @@ +/** + * 离线图标加载器 + * + * 用于在内网环境下支持 Iconify 图标的离线加载。 + * 通过预加载图标集数据,避免运行时从 CDN 获取图标。 + * + * 使用方式: + * 1. 安装所需图标集:pnpm add -D @iconify-json/[icon-set-name] + * 2. 在此文件中导入并注册图标集 + * 3. 在组件中使用: + * + * @module utils/ui/iconify-loader + * @author Art Design Pro Team + */ + +// import { addCollection } from '@iconify/vue' + +// // 导入离线图标数据 + +// // 系统必要图标库 +// import riIcons from '@iconify-json/ri/icons.json' + +// // 演示图标库(可选,生产环境可移除) +// import svgSpinners from '@iconify-json/svg-spinners/icons.json' +// import lineMd from '@iconify-json/line-md/icons.json' + +// // 注册离线图标集 + +// addCollection(riIcons) +// addCollection(svgSpinners) +// addCollection(lineMd) diff --git a/src/utils/ui/index.ts b/src/utils/ui/index.ts new file mode 100644 index 0000000..9ca1049 --- /dev/null +++ b/src/utils/ui/index.ts @@ -0,0 +1,11 @@ +/** + * UI 相关工具函数统一导出 + * + * @module utils/ui/index + * @author Art Design Pro Team + */ + +export * from './colors' +export * from './loading' +export * from './tabs' +export * from './emojo' diff --git a/src/utils/ui/loading.ts b/src/utils/ui/loading.ts new file mode 100644 index 0000000..6580e02 --- /dev/null +++ b/src/utils/ui/loading.ts @@ -0,0 +1,84 @@ +/** + * 全局 Loading 加载管理模块 + * + * 提供统一的全屏加载动画管理 + * + * ## 主要功能 + * + * - 全屏 Loading 显示和隐藏 + * - 自动适配明暗主题背景色 + * - 自定义 SVG 加载动画 + * - 单例模式防止重复创建 + * - 锁定页面交互 + * + * ## 使用场景 + * + * - 页面初始化加载 + * - 大量数据请求 + * - 路由切换过渡 + * - 异步操作等待 + * + * ## 特性 + * + * - 自动检测当前主题并应用对应背景色 + * - 使用自定义 SVG 动画(四点旋转) + * - 单例模式确保同时只有一个 Loading + * - 提供便捷的显示/隐藏方法 + * + * @module utils/ui/loading + * @author Art Design Pro Team + */ +import { fourDotsSpinnerSvg } from '@/assets/svg/loading' + +/** + * 获取当前主题对应的loading背景色 + * @returns 背景色字符串 + */ +const getLoadingBackground = (): string => { + const isDark = document.documentElement.classList.contains('dark') + return isDark ? 'rgba(7, 7, 7, 0.85)' : '#fff' +} + +const DEFAULT_LOADING_CONFIG = { + lock: true, + get background() { + return getLoadingBackground() + }, + svg: fourDotsSpinnerSvg, + svgViewBox: '0 0 40 40', + customClass: 'art-loading-fix' +} as const + +interface LoadingInstance { + close: () => void +} + +let loadingInstance: LoadingInstance | null = null + +export const loadingService = { + /** + * 显示 loading + * @returns 关闭 loading 的函数 + */ + showLoading(): () => void { + if (!loadingInstance) { + // 每次显示时获取最新的配置,确保背景色与当前主题同步 + const config = { + ...DEFAULT_LOADING_CONFIG, + background: getLoadingBackground() + } + loadingInstance = ElLoading.service(config) + } + return () => this.hideLoading() + }, + + /** + * 隐藏 loading + */ + hideLoading(): void { + if (loadingInstance) { + loadingInstance.close() + loadingInstance = null + } + } +} diff --git a/src/utils/ui/tabs.ts b/src/utils/ui/tabs.ts new file mode 100644 index 0000000..5f53ea5 --- /dev/null +++ b/src/utils/ui/tabs.ts @@ -0,0 +1,60 @@ +/** + * 标签页布局配置模块 + * + * 提供不同标签页样式的高度和间距配置 + * + * ## 主要功能 + * + * - 多种标签页样式配置(默认、卡片、谷歌风格) + * - 标签页打开/关闭状态的高度管理 + * - 顶部间距自动计算 + * - 配置获取和默认值处理 + * + * ## 使用场景 + * + * - 工作标签页(Worktab)布局计算 + * - 页面内容区域高度调整 + * - 标签页显示/隐藏时的动画 + * - 响应式布局适配 + * + * ## 配置项说明 + * + * - openTop: 标签页显示时,内容区域距离顶部的距离 + * - closeTop: 标签页隐藏时,内容区域距离顶部的距离 + * - openHeight: 标签页显示时的总高度(包含标签栏) + * - closeHeight: 标签页隐藏时的总高度(仅头部) + * + * ## 支持的样式 + * + * - tab-default: 默认标签页样式 + * - tab-card: 卡片式标签页 + * - tab-google: 谷歌浏览器风格标签页 + * + * @module utils/ui/tabs + * @author Art Design Pro Team + */ +export const TAB_CONFIG = { + 'tab-default': { + openTop: 106, + closeTop: 60, + openHeight: 121, + closeHeight: 75 + }, + 'tab-card': { + openTop: 122, + closeTop: 78, + openHeight: 139, + closeHeight: 95 + }, + 'tab-google': { + openTop: 122, + closeTop: 78, + openHeight: 139, + closeHeight: 95 + } +} + +// 获取当前 tab 样式配置,设置默认值 +export const getTabConfig = (style: string) => { + return TAB_CONFIG[style as keyof typeof TAB_CONFIG] || TAB_CONFIG['tab-card'] // 默认使用 tab-card 配置 +} diff --git a/src/views/announcement-drafts/index.vue b/src/views/announcement-drafts/index.vue new file mode 100644 index 0000000..fde6eb1 --- /dev/null +++ b/src/views/announcement-drafts/index.vue @@ -0,0 +1,717 @@ + + + diff --git a/src/views/app/announcements/detail.vue b/src/views/app/announcements/detail.vue new file mode 100644 index 0000000..eabebe5 --- /dev/null +++ b/src/views/app/announcements/detail.vue @@ -0,0 +1,260 @@ + + + + diff --git a/src/views/app/announcements/index.vue b/src/views/app/announcements/index.vue new file mode 100644 index 0000000..98b8516 --- /dev/null +++ b/src/views/app/announcements/index.vue @@ -0,0 +1,333 @@ + + + + diff --git a/src/views/auth/forget-password/index.vue b/src/views/auth/forget-password/index.vue new file mode 100644 index 0000000..147259e --- /dev/null +++ b/src/views/auth/forget-password/index.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/views/auth/login/index.vue b/src/views/auth/login/index.vue new file mode 100644 index 0000000..0ef161c --- /dev/null +++ b/src/views/auth/login/index.vue @@ -0,0 +1,355 @@ + + + + + + + + diff --git a/src/views/auth/login/style.css b/src/views/auth/login/style.css new file mode 100644 index 0000000..bd8c3a9 --- /dev/null +++ b/src/views/auth/login/style.css @@ -0,0 +1,38 @@ +@reference '@styles/core/tailwind.css'; + +/* 授权页右侧区域 */ +.auth-right-wrap { + @apply absolute inset-0 w-[440px] h-[650px] py-[5px] m-auto overflow-hidden + max-sm:px-7 max-sm:w-full + animate-[slideInRight_0.6s_cubic-bezier(0.25,0.46,0.45,0.94)_forwards] + max-md:animate-none; + + .form { + @apply h-full py-[40px]; + } + + .title { + @apply text-g-900 text-4xl font-semibold max-md:text-3xl max-sm:pt-10; + } + + .sub-title { + @apply mt-[10px] text-g-600 text-sm; + } + + .custom-height { + @apply !h-[40px]; + } +} + +/* 滑入动画 */ +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(30px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/src/views/auth/register/index.vue b/src/views/auth/register/index.vue new file mode 100644 index 0000000..1e8683c --- /dev/null +++ b/src/views/auth/register/index.vue @@ -0,0 +1,668 @@ + + + diff --git a/src/views/auth/reset-password/index.vue b/src/views/auth/reset-password/index.vue new file mode 100644 index 0000000..b3ed715 --- /dev/null +++ b/src/views/auth/reset-password/index.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/src/views/dashboard/console/index.vue b/src/views/dashboard/console/index.vue new file mode 100644 index 0000000..154c330 --- /dev/null +++ b/src/views/dashboard/console/index.vue @@ -0,0 +1,41 @@ + + + + diff --git a/src/views/dashboard/console/modules/about-project.vue b/src/views/dashboard/console/modules/about-project.vue new file mode 100644 index 0000000..ed946ce --- /dev/null +++ b/src/views/dashboard/console/modules/about-project.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/views/dashboard/console/modules/active-user.vue b/src/views/dashboard/console/modules/active-user.vue new file mode 100644 index 0000000..da740f2 --- /dev/null +++ b/src/views/dashboard/console/modules/active-user.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/views/dashboard/console/modules/card-list.vue b/src/views/dashboard/console/modules/card-list.vue new file mode 100644 index 0000000..5fc76a7 --- /dev/null +++ b/src/views/dashboard/console/modules/card-list.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/views/dashboard/console/modules/dynamic-stats.vue b/src/views/dashboard/console/modules/dynamic-stats.vue new file mode 100644 index 0000000..1876950 --- /dev/null +++ b/src/views/dashboard/console/modules/dynamic-stats.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/views/dashboard/console/modules/new-user.vue b/src/views/dashboard/console/modules/new-user.vue new file mode 100644 index 0000000..9d39522 --- /dev/null +++ b/src/views/dashboard/console/modules/new-user.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/src/views/dashboard/console/modules/sales-overview.vue b/src/views/dashboard/console/modules/sales-overview.vue new file mode 100644 index 0000000..32904b8 --- /dev/null +++ b/src/views/dashboard/console/modules/sales-overview.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/views/dashboard/console/modules/todo-list.vue b/src/views/dashboard/console/modules/todo-list.vue new file mode 100644 index 0000000..ab9a86c --- /dev/null +++ b/src/views/dashboard/console/modules/todo-list.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/views/exception/403/index.vue b/src/views/exception/403/index.vue new file mode 100644 index 0000000..2756c42 --- /dev/null +++ b/src/views/exception/403/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/src/views/exception/404/index.vue b/src/views/exception/404/index.vue new file mode 100644 index 0000000..6b64f45 --- /dev/null +++ b/src/views/exception/404/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/src/views/exception/500/index.vue b/src/views/exception/500/index.vue new file mode 100644 index 0000000..1b26377 --- /dev/null +++ b/src/views/exception/500/index.vue @@ -0,0 +1,16 @@ + + + + diff --git a/src/views/index/index.vue b/src/views/index/index.vue new file mode 100644 index 0000000..415a436 --- /dev/null +++ b/src/views/index/index.vue @@ -0,0 +1,29 @@ + + + + + + diff --git a/src/views/index/style.scss b/src/views/index/style.scss new file mode 100644 index 0000000..c89f354 --- /dev/null +++ b/src/views/index/style.scss @@ -0,0 +1,93 @@ +.app-layout { + display: flex; + width: 100%; + min-height: 100vh; + background: var(--default-bg-color); + + #app-sidebar { + flex-shrink: 0; + } + + #app-main { + display: flex; + flex: 1; + flex-direction: column; + min-width: 0; + height: 100vh; + overflow: auto; + + #app-header { + position: sticky; + top: 0; + z-index: 50; + flex-shrink: 0; + width: 100%; + } + + #app-content { + flex: 1; + + :deep(.layout-content) { + box-sizing: border-box; + width: calc(100% - 40px); + margin: auto; + + // 子页面默认 style + .page-content { + position: relative; + box-sizing: border-box; + padding: 20px; + overflow: hidden; + background: var(--default-box-color); + border-radius: calc(var(--custom-radius) / 2 + 2px) !important; + } + } + } + } +} + +@media only screen and (width <= 1180px) { + .app-layout { + #app-main { + height: 100dvh; + } + } +} + +@media only screen and (width <= 800px) { + .app-layout { + position: relative; + + #app-sidebar { + position: fixed; + top: 0; + left: 0; + z-index: 300; + height: 100vh; + } + + #app-main { + width: 100%; + height: auto; + overflow: visible; + + #app-content { + :deep(.layout-content) { + width: calc(100% - 40px); + } + } + } + } +} + +@media only screen and (width <= 640px) { + .app-layout { + #app-main { + #app-content { + :deep(.layout-content) { + width: calc(100% - 30px); + } + } + } + } +} diff --git a/src/views/merchant/detail/components/AuditHistoryTab.vue b/src/views/merchant/detail/components/AuditHistoryTab.vue new file mode 100644 index 0000000..04c2d7f --- /dev/null +++ b/src/views/merchant/detail/components/AuditHistoryTab.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/views/merchant/detail/components/BasicInfo.vue b/src/views/merchant/detail/components/BasicInfo.vue new file mode 100644 index 0000000..a0d156d --- /dev/null +++ b/src/views/merchant/detail/components/BasicInfo.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/views/merchant/detail/components/ChangeHistoryTab.vue b/src/views/merchant/detail/components/ChangeHistoryTab.vue new file mode 100644 index 0000000..bd28581 --- /dev/null +++ b/src/views/merchant/detail/components/ChangeHistoryTab.vue @@ -0,0 +1,71 @@ + + + diff --git a/src/views/merchant/detail/components/StoresTab.vue b/src/views/merchant/detail/components/StoresTab.vue new file mode 100644 index 0000000..a712213 --- /dev/null +++ b/src/views/merchant/detail/components/StoresTab.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/views/merchant/detail/components/SubjectInfo.vue b/src/views/merchant/detail/components/SubjectInfo.vue new file mode 100644 index 0000000..704a04a --- /dev/null +++ b/src/views/merchant/detail/components/SubjectInfo.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/views/merchant/detail/index.vue b/src/views/merchant/detail/index.vue new file mode 100644 index 0000000..389f472 --- /dev/null +++ b/src/views/merchant/detail/index.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/src/views/merchant/detail/modules/EditDialog.vue b/src/views/merchant/detail/modules/EditDialog.vue new file mode 100644 index 0000000..d434e2d --- /dev/null +++ b/src/views/merchant/detail/modules/EditDialog.vue @@ -0,0 +1,163 @@ + + + diff --git a/src/views/merchant/list/index.vue b/src/views/merchant/list/index.vue new file mode 100644 index 0000000..5e2dac6 --- /dev/null +++ b/src/views/merchant/list/index.vue @@ -0,0 +1,200 @@ + + + diff --git a/src/views/merchant/list/modules/merchant-search.vue b/src/views/merchant/list/modules/merchant-search.vue new file mode 100644 index 0000000..6b03c23 --- /dev/null +++ b/src/views/merchant/list/modules/merchant-search.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/views/merchant/list/types.ts b/src/views/merchant/list/types.ts new file mode 100644 index 0000000..be31f7a --- /dev/null +++ b/src/views/merchant/list/types.ts @@ -0,0 +1,9 @@ +import type { MerchantStatus } from '@/enums/MerchantStatus' +import type { OperatingMode } from '@/enums/OperatingMode' + +export interface MerchantListSearchForm { + keyword: string + status?: MerchantStatus + operatingMode?: OperatingMode + tenantId?: string +} diff --git a/src/views/merchant/review/components/ReviewDialog.vue b/src/views/merchant/review/components/ReviewDialog.vue new file mode 100644 index 0000000..18e395e --- /dev/null +++ b/src/views/merchant/review/components/ReviewDialog.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/views/merchant/review/index.vue b/src/views/merchant/review/index.vue new file mode 100644 index 0000000..545199c --- /dev/null +++ b/src/views/merchant/review/index.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/src/views/merchant/review/modules/review-search.vue b/src/views/merchant/review/modules/review-search.vue new file mode 100644 index 0000000..b0f60df --- /dev/null +++ b/src/views/merchant/review/modules/review-search.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/views/merchant/review/types/index.ts b/src/views/merchant/review/types/index.ts new file mode 100644 index 0000000..d9d64a2 --- /dev/null +++ b/src/views/merchant/review/types/index.ts @@ -0,0 +1,6 @@ +import type { OperatingMode } from '@/enums/OperatingMode' + +export interface MerchantReviewSearchForm { + keyword: string + operatingMode?: OperatingMode +} diff --git a/src/views/onboarding/error/index.vue b/src/views/onboarding/error/index.vue new file mode 100644 index 0000000..50e1028 --- /dev/null +++ b/src/views/onboarding/error/index.vue @@ -0,0 +1,459 @@ + + + + diff --git a/src/views/onboarding/pricing/index.vue b/src/views/onboarding/pricing/index.vue new file mode 100644 index 0000000..29837d9 --- /dev/null +++ b/src/views/onboarding/pricing/index.vue @@ -0,0 +1,513 @@ + + + + + diff --git a/src/views/onboarding/status/index.vue b/src/views/onboarding/status/index.vue new file mode 100644 index 0000000..db995af --- /dev/null +++ b/src/views/onboarding/status/index.vue @@ -0,0 +1,689 @@ + + + + + diff --git a/src/views/onboarding/terms-of-service/index.vue b/src/views/onboarding/terms-of-service/index.vue new file mode 100644 index 0000000..b4df1df --- /dev/null +++ b/src/views/onboarding/terms-of-service/index.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/src/views/onboarding/waiting/index.vue b/src/views/onboarding/waiting/index.vue new file mode 100644 index 0000000..ac437cd --- /dev/null +++ b/src/views/onboarding/waiting/index.vue @@ -0,0 +1,376 @@ + + + + + diff --git a/src/views/outside/Iframe.vue b/src/views/outside/Iframe.vue new file mode 100644 index 0000000..33ea0dc --- /dev/null +++ b/src/views/outside/Iframe.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/views/platform/announcements/create.vue b/src/views/platform/announcements/create.vue new file mode 100644 index 0000000..e5e0f62 --- /dev/null +++ b/src/views/platform/announcements/create.vue @@ -0,0 +1,434 @@ + + + diff --git a/src/views/platform/announcements/detail.vue b/src/views/platform/announcements/detail.vue new file mode 100644 index 0000000..9e6de58 --- /dev/null +++ b/src/views/platform/announcements/detail.vue @@ -0,0 +1,504 @@ + + + + + diff --git a/src/views/platform/announcements/edit.vue b/src/views/platform/announcements/edit.vue new file mode 100644 index 0000000..53f56d4 --- /dev/null +++ b/src/views/platform/announcements/edit.vue @@ -0,0 +1,564 @@ + + + diff --git a/src/views/platform/announcements/index.vue b/src/views/platform/announcements/index.vue new file mode 100644 index 0000000..c602b0f --- /dev/null +++ b/src/views/platform/announcements/index.vue @@ -0,0 +1,460 @@ + + + diff --git a/src/views/platform/qualification-alerts/index.vue b/src/views/platform/qualification-alerts/index.vue new file mode 100644 index 0000000..503514d --- /dev/null +++ b/src/views/platform/qualification-alerts/index.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/src/views/platform/store-audits/components/StoreAuditDetailDrawer.vue b/src/views/platform/store-audits/components/StoreAuditDetailDrawer.vue new file mode 100644 index 0000000..f46d6fb --- /dev/null +++ b/src/views/platform/store-audits/components/StoreAuditDetailDrawer.vue @@ -0,0 +1,545 @@ + + + + + diff --git a/src/views/platform/store-audits/components/StoreRiskControlDialog.vue b/src/views/platform/store-audits/components/StoreRiskControlDialog.vue new file mode 100644 index 0000000..f39ee6c --- /dev/null +++ b/src/views/platform/store-audits/components/StoreRiskControlDialog.vue @@ -0,0 +1,178 @@ + + + diff --git a/src/views/platform/store-audits/index.vue b/src/views/platform/store-audits/index.vue new file mode 100644 index 0000000..4257826 --- /dev/null +++ b/src/views/platform/store-audits/index.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/src/views/result/fail/index.vue b/src/views/result/fail/index.vue new file mode 100644 index 0000000..8fe2583 --- /dev/null +++ b/src/views/result/fail/index.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/views/result/success/index.vue b/src/views/result/success/index.vue new file mode 100644 index 0000000..ae57aba --- /dev/null +++ b/src/views/result/success/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/views/store/store-detail/components/BusinessHoursPanel.vue b/src/views/store/store-detail/components/BusinessHoursPanel.vue new file mode 100644 index 0000000..fec890c --- /dev/null +++ b/src/views/store/store-detail/components/BusinessHoursPanel.vue @@ -0,0 +1,227 @@ + + + diff --git a/src/views/store/store-detail/components/DeliveryZoneMapEditor.vue b/src/views/store/store-detail/components/DeliveryZoneMapEditor.vue new file mode 100644 index 0000000..a770dca --- /dev/null +++ b/src/views/store/store-detail/components/DeliveryZoneMapEditor.vue @@ -0,0 +1,485 @@ + + + diff --git a/src/views/store/store-detail/components/DeliveryZonePolygonDialog.vue b/src/views/store/store-detail/components/DeliveryZonePolygonDialog.vue new file mode 100644 index 0000000..a12104d --- /dev/null +++ b/src/views/store/store-detail/components/DeliveryZonePolygonDialog.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/src/views/store/store-detail/components/StoreFeePanel.vue b/src/views/store/store-detail/components/StoreFeePanel.vue new file mode 100644 index 0000000..e12c9be --- /dev/null +++ b/src/views/store/store-detail/components/StoreFeePanel.vue @@ -0,0 +1,549 @@ + + + + + diff --git a/src/views/store/store-detail/components/StoreQualificationPanel.vue b/src/views/store/store-detail/components/StoreQualificationPanel.vue new file mode 100644 index 0000000..23186d5 --- /dev/null +++ b/src/views/store/store-detail/components/StoreQualificationPanel.vue @@ -0,0 +1,431 @@ + + + diff --git a/src/views/store/store-detail/components/TemporaryHoursPanel.vue b/src/views/store/store-detail/components/TemporaryHoursPanel.vue new file mode 100644 index 0000000..f0b726e --- /dev/null +++ b/src/views/store/store-detail/components/TemporaryHoursPanel.vue @@ -0,0 +1,348 @@ + + + diff --git a/src/views/store/store-list/components/BusinessStatusDialog.vue b/src/views/store/store-list/components/BusinessStatusDialog.vue new file mode 100644 index 0000000..4d763b8 --- /dev/null +++ b/src/views/store/store-list/components/BusinessStatusDialog.vue @@ -0,0 +1,222 @@ + + + diff --git a/src/views/store/store-list/components/StoreDetailDrawer.vue b/src/views/store/store-list/components/StoreDetailDrawer.vue new file mode 100644 index 0000000..8240a57 --- /dev/null +++ b/src/views/store/store-list/components/StoreDetailDrawer.vue @@ -0,0 +1,232 @@ + + + diff --git a/src/views/store/store-list/components/StoreFormDialog.vue b/src/views/store/store-list/components/StoreFormDialog.vue new file mode 100644 index 0000000..4c36f59 --- /dev/null +++ b/src/views/store/store-list/components/StoreFormDialog.vue @@ -0,0 +1,662 @@ + + + + + diff --git a/src/views/store/store-list/index.vue b/src/views/store/store-list/index.vue new file mode 100644 index 0000000..bfaf744 --- /dev/null +++ b/src/views/store/store-list/index.vue @@ -0,0 +1,326 @@ + + + diff --git a/src/views/store/store-list/modules/store-search.vue b/src/views/store/store-list/modules/store-search.vue new file mode 100644 index 0000000..088bace --- /dev/null +++ b/src/views/store/store-list/modules/store-search.vue @@ -0,0 +1,111 @@ + + + diff --git a/src/views/system/dictionary-label-override/components/PlatformLabelOverrideFormDialog.vue b/src/views/system/dictionary-label-override/components/PlatformLabelOverrideFormDialog.vue new file mode 100644 index 0000000..47b6475 --- /dev/null +++ b/src/views/system/dictionary-label-override/components/PlatformLabelOverrideFormDialog.vue @@ -0,0 +1,373 @@ + + + + + diff --git a/src/views/system/dictionary-label-override/index.vue b/src/views/system/dictionary-label-override/index.vue new file mode 100644 index 0000000..0e3b08d --- /dev/null +++ b/src/views/system/dictionary-label-override/index.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/src/views/system/dictionary-metrics/index.vue b/src/views/system/dictionary-metrics/index.vue new file mode 100644 index 0000000..8c3fbbf --- /dev/null +++ b/src/views/system/dictionary-metrics/index.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/src/views/system/dictionary/components/GroupFormDialog.vue b/src/views/system/dictionary/components/GroupFormDialog.vue new file mode 100644 index 0000000..f0bc028 --- /dev/null +++ b/src/views/system/dictionary/components/GroupFormDialog.vue @@ -0,0 +1,222 @@ + + + + + diff --git a/src/views/system/dictionary/components/GroupList.vue b/src/views/system/dictionary/components/GroupList.vue new file mode 100644 index 0000000..d4aafe2 --- /dev/null +++ b/src/views/system/dictionary/components/GroupList.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/src/views/system/dictionary/components/I18nValueEditor.vue b/src/views/system/dictionary/components/I18nValueEditor.vue new file mode 100644 index 0000000..87317d1 --- /dev/null +++ b/src/views/system/dictionary/components/I18nValueEditor.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/src/views/system/dictionary/components/ImportDialog.vue b/src/views/system/dictionary/components/ImportDialog.vue new file mode 100644 index 0000000..6662158 --- /dev/null +++ b/src/views/system/dictionary/components/ImportDialog.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/src/views/system/dictionary/components/ItemFormDialog.vue b/src/views/system/dictionary/components/ItemFormDialog.vue new file mode 100644 index 0000000..4e74357 --- /dev/null +++ b/src/views/system/dictionary/components/ItemFormDialog.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/src/views/system/dictionary/components/ItemTable.vue b/src/views/system/dictionary/components/ItemTable.vue new file mode 100644 index 0000000..02096f1 --- /dev/null +++ b/src/views/system/dictionary/components/ItemTable.vue @@ -0,0 +1,424 @@ + + + + + diff --git a/src/views/system/dictionary/components/__tests__/I18nValueEditor.spec.ts b/src/views/system/dictionary/components/__tests__/I18nValueEditor.spec.ts new file mode 100644 index 0000000..c673b14 --- /dev/null +++ b/src/views/system/dictionary/components/__tests__/I18nValueEditor.spec.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from 'vitest' +import { defineComponent, nextTick } from 'vue' +import { mount } from '@vue/test-utils' +import { createI18n } from 'vue-i18n' +import I18nValueEditor from '../I18nValueEditor.vue' + +const ElTabsStub = defineComponent({ + name: 'ElTabs', + props: { + modelValue: { + type: String, + default: '' + } + }, + emits: ['update:modelValue'], + template: '
' +}) + +const ElTabPaneStub = defineComponent({ + name: 'ElTabPane', + props: { + name: { + type: String, + default: '' + }, + label: { + type: String, + default: '' + } + }, + template: '
' +}) + +const ElInputStub = defineComponent({ + name: 'ElInput', + props: { + modelValue: { + type: String, + default: '' + } + }, + emits: ['update:modelValue', 'input'], + template: '', + methods: { + handleInput(event: Event) { + const target = event.target as HTMLInputElement + this.$emit('update:modelValue', target.value) + this.$emit('input', target.value) + } + } +}) + +describe('I18nValueEditor', () => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { + en: { + dictionary: { + i18n: { + zh: 'Chinese', + en: 'English', + zhPlaceholder: 'Chinese content', + enPlaceholder: 'English content', + hint: 'Fill at least one language.' + } + } + } + } + }) + + const mountEditor = (props?: Record) => { + return mount(I18nValueEditor, { + props, + global: { + plugins: [i18n], + stubs: { + ElTabs: ElTabsStub, + ElTabPane: ElTabPaneStub, + ElInput: ElInputStub + } + } + }) + } + + it('renders tabs for zh-CN and en', () => { + const wrapper = mountEditor() + const panes = wrapper.findAll('.tab-pane') + + expect(panes).toHaveLength(2) + expect(panes[0].attributes('data-name')).toBe('zh-CN') + expect(panes[1].attributes('data-name')).toBe('en') + }) + + it('validates at least one language is filled', async () => { + const wrapper = mountEditor({ modelValue: {} }) + + expect(wrapper.find('.i18n-hint').exists()).toBe(true) + + await wrapper.setProps({ modelValue: { 'zh-CN': '测试' } }) + await nextTick() + + expect(wrapper.find('.i18n-hint').exists()).toBe(false) + }) + + it('emits correct value object on input', async () => { + const wrapper = mountEditor() + const inputs = wrapper.findAll('input') + + await inputs[0].setValue('你好') + + const emitted = wrapper.emitted('update:modelValue') + expect(emitted).toBeTruthy() + expect(emitted?.[0][0]).toMatchObject({ 'zh-CN': '你好' }) + }) +}) diff --git a/src/views/system/dictionary/index.vue b/src/views/system/dictionary/index.vue new file mode 100644 index 0000000..498eedd --- /dev/null +++ b/src/views/system/dictionary/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue new file mode 100644 index 0000000..973b1e7 --- /dev/null +++ b/src/views/system/menu/index.vue @@ -0,0 +1,479 @@ + + + + diff --git a/src/views/system/menu/modules/menu-dialog.vue b/src/views/system/menu/modules/menu-dialog.vue new file mode 100644 index 0000000..f512301 --- /dev/null +++ b/src/views/system/menu/modules/menu-dialog.vue @@ -0,0 +1,384 @@ + + + diff --git a/src/views/system/permission/index.vue b/src/views/system/permission/index.vue new file mode 100644 index 0000000..6338d4a --- /dev/null +++ b/src/views/system/permission/index.vue @@ -0,0 +1,274 @@ + + + diff --git a/src/views/system/role-template/index.vue b/src/views/system/role-template/index.vue new file mode 100644 index 0000000..321a2cd --- /dev/null +++ b/src/views/system/role-template/index.vue @@ -0,0 +1,276 @@ + + + + + + diff --git a/src/views/system/role-template/modules/role-template-dialog.vue b/src/views/system/role-template/modules/role-template-dialog.vue new file mode 100644 index 0000000..b757b72 --- /dev/null +++ b/src/views/system/role-template/modules/role-template-dialog.vue @@ -0,0 +1,182 @@ + + + diff --git a/src/views/system/role-template/modules/role-template-permission-dialog.vue b/src/views/system/role-template/modules/role-template-permission-dialog.vue new file mode 100644 index 0000000..2308269 --- /dev/null +++ b/src/views/system/role-template/modules/role-template-permission-dialog.vue @@ -0,0 +1,452 @@ + + + + + diff --git a/src/views/system/role-template/modules/role-template-search.vue b/src/views/system/role-template/modules/role-template-search.vue new file mode 100644 index 0000000..89280c4 --- /dev/null +++ b/src/views/system/role-template/modules/role-template-search.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/views/system/tenant-role/index.vue b/src/views/system/tenant-role/index.vue new file mode 100644 index 0000000..6a87b81 --- /dev/null +++ b/src/views/system/tenant-role/index.vue @@ -0,0 +1,305 @@ + + + + + + diff --git a/src/views/system/tenant-role/modules/role-edit-dialog.vue b/src/views/system/tenant-role/modules/role-edit-dialog.vue new file mode 100644 index 0000000..dd161cc --- /dev/null +++ b/src/views/system/tenant-role/modules/role-edit-dialog.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/src/views/system/tenant-role/modules/role-permission-dialog.vue b/src/views/system/tenant-role/modules/role-permission-dialog.vue new file mode 100644 index 0000000..8b47698 --- /dev/null +++ b/src/views/system/tenant-role/modules/role-permission-dialog.vue @@ -0,0 +1,433 @@ + + + + + diff --git a/src/views/system/tenant-role/modules/role-search.vue b/src/views/system/tenant-role/modules/role-search.vue new file mode 100644 index 0000000..3ce769e --- /dev/null +++ b/src/views/system/tenant-role/modules/role-search.vue @@ -0,0 +1,66 @@ + + + diff --git a/src/views/system/user-center/index.vue b/src/views/system/user-center/index.vue new file mode 100644 index 0000000..1093050 --- /dev/null +++ b/src/views/system/user-center/index.vue @@ -0,0 +1,247 @@ + + + + diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue new file mode 100644 index 0000000..6d2d3b0 --- /dev/null +++ b/src/views/system/user/index.vue @@ -0,0 +1,1211 @@ + + + + + diff --git a/src/views/system/user/modules/user-dialog.vue b/src/views/system/user/modules/user-dialog.vue new file mode 100644 index 0000000..8c7ba32 --- /dev/null +++ b/src/views/system/user/modules/user-dialog.vue @@ -0,0 +1,496 @@ + + + diff --git a/src/views/system/user/modules/user-search.vue b/src/views/system/user/modules/user-search.vue new file mode 100644 index 0000000..4f65d7b --- /dev/null +++ b/src/views/system/user/modules/user-search.vue @@ -0,0 +1,174 @@ + + + diff --git a/src/views/tenant/announcements/components/AnnouncementDetailDrawer.vue b/src/views/tenant/announcements/components/AnnouncementDetailDrawer.vue new file mode 100644 index 0000000..ea74cbc --- /dev/null +++ b/src/views/tenant/announcements/components/AnnouncementDetailDrawer.vue @@ -0,0 +1,404 @@ + + + diff --git a/src/views/tenant/announcements/components/AnnouncementFormDialog.vue b/src/views/tenant/announcements/components/AnnouncementFormDialog.vue new file mode 100644 index 0000000..0c71481 --- /dev/null +++ b/src/views/tenant/announcements/components/AnnouncementFormDialog.vue @@ -0,0 +1,463 @@ + + + diff --git a/src/views/tenant/announcements/create.vue b/src/views/tenant/announcements/create.vue new file mode 100644 index 0000000..937859b --- /dev/null +++ b/src/views/tenant/announcements/create.vue @@ -0,0 +1,746 @@ + + + diff --git a/src/views/tenant/announcements/edit.vue b/src/views/tenant/announcements/edit.vue new file mode 100644 index 0000000..9c2e29c --- /dev/null +++ b/src/views/tenant/announcements/edit.vue @@ -0,0 +1,621 @@ + + + diff --git a/src/views/tenant/announcements/index.vue b/src/views/tenant/announcements/index.vue new file mode 100644 index 0000000..6b12240 --- /dev/null +++ b/src/views/tenant/announcements/index.vue @@ -0,0 +1,698 @@ + + + + + diff --git a/src/views/tenant/billing/components/BatchActionToolbar.vue b/src/views/tenant/billing/components/BatchActionToolbar.vue new file mode 100644 index 0000000..5599881 --- /dev/null +++ b/src/views/tenant/billing/components/BatchActionToolbar.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/views/tenant/billing/components/BillingDetailDrawer.vue b/src/views/tenant/billing/components/BillingDetailDrawer.vue new file mode 100644 index 0000000..fb25f54 --- /dev/null +++ b/src/views/tenant/billing/components/BillingDetailDrawer.vue @@ -0,0 +1,545 @@ + + + diff --git a/src/views/tenant/billing/components/CreateBillingDialog.vue b/src/views/tenant/billing/components/CreateBillingDialog.vue new file mode 100644 index 0000000..c1425ff --- /dev/null +++ b/src/views/tenant/billing/components/CreateBillingDialog.vue @@ -0,0 +1,373 @@ + + + diff --git a/src/views/tenant/billing/components/ExportDialog.vue b/src/views/tenant/billing/components/ExportDialog.vue new file mode 100644 index 0000000..8961635 --- /dev/null +++ b/src/views/tenant/billing/components/ExportDialog.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/src/views/tenant/billing/components/RecordPaymentDialog.vue b/src/views/tenant/billing/components/RecordPaymentDialog.vue new file mode 100644 index 0000000..1764ef9 --- /dev/null +++ b/src/views/tenant/billing/components/RecordPaymentDialog.vue @@ -0,0 +1,289 @@ + + + + + diff --git a/src/views/tenant/billing/index.vue b/src/views/tenant/billing/index.vue new file mode 100644 index 0000000..8c9ff7d --- /dev/null +++ b/src/views/tenant/billing/index.vue @@ -0,0 +1,1233 @@ + + + + + diff --git a/src/views/tenant/billing/statistics.vue b/src/views/tenant/billing/statistics.vue new file mode 100644 index 0000000..fe4f40b --- /dev/null +++ b/src/views/tenant/billing/statistics.vue @@ -0,0 +1,443 @@ + + + + + diff --git a/src/views/tenant/dashboard/index.vue b/src/views/tenant/dashboard/index.vue new file mode 100644 index 0000000..ced627e --- /dev/null +++ b/src/views/tenant/dashboard/index.vue @@ -0,0 +1,538 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/CustomItemsPanel.vue b/src/views/tenant/dictionary-override/components/CustomItemsPanel.vue new file mode 100644 index 0000000..a280abf --- /dev/null +++ b/src/views/tenant/dictionary-override/components/CustomItemsPanel.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/DualPaneView.vue b/src/views/tenant/dictionary-override/components/DualPaneView.vue new file mode 100644 index 0000000..b675863 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/DualPaneView.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/LabelOverrideFormDialog.vue b/src/views/tenant/dictionary-override/components/LabelOverrideFormDialog.vue new file mode 100644 index 0000000..3b1aae2 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/LabelOverrideFormDialog.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/LabelOverridePanel.vue b/src/views/tenant/dictionary-override/components/LabelOverridePanel.vue new file mode 100644 index 0000000..2a597a8 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/LabelOverridePanel.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/OverrideToggle.vue b/src/views/tenant/dictionary-override/components/OverrideToggle.vue new file mode 100644 index 0000000..a6136c9 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/OverrideToggle.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/SortableDragDrop.vue b/src/views/tenant/dictionary-override/components/SortableDragDrop.vue new file mode 100644 index 0000000..8336493 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/SortableDragDrop.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/components/SystemItemsPanel.vue b/src/views/tenant/dictionary-override/components/SystemItemsPanel.vue new file mode 100644 index 0000000..8776873 --- /dev/null +++ b/src/views/tenant/dictionary-override/components/SystemItemsPanel.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/views/tenant/dictionary-override/index.vue b/src/views/tenant/dictionary-override/index.vue new file mode 100644 index 0000000..5a8d81a --- /dev/null +++ b/src/views/tenant/dictionary-override/index.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/src/views/tenant/dictionary/index.vue b/src/views/tenant/dictionary/index.vue new file mode 100644 index 0000000..30da699 --- /dev/null +++ b/src/views/tenant/dictionary/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/src/views/tenant/package/composables/useFeatureStrategyTable.ts b/src/views/tenant/package/composables/useFeatureStrategyTable.ts new file mode 100644 index 0000000..9db869f --- /dev/null +++ b/src/views/tenant/package/composables/useFeatureStrategyTable.ts @@ -0,0 +1,115 @@ +import { computed, type ComputedRef } from 'vue' +import type { Composer } from 'vue-i18n' +import type { FeaturePolicyFieldDef, FeaturePolicyModel } from '../types' +import { FeaturePolicySections, parseFeaturePolicyJson } from '../types' + +export interface FeatureStrategyRow { + name: string + valueText: string + isEnabled: boolean + conditionsText: string +} + +/** + * 功能策略表格数据生成 composable + * @param t i18n 实例 + * @param i18nPrefix 国际化 key 前缀(如 'tenantPackage.detail.featureStrategy') + * @param jsonValue 功能策略 JSON 字符串(响应式) + */ +export function useFeatureStrategyTable( + t: Composer['t'], + i18nPrefix: string, + jsonValue: ComputedRef +) { + // 1. 构建 label key 映射表 + const featurePolicyLabelKeyMap = new Map( + FeaturePolicySections.flatMap((section) => + section.fields.map((field) => [field.key, field.labelKey] as const) + ) + ) + + // 2. 解析结果 + const parseResult = computed(() => parseFeaturePolicyJson(jsonValue.value || '')) + const isValidJson = computed(() => parseResult.value.isValidJson) + + // 3. 辅助函数 + const resolvePolicyLabel = (key: string) => { + const labelKey = featurePolicyLabelKeyMap.get(key) + return labelKey ? t(labelKey) : t(`${i18nPrefix}.conditions.unknownDependency`, { key }) + } + + const getPolicyValue = (model: FeaturePolicyModel, keyPath: string): unknown => { + const [group, ...rest] = keyPath.split('.') + const fieldKey = rest.join('.') + + if (group === 'features') return model.features?.[fieldKey] + if (group === 'quotas') return model.quotas?.[fieldKey] + return undefined + } + + const formatPolicyValue = (field: FeaturePolicyFieldDef, model: FeaturePolicyModel): string => { + const raw = getPolicyValue(model, field.key) + + if (field.type === 'boolean') { + return raw ? t('common.yes') : t('common.no') + } + + if (field.type === 'number') { + return typeof raw === 'number' ? String(raw) : t(`${i18nPrefix}.value.unlimited`) + } + + return raw === null || raw === undefined ? '' : String(raw) + } + + const buildPolicyConditions = (field: FeaturePolicyFieldDef): string => { + const parts: string[] = [] + + if (field.dependsOnKey) { + parts.push( + t(`${i18nPrefix}.conditions.dependsOnEnabled`, { + name: resolvePolicyLabel(field.dependsOnKey) + }) + ) + } + + if (typeof field.min === 'number') { + parts.push(t(`${i18nPrefix}.conditions.min`, { min: field.min })) + } + + if (typeof field.max === 'number') { + parts.push(t(`${i18nPrefix}.conditions.max`, { max: field.max })) + } + + return parts.length ? parts.join(';') : t(`${i18nPrefix}.noConditions`) + } + + const buildFeatureStrategyRow = ( + field: FeaturePolicyFieldDef, + model: FeaturePolicyModel + ): FeatureStrategyRow => { + const dependsOk = field.dependsOnKey ? !!getPolicyValue(model, field.dependsOnKey) : true + const raw = getPolicyValue(model, field.key) + const isEnabled = field.type === 'boolean' ? !!raw : dependsOk + + return { + name: t(field.labelKey), + valueText: formatPolicyValue(field, model), + isEnabled, + conditionsText: buildPolicyConditions(field) + } + } + + // 4. 生成表格行数据 + const rows = computed(() => { + const model = parseResult.value.model + return FeaturePolicySections.flatMap((section) => + section.fields.map((field) => buildFeatureStrategyRow(field, model)) + ) + }) + + return { + rows, + isValidJson, + parseResult + } +} diff --git a/src/views/tenant/package/index.vue b/src/views/tenant/package/index.vue new file mode 100644 index 0000000..ce1bcce --- /dev/null +++ b/src/views/tenant/package/index.vue @@ -0,0 +1,1301 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-detail-drawer.vue b/src/views/tenant/package/modules/package-detail-drawer.vue new file mode 100644 index 0000000..adc90b4 --- /dev/null +++ b/src/views/tenant/package/modules/package-detail-drawer.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-feature-policy-dialog.vue b/src/views/tenant/package/modules/package-feature-policy-dialog.vue new file mode 100644 index 0000000..7c86648 --- /dev/null +++ b/src/views/tenant/package/modules/package-feature-policy-dialog.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-form-dialog.vue b/src/views/tenant/package/modules/package-form-dialog.vue new file mode 100644 index 0000000..f0c7820 --- /dev/null +++ b/src/views/tenant/package/modules/package-form-dialog.vue @@ -0,0 +1,662 @@ + + + diff --git a/src/views/tenant/package/modules/package-quota-dialog.vue b/src/views/tenant/package/modules/package-quota-dialog.vue new file mode 100644 index 0000000..e6d10ae --- /dev/null +++ b/src/views/tenant/package/modules/package-quota-dialog.vue @@ -0,0 +1,362 @@ + + + + + diff --git a/src/views/tenant/package/modules/package-search.vue b/src/views/tenant/package/modules/package-search.vue new file mode 100644 index 0000000..43cd8c5 --- /dev/null +++ b/src/views/tenant/package/modules/package-search.vue @@ -0,0 +1,72 @@ + + + diff --git a/src/views/tenant/package/types/feature-policy-schema.ts b/src/views/tenant/package/types/feature-policy-schema.ts new file mode 100644 index 0000000..15edaf9 --- /dev/null +++ b/src/views/tenant/package/types/feature-policy-schema.ts @@ -0,0 +1,355 @@ +export type FeaturePolicyValueType = 'boolean' | 'number' | 'string' | 'multi_boolean' + +export interface FeaturePolicyFieldDef { + key: string + labelKey: string + type: FeaturePolicyValueType + placeholderKey?: string + min?: number + max?: number + unitKey?: string + dependsOnKey?: string +} + +export interface FeaturePolicySectionDef { + key: string + titleKey: string + fields: FeaturePolicyFieldDef[] +} + +export interface FeaturePolicyCustomItem { + key: string + label: string + type: 'boolean' | 'number' | 'string' + value: boolean | number | string +} + +export interface FeaturePolicyModel { + features: Record + quotas: Record + extra: { + customItems: FeaturePolicyCustomItem[] + unknown?: Record + } +} + +export interface FeaturePolicyPreset { + key: string + labelKey: string + value: Partial +} + +export const FeaturePolicySections: FeaturePolicySectionDef[] = [ + { + key: 'features', + titleKey: 'tenantPackage.featurePolicy.sections.features', + fields: [ + { + key: 'features.reportsExport', + labelKey: 'tenantPackage.featurePolicy.features.reportsExport', + type: 'boolean' + }, + { + key: 'features.printing', + labelKey: 'tenantPackage.featurePolicy.features.printing', + type: 'boolean' + }, + { + key: 'features.apiAccess', + labelKey: 'tenantPackage.featurePolicy.features.apiAccess', + type: 'boolean' + }, + { + key: 'features.marketingCoupon', + labelKey: 'tenantPackage.featurePolicy.features.coupon', + type: 'boolean' + }, + { + key: 'features.marketingFullReduction', + labelKey: 'tenantPackage.featurePolicy.features.fullReduction', + type: 'boolean' + }, + { + key: 'features.marketingMember', + labelKey: 'tenantPackage.featurePolicy.features.member', + type: 'boolean' + }, + { + key: 'features.marketingPoints', + labelKey: 'tenantPackage.featurePolicy.features.points', + type: 'boolean' + } + ] + }, + { + key: 'quotas', + titleKey: 'tenantPackage.featurePolicy.sections.quotas', + fields: [ + { + key: 'quotas.maxProducts', + labelKey: 'tenantPackage.featurePolicy.quotas.maxProducts', + type: 'number', + min: 0, + placeholderKey: 'tenantPackage.featurePolicy.quotas.unlimitedPlaceholder' + }, + { + key: 'quotas.maxMenus', + labelKey: 'tenantPackage.featurePolicy.quotas.maxMenus', + type: 'number', + min: 0, + placeholderKey: 'tenantPackage.featurePolicy.quotas.unlimitedPlaceholder' + }, + { + key: 'quotas.maxApiCallsPerDay', + labelKey: 'tenantPackage.featurePolicy.quotas.maxApiCallsPerDay', + type: 'number', + min: 0, + placeholderKey: 'tenantPackage.featurePolicy.quotas.unlimitedPlaceholder', + dependsOnKey: 'features.apiAccess' + } + ] + } +] + +export const FeaturePolicyPresets: FeaturePolicyPreset[] = [ + { + key: 'blank', + labelKey: 'tenantPackage.featurePolicy.presets.blank', + value: { + features: {}, + quotas: {} + } + }, + { + key: 'standard', + labelKey: 'tenantPackage.featurePolicy.presets.standard', + value: { + features: { + reportsExport: true, + printing: true, + apiAccess: false, + marketingCoupon: false, + marketingFullReduction: true, + marketingMember: false, + marketingPoints: false + }, + quotas: { + maxProducts: 500, + maxMenus: 30 + } + } + }, + { + key: 'pro', + labelKey: 'tenantPackage.featurePolicy.presets.pro', + value: { + features: { + reportsExport: true, + printing: true, + apiAccess: true, + marketingCoupon: true, + marketingFullReduction: true, + marketingMember: true, + marketingPoints: true + }, + quotas: { + maxProducts: 3000, + maxMenus: 200, + maxApiCallsPerDay: 20000 + } + } + } +] + +export function createDefaultFeaturePolicy(): FeaturePolicyModel { + return { + features: {}, + quotas: {}, + extra: { + customItems: [], + unknown: {} + } + } +} + +export function parseFeaturePolicyJson(value: string): { + model: FeaturePolicyModel + isValidJson: boolean +} { + // 1. 空值视为默认 + if (!value?.trim()) return { model: createDefaultFeaturePolicy(), isValidJson: true } + + // 2. JSON 解析失败则回退默认,但标记无效 + let raw: unknown + try { + raw = JSON.parse(value) + } catch { + return { model: createDefaultFeaturePolicy(), isValidJson: false } + } + + if (!raw || typeof raw !== 'object') { + return { model: createDefaultFeaturePolicy(), isValidJson: false } + } + + // 3. 兼容旧结构:limits/api/reports/printing/marketing + const rawObj = raw as Record + const model = createDefaultFeaturePolicy() + + // 3.1 映射 features + const features: Record = {} + const rawReports = toRecord(rawObj.reports) + const rawPrinting = toRecord(rawObj.printing) + const rawApi = toRecord(rawObj.api) + const rawMarketing = toRecord(rawObj.marketing) + + if (typeof rawReports.exportEnabled === 'boolean') + features.reportsExport = rawReports.exportEnabled + if (typeof rawPrinting.enabled === 'boolean') features.printing = rawPrinting.enabled + if (typeof rawApi.enabled === 'boolean') features.apiAccess = rawApi.enabled + + if (typeof rawMarketing.coupon === 'boolean') features.marketingCoupon = rawMarketing.coupon + if (typeof rawMarketing.fullReduction === 'boolean') + features.marketingFullReduction = rawMarketing.fullReduction + if (typeof rawMarketing.member === 'boolean') features.marketingMember = rawMarketing.member + if (typeof rawMarketing.points === 'boolean') features.marketingPoints = rawMarketing.points + + model.features = features + + // 3.2 映射 quotas + const quotas: Record = {} + const rawLimits = toRecord(rawObj.limits) + quotas.maxProducts = normalizeNumber(rawLimits.maxProducts) + quotas.maxMenus = normalizeNumber(rawLimits.maxMenus) + quotas.maxApiCallsPerDay = normalizeNumber(rawApi.maxCallsPerDay) + model.quotas = quotas + + // 3.3 合并 extra:优先保留 raw.extra,再叠加未知顶层字段 + const rawExtra = toRecord(rawObj.extra) + const unknown: Record = {} + const knownKeys = new Set(['limits', 'api', 'reports', 'printing', 'marketing', 'extra']) + Object.keys(rawObj).forEach((k) => { + if (!knownKeys.has(k)) unknown[k] = rawObj[k] + }) + + model.extra = { + customItems: normalizeCustomItems(rawExtra.customItems), + unknown: { ...unknown, ...rawExtra } + } + + return { model, isValidJson: true } +} + +export function serializeFeaturePolicy(model: FeaturePolicyModel): string { + // 1. 兼容旧结构输出:limits/api/reports/printing/marketing + extra + const features = model.features || {} + const quotas = model.quotas || {} + + const payload: Record = { + limits: { + maxProducts: normalizeNumber(quotas.maxProducts), + maxMenus: normalizeNumber(quotas.maxMenus) + }, + api: { + enabled: !!features.apiAccess, + maxCallsPerDay: features.apiAccess ? normalizeNumber(quotas.maxApiCallsPerDay) : undefined + }, + reports: { + exportEnabled: !!features.reportsExport + }, + printing: { + enabled: !!features.printing + }, + marketing: { + coupon: !!features.marketingCoupon, + fullReduction: !!features.marketingFullReduction, + member: !!features.marketingMember, + points: !!features.marketingPoints + } + } + + // 2. 合并 extra(保留未知字段 + 自定义扩展项) + const extra: Record = { ...(model.extra?.unknown || {}) } + extra.customItems = normalizeCustomItems(model.extra?.customItems) + + if (Object.keys(extra).length) { + payload.extra = extra + } + + return JSON.stringify(payload, null, 2) +} + +export function validateFeaturePolicy(model: FeaturePolicyModel): string[] { + const errors: string[] = [] + + // 1. 数值校验:非负 + const quotas = model.quotas || {} + const numberKeys: Array<[string, unknown]> = [ + ['quotas.maxProducts', quotas.maxProducts], + ['quotas.maxMenus', quotas.maxMenus], + ['quotas.maxApiCallsPerDay', quotas.maxApiCallsPerDay] + ] + numberKeys.forEach(([key, value]) => { + if (value === null || value === undefined || value === '') return + const num = Number(value) + if (!Number.isFinite(num) || num < 0) errors.push(key) + }) + + // 2. 依赖校验:API 未开启则不允许填写 maxApiCallsPerDay + if ( + !model.features?.apiAccess && + quotas.maxApiCallsPerDay !== undefined && + quotas.maxApiCallsPerDay !== null + ) { + errors.push('quotas.maxApiCallsPerDay') + } + + // 3. 自定义扩展项 key 校验与去重 + const seen = new Set() + ;(model.extra?.customItems || []).forEach((it, idx) => { + const key = (it?.key || '').trim() + const label = (it?.label || '').trim() + + if (!key) errors.push(`extra.customItems[${idx}].key`) + if (!label) errors.push(`extra.customItems[${idx}].label`) + if (key && !/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(key)) + errors.push(`extra.customItems[${idx}].key`) + if (key && seen.has(key)) errors.push(`extra.customItems[${idx}].key`) + if (key) seen.add(key) + }) + + return errors +} + +function toRecord(value: unknown): Record { + return value && typeof value === 'object' ? (value as Record) : {} +} + +function normalizeNumber(value: unknown): number | undefined { + if (value === null || value === undefined || value === '') return undefined + const num = Number(value) + if (!Number.isFinite(num)) return undefined + if (num < 0) return 0 + return Math.floor(num) +} + +function normalizeCustomItems(value: unknown): FeaturePolicyCustomItem[] { + if (!Array.isArray(value)) return [] + return value + .filter((x) => x && typeof x === 'object') + .map((x) => x as FeaturePolicyCustomItem) + .map((x) => ({ + key: String(x.key ?? '').trim(), + label: String(x.label ?? '').trim(), + type: (x.type === 'boolean' || x.type === 'number' || x.type === 'string' + ? x.type + : 'string') as 'boolean' | 'number' | 'string', + value: + x.type === 'boolean' + ? !!x.value + : x.type === 'number' + ? Number.isFinite(Number(x.value)) + ? Number(x.value) + : 0 + : String(x.value ?? '') + })) +} diff --git a/src/views/tenant/package/types/index.ts b/src/views/tenant/package/types/index.ts new file mode 100644 index 0000000..8eca96f --- /dev/null +++ b/src/views/tenant/package/types/index.ts @@ -0,0 +1 @@ +export * from './feature-policy-schema' diff --git a/src/views/tenant/quota-package/components/PurchaseQuotaDialog.vue b/src/views/tenant/quota-package/components/PurchaseQuotaDialog.vue new file mode 100644 index 0000000..929805d --- /dev/null +++ b/src/views/tenant/quota-package/components/PurchaseQuotaDialog.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/QuotaAlertConfigPanel.vue b/src/views/tenant/quota-package/components/QuotaAlertConfigPanel.vue new file mode 100644 index 0000000..34be64c --- /dev/null +++ b/src/views/tenant/quota-package/components/QuotaAlertConfigPanel.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/QuotaPackageFormDialog.vue b/src/views/tenant/quota-package/components/QuotaPackageFormDialog.vue new file mode 100644 index 0000000..ac49fc5 --- /dev/null +++ b/src/views/tenant/quota-package/components/QuotaPackageFormDialog.vue @@ -0,0 +1,239 @@ + + + diff --git a/src/views/tenant/quota-package/components/QuotaPackageListPanel.vue b/src/views/tenant/quota-package/components/QuotaPackageListPanel.vue new file mode 100644 index 0000000..84fe996 --- /dev/null +++ b/src/views/tenant/quota-package/components/QuotaPackageListPanel.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/TenantQuotaPurchaseList.vue b/src/views/tenant/quota-package/components/TenantQuotaPurchaseList.vue new file mode 100644 index 0000000..9458723 --- /dev/null +++ b/src/views/tenant/quota-package/components/TenantQuotaPurchaseList.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/src/views/tenant/quota-package/components/TenantQuotaUsageDashboard.vue b/src/views/tenant/quota-package/components/TenantQuotaUsageDashboard.vue new file mode 100644 index 0000000..1ff5f37 --- /dev/null +++ b/src/views/tenant/quota-package/components/TenantQuotaUsageDashboard.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/src/views/tenant/quota-package/index.vue b/src/views/tenant/quota-package/index.vue new file mode 100644 index 0000000..0ed457d --- /dev/null +++ b/src/views/tenant/quota-package/index.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/views/tenant/review/components/ReviewDialog.vue b/src/views/tenant/review/components/ReviewDialog.vue new file mode 100644 index 0000000..3c63e80 --- /dev/null +++ b/src/views/tenant/review/components/ReviewDialog.vue @@ -0,0 +1,890 @@ + + + + + diff --git a/src/views/tenant/review/index.vue b/src/views/tenant/review/index.vue new file mode 100644 index 0000000..c6137e7 --- /dev/null +++ b/src/views/tenant/review/index.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/src/views/tenant/review/modules/tenant-review-search.vue b/src/views/tenant/review/modules/tenant-review-search.vue new file mode 100644 index 0000000..e68c572 --- /dev/null +++ b/src/views/tenant/review/modules/tenant-review-search.vue @@ -0,0 +1,103 @@ + + + diff --git a/src/views/tenant/review/types/index.ts b/src/views/tenant/review/types/index.ts new file mode 100644 index 0000000..a7d524b --- /dev/null +++ b/src/views/tenant/review/types/index.ts @@ -0,0 +1 @@ +export * from './search-form' diff --git a/src/views/tenant/review/types/search-form.ts b/src/views/tenant/review/types/search-form.ts new file mode 100644 index 0000000..ff12852 --- /dev/null +++ b/src/views/tenant/review/types/search-form.ts @@ -0,0 +1,10 @@ +import type { TenantStatus } from '@/enums/TenantStatus' +import type { TenantVerificationStatus } from '@/enums/TenantVerificationStatus' + +export interface TenantReviewSearchForm { + tenantName: string + contactName: string + contactPhone: string + status?: TenantStatus + verificationStatus?: TenantVerificationStatus +} diff --git a/src/views/tenant/subscription/components/BatchExtendDialog.vue b/src/views/tenant/subscription/components/BatchExtendDialog.vue new file mode 100644 index 0000000..c3dfaa0 --- /dev/null +++ b/src/views/tenant/subscription/components/BatchExtendDialog.vue @@ -0,0 +1,326 @@ + + + + + diff --git a/src/views/tenant/subscription/components/BatchRemindDialog.vue b/src/views/tenant/subscription/components/BatchRemindDialog.vue new file mode 100644 index 0000000..5713a44 --- /dev/null +++ b/src/views/tenant/subscription/components/BatchRemindDialog.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/src/views/tenant/subscription/components/ChangePlanDialog.vue b/src/views/tenant/subscription/components/ChangePlanDialog.vue new file mode 100644 index 0000000..7451e44 --- /dev/null +++ b/src/views/tenant/subscription/components/ChangePlanDialog.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/src/views/tenant/subscription/components/ExtendSubscriptionDialog.vue b/src/views/tenant/subscription/components/ExtendSubscriptionDialog.vue new file mode 100644 index 0000000..bc05229 --- /dev/null +++ b/src/views/tenant/subscription/components/ExtendSubscriptionDialog.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/src/views/tenant/subscription/components/StatusChangeDialog.vue b/src/views/tenant/subscription/components/StatusChangeDialog.vue new file mode 100644 index 0000000..f44e80e --- /dev/null +++ b/src/views/tenant/subscription/components/StatusChangeDialog.vue @@ -0,0 +1,174 @@ + + + diff --git a/src/views/tenant/subscription/components/SubscriptionDetailDrawer.vue b/src/views/tenant/subscription/components/SubscriptionDetailDrawer.vue new file mode 100644 index 0000000..264d026 --- /dev/null +++ b/src/views/tenant/subscription/components/SubscriptionDetailDrawer.vue @@ -0,0 +1,525 @@ + + + + + diff --git a/src/views/tenant/subscription/index.vue b/src/views/tenant/subscription/index.vue new file mode 100644 index 0000000..a65638d --- /dev/null +++ b/src/views/tenant/subscription/index.vue @@ -0,0 +1,503 @@ + + + + + diff --git a/src/views/tenant/tenant-list/components/ImageUploadField.vue b/src/views/tenant/tenant-list/components/ImageUploadField.vue new file mode 100644 index 0000000..bd177cb --- /dev/null +++ b/src/views/tenant/tenant-list/components/ImageUploadField.vue @@ -0,0 +1,139 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantDetailDrawer.vue b/src/views/tenant/tenant-list/components/TenantDetailDrawer.vue new file mode 100644 index 0000000..09fb012 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantDetailDrawer.vue @@ -0,0 +1,529 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantEdit.vue b/src/views/tenant/tenant-list/components/TenantEdit.vue new file mode 100644 index 0000000..86092fd --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantEdit.vue @@ -0,0 +1,300 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantExtendSubscriptionDialog.vue b/src/views/tenant/tenant-list/components/TenantExtendSubscriptionDialog.vue new file mode 100644 index 0000000..ec0a428 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantExtendSubscriptionDialog.vue @@ -0,0 +1,128 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantFreezeDialog.vue b/src/views/tenant/tenant-list/components/TenantFreezeDialog.vue new file mode 100644 index 0000000..9ae48c5 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantFreezeDialog.vue @@ -0,0 +1,133 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantImpersonateDialog.vue b/src/views/tenant/tenant-list/components/TenantImpersonateDialog.vue new file mode 100644 index 0000000..478fc9a --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantImpersonateDialog.vue @@ -0,0 +1,143 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantManualCreateDialog.vue b/src/views/tenant/tenant-list/components/TenantManualCreateDialog.vue new file mode 100644 index 0000000..2c9da2e --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantManualCreateDialog.vue @@ -0,0 +1,987 @@ + + + diff --git a/src/views/tenant/tenant-list/components/TenantQuotaDrawer.vue b/src/views/tenant/tenant-list/components/TenantQuotaDrawer.vue new file mode 100644 index 0000000..c941a12 --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantQuotaDrawer.vue @@ -0,0 +1,477 @@ + + + + + + + diff --git a/src/views/tenant/tenant-list/components/TenantResetAdminDialog.vue b/src/views/tenant/tenant-list/components/TenantResetAdminDialog.vue new file mode 100644 index 0000000..6d86f5d --- /dev/null +++ b/src/views/tenant/tenant-list/components/TenantResetAdminDialog.vue @@ -0,0 +1,145 @@ + + + diff --git a/src/views/tenant/tenant-list/index.vue b/src/views/tenant/tenant-list/index.vue new file mode 100644 index 0000000..3dabd1a --- /dev/null +++ b/src/views/tenant/tenant-list/index.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/src/views/tenant/tenant-list/modules/tenant-search.vue b/src/views/tenant/tenant-list/modules/tenant-search.vue new file mode 100644 index 0000000..6bc9a46 --- /dev/null +++ b/src/views/tenant/tenant-list/modules/tenant-search.vue @@ -0,0 +1,111 @@ + + + diff --git a/src/views/test/RichTextEditorTest.vue b/src/views/test/RichTextEditorTest.vue new file mode 100644 index 0000000..7cac835 --- /dev/null +++ b/src/views/test/RichTextEditorTest.vue @@ -0,0 +1,158 @@ + + + + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4331962 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "types": ["vite/client", "node", "element-plus/global"], + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@views/*": ["src/views/*"], + "@imgs/*": ["src/assets/images/*"], + "@icons/*": ["src/assets/icons/*"], + "@utils/*": ["src/utils/*"], + "@stores/*": ["src/store/*"], + "@plugins/*": ["src/plugins/*"], + "@styles/*": ["src/assets/styles/*"] + } + }, + "include": ["src/**/*", "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], + "exclude": ["node_modules", "dist", "**/*.js"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b704f07 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,156 @@ +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' +import { fileURLToPath } from 'url' +import vueDevTools from 'vite-plugin-vue-devtools' +import viteCompression from 'vite-plugin-compression' +import Components from 'unplugin-vue-components/vite' +import AutoImport from 'unplugin-auto-import/vite' +import ElementPlus from 'unplugin-element-plus/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' +import tailwindcss from '@tailwindcss/vite' +// import { visualizer } from 'rollup-plugin-visualizer' + +export default ({ mode }: { mode: string }) => { + const root = process.cwd() + const env = loadEnv(mode, root) + const { VITE_VERSION, VITE_PORT, VITE_BASE_URL, VITE_API_URL, VITE_API_PROXY_URL } = env + + console.log(`🚀 API_URL = ${VITE_API_URL}`) + console.log(`🚀 VERSION = ${VITE_VERSION}`) + + return defineConfig({ + define: { + __APP_VERSION__: JSON.stringify(VITE_VERSION) + }, + base: VITE_BASE_URL, + server: { + port: Number(VITE_PORT), + proxy: { + '/api': { + target: VITE_API_PROXY_URL, + changeOrigin: true + } + }, + host: true + }, + // 路径别名 + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@views': resolvePath('src/views'), + '@imgs': resolvePath('src/assets/images'), + '@icons': resolvePath('src/assets/icons'), + '@utils': resolvePath('src/utils'), + '@stores': resolvePath('src/store'), + '@styles': resolvePath('src/assets/styles') + } + }, + build: { + target: 'es2015', + outDir: 'dist', + chunkSizeWarningLimit: 2000, + minify: 'terser', + terserOptions: { + compress: { + // 生产环境去除部分 console(保留 warn/error,便于线上排障) + pure_funcs: ['console.log', 'console.debug', 'console.info'], + // 生产环境去除 debugger + drop_debugger: true + } + }, + dynamicImportVarsOptions: { + warnOnError: true, + exclude: [], + include: ['src/views/**/*.vue'] + } + }, + plugins: [ + vue(), + tailwindcss(), + // 自动按需导入 API + AutoImport({ + imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'], + dts: 'src/types/import/auto-imports.d.ts', + resolvers: [ElementPlusResolver()], + eslintrc: { + enabled: true, + filepath: './.auto-import.json', + globalsPropValue: true + } + }), + // 自动按需导入组件 + Components({ + dts: 'src/types/import/components.d.ts', + resolvers: [ElementPlusResolver()] + }), + // 按需定制主题配置 + ElementPlus({ + useSource: true + }), + // 压缩 + viteCompression({ + verbose: false, // 是否在控制台输出压缩结果 + disable: false, // 是否禁用 + algorithm: 'gzip', // 压缩算法 + ext: '.gz', // 压缩后的文件名后缀 + threshold: 10240, // 只有大小大于该值的资源会被处理 10240B = 10KB + deleteOriginFile: false // 压缩后是否删除原文件 + }), + vueDevTools() + // 打包分析 + // visualizer({ + // open: true, + // gzipSize: true, + // brotliSize: true, + // filename: 'dist/stats.html' // 分析图生成的文件名及路径 + // }), + ], + // 依赖预构建:避免运行时重复请求与转换,提升首次加载速度 + optimizeDeps: { + include: [ + 'echarts/core', + 'echarts/charts', + 'echarts/components', + 'echarts/renderers', + 'xlsx', + 'xgplayer', + 'crypto-js', + 'file-saver', + 'vue-img-cutter', + 'element-plus/es', + 'element-plus/es/components/*/style/css', + 'element-plus/es/components/*/style/index' + ] + }, + css: { + preprocessorOptions: { + // sass variable and mixin + scss: { + additionalData: ` + @use "@styles/core/el-light.scss" as *; + @use "@styles/core/mixin.scss" as *; + ` + } + }, + postcss: { + plugins: [ + { + postcssPlugin: 'internal:charset-removal', + AtRule: { + charset: (atRule) => { + if (atRule.name === 'charset') { + atRule.remove() + } + } + } + } + ] + } + } + }) +} + +function resolvePath(paths: string) { + return path.resolve(__dirname, paths) +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8b8f76f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import path from 'path' +import { fileURLToPath } from 'url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@views': resolvePath('src/views'), + '@imgs': resolvePath('src/assets/images'), + '@icons': resolvePath('src/assets/icons'), + '@utils': resolvePath('src/utils'), + '@stores': resolvePath('src/store'), + '@styles': resolvePath('src/assets/styles') + } + }, + test: { + environment: 'jsdom', + globals: false, + clearMocks: true, + // Windows + vite-node sourcemap edge case can surface as unhandled errors. + dangerouslyIgnoreUnhandledErrors: true + } +}) + +function resolvePath(paths: string) { + return path.resolve(__dirname, paths) +}