From fec285d150b377d004e47f0973d298b92fe4c711 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期五, 27 三月 2026 23:06:39 +0800
Subject: [PATCH] #前端
---
rsf-design/src/api/system-manage.js | 19
rsf-design/src/config/modules/component.js | 58
rsf-design/skill/art-design-pro/templates/tailwind.config.js.template | 50
rsf-design/skill/art-design-pro/docs/core-concepts/permission.md | 199
rsf-design/src/views/dashboard/console/modules/todo-list.vue | 61
rsf-design/src/store/modules/setting.js | 225
rsf-design/src/components/core/cards/art-line-chart-card/index.vue | 109
rsf-design/src/router/modules/system.js | 64
rsf-design/src/assets/images/lock/bg_dark.webp | 0
rsf-design/src/assets/styles/custom/one-dark-pro.scss | 98
rsf-design/index.html | 47
rsf-design/src/components/core/views/exception/ArtException.vue | 35
rsf-design/src/components/core/base/art-back-to-top/index.vue | 36
rsf-design/src/components/core/layouts/art-global-search/index.vue | 377
rsf-design/.env | 25
rsf-design/.env.production | 10
rsf-design/src/router/modules/exception.js | 45
rsf-design/src/views/dashboard/console/modules/dynamic-stats.vue | 69
rsf-design/skill/art-design-pro/scripts/generate.py | 816 +
rsf-design/src/assets/styles/core/el-dark.scss | 2
rsf-design/src/hooks/core/useCeremony.js | 83
rsf-design/src/components/core/charts/art-bar-chart/index.vue | 157
rsf-design/src/assets/images/ceremony/hb.png | 0
rsf-design/src/assets/styles/core/reset.scss | 41
rsf-design/skill/art-design-pro/docs/getting-started/style-guide.md | 355
rsf-design/src/assets/styles/core/theme-change.scss | 11
rsf-design/src/enums/appEnum.js | 34
rsf-design/src/components/core/forms/art-search-bar/index.vue | 432
rsf-design/src/components/core/layouts/art-menus/art-mixed-menu/index.vue | 195
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue | 120
rsf-design/src/assets/styles/core/el-light.scss | 34
rsf-design/.auto-import.json | 325
rsf-design/skill/art-design-pro/scripts/init.py | 423
rsf-design/skill/art-design-pro/docs/examples/index.md | 197
rsf-design/src/views/system/user/modules/user-search.vue | 92
rsf-design/src/views/system/role/modules/role-edit-dialog.vue | 109
rsf-design/src/utils/navigation/worktab.js | 37
rsf-design/src/assets/images/avatar/avatar8.webp | 0
rsf-design/src/components/core/layouts/art-settings-panel/style.scss | 92
rsf-design/src/utils/ui/loading.js | 41
rsf-design/skill/art-design-pro/docs/generator-guide.md | 378
rsf-design/src/components/core/others/art-menu-right/index.vue | 303
rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue | 31
rsf-design/tests/manual-chunks.test.mjs | 37
rsf-design/src/router/core/RouteRegistry.js | 63
rsf-design/.prettierrc | 20
rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue | 29
rsf-design/src/views/result/success/index.vue | 21
rsf-design/src/components/core/theme/theme-svg/index.vue | 81
rsf-design/src/directives/business/ripple.js | 47
rsf-design/src/views/exception/500/index.vue | 17
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue | 39
rsf-design/src/assets/images/avatar/avatar6.webp | 0
rsf-design/src/config/modules/festival.js | 21
rsf-design/src/config/setting.js | 74
rsf-design/src/mock/temp/formData.js | 250
rsf-design/src/views/dashboard/console/modules/about-project.vue | 37
rsf-design/src/utils/index.js | 9
rsf-design/src/store/modules/user.js | 115
rsf-design/src/assets/styles/core/el-ui.scss | 519
rsf-design/src/assets/styles/core/app.scss | 292
rsf-design/src/utils/sys/mittBus.js | 4
rsf-design/src/components/core/layouts/art-settings-panel/index.vue | 58
rsf-design/src/components/core/layouts/art-work-tab/index.vue | 494
rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue | 94
rsf-design/skill/art-design-pro/scripts/verify.py | 267
rsf-design/src/components/core/banners/art-basic-banner/index.vue | 261
rsf-design/src/views/exception/403/index.vue | 17
rsf-design/src/views/index/style.scss | 93
rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue | 39
rsf-design/src/locales/index.js | 54
rsf-design/src/router/core/RoutePermissionValidator.js | 105
rsf-design/src/assets/images/ceremony/xc.png | 0
rsf-design/src/hooks/core/useTheme.js | 95
rsf-design/src/views/system/user/index.vue | 222
rsf-design/scripts/clean-dev.js | 133
rsf-design/src/components/core/charts/art-ring-chart/index.vue | 116
rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue | 427
rsf-design/src/utils/http/error.js | 76
rsf-design/skill/art-design-pro/SKILL.md | 750 +
rsf-design/skill/art-design-pro/templates/env/.env.example | 16
rsf-design/src/utils/router.js | 34
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsState.js | 29
rsf-design/src/components/core/banners/art-card-banner/index.vue | 71
rsf-design/src/assets/images/svg/403.svg | 1
rsf-design/src/mock/upgrade/changeLog.js | 3
rsf-design/src/utils/http/index.js | 152
rsf-design/src/components/core/forms/art-wang-editor/style.scss | 273
rsf-design/src/router/index.js | 18
rsf-design/src/assets/images/ceremony/yd.png | 0
rsf-design/src/assets/images/lock/bg_light.webp | 0
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js | 170
rsf-design/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue | 36
rsf-design/src/directives/index.js | 11
rsf-design/src/assets/images/settings/menu_layouts/horizontal.png | 0
rsf-design/src/plugins/echarts.js | 50
rsf-design/tests/clean-dev-helpers.test.mjs | 112
rsf-design/tests/iconify-local-prefixes.test.mjs | 50
rsf-design/src/config/assets/images.js | 42
rsf-design/skill/art-design-pro/docs/examples/permission/button-permission.md | 306
rsf-design/src/hooks/core/useFastEnter.js | 27
rsf-design/src/config/index.js | 95
rsf-design/src/utils/constants/index.js | 1
rsf-design/skill/art-design-pro/docs/core-concepts/project-structure.md | 75
rsf-design/src/directives/core/auth.js | 21
rsf-design/src/components/core/forms/art-wang-editor/index.vue | 179
rsf-design/src/router/modules/dashboard.js | 25
rsf-design/src/assets/images/settings/menu_layouts/mixed.png | 0
rsf-design/src/hooks/core/useLayoutHeight.js | 85
rsf-design/src/components/core/views/login/LoginLeftView.vue | 600
rsf-design/src/locales/langs/en.json | 296
rsf-design/src/components/core/cards/art-timeline-list-card/index.vue | 41
rsf-design/src/hooks/core/useAppMode.js | 13
rsf-design/src/views/system/user-center/index.vue | 209
rsf-design/skill/art-design-pro/docs/examples/forms/search-bar.md | 302
rsf-design/.stylelintignore | 9
rsf-design/LICENSE | 21
rsf-design/skill/art-design-pro/templates/vite.config.js.template | 71
rsf-design/src/assets/images/avatar/avatar2.webp | 0
rsf-design/src/components/core/cards/art-progress-card/index.vue | 65
rsf-design/src/router/core/index.js | 16
rsf-design/src/components/core/widget/art-icon-button/index.vue | 18
rsf-design/src/router/guards/beforeEach.js | 223
rsf-design/src/utils/ui/index.js | 4
rsf-design/eslint.config.mjs | 72
rsf-design/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue | 35
rsf-design/skill/art-design-pro/docs/core-concepts/routing.md | 149
rsf-design/src/assets/images/settings/theme_styles/system.png | 0
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss | 251
rsf-design/src/hooks/index.js | 27
rsf-design/src/hooks/core/useTable.js | 457
rsf-design/src/views/dashboard/console/modules/sales-overview.vue | 35
rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue | 76
rsf-design/src/views/exception/404/index.vue | 17
rsf-design/commitlint.config.cjs | 97
rsf-design/src/assets/styles/index.scss | 23
rsf-design/skill/art-design-pro/scripts/search.py | 176
rsf-design/skill/art-design-pro/docs/getting-started/components-basics.md | 137
rsf-design/skill/art-design-pro/docs/getting-started/04-standard.md | 106
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.js | 121
rsf-design/src/assets/images/avatar/avatar10.webp | 0
rsf-design/src/components/core/layouts/art-chat-window/index.vue | 228
rsf-design/src/views/outside/Iframe.vue | 29
rsf-design/skill/art-design-pro/docs/components/art-form.md | 632
rsf-design/src/store/modules/worktab.js | 343
rsf-design/src/assets/images/avatar/avatar1.webp | 0
rsf-design/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue | 33
rsf-design/vite.config.js | 152
rsf-design/src/views/dashboard/console/index.vue | 44
rsf-design/src/components/core/others/art-watermark/index.vue | 38
rsf-design/src/utils/constants/links.js | 21
rsf-design/src/utils/navigation/route.js | 34
rsf-design/src/utils/sys/upgrade.js | 181
rsf-design/src/assets/images/avatar/avatar7.webp | 0
rsf-design/scripts/clean-dev.helpers.mjs | 265
rsf-design/skill/art-design-pro/docs/hooks/use-table.md | 551
rsf-design/src/assets/images/login/lf_icon2.webp | 0
rsf-design/src/assets/images/settings/menu_styles/design.png | 0
rsf-design/src/utils/storage/storage.js | 160
rsf-design/public/favicon.ico | 0
rsf-design/src/components/core/forms/art-excel-import/index.vue | 49
rsf-design/src/hooks/core/useAuth.js | 22
rsf-design/src/views/system/role/index.vue | 213
rsf-design/src/directives/core/roles.js | 27
rsf-design/src/enums/formEnum.js | 12
rsf-design/src/components/core/base/art-svg-icon/index.vue | 20
rsf-design/src/components/core/layouts/art-page-content/index.vue | 112
rsf-design/src/hooks/core/useHeaderBar.js | 123
rsf-design/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue | 139
rsf-design/src/utils/ui/iconify-loader.js | 0
rsf-design/skill/art-design-pro/docs/getting-started/configuration-guide.md | 286
rsf-design/src/assets/images/draw/draw1.png | 0
rsf-design/src/plugins/iconify.js | 18
rsf-design/src/main.js | 20
rsf-design/src/assets/images/settings/menu_styles/light.png | 0
rsf-design/src/assets/images/avatar/avatar9.webp | 0
rsf-design/src/hooks/core/useTableColumns.js | 164
rsf-design/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue | 68
rsf-design/src/components/core/charts/art-dual-bar-compare-chart/index.vue | 159
rsf-design/src/assets/images/svg/404.svg | 1
rsf-design/src/hooks/core/useTableHeight.js | 54
rsf-design/src/assets/images/ceremony/sd.png | 0
rsf-design/src/components/core/forms/art-form/index.vue | 367
rsf-design/src/hooks/core/useCommon.js | 43
rsf-design/src/components/core/text-effect/art-count-to/index.vue | 209
rsf-design/src/router/modules/result.js | 32
rsf-design/src/assets/styles/core/tailwind.css | 208
rsf-design/src/components/core/charts/art-h-bar-chart/index.vue | 162
rsf-design/src/assets/images/avatar/avatar.webp | 0
rsf-design/src/components/core/views/login/AuthTopBar.vue | 143
rsf-design/src/utils/sys/console.js | 11
rsf-design/src/components/core/cards/art-bar-chart-card/index.vue | 84
rsf-design/src/utils/table/tableUtils.js | 188
rsf-design/src/utils/navigation/index.js | 3
rsf-design/src/components/core/forms/art-drag-verify/index.vue | 301
rsf-design/src/assets/svg/loading.js | 34
rsf-design/src/router/routes/staticRoutes.js | 61
rsf-design/skill/art-design-pro/templates/package.json.template | 52
rsf-design/src/components/core/views/result/ArtResultPage.vue | 31
rsf-design/skill/art-design-pro/data/components.csv | 58
rsf-design/src/utils/storage/storage-config.js | 74
rsf-design/skill/art-design-pro/docs/getting-started/02-quick-start.md | 55
rsf-design/src/utils/ui/emojo.js | 12
rsf-design/src/views/auth/forget-password/index.vue | 58
rsf-design/src/utils/form/validator.js | 172
rsf-design/src/utils/sys/index.js | 3
rsf-design/skill/art-design-pro/scripts/list.py | 77
rsf-design/src/views/dashboard/console/modules/active-user.vue | 34
rsf-design/src/assets/images/svg/login_icon.svg | 1
rsf-design/src/config/modules/fastEnter.js | 71
rsf-design/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue | 28
rsf-design/src/components/core/forms/art-excel-export/index.vue | 223
rsf-design/src/components/core/charts/art-radar-chart/index.vue | 94
rsf-design/src/components/core/cards/art-donut-chart-card/index.vue | 102
rsf-design/src/router/core/IframeRouteManager.js | 63
rsf-design/pnpm-lock.yaml | 5961 ++++++++
rsf-design/.gitignore | 14
rsf-design/src/assets/images/settings/menu_layouts/dual_column.png | 0
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue | 70
rsf-design/src/api/auth.js | 19
rsf-design/src/router/guards/afterEach.js | 26
rsf-design/src/router/routes/asyncRoutes.js | 3
rsf-design/src/views/result/fail/index.vue | 28
rsf-design/src/utils/sys/error-handle.js | 73
rsf-design/src/assets/images/settings/theme_styles/light.png | 0
rsf-design/src/components/core/text-effect/art-text-scroll/index.vue | 197
rsf-design/src/config/modules/headerBar.js | 49
rsf-design/src/utils/storage/index.js | 3
rsf-design/src/utils/socket/index.js | 331
rsf-design/package.json | 86
rsf-design/src/components/core/layouts/art-global-component/index.vue | 14
rsf-design/src/router/core/RouteTransformer.js | 84
rsf-design/src/plugins/iconify.collections.js | 552
rsf-design/src/components/core/charts/art-scatter-chart/index.vue | 101
rsf-design/src/components/core/layouts/art-fast-enter/index.vue | 89
rsf-design/src/components/core/cards/art-image-card/index.vue | 67
rsf-design/skill/art-design-pro/docs/getting-started/01-introduce.md | 56
rsf-design/src/views/index/index.vue | 29
rsf-design/src/plugins/index.js | 1
rsf-design/src/components/core/layouts/art-breadcrumb/index.vue | 99
rsf-design/src/components/core/tables/art-table/index.vue | 257
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue | 283
rsf-design/src/utils/form/responsive.js | 26
rsf-design/src/utils/navigation/jump.js | 31
rsf-design/src/assets/styles/core/router-transition.scss | 104
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.js | 231
rsf-design/skill/art-design-pro/docs/getting-started/03-must-read.md | 88
rsf-design/src/assets/images/svg/500.svg | 5
rsf-design/src/assets/styles/core/mixin.scss | 157
rsf-design/src/router/core/RouteValidator.js | 125
rsf-design/src/store/modules/table.js | 37
rsf-design/src/assets/images/avatar/avatar3.webp | 0
rsf-design/skill/art-design-pro/docs/components/art-search-bar.md | 577
rsf-design/src/utils/ui/tabs.js | 24
rsf-design/scripts/build-local-iconify-collections.mjs | 161
rsf-design/src/router/core/MenuProcessor.js | 208
rsf-design/.env.development | 13
rsf-design/src/router/routesAlias.js | 6
rsf-design/src/utils/storage/storage-key-manager.js | 52
rsf-design/src/assets/images/settings/theme_styles/dark.png | 0
rsf-design/src/views/auth/login/style.css | 38
rsf-design/src/components/core/cards/art-stats-card/index.vue | 51
rsf-design/src/assets/styles/core/dark.scss | 93
rsf-design/src/components/core/cards/art-data-list-card/index.vue | 43
rsf-design/src/components/core/layouts/art-notification/index.vue | 357
rsf-design/src/views/system/user/modules/user-dialog.vue | 106
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue | 172
rsf-design/src/router/modules/index.js | 13
rsf-design/src/views/auth/login/index.vue | 233
rsf-design/src/assets/images/common/logo.webp | 0
rsf-design/src/components/core/base/art-logo/index.vue | 14
rsf-design/src/components/core/tables/art-table/style.scss | 99
rsf-design/src/utils/ui/animation.js | 33
rsf-design/src/views/system/role/modules/role-search.vue | 87
rsf-design/src/store/index.js | 19
rsf-design/src/components/core/forms/art-button-more/index.vue | 41
rsf-design/src/views/dashboard/console/modules/new-user.vue | 145
rsf-design/src/components/core/media/art-video-player/index.vue | 67
rsf-design/tests/repo-hygiene.test.mjs | 37
rsf-design/.prettierignore | 3
rsf-design/src/assets/images/user/bg.webp | 0
rsf-design/src/router/core/ComponentLoader.js | 59
rsf-design/src/assets/styles/core/md.scss | 1036 +
rsf-design/src/components/core/media/art-cutter-img/index.vue | 244
rsf-design/tests/iconify-local-minimal.test.mjs | 97
rsf-design/src/assets/images/avatar/avatar5.webp | 0
rsf-design/src/assets/images/settings/menu_layouts/vertical.png | 0
rsf-design/src/utils/table/tableConfig.js | 20
rsf-design/src/assets/images/favicon.ico | 0
rsf-design/src/components/core/forms/art-button-table/index.vue | 41
rsf-design/src/components/core/tables/art-table-header/index.vue | 247
rsf-design/src/components/core/charts/art-line-chart/index.vue | 288
rsf-design/skill/art-design-pro/docs/00-index.md | 100
rsf-design/src/views/system/role/modules/role-permission-dialog.vue | 153
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue | 16
rsf-design/src/hooks/core/useChart.js | 534
rsf-design/src/components/core/layouts/art-screen-lock/index.vue | 437
rsf-design/src/App.vue | 37
rsf-design/src/utils/table/tableCache.js | 154
rsf-design/src/views/system/menu/modules/menu-dialog.vue | 287
rsf-design/src/views/system/menu/index.vue | 352
rsf-design/.gitattributes | 2
rsf-design/src/components/core/charts/art-k-line-chart/index.vue | 139
rsf-design/src/assets/images/user/avatar.webp | 0
rsf-design/skill/art-design-pro/docs/examples/tables/basic-table.md | 180
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss | 258
rsf-design/src/utils/http/status.js | 17
rsf-design/src/directives/business/highlight.js | 148
rsf-design/.stylelintrc.cjs | 82
rsf-design/skill/art-design-pro/docs/examples/templates/crud-page.md | 350
rsf-design/src/assets/styles/core/theme-animation.scss | 63
rsf-design/src/store/modules/menu.js | 40
rsf-design/src/locales/langs/zh.json | 296
rsf-design/src/utils/form/index.js | 2
rsf-design/src/assets/images/avatar/avatar4.webp | 0
rsf-design/src/views/auth/register/index.vue | 178
rsf-design/src/assets/images/settings/menu_styles/dark.png | 0
rsf-design/src/views/dashboard/console/modules/card-list.vue | 61
rsf-design/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue | 15
rsf-design/src/utils/ui/colors.js | 125
rsf-design/src/components/core/layouts/art-header-bar/index.vue | 416
321 files changed, 42,074 insertions(+), 0 deletions(-)
diff --git a/rsf-design/.auto-import.json b/rsf-design/.auto-import.json
new file mode 100644
index 0000000..2f09d16
--- /dev/null
+++ b/rsf-design/.auto-import.json
@@ -0,0 +1,325 @@
+{
+ "globals": {
+ "Component": true,
+ "ComponentPublicInstance": true,
+ "ComputedRef": true,
+ "DirectiveBinding": true,
+ "EffectScope": true,
+ "ElLoading": true,
+ "ElMessage": true,
+ "ExtractDefaultPropTypes": true,
+ "ExtractPropTypes": true,
+ "ExtractPublicPropTypes": true,
+ "InjectionKey": true,
+ "MaybeRef": true,
+ "MaybeRefOrGetter": true,
+ "PropType": true,
+ "Ref": true,
+ "ShallowRef": true,
+ "Slot": true,
+ "Slots": true,
+ "VNode": true,
+ "WritableComputedRef": true,
+ "acceptHMRUpdate": true,
+ "asyncComputed": true,
+ "autoResetRef": true,
+ "computed": true,
+ "computedAsync": true,
+ "computedEager": true,
+ "computedInject": true,
+ "computedWithControl": true,
+ "controlledComputed": true,
+ "controlledRef": true,
+ "createApp": true,
+ "createEventHook": true,
+ "createGlobalState": true,
+ "createInjectionState": true,
+ "createPinia": true,
+ "createReactiveFn": true,
+ "createRef": true,
+ "createReusableTemplate": true,
+ "createSharedComposable": true,
+ "createTemplatePromise": true,
+ "createUnrefFn": true,
+ "customRef": true,
+ "debouncedRef": true,
+ "debouncedWatch": true,
+ "defineAsyncComponent": true,
+ "defineComponent": true,
+ "defineStore": true,
+ "eagerComputed": true,
+ "effectScope": true,
+ "extendRef": true,
+ "getActivePinia": true,
+ "getCurrentInstance": true,
+ "getCurrentScope": true,
+ "getCurrentWatcher": true,
+ "h": true,
+ "ignorableWatch": true,
+ "inject": true,
+ "injectLocal": true,
+ "isDefined": true,
+ "isProxy": true,
+ "isReactive": true,
+ "isReadonly": true,
+ "isRef": true,
+ "isShallow": true,
+ "makeDestructurable": true,
+ "mapActions": true,
+ "mapGetters": true,
+ "mapState": true,
+ "mapStores": true,
+ "mapWritableState": true,
+ "markRaw": true,
+ "nextTick": true,
+ "onActivated": true,
+ "onBeforeMount": true,
+ "onBeforeRouteLeave": true,
+ "onBeforeRouteUpdate": true,
+ "onBeforeUnmount": true,
+ "onBeforeUpdate": true,
+ "onClickOutside": true,
+ "onDeactivated": true,
+ "onElementRemoval": true,
+ "onErrorCaptured": true,
+ "onKeyStroke": true,
+ "onLongPress": true,
+ "onMounted": true,
+ "onRenderTracked": true,
+ "onRenderTriggered": true,
+ "onScopeDispose": true,
+ "onServerPrefetch": true,
+ "onStartTyping": true,
+ "onUnmounted": true,
+ "onUpdated": true,
+ "onWatcherCleanup": true,
+ "pausableWatch": true,
+ "provide": true,
+ "provideLocal": true,
+ "reactify": true,
+ "reactifyObject": true,
+ "reactive": true,
+ "reactiveComputed": true,
+ "reactiveOmit": true,
+ "reactivePick": true,
+ "readonly": true,
+ "ref": true,
+ "refAutoReset": true,
+ "refDebounced": true,
+ "refDefault": true,
+ "refThrottled": true,
+ "refWithControl": true,
+ "resolveComponent": true,
+ "resolveRef": true,
+ "resolveUnref": true,
+ "setActivePinia": true,
+ "setMapStoreSuffix": true,
+ "shallowReactive": true,
+ "shallowReadonly": true,
+ "shallowRef": true,
+ "storeToRefs": true,
+ "syncRef": true,
+ "syncRefs": true,
+ "templateRef": true,
+ "throttledRef": true,
+ "throttledWatch": true,
+ "toRaw": true,
+ "toReactive": true,
+ "toRef": true,
+ "toRefs": true,
+ "toValue": true,
+ "triggerRef": true,
+ "tryOnBeforeMount": true,
+ "tryOnBeforeUnmount": true,
+ "tryOnMounted": true,
+ "tryOnScopeDispose": true,
+ "tryOnUnmounted": true,
+ "unref": true,
+ "unrefElement": true,
+ "until": true,
+ "useActiveElement": true,
+ "useAnimate": true,
+ "useArrayDifference": true,
+ "useArrayEvery": true,
+ "useArrayFilter": true,
+ "useArrayFind": true,
+ "useArrayFindIndex": true,
+ "useArrayFindLast": true,
+ "useArrayIncludes": true,
+ "useArrayJoin": true,
+ "useArrayMap": true,
+ "useArrayReduce": true,
+ "useArraySome": true,
+ "useArrayUnique": true,
+ "useAsyncQueue": true,
+ "useAsyncState": true,
+ "useAttrs": true,
+ "useBase64": true,
+ "useBattery": true,
+ "useBluetooth": true,
+ "useBreakpoints": true,
+ "useBroadcastChannel": true,
+ "useBrowserLocation": true,
+ "useCached": true,
+ "useClipboard": true,
+ "useClipboardItems": true,
+ "useCloned": true,
+ "useColorMode": true,
+ "useConfirmDialog": true,
+ "useCountdown": true,
+ "useCounter": true,
+ "useCssModule": true,
+ "useCssVar": true,
+ "useCssVars": true,
+ "useCurrentElement": true,
+ "useCycleList": true,
+ "useDark": true,
+ "useDateFormat": true,
+ "useDebounce": true,
+ "useDebounceFn": true,
+ "useDebouncedRefHistory": true,
+ "useDeviceMotion": true,
+ "useDeviceOrientation": true,
+ "useDevicePixelRatio": true,
+ "useDevicesList": true,
+ "useDisplayMedia": true,
+ "useDocumentVisibility": true,
+ "useDraggable": true,
+ "useDropZone": true,
+ "useElementBounding": true,
+ "useElementByPoint": true,
+ "useElementHover": true,
+ "useElementSize": true,
+ "useElementVisibility": true,
+ "useEventBus": true,
+ "useEventListener": true,
+ "useEventSource": true,
+ "useEyeDropper": true,
+ "useFavicon": true,
+ "useFetch": true,
+ "useFileDialog": true,
+ "useFileSystemAccess": true,
+ "useFocus": true,
+ "useFocusWithin": true,
+ "useFps": true,
+ "useFullscreen": true,
+ "useGamepad": true,
+ "useGeolocation": true,
+ "useId": true,
+ "useIdle": true,
+ "useImage": true,
+ "useInfiniteScroll": true,
+ "useIntersectionObserver": true,
+ "useInterval": true,
+ "useIntervalFn": true,
+ "useKeyModifier": true,
+ "useLastChanged": true,
+ "useLink": true,
+ "useLocalStorage": true,
+ "useMagicKeys": true,
+ "useManualRefHistory": true,
+ "useMediaControls": true,
+ "useMediaQuery": true,
+ "useMemoize": true,
+ "useMemory": true,
+ "useModel": true,
+ "useMounted": true,
+ "useMouse": true,
+ "useMouseInElement": true,
+ "useMousePressed": true,
+ "useMutationObserver": true,
+ "useNavigatorLanguage": true,
+ "useNetwork": true,
+ "useNow": true,
+ "useObjectUrl": true,
+ "useOffsetPagination": true,
+ "useOnline": true,
+ "usePageLeave": true,
+ "useParallax": true,
+ "useParentElement": true,
+ "usePerformanceObserver": true,
+ "usePermission": true,
+ "usePointer": true,
+ "usePointerLock": true,
+ "usePointerSwipe": true,
+ "usePreferredColorScheme": true,
+ "usePreferredContrast": true,
+ "usePreferredDark": true,
+ "usePreferredLanguages": true,
+ "usePreferredReducedMotion": true,
+ "usePreferredReducedTransparency": true,
+ "usePrevious": true,
+ "useRafFn": true,
+ "useRefHistory": true,
+ "useResizeObserver": true,
+ "useRoute": true,
+ "useRouter": true,
+ "useSSRWidth": true,
+ "useScreenOrientation": true,
+ "useScreenSafeArea": true,
+ "useScriptTag": true,
+ "useScroll": true,
+ "useScrollLock": true,
+ "useSessionStorage": true,
+ "useShare": true,
+ "useSlots": true,
+ "useSorted": true,
+ "useSpeechRecognition": true,
+ "useSpeechSynthesis": true,
+ "useStepper": true,
+ "useStorage": true,
+ "useStorageAsync": true,
+ "useStyleTag": true,
+ "useSupported": true,
+ "useSwipe": true,
+ "useTemplateRef": true,
+ "useTemplateRefsList": true,
+ "useTextDirection": true,
+ "useTextSelection": true,
+ "useTextareaAutosize": true,
+ "useThrottle": true,
+ "useThrottleFn": true,
+ "useThrottledRefHistory": true,
+ "useTimeAgo": true,
+ "useTimeAgoIntl": true,
+ "useTimeout": true,
+ "useTimeoutFn": true,
+ "useTimeoutPoll": true,
+ "useTimestamp": true,
+ "useTitle": true,
+ "useToNumber": true,
+ "useToString": true,
+ "useToggle": true,
+ "useTransition": true,
+ "useUrlSearchParams": true,
+ "useUserMedia": true,
+ "useVModel": true,
+ "useVModels": true,
+ "useVibrate": true,
+ "useVirtualList": true,
+ "useWakeLock": true,
+ "useWebNotification": true,
+ "useWebSocket": true,
+ "useWebWorker": true,
+ "useWebWorkerFn": true,
+ "useWindowFocus": true,
+ "useWindowScroll": true,
+ "useWindowSize": true,
+ "watch": true,
+ "watchArray": true,
+ "watchAtMost": true,
+ "watchDebounced": true,
+ "watchDeep": true,
+ "watchEffect": true,
+ "watchIgnorable": true,
+ "watchImmediate": true,
+ "watchOnce": true,
+ "watchPausable": true,
+ "watchPostEffect": true,
+ "watchSyncEffect": true,
+ "watchThrottled": true,
+ "watchTriggerable": true,
+ "watchWithFilter": true,
+ "whenever": true
+ }
+}
diff --git a/rsf-design/.env b/rsf-design/.env
new file mode 100644
index 0000000..fc05e05
--- /dev/null
+++ b/rsf-design/.env
@@ -0,0 +1,25 @@
+# 銆愰�氱敤銆戠幆澧冨彉閲�
+
+# 鐗堟湰鍙�
+VITE_VERSION = 3.0.2
+
+# 绔彛鍙�
+VITE_PORT = 3006
+
+# 搴旂敤閮ㄧ讲鍩虹璺緞锛堝閮ㄧ讲鍦ㄥ瓙鐩綍 /admin锛屽垯璁剧疆涓� /admin/锛�
+VITE_BASE_URL = /
+
+# 鏉冮檺妯″紡銆� frontend 鍓嶇妯″紡 / backend 鍚庣妯″紡 銆�
+VITE_ACCESS_MODE = frontend
+
+# 璺ㄥ煙璇锋眰鏃舵槸鍚︽惡甯� Cookie锛堝紑鍚墠闇�纭繚鍚庣鏀寔锛�
+VITE_WITH_CREDENTIALS = false
+
+# 鏄惁鎵撳紑璺敱淇℃伅
+VITE_OPEN_ROUTE_INFO = false
+
+# 閿佸睆鍔犲瘑瀵嗛挜
+VITE_LOCK_ENCRYPT_KEY = s3cur3k3y4adpro
+
+# 鐧诲綍 / WebSocket 绀轰緥榛樿鍦板潃
+VITE_LOGIN_WEBSOCKET = ws://localhost:8080/ws
diff --git a/rsf-design/.env.development b/rsf-design/.env.development
new file mode 100644
index 0000000..38d70b4
--- /dev/null
+++ b/rsf-design/.env.development
@@ -0,0 +1,13 @@
+# 銆愬紑鍙戙�戠幆澧冨彉閲�
+
+# 搴旂敤閮ㄧ讲鍩虹璺緞锛堝閮ㄧ讲鍦ㄥ瓙鐩綍 /admin锛屽垯璁剧疆涓� /admin/锛�
+VITE_BASE_URL = /
+
+# API 璇锋眰鍩虹璺緞锛堝紑鍙戠幆澧冭缃负 / 浣跨敤浠g悊锛岀敓浜х幆澧冭缃负瀹屾暣鍚庣鍦板潃锛�
+VITE_API_URL = /
+
+# 浠g悊鐩爣鍦板潃锛堝紑鍙戠幆澧冮�氳繃 Vite 浠g悊杞彂璇锋眰鍒版鍦板潃锛岃В鍐宠法鍩熼棶棰橈級
+VITE_API_PROXY_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+
+# Delete console
+VITE_DROP_CONSOLE = false
diff --git a/rsf-design/.env.production b/rsf-design/.env.production
new file mode 100644
index 0000000..5941f31
--- /dev/null
+++ b/rsf-design/.env.production
@@ -0,0 +1,10 @@
+# 銆愮敓浜с�戠幆澧冨彉閲�
+
+# 搴旂敤閮ㄧ讲鍩虹璺緞锛堝閮ㄧ讲鍦ㄥ瓙鐩綍 /admin锛屽垯璁剧疆涓� /admin/锛�
+VITE_BASE_URL = /
+
+# API 鍦板潃鍓嶇紑
+VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+
+# Delete console
+VITE_DROP_CONSOLE = true
diff --git a/rsf-design/.gitattributes b/rsf-design/.gitattributes
new file mode 100644
index 0000000..866e8ee
--- /dev/null
+++ b/rsf-design/.gitattributes
@@ -0,0 +1,2 @@
+*.html linguist-detectable=false
+*.vue linguist-detectable=true
diff --git a/rsf-design/.gitignore b/rsf-design/.gitignore
new file mode 100644
index 0000000..dfbd107
--- /dev/null
+++ b/rsf-design/.gitignore
@@ -0,0 +1,14 @@
+node_modules/
+dist/
+dist-ssr/
+.vite/
+.playwright-cli/
+output/
+coverage/
+*.log
+__pycache__/
+*.pyc
+*.local
+.DS_Store
+Thumbs.db
+.cursorrules
diff --git a/rsf-design/.prettierignore b/rsf-design/.prettierignore
new file mode 100644
index 0000000..9e96efc
--- /dev/null
+++ b/rsf-design/.prettierignore
@@ -0,0 +1,3 @@
+/node_modules/*
+/dist/*
+/src/main.ts
\ No newline at end of file
diff --git a/rsf-design/.prettierrc b/rsf-design/.prettierrc
new file mode 100644
index 0000000..f3d6ad5
--- /dev/null
+++ b/rsf-design/.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/rsf-design/.stylelintignore b/rsf-design/.stylelintignore
new file mode 100644
index 0000000..476ea45
--- /dev/null
+++ b/rsf-design/.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/rsf-design/.stylelintrc.cjs b/rsf-design/.stylelintrc.cjs
new file mode 100644
index 0000000..9dbea0b
--- /dev/null
+++ b/rsf-design/.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 銆乪xport 銆乨eep浼被
+ '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/rsf-design/LICENSE b/rsf-design/LICENSE
new file mode 100644
index 0000000..68322de
--- /dev/null
+++ b/rsf-design/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 SuperManTT
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/rsf-design/commitlint.config.cjs b/rsf-design/commitlint.config.cjs
new file mode 100644
index 0000000..d2ef1bd
--- /dev/null
+++ b/rsf-design/commitlint.config.cjs
@@ -0,0 +1,97 @@
+/**
+ * commitlint 閰嶇疆鏂囦欢
+ * 鏂囨。
+ * https://commitlint.js.org/#/reference-rules
+ * https://cz-git.qbb.sh/zh/guide/
+ */
+
+module.exports = {
+ // 缁ф壙鐨勮鍒�
+ extends: ['@commitlint/config-conventional'],
+ // 鑷畾涔夎鍒�
+ rules: {
+ // 鎻愪氦绫诲瀷鏋氫妇锛実it鎻愪氦type蹇呴』鏄互涓嬬被鍨�
+ 'type-enum': [
+ 2,
+ 'always',
+ [
+ 'feat', // 鏂板鍔熻兘
+ 'fix', // 淇缂洪櫡
+ 'docs', // 鏂囨。鍙樻洿
+ 'style', // 浠g爜鏍煎紡锛堜笉褰卞搷鍔熻兘锛屼緥濡傜┖鏍笺�佸垎鍙风瓑鏍煎紡淇锛�
+ 'refactor', // 浠g爜閲嶆瀯锛堜笉鍖呮嫭 bug 淇銆佸姛鑳芥柊澧烇級
+ 'perf', // 鎬ц兘浼樺寲
+ 'test', // 娣诲姞鐤忔紡娴嬭瘯鎴栧凡鏈夋祴璇曟敼鍔�
+ 'build', // 鏋勫缓娴佺▼銆佸閮ㄤ緷璧栧彉鏇达紙濡傚崌绾� npm 鍖呫�佷慨鏀� webpack 閰嶇疆绛夛級
+ 'ci', // 淇敼 CI 閰嶇疆銆佽剼鏈�
+ 'revert', // 鍥炴粴 commit
+ 'chore', // 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鍜屽簱鐨勬洿鏀癸紙涓嶅奖鍝嶆簮鏂囦欢銆佹祴璇曠敤渚嬶級
+ 'wip' // 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鍜屽簱鐨勬洿鏀癸紙涓嶅奖鍝嶆簮鏂囦欢銆佹祴璇曠敤渚嬶級
+ ]
+ ],
+ 'subject-case': [0] // subject澶у皬鍐欎笉鍋氭牎楠�
+ },
+
+ prompt: {
+ messages: {
+ type: '閫夋嫨浣犺鎻愪氦鐨勭被鍨� :',
+ scope: '閫夋嫨涓�涓彁浜よ寖鍥达紙鍙�夛級:',
+ customScope: '璇疯緭鍏ヨ嚜瀹氫箟鐨勬彁浜よ寖鍥� :',
+ subject: '濉啓绠�鐭簿鐐肩殑鍙樻洿鎻忚堪 :\n',
+ body: '濉啓鏇村姞璇︾粏鐨勫彉鏇存弿杩帮紙鍙�夛級銆備娇鐢� "|" 鎹㈣ :\n',
+ breaking: '鍒椾妇闈炲吋瀹规�ч噸澶х殑鍙樻洿锛堝彲閫夛級銆備娇鐢� "|" 鎹㈣ :\n',
+ footerPrefixesSelect: '閫夋嫨鍏宠仈issue鍓嶇紑锛堝彲閫夛級:',
+ customFooterPrefix: '杈撳叆鑷畾涔塱ssue鍓嶇紑 :',
+ footer: '鍒椾妇鍏宠仈issue (鍙��) 渚嬪: #31, #I3244 :\n',
+ generatingByAI: '姝e湪閫氳繃 AI 鐢熸垚浣犵殑鎻愪氦绠�鐭弿杩�...',
+ generatedSelectByAI: '閫夋嫨涓�涓� AI 鐢熸垚鐨勭畝鐭弿杩�:',
+ confirmCommit: '鏄惁鎻愪氦鎴栦慨鏀筩ommit ?'
+ },
+ // prettier-ignore
+ types: [
+ { value: "feat", name: "feat: 鏂板鍔熻兘" },
+ { value: "fix", name: "fix: 淇缂洪櫡" },
+ { value: "docs", name: "docs: 鏂囨。鍙樻洿" },
+ { value: "style", name: "style: 浠g爜鏍煎紡锛堜笉褰卞搷鍔熻兘锛屼緥濡傜┖鏍笺�佸垎鍙风瓑鏍煎紡淇锛�" },
+ { value: "refactor", name: "refactor: 浠g爜閲嶆瀯锛堜笉鍖呮嫭 bug 淇銆佸姛鑳芥柊澧烇級" },
+ { value: "perf", name: "perf: 鎬ц兘浼樺寲" },
+ { value: "test", name: "test: 娣诲姞鐤忔紡娴嬭瘯鎴栧凡鏈夋祴璇曟敼鍔�" },
+ { value: "build", name: "build: 鏋勫缓娴佺▼銆佸閮ㄤ緷璧栧彉鏇达紙濡傚崌绾� npm 鍖呫�佷慨鏀� vite 閰嶇疆绛夛級" },
+ { value: "ci", name: "ci: 淇敼 CI 閰嶇疆銆佽剼鏈�" },
+ { value: "revert", name: "revert: 鍥炴粴 commit" },
+ { value: "chore", name: "chore: 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鍜屽簱鐨勬洿鏀癸紙涓嶅奖鍝嶆簮鏂囦欢銆佹祴璇曠敤渚嬶級" },
+ ],
+ useEmoji: true,
+ emojiAlign: 'center',
+ useAI: false,
+ aiNumber: 1,
+ themeColorCode: '',
+ scopes: [],
+ allowCustomScopes: true,
+ allowEmptyScopes: true,
+ customScopesAlign: 'bottom',
+ customScopesAlias: 'custom',
+ emptyScopesAlias: 'empty',
+ upperCaseSubject: false,
+ markBreakingChangeMode: false,
+ allowBreakingChanges: ['feat', 'fix'],
+ breaklineNumber: 100,
+ breaklineChar: '|',
+ skipQuestions: ['breaking', 'footerPrefix', 'footer'], // 璺宠繃鐨勬楠�
+ issuePrefixes: [{ value: 'closed', name: 'closed: ISSUES has been processed' }],
+ customIssuePrefixAlign: 'top',
+ emptyIssuePrefixAlias: 'skip',
+ customIssuePrefixAlias: 'custom',
+ allowCustomIssuePrefix: true,
+ allowEmptyIssuePrefix: true,
+ confirmColorize: true,
+ maxHeaderLength: Infinity,
+ maxSubjectLength: Infinity,
+ minSubjectLength: 0,
+ scopeOverrides: undefined,
+ defaultBody: '',
+ defaultIssues: '',
+ defaultScope: '',
+ defaultSubject: ''
+ }
+}
diff --git a/rsf-design/eslint.config.mjs b/rsf-design/eslint.config.mjs
new file mode 100644
index 0000000..8edea52
--- /dev/null
+++ b/rsf-design/eslint.config.mjs
@@ -0,0 +1,72 @@
+import fs from 'fs'
+import path, { dirname } from 'path'
+import { fileURLToPath } from 'url'
+
+import pluginJs from '@eslint/js'
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
+import pluginVue from 'eslint-plugin-vue'
+import globals from 'globals'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = dirname(__filename)
+const autoImportPath = path.resolve(__dirname, '.auto-import.json')
+const autoImportConfig = loadAutoImportConfig()
+
+export default [
+ {
+ ignores: [
+ 'node_modules',
+ 'dist',
+ 'public',
+ '.vscode/**',
+ 'src/assets/**',
+ 'src/utils/console.ts'
+ ]
+ },
+ pluginJs.configs.recommended,
+ ...pluginVue.configs['flat/essential'],
+ {
+ files: ['**/*.{js,mjs,cjs,vue}'],
+ languageOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ globals: {
+ ...globals.browser,
+ ...globals.node,
+ ...autoImportConfig.globals,
+ Api: 'readonly',
+ __APP_VERSION__: 'readonly'
+ }
+ },
+ rules: {
+ quotes: ['error', 'single'],
+ semi: ['error', 'never'],
+ 'no-var': 'error',
+ 'vue/multi-word-component-names': 'off',
+ 'no-multiple-empty-lines': ['warn', { max: 1 }],
+ 'no-unexpected-multiline': 'error'
+ }
+ },
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module'
+ }
+ }
+ },
+ eslintPluginPrettierRecommended
+]
+
+function loadAutoImportConfig() {
+ if (!fs.existsSync(autoImportPath)) {
+ return { globals: {} }
+ }
+
+ try {
+ return JSON.parse(fs.readFileSync(autoImportPath, 'utf-8'))
+ } catch {
+ return { globals: {} }
+ }
+}
diff --git a/rsf-design/index.html b/rsf-design/index.html
new file mode 100644
index 0000000..38c6302
--- /dev/null
+++ b/rsf-design/index.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Art Design Pro</title>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta
+ name="description"
+ content="Art Design Pro - A modern admin dashboard template built with Vue 3, JavaScript, and Element Plus."
+ />
+ <link rel="shortcut icon" type="image/x-icon" href="src/assets/images/favicon.ico" />
+
+ <style>
+ /* 闃叉椤甸潰鍒锋柊鏃剁櫧灞忕殑鍒濆鏍峰紡 */
+ html {
+ background-color: #fafbfc;
+ }
+
+ html.dark {
+ background-color: #070707;
+ }
+ </style>
+
+ <script>
+ // 鍒濆鍖� html class 涓婚灞炴��
+ ;(function () {
+ try {
+ if (typeof Storage === 'undefined' || !window.localStorage) {
+ return
+ }
+
+ const themeType = localStorage.getItem('sys-theme')
+ if (themeType === 'dark') {
+ document.documentElement.classList.add('dark')
+ }
+ } catch (e) {
+ console.warn('Failed to apply initial theme:', e)
+ }
+ })()
+ </script>
+ </head>
+
+ <body>
+ <div id="app"></div>
+ <script type="module" src="/src/main.js"></script>
+ </body>
+</html>
diff --git a/rsf-design/package.json b/rsf-design/package.json
new file mode 100644
index 0000000..b5e96f7
--- /dev/null
+++ b/rsf-design/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "art-design-pro",
+ "version": "0.0.0",
+ "type": "module",
+ "engines": {
+ "node": ">=20.19.0",
+ "pnpm": ">=8.8.0"
+ },
+ "scripts": {
+ "dev": "vite --open",
+ "build": "vite build",
+ "serve": "vite preview",
+ "icons:build-local": "node scripts/build-local-iconify-collections.mjs",
+ "clean:dev": "node scripts/clean-dev.js",
+ "lint": "eslint . --ext .js,.mjs,.cjs,.vue",
+ "fix": "eslint . --ext .js,.mjs,.cjs,.vue --fix",
+ "lint:prettier": "prettier --write \"**/*.{js,mjs,cjs,json,css,less,scss,vue,html,md}\"",
+ "lint:stylelint": "stylelint \"**/*.{css,scss,vue}\" --fix"
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "^2.3.2",
+ "@iconify-json/fluent": "^1.2.42",
+ "@iconify-json/icon-park-outline": "^1.2.4",
+ "@iconify-json/iconamoon": "^1.2.2",
+ "@iconify-json/ix": "^1.2.11",
+ "@iconify-json/line-md": "^1.2.16",
+ "@iconify-json/ri": "^1.2.10",
+ "@iconify-json/svg-spinners": "^1.2.4",
+ "@iconify-json/system-uicons": "^1.2.4",
+ "@iconify-json/vaadin": "^1.2.1",
+ "@iconify/vue": "^5.0.0",
+ "@tailwindcss/vite": "^4.1.14",
+ "@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",
+ "echarts": "^6.0.0",
+ "element-plus": "^2.11.2",
+ "file-saver": "^2.0.5",
+ "highlight.js": "^11.10.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",
+ "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": {
+ "@eslint/js": "^9.9.1",
+ "@types/node": "^24.0.5",
+ "@vitejs/plugin-vue": "^6.0.1",
+ "@vue/compiler-sfc": "^3.0.5",
+ "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",
+ "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",
+ "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",
+ "vue-demi": "^0.14.9",
+ "vue-img-cutter": "^3.0.5"
+ }
+}
diff --git a/rsf-design/pnpm-lock.yaml b/rsf-design/pnpm-lock.yaml
new file mode 100644
index 0000000..b62433e
--- /dev/null
+++ b/rsf-design/pnpm-lock.yaml
@@ -0,0 +1,5961 @@
+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-json/fluent':
+ specifier: ^1.2.42
+ version: 1.2.42
+ '@iconify-json/icon-park-outline':
+ specifier: ^1.2.4
+ version: 1.2.4
+ '@iconify-json/iconamoon':
+ specifier: ^1.2.2
+ version: 1.2.2
+ '@iconify-json/ix':
+ specifier: ^1.2.11
+ version: 1.2.11
+ '@iconify-json/line-md':
+ specifier: ^1.2.16
+ version: 1.2.16
+ '@iconify-json/ri':
+ specifier: ^1.2.10
+ version: 1.2.10
+ '@iconify-json/svg-spinners':
+ specifier: ^1.2.4
+ version: 1.2.4
+ '@iconify-json/system-uicons':
+ specifier: ^1.2.4
+ version: 1.2.4
+ '@iconify-json/vaadin':
+ specifier: ^1.2.1
+ version: 1.2.1
+ '@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))
+ '@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
+ echarts:
+ specifier: ^6.0.0
+ version: 6.0.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
+ 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
+ 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:
+ '@eslint/js':
+ specifier: ^9.9.1
+ version: 9.36.0
+ '@types/node':
+ specifier: ^24.0.5
+ version: 24.8.1
+ '@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
+ 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
+ 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
+ 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))
+ 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)
+
+packages:
+
+ '@antfu/utils@0.7.10':
+ resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
+
+ '@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==}
+
+ '@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.25.10':
+ resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@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.25.10':
+ resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.10':
+ resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.10':
+ resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.10':
+ resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.10':
+ resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.25.10':
+ resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.25.10':
+ resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ 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.25.10':
+ resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.10':
+ resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.10':
+ resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ 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.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.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.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.25.10':
+ resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.10':
+ resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ 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.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-json/fluent@1.2.42':
+ resolution: {integrity: sha512-C0s0x/RHLcmG4fy710yA206ruiXegHqHwFFIzMh3KaFIPBPkNLfp5dKKjAnc5A60T8x/bbef1IxdSNHukF8vZA==}
+
+ '@iconify-json/icon-park-outline@1.2.4':
+ resolution: {integrity: sha512-NyZxXe2gD2TbTOyoRRMdtEJhr6i2KQCdDlYYoOn5oZLndQjwpIhw79hzeFhXvP38/o40D3gQ+l+IaSJgbB+0TQ==}
+
+ '@iconify-json/iconamoon@1.2.2':
+ resolution: {integrity: sha512-Xn7YeSDniPgutPr0qil/iQwQelq975OYQ/i2twGjcK4DjGOXBrBC+6q45WtVuQbFfXzM7bgijv4yVnxy0OqUdQ==}
+
+ '@iconify-json/ix@1.2.11':
+ resolution: {integrity: sha512-3mMMD+3d2jVRNGhsLZgxCIw+mpDD3B7a/rBv+V+DzwUPnuqoFYzAf/lTq9GhgI5/8DOpzpYJ4Toho+2TLQVMGg==}
+
+ '@iconify-json/line-md@1.2.16':
+ resolution: {integrity: sha512-esHPUQUp30NyqJWa7MNu6ExdG8z/aEFC5r56rVyRGhtJ7acOme6YtYPnDOgVu6p+kk24rM8602xkFuBC6g6x+w==}
+
+ '@iconify-json/ri@1.2.10':
+ resolution: {integrity: sha512-WWMhoncVVM+Xmu9T5fgu2lhYRrKTEWhKk3Com0KiM111EeEsRLiASjpsFKnC/SrB6covhUp95r2mH8tGxhgd5Q==}
+
+ '@iconify-json/svg-spinners@1.2.4':
+ resolution: {integrity: sha512-ayn0pogFPwJA1WFZpDnoq9/hjDxN+keeCMyThaX4d3gSJ3y0mdKUxIA/b1YXWGtY9wVtZmxwcvOIeEieG4+JNg==}
+
+ '@iconify-json/system-uicons@1.2.4':
+ resolution: {integrity: sha512-9WB9dmEm+TRCXI5Ml2IY8zQAPZES8euKxY0VOaf8D6E6ZaEr7ztO6DChMlGg7qWECs3m3FjFUqNgBx8ZpB+djw==}
+
+ '@iconify-json/vaadin@1.2.1':
+ resolution: {integrity: sha512-dT2PWQp5tPxqJsWCc7qXDRVMcGeOf3e+Hys1dcMQrfVVnaRgXxYXgA0Wj98N5QjZMIf9+/iqAKvY1Yuk484hEg==}
+
+ '@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/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'}
+
+ '@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]
+ libc: [glibc]
+
+ '@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]
+ libc: [musl]
+
+ '@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]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-arm64-musl@2.5.1':
+ resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@parcel/watcher-linux-x64-glibc@2.5.1':
+ resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-x64-musl@2.5.1':
+ resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@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'}
+
+ '@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]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.52.3':
+ resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.52.3':
+ resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.52.3':
+ resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loong64-gnu@4.52.3':
+ resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.52.3':
+ resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.52.3':
+ resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.52.3':
+ resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.52.3':
+ resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.52.3':
+ resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.52.3':
+ resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@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]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.14':
+ resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.14':
+ resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.14':
+ resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@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/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/web-bluetooth@0.0.16':
+ resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
+
+ '@types/web-bluetooth@0.0.21':
+ resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
+
+ '@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
+
+ '@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/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/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==}
+
+ '@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
+
+ 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'}
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ajv@8.17.1:
+ resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ 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-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
+ 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==}
+
+ 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==}
+
+ baseline-browser-mapping@2.8.8:
+ resolution: {integrity: sha512-be0PUaPsQX/gPWWgFsdD+GFzaoig5PXaUC1xLkQiYdDnANU8sMnHoQd8JhbJQuvTWrWLyeFN9Imb5Qtfvr4RrQ==}
+ hasBin: true
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
+ birpc@2.6.1:
+ resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ 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==}
+
+ bundle-name@4.1.0:
+ resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
+ engines: {node: '>=18'}
+
+ cacheable@2.0.2:
+ resolution: {integrity: sha512-dWjhLx8RWnPsAWVKwW/wI6OJpQ/hSVb1qS0NUif8TR9vRiSwci7Gey8x04kRU9iAF+Rnbtex5Kjjfg/aB5w8Pg==}
+
+ 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'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ 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'}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ codepage@1.15.0:
+ resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
+ engines: {node: '>=0.8'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ colord@2.9.3:
+ resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+ 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==}
+
+ 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@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
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ 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==}
+
+ dayjs@1.11.18:
+ resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ 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'}
+
+ 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-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'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
+ 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'}
+
+ echarts@6.0.0:
+ resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
+
+ electron-to-chromium@1.5.227:
+ resolution: {integrity: sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==}
+
+ element-plus@2.11.4:
+ resolution: {integrity: sha512-sLq+Ypd0cIVilv8wGGMEGvzRVBBsRpJjnAS5PsI/1JU1COZXqzH3N1UYMUc/HCdvdjf6dfrBy80Sj7KcACsT7w==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ 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'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ 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.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@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==}
+
+ execa@9.6.0:
+ resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==}
+ engines: {node: ^18.19.0 || >=20.5.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==}
+
+ 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@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-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ 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
+
+ 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'}
+
+ 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-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@9.0.1:
+ resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
+ engines: {node: '>=18'}
+
+ get-tsconfig@4.10.1:
+ resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
+
+ 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'}
+
+ global-modules@2.0.0:
+ resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
+ engines: {node: '>=6'}
+
+ 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==}
+
+ 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'}
+
+ highlight.js@11.11.1:
+ resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
+ engines: {node: '>=12.0.0'}
+
+ hookable@5.5.3:
+ resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
+
+ hookified@1.12.1:
+ resolution: {integrity: sha512-xnKGl+iMIlhrZmGHB729MqlmPoWBznctSQTYCpFKqNsCgimJQmithcW0xSQMMFzYnV2iKUh25alswn6epgxS0Q==}
+
+ 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==}
+
+ human-signals@8.0.1:
+ resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
+ engines: {node: '>=18.18.0'}
+
+ i18next@20.6.1:
+ resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
+
+ 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'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ 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-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-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ 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-stream@4.0.1:
+ resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
+ engines: {node: '>=18'}
+
+ 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-what@4.1.16:
+ resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
+ engines: {node: '>=12.13'}
+
+ 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==}
+
+ jiti@2.6.0:
+ resolution: {integrity: sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==}
+ hasBin: true
+
+ 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
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ 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==}
+
+ 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]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.30.1:
+ resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.30.1:
+ resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.30.1:
+ resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ 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'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ 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'}
+
+ 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.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ 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@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+ 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@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ engines: {node: '>=18'}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ 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'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ 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==}
+
+ 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==}
+
+ 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@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==}
+
+ ohash@2.0.11:
+ resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
+ 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'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ 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'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ 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-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ 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'}
+
+ 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'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ 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==}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ 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'}
+
+ 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==}
+
+ 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
+
+ run-applescript@7.1.0:
+ resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
+ engines: {node: '>=18'}
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ sass@1.93.2:
+ resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ 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'}
+
+ 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'}
+
+ 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'}
+
+ 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==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ 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@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==}
+
+ 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
+
+ tiny-warning@1.0.3:
+ resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.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'}
+
+ tslib@2.3.0:
+ resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
+ 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@2.7.3:
+ resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
+
+ 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.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@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==}
+
+ 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-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@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
+
+ 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@3.5.22:
+ resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ webpack-virtual-modules@0.6.2:
+ resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+ 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
+
+ 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'}
+
+ write-file-atomic@5.0.1:
+ resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ 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'}
+
+ 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'}
+
+ 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': {}
+
+ '@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': {}
+
+ '@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.25.10':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/android-arm@0.25.10':
+ optional: true
+
+ '@esbuild/android-x64@0.25.10':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.10':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.10':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.10':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.10':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.10':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.10':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.10':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.10':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.10':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.10':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.10':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.10':
+ 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-json/fluent@1.2.42':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/icon-park-outline@1.2.4':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/iconamoon@1.2.2':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/ix@1.2.11':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/line-md@1.2.16':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/ri@1.2.10':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/svg-spinners@1.2.4':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/system-uicons@1.2.4':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify-json/vaadin@1.2.1':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@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/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
+
+ '@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
+
+ '@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/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/web-bluetooth@0.0.16': {}
+
+ '@types/web-bluetooth@0.0.21': {}
+
+ '@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)
+
+ '@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/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/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': {}
+
+ '@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
+
+ acorn-jsx@5.3.2(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ adler-32@1.3.1: {}
+
+ 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
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+
+ argparse@2.0.1: {}
+
+ array-union@2.1.0: {}
+
+ astral-regex@2.0.0: {}
+
+ async-validator@4.2.5: {}
+
+ asynckit@0.4.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: {}
+
+ baseline-browser-mapping@2.8.8: {}
+
+ binary-extensions@2.3.0: {}
+
+ birpc@2.6.1: {}
+
+ boolbase@1.0.0: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ 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: {}
+
+ bundle-name@4.1.0:
+ dependencies:
+ run-applescript: 7.1.0
+
+ 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
+
+ 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
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.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: {}
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ codepage@1.15.0: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ colord@2.9.3: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ commander@2.20.3: {}
+
+ compute-scroll-into-view@1.0.20: {}
+
+ concat-map@0.0.1: {}
+
+ confbox@0.1.8: {}
+
+ confbox@0.2.2: {}
+
+ convert-source-map@2.0.0: {}
+
+ copy-anything@3.0.5:
+ dependencies:
+ is-what: 4.1.16
+
+ core-js@3.45.1: {}
+
+ 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: {}
+
+ csstype@3.1.3: {}
+
+ 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
+
+ dayjs@1.11.18: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ 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
+
+ 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-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
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
+ 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
+
+ echarts@6.0.0:
+ dependencies:
+ tslib: 2.3.0
+ zrender: 6.0.0
+
+ electron-to-chromium@1.5.227: {}
+
+ 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@8.0.0: {}
+
+ enhanced-resolve@5.18.3:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
+ entities@4.5.0: {}
+
+ env-paths@2.2.1: {}
+
+ 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.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@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: {}
+
+ 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
+
+ exsolve@1.0.7: {}
+
+ ext@1.7.0:
+ dependencies:
+ type: 2.7.3
+
+ 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@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-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ 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: {}
+
+ 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
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ 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@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
+ optional: true
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ global-modules@2.0.0:
+ dependencies:
+ global-prefix: 3.0.0
+
+ 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: {}
+
+ 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
+
+ highlight.js@11.11.1: {}
+
+ hookable@5.5.3: {}
+
+ hookified@1.12.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
+
+ human-signals@8.0.1: {}
+
+ i18next@20.6.1:
+ dependencies:
+ '@babel/runtime': 7.28.4
+
+ 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
+
+ imurmurhash@0.1.4: {}
+
+ ini@1.3.8: {}
+
+ 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-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-number@7.0.0: {}
+
+ is-plain-obj@4.1.0: {}
+
+ is-plain-object@5.0.0: {}
+
+ is-stream@4.0.1: {}
+
+ is-unicode-supported@2.1.0: {}
+
+ is-url@1.2.4: {}
+
+ is-what@4.1.16: {}
+
+ 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: {}
+
+ jiti@2.6.0: {}
+
+ js-tokens@4.0.0: {}
+
+ js-tokens@9.0.1: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ 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
+
+ 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
+
+ lines-and-columns@1.2.4: {}
+
+ 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
+
+ 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.merge@4.6.2: {}
+
+ lodash.throttle@4.1.1: {}
+
+ lodash.toarray@4.4.0: {}
+
+ lodash.truncate@4.4.2: {}
+
+ lodash@4.17.21: {}
+
+ 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@13.2.0: {}
+
+ merge2@1.4.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
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ 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: {}
+
+ 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: {}
+
+ normalize-path@3.0.0: {}
+
+ normalize-wheel-es@1.2.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
+
+ ohash@2.0.11: {}
+
+ 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
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ 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: {}
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
+ path-type@4.0.0: {}
+
+ pathe@2.0.3: {}
+
+ perfect-debounce@1.0.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.3: {}
+
+ 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: {}
+
+ proxy-from-env@1.1.0: {}
+
+ 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: {}
+
+ queue-microtask@1.2.3: {}
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.1
+
+ readdirp@4.1.2: {}
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-from@5.0.0: {}
+
+ resolve-pkg-maps@1.0.0:
+ optional: true
+
+ 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
+
+ run-applescript@7.1.0: {}
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ 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
+
+ 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: {}
+
+ 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
+
+ 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: {}
+
+ ssf@0.11.2:
+ dependencies:
+ frac: 1.1.2
+
+ ssr-window@3.0.0: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ 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@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: {}
+
+ 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
+
+ tiny-warning@1.0.3: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ totalist@3.0.1: {}
+
+ tslib@2.3.0: {}
+
+ tsx@4.20.6:
+ dependencies:
+ esbuild: 0.25.10
+ get-tsconfig: 4.10.1
+ optionalDependencies:
+ fsevents: 2.3.3
+ optional: true
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ type-fest@0.20.2: {}
+
+ type@2.7.3: {}
+
+ typescript@5.6.3:
+ optional: true
+
+ ufo@1.6.1: {}
+
+ undici-types@7.14.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@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
+
+ 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-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@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
+
+ 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@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
+
+ webpack-virtual-modules@0.6.2: {}
+
+ which@1.3.1:
+ dependencies:
+ isexe: 2.0.0
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ 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
+
+ write-file-atomic@5.0.1:
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 4.1.0
+
+ 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: {}
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yallist@5.0.0: {}
+
+ yaml@2.8.1:
+ optional: true
+
+ 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: {}
+
+ yoctocolors@2.1.2: {}
+
+ zrender@6.0.0:
+ dependencies:
+ tslib: 2.3.0
diff --git a/rsf-design/public/favicon.ico b/rsf-design/public/favicon.ico
new file mode 100644
index 0000000..df36fcf
--- /dev/null
+++ b/rsf-design/public/favicon.ico
Binary files differ
diff --git a/rsf-design/scripts/build-local-iconify-collections.mjs b/rsf-design/scripts/build-local-iconify-collections.mjs
new file mode 100644
index 0000000..be0d8dc
--- /dev/null
+++ b/rsf-design/scripts/build-local-iconify-collections.mjs
@@ -0,0 +1,161 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { inspect } from 'node:util'
+
+import { icons as fluentIcons } from '@iconify-json/fluent'
+import { icons as iconParkOutlineIcons } from '@iconify-json/icon-park-outline'
+import { icons as iconamoonIcons } from '@iconify-json/iconamoon'
+import { icons as ixIcons } from '@iconify-json/ix'
+import { icons as lineMdIcons } from '@iconify-json/line-md'
+import { icons as remixIcons } from '@iconify-json/ri'
+import { icons as svgSpinnersIcons } from '@iconify-json/svg-spinners'
+import { icons as systemUiconsIcons } from '@iconify-json/system-uicons'
+import { icons as vaadinIcons } from '@iconify-json/vaadin'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const projectRoot = path.resolve(__dirname, '..')
+const srcRoot = path.join(projectRoot, 'src')
+const outputPath = path.join(srcRoot, 'plugins', 'iconify.collections.js')
+
+const iconCollections = {
+ fluent: fluentIcons,
+ 'icon-park-outline': iconParkOutlineIcons,
+ iconamoon: iconamoonIcons,
+ ix: ixIcons,
+ 'line-md': lineMdIcons,
+ ri: remixIcons,
+ 'svg-spinners': svgSpinnersIcons,
+ 'system-uicons': systemUiconsIcons,
+ vaadin: vaadinIcons
+}
+
+function collectSourceFiles(dir) {
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
+
+ return entries.flatMap((entry) => {
+ const fullPath = path.join(dir, entry.name)
+
+ if (entry.isDirectory()) {
+ return collectSourceFiles(fullPath)
+ }
+
+ return /\.(vue|js)$/.test(entry.name) ? [fullPath] : []
+ })
+}
+
+function collectUsedIconsByPrefix() {
+ const iconPattern = /icon\s*[:=]\s*["']([a-z0-9-]+):([a-z0-9-]+)["']/g
+ const usedIconsByPrefix = new Map()
+
+ for (const filePath of collectSourceFiles(srcRoot)) {
+ const content = fs.readFileSync(filePath, 'utf8')
+
+ for (const [, prefix, name] of content.matchAll(iconPattern)) {
+ const names = usedIconsByPrefix.get(prefix) || new Set()
+ names.add(name)
+ usedIconsByPrefix.set(prefix, names)
+ }
+ }
+
+ return usedIconsByPrefix
+}
+
+function buildSubsetCollection(collection, usedNames) {
+ const icons = {}
+ const aliases = {}
+ const sourceAliases = collection.aliases || {}
+ const requiredIconNames = new Set()
+ const requiredAliasNames = new Set()
+
+ function include(name) {
+ if (collection.icons[name]) {
+ requiredIconNames.add(name)
+ return
+ }
+
+ const alias = sourceAliases[name]
+
+ if (!alias) {
+ throw new Error(`Icon "${collection.prefix}:${name}" is missing from source collection`)
+ }
+
+ if (requiredAliasNames.has(name)) {
+ return
+ }
+
+ requiredAliasNames.add(name)
+ include(alias.parent)
+ }
+
+ ;[...usedNames].sort().forEach((name) => include(name))
+
+ for (const name of [...requiredIconNames].sort()) {
+ icons[name] = collection.icons[name]
+ }
+
+ for (const name of [...requiredAliasNames].sort()) {
+ aliases[name] = sourceAliases[name]
+ }
+
+ const subset = {
+ prefix: collection.prefix,
+ icons
+ }
+
+ if (collection.width) {
+ subset.width = collection.width
+ }
+
+ if (collection.height) {
+ subset.height = collection.height
+ }
+
+ if (Object.keys(aliases).length > 0) {
+ subset.aliases = aliases
+ }
+
+ return subset
+}
+
+function buildLocalCollections() {
+ const usedIconsByPrefix = collectUsedIconsByPrefix()
+ const localCollections = {}
+
+ for (const [prefix, usedNames] of [...usedIconsByPrefix.entries()].sort(([a], [b]) =>
+ a.localeCompare(b)
+ )) {
+ const sourceCollection = iconCollections[prefix]
+
+ if (!sourceCollection) {
+ throw new Error(`Missing source Iconify collection for prefix "${prefix}"`)
+ }
+
+ localCollections[prefix] = buildSubsetCollection(sourceCollection, usedNames)
+ }
+
+ return localCollections
+}
+
+function writeCollectionsFile(localCollections) {
+ const serializedCollections = inspect(localCollections, {
+ depth: null,
+ compact: false,
+ sorted: true,
+ maxArrayLength: null,
+ maxStringLength: null
+ })
+
+ const fileContents = `// Auto-generated by scripts/build-local-iconify-collections.mjs
+// Do not edit manually.
+
+export const LOCAL_ICON_COLLECTIONS = Object.freeze(${serializedCollections})
+`
+
+ fs.writeFileSync(outputPath, fileContents)
+}
+
+const localCollections = buildLocalCollections()
+writeCollectionsFile(localCollections)
+
+console.log(`Wrote local Iconify collections to ${outputPath}`)
diff --git a/rsf-design/scripts/clean-dev.helpers.mjs b/rsf-design/scripts/clean-dev.helpers.mjs
new file mode 100644
index 0000000..7099823
--- /dev/null
+++ b/rsf-design/scripts/clean-dev.helpers.mjs
@@ -0,0 +1,265 @@
+export const CLEAN_DEV_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.js',
+ 'src/mock/temp/commentDetail.js',
+ 'src/mock/temp/commentList.js',
+ 'src/assets/images/cover',
+ 'src/assets/images/safeguard',
+ 'src/assets/images/3d',
+ 'src/components/core/charts/art-map-chart',
+ 'src/components/business/comment-widget'
+]
+
+export const ROUTE_MODULES_TO_REMOVE = [
+ 'template.js',
+ 'widgets.js',
+ 'examples.js',
+ 'article.js',
+ 'safeguard.js',
+ 'help.js'
+]
+
+export function buildMinimalRouteModuleFiles() {
+ return {
+ dashboard: `const dashboardRoutes = {
+ 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',
+ icon: 'ri:home-smile-2-line',
+ keepAlive: false,
+ fixedTab: true
+ }
+ }
+ ]
+}
+
+export { dashboardRoutes }
+`,
+ system: `const systemRoutes = {
+ 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',
+ icon: 'ri:user-line',
+ keepAlive: true,
+ roles: ['R_SUPER', 'R_ADMIN']
+ }
+ },
+ {
+ path: 'role',
+ name: 'Role',
+ component: '/system/role',
+ meta: {
+ title: 'menus.system.role',
+ icon: 'ri:user-settings-line',
+ keepAlive: true,
+ roles: ['R_SUPER']
+ }
+ },
+ {
+ path: 'user-center',
+ name: 'UserCenter',
+ component: '/system/user-center',
+ meta: {
+ title: 'menus.system.userCenter',
+ icon: 'ri:user-line',
+ isHide: true,
+ keepAlive: true,
+ isHideTab: true
+ }
+ },
+ {
+ path: 'menu',
+ name: 'Menus',
+ component: '/system/menu',
+ meta: {
+ title: 'menus.system.menu',
+ icon: 'ri:menu-line',
+ keepAlive: true,
+ roles: ['R_SUPER'],
+ authList: [
+ { title: '鏂板', authMark: 'add' },
+ { title: '缂栬緫', authMark: 'edit' },
+ { title: '鍒犻櫎', authMark: 'delete' }
+ ]
+ }
+ }
+ ]
+}
+
+export { systemRoutes }
+`,
+ index: `import { dashboardRoutes } from './dashboard'
+import { systemRoutes } from './system'
+import { resultRoutes } from './result'
+import { exceptionRoutes } from './exception'
+
+const routeModules = [
+ dashboardRoutes,
+ systemRoutes,
+ resultRoutes,
+ exceptionRoutes
+]
+
+export { routeModules }
+`
+ }
+}
+
+export function createMinimalRoutesAliasContent() {
+ return `const RoutesAlias = Object.freeze({
+ Layout: '/index/index',
+ Login: '/auth/login'
+})
+
+export { RoutesAlias }
+`
+}
+
+export function createMinimalChangeLogContent() {
+ return `import { ref } from 'vue'
+
+export const upgradeLogList = ref([])
+`
+}
+
+export function pruneLocaleMessages(localeMessages) {
+ const nextLocale = structuredClone(localeMessages)
+ const menus = nextLocale.menus || {}
+ const menusToRemove = ['widgets', 'template', 'article', 'examples', 'safeguard', 'plan', 'help']
+
+ menusToRemove.forEach((menuKey) => {
+ delete menus[menuKey]
+ })
+
+ if (menus.dashboard) {
+ delete menus.dashboard.analysis
+ delete menus.dashboard.ecommerce
+ }
+
+ if (menus.system) {
+ ;['nested', 'menu1', 'menu2', 'menu21', 'menu3', 'menu31', 'menu32', 'menu321'].forEach(
+ (menuKey) => {
+ delete menus.system[menuKey]
+ }
+ )
+ }
+
+ nextLocale.menus = menus
+ return nextLocale
+}
+
+export function createMinimalFastEnterContent() {
+ return `import { WEB_LINKS } from '@/utils/constants'
+
+const 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)
+`
+}
+
+export function rewriteMenuApiContent(content) {
+ return content.replace("/api/v3/system/menus'", "/api/v3/system/menus/simple'")
+}
diff --git a/rsf-design/scripts/clean-dev.js b/rsf-design/scripts/clean-dev.js
new file mode 100644
index 0000000..05cfd98
--- /dev/null
+++ b/rsf-design/scripts/clean-dev.js
@@ -0,0 +1,133 @@
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import readline from 'node:readline/promises'
+import { stdin as input, stdout as output } from 'node:process'
+
+import {
+ CLEAN_DEV_TARGETS,
+ ROUTE_MODULES_TO_REMOVE,
+ buildMinimalRouteModuleFiles,
+ createMinimalChangeLogContent,
+ createMinimalFastEnterContent,
+ createMinimalRoutesAliasContent,
+ pruneLocaleMessages,
+ rewriteMenuApiContent
+} from './clean-dev.helpers.mjs'
+
+const projectRoot = process.cwd()
+
+async function pathExists(relativePath) {
+ try {
+ await fs.access(path.resolve(projectRoot, relativePath))
+ return true
+ } catch {
+ return false
+ }
+}
+
+async function countTargetEntries(targets) {
+ let count = 0
+
+ for (const target of targets) {
+ if (await pathExists(target)) {
+ count += 1
+ }
+ }
+
+ return count
+}
+
+async function removeTarget(relativePath) {
+ await fs.rm(path.resolve(projectRoot, relativePath), {
+ recursive: true,
+ force: true
+ })
+}
+
+async function writeText(relativePath, content) {
+ await fs.writeFile(path.resolve(projectRoot, relativePath), content, 'utf8')
+}
+
+async function rewriteLocale(relativePath) {
+ const fullPath = path.resolve(projectRoot, relativePath)
+ const locale = JSON.parse(await fs.readFile(fullPath, 'utf8'))
+ const nextLocale = pruneLocaleMessages(locale)
+
+ await fs.writeFile(fullPath, `${JSON.stringify(nextLocale, null, 2)}\n`, 'utf8')
+}
+
+async function rewriteMenuApi() {
+ const relativePath = 'src/api/system-manage.js'
+ const fullPath = path.resolve(projectRoot, relativePath)
+ const currentContent = await fs.readFile(fullPath, 'utf8')
+ const nextContent = rewriteMenuApiContent(currentContent)
+
+ await fs.writeFile(fullPath, nextContent, 'utf8')
+}
+
+function printSummary(existingTargetCount) {
+ console.log('Art Design Pro clean:dev')
+ console.log()
+ console.log('灏嗘墽琛屾渶灏忓紑鍙戠増瑁佸壀锛�')
+ console.log('- 鍒犻櫎婕旂ず椤甸潰銆佹紨绀鸿祫婧愬拰澶氫綑 mock 鏁版嵁')
+ console.log('- 閲嶅啓璺敱妯″潡锛屽彧淇濈暀 dashboard/system/result/exception')
+ console.log('- 娓呯悊璇█鍖呬腑鐨勬紨绀鸿彍鍗曢」')
+ console.log('- 閲嶅啓蹇�熷叆鍙c�佽矾鐢卞埆鍚嶃�佹洿鏂版棩蹇楁暟鎹拰鑿滃崟鎺ュ彛')
+ console.log()
+ console.log(`妫�娴嬪埌 ${existingTargetCount} 涓緟娓呯悊鐩爣瀛樺湪浜庡綋鍓嶄粨搴撲腑銆俙)
+ console.log('杈撳叆 yes 缁х画锛屾寜 Enter 鍙栨秷銆�')
+ console.log()
+}
+
+async function confirmExecution() {
+ const rl = readline.createInterface({ input, output })
+
+ try {
+ const answer = await rl.question('> ')
+ return answer.trim().toLowerCase() === 'yes'
+ } finally {
+ rl.close()
+ }
+}
+
+async function runCleanup() {
+ const existingTargetCount = await countTargetEntries(CLEAN_DEV_TARGETS)
+ printSummary(existingTargetCount)
+
+ const confirmed = await confirmExecution()
+ if (!confirmed) {
+ console.log('宸插彇娑� clean:dev銆�')
+ return
+ }
+
+ for (const target of CLEAN_DEV_TARGETS) {
+ await removeTarget(target)
+ }
+
+ const routeFiles = buildMinimalRouteModuleFiles()
+ for (const moduleName of ROUTE_MODULES_TO_REMOVE) {
+ await removeTarget(path.posix.join('src/router/modules', moduleName))
+ }
+
+ await writeText('src/router/modules/dashboard.js', routeFiles.dashboard)
+ await writeText('src/router/modules/system.js', routeFiles.system)
+ await writeText('src/router/modules/index.js', routeFiles.index)
+ await writeText('src/router/routesAlias.js', createMinimalRoutesAliasContent())
+ await writeText('src/mock/upgrade/changeLog.js', createMinimalChangeLogContent())
+ await writeText('src/config/modules/fastEnter.js', createMinimalFastEnterContent())
+
+ await rewriteLocale('src/locales/langs/zh.json')
+ await rewriteLocale('src/locales/langs/en.json')
+ await rewriteMenuApi()
+
+ console.log()
+ console.log('clean:dev 鎵ц瀹屾垚銆�')
+ console.log('褰撳墠浠撳簱宸插垏鎹负鏈�灏忓紑鍙戠増缁撴瀯銆�')
+}
+
+runCleanup().catch((error) => {
+ console.error()
+ console.error('clean:dev 鎵ц澶辫触銆�')
+ console.error(error)
+ process.exit(1)
+})
diff --git a/rsf-design/skill/art-design-pro/SKILL.md b/rsf-design/skill/art-design-pro/SKILL.md
new file mode 100644
index 0000000..009701f
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/SKILL.md
@@ -0,0 +1,750 @@
+---
+name: art-design-pro
+description: "Use when 鍦ㄥ綋鍓� Art Design Pro JavaScript 鐗堜粨搴撲腑缂栧啓 Vue3 椤甸潰銆佺粍浠躲�佽〃鏍笺�佽〃鍗曘�佸浘琛ㄦ垨甯冨眬锛屽苟涓旈渶瑕佷紭鍏堝鐢ㄧ幇鏈夌粍浠躲�乭ooks銆侀〉闈㈡ā寮忓拰鏈湴鏂囨。鏃躲��"
+---
+
+# Art Design Pro 缁勪欢搴撴櫤鑳藉姪鎵�
+
+璁� Claude Code 鍦ㄧ紪鍐� Vue3 椤甸潰鏃跺噯纭娇鐢ㄩ」鐩腑宸叉湁鐨� Art Design Pro 缁勪欢锛岄伩鍏嶉噸澶嶉�犺疆瀛愩��
+
+## 馃摉 鍏充簬 Art Design Pro
+
+**Art Design Pro** 鏄竴涓熀浜� Vue3 + JavaScript + Vite 鐨勫悗鍙扮鐞嗙郴缁熸ā鏉匡紝鎻愪緵涓板瘜鐨勭粍浠跺拰宸ュ叿鍑芥暟銆�
+
+- **GitHub**: https://github.com/Daymychen/art-design-pro
+- **Gitee**: https://gitee.com/lingchen163/art-design-pro
+- **瀹樼綉**: https://www.artd.pro
+- **鏂囨。**: https://www.artd.pro/docs
+
+### 鈿狅笍 浣跨敤鍓嶅繀璇�
+
+**鏈� Skill 鍋囪浣犵殑椤圭洰宸茬粡闆嗘垚浜� Art Design Pro 缁勪欢**銆傚鏋滀綘杩樻病鏈夐泦鎴愶紝闇�瑕佸厛瀹屾垚浠ヤ笅姝ラ锛�
+
+1. **鑾峰彇 Art Design Pro**
+ ```bash
+ # 鏂瑰紡1: 鍏嬮殕瀹屾暣椤圭洰锛堟帹鑽愮敤浜庢柊寤洪」鐩級
+ git clone https://github.com/Daymychen/art-design-pro.git your-project
+
+ # 鏂瑰紡2: 澶嶅埗缁勪欢鐩綍锛堥�傜敤浜庣幇鏈夐」鐩級
+ # 灏� art-design-pro/src/components/core 澶嶅埗鍒颁綘鐨勯」鐩�
+ ```
+
+2. **瀹夎渚濊禆**
+ ```bash
+ pnpm add element-plus
+ # 鎴� npm install element-plus
+ ```
+
+3. **閰嶇疆璺緞鍒悕** (vite.config.js)
+ ```js
+ export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src')
+ }
+ }
+ })
+ ```
+
+4. **鍒濆鍖� Skill 閰嶇疆**
+ ```bash
+ cd skill/art-design-pro
+ python scripts/init.py
+ ```
+
+**閲嶈鎻愮ず**锛�
+- 鏈� skill 涓殑缁勪欢璺緞浣跨敤 `@/` 鍒悕锛屽鏋滀綘鐨勯」鐩娇鐢ㄥ叾浠栧埆鍚嶏紙濡� `~/`锛夛紝闇�瑕佸湪 `project-config.json` 涓慨鏀�
+- 缁勪欢鏀寔鑷姩瀵煎叆锛岄渶瑕侀厤缃� `unplugin-vue-components`
+- 褰撳墠浠撳簱宸茬粡鏄函 JavaScript 鐗堟湰锛岀ず渚嬩笌鐢熸垚鍣ㄩ粯璁よ緭鍑� `.js` / `<script setup>`
+
+## 鏍稿績鍘熷垯
+
+**浼樺厛浣跨敤鐜版湁缁勪欢锛岃�屼笉鏄粠闆剁紪鍐� Vue 浠g爜銆�**
+
+## 馃帹 鏍峰紡瑙勮寖锛堜弗鏍兼墽琛岋級
+
+### 鈿狅笍 閲嶈锛氭牱寮忎娇鐢ㄨ鍒�
+
+**涓ョ鑷垜鍙戞尌缂栧啓鏍峰紡**锛屽繀椤讳弗鏍奸伒寰互涓嬭鑼冿細
+
+#### 1. 浼樺厛浣跨敤 Tailwind CSS 宸ュ叿绫�
+
+鉁� **鎺ㄨ崘**锛氫娇鐢� Tailwind CSS 宸ュ叿绫�
+```vue
+<template>
+ <!-- 鉁� 浣跨敤 Tailwind 宸ュ叿绫� -->
+ <div class="px-4 py-2 flex flex-wrap gap-2">
+ <div class="w-full md:w-1/2">鍐呭</div>
+ </div>
+</template>
+```
+
+鉂� **绂佹**锛氳嚜宸辩紪鍐欏唴鑱旀牱寮忔垨鑷畾涔夌被
+```vue
+<template>
+ <!-- 鉂� 涓嶈杩欐牱鍋� -->
+ <div style="padding: 16px; display: flex;">
+ <div class="my-custom-class">鍐呭</div>
+ </div>
+</template>
+
+<style scoped>
+/* 鉂� 涓嶈缂栧啓鑷畾涔夋牱寮� */
+.my-custom-class {
+ padding: 16px;
+ display: flex;
+}
+</style>
+```
+
+#### 2. 浣跨敤椤圭洰瀹氫箟鐨� CSS 鍙橀噺
+
+鉁� **鎺ㄨ崘**锛氫娇鐢ㄩ」鐩� CSS 鍙橀噺
+```scss
+color: var(--art-gray-800);
+background-color: var(--art-main-bg-color);
+border: 1px solid var(--art-border-color);
+```
+
+鉂� **绂佹**锛氱‖缂栫爜棰滆壊鍊�
+```scss
+/* 鉂� 涓嶈纭紪鐮侀鑹� */
+color: #333;
+background-color: #fff;
+border: 1px solid #ddd;
+```
+
+#### 3. 浣跨敤椤圭洰鐨勫搷搴斿紡鏂偣
+
+鉁� **鎺ㄨ崘**锛氫娇鐢� Tailwind 鍝嶅簲寮忓墠缂�
+```vue
+<template>
+ <!-- 鉁� 浣跨敤鍝嶅簲寮忕被 -->
+ <div class="px-4 md:px-6 lg:px-8">
+ <div class="w-full md:w-1/2 lg:w-1/3">
+ </div>
+</template>
+```
+
+**椤圭洰鏂偣**锛�
+- `sm:`: 640px (鎵嬫満)
+- `md:`: 768px (骞虫澘绔栧睆)
+- `lg:`: 1024px (骞虫澘妯睆)
+- `xl:`: 1280px (灏忕瑪璁版湰)
+- `2xl:`: 1536px (妗岄潰)
+
+#### 4. 浣跨敤椤圭洰 Mixin锛堝闇�瑕侊級
+
+```scss
+// 浣跨敤椤圭洰瀹氫箟鐨� mixin
+@include art-card-base(var(--art-card-border));
+```
+
+#### 5. 绂佹鐨勮涓烘竻鍗�
+
+鉂� **缁濆绂佹**锛�
+- [ ] 缂栧啓鑷畾涔� CSS 绫伙紙闄ょ粍浠跺簱瑕佹眰澶栵級
+- [ ] 浣跨敤鍐呰仈 `style` 灞炴�э紙闄ら潪鏄姩鎬佺粦瀹氾級
+- [ ] 纭紪鐮侀鑹插�笺�侀棿璺濆��
+- [ ] 浣跨敤鏈畾涔夌殑 CSS 鍙橀噺
+- [ ] 寮曞叆澶栭儴鏍峰紡搴擄紙Tailwind銆丒lement Plus 闄ゅ锛�
+- [ ] 瑕嗙洊 Element Plus 缁勪欢鐨勯粯璁ゆ牱寮�
+- [ ] 缂栧啓 `!important` 鏍峰紡锛堥櫎闈炰慨澶嶇粍浠跺簱闂锛�
+
+### 馃搵 鏍峰紡妫�鏌ユ竻鍗�
+
+鍦ㄧ敓鎴愪唬鐮佸墠锛岀‘璁わ細
+- [ ] 鎵�鏈夋牱寮忎娇鐢� Tailwind CSS 宸ュ叿绫�
+- [ ] 棰滆壊浣跨敤 CSS 鍙橀噺锛坄var(--art-*)`锛�
+- [ ] 闂磋窛浣跨敤 Tailwind 绫伙紙`p-4`, `m-2`, `gap-4` 绛夛級
+- [ ] 鍝嶅簲寮忎娇鐢ㄦ柇鐐瑰墠缂�锛坄md:`, `lg:` 绛夛級
+- [ ] 娌℃湁鑷畾涔� `<style scoped>` 鍧�
+- [ ] 娌℃湁鍐呰仈 `style` 灞炴��
+
+## 浣曟椂浣跨敤
+
+鍦ㄤ互涓嬪満鏅腑锛�**蹇呴』**鍏堜娇鐢ㄦ湰 skill 鏌ユ壘鍙敤缁勪欢锛�
+
+1. 鉁� 鍒涘缓鏂扮殑椤甸潰鎴栬鍥�
+2. 鉁� 娣诲姞琛ㄦ牸銆佽〃鍗曘�佸浘琛ㄧ瓑 UI 鍏冪礌
+3. 鉁� 瀹炵幇鎼滅储銆佺瓫閫夈�佸鍑虹瓑鍔熻兘
+4. 鉁� 娣诲姞甯冨眬缁勪欢锛堥潰鍖呭睉銆侀〉澶淬�佹爣绛鹃〉绛夛級
+5. 鉁� 闇�瑕佹暟鎹彲瑙嗗寲锛堝浘琛ㄣ�佺粺璁″崱鐗囩瓑锛�
+
+## 缁勪欢鍒嗙被閫熸煡
+
+### 馃搳 **琛ㄦ牸涓庤〃鍗�**
+| 缁勪欢 | 鐢ㄩ�� | 瀵煎叆璺緞 |
+|------|------|---------|
+| `ArtTable` | 鏁版嵁琛ㄦ牸锛堟敮鎸佸垎椤点�佹帓搴忋�佽嚜瀹氫箟鍒楋級 | `@/components/core/tables/art-table` |
+| `ArtTableHeader` | 琛ㄦ牸澶撮儴宸ュ叿鏍� | `@/components/core/tables/art-table-header` |
+| `ArtForm` | 琛ㄥ崟锛堟敮鎸佸搷搴斿紡銆佹牎楠屻�佸悇绉嶈〃鍗曢」锛� | `@/components/core/forms/art-form` |
+| `ArtSearchBar` | 鎼滅储鏍� | `@/components/core/forms/art-search-bar` |
+| `ArtButtonTable` | 琛ㄦ牸鎿嶄綔鎸夐挳缁� | `@/components/core/forms/art-button-table` |
+| `ArtButtonMore` | 鏇村鎿嶄綔涓嬫媺鎸夐挳 | `@/components/core/forms/art-button-more` |
+| `ArtDragVerify` | 鎷栨嫿楠岃瘉 | `@/components/core/forms/art-drag-verify` |
+
+### 馃搱 **鍥捐〃涓庢暟鎹睍绀�**
+| 缁勪欢 | 鐢ㄩ�� | 瀵煎叆璺緞 |
+|------|------|---------|
+| `ArtStatsCard` | 缁熻鍗$墖 | `@/components/core/cards/art-stats-card` |
+| `ArtBarChartCard` | 鏌辩姸鍥惧崱鐗� | `@/components/core/cards/art-bar-chart-card` |
+| `ArtLineChartCard` | 鎶樼嚎鍥惧崱鐗� | `@/components/core/cards/art-line-chart-card` |
+| `ArtDonutChartCard` | 鐜舰鍥惧崱鐗� | `@/components/core/cards/art-donut-chart-card` |
+| `ArtProgressCard` | 杩涘害鍗$墖 | `@/components/core/cards/art-progress-card` |
+| `ArtDataListCard` | 鏁版嵁鍒楄〃鍗$墖 | `@/components/core/cards/art-data-list-card` |
+| `ArtTimelineListCard` | 鏃堕棿杞村垪琛ㄥ崱鐗� | `@/components/core/cards/art-timeline-list-card` |
+| `ArtImageCard` | 鍥剧墖鍗$墖 | `@/components/core/cards/art-image-card` |
+| `ArtBarChart` | 鏌辩姸鍥� | `@/components/core/charts/art-bar-chart` |
+| `ArtLineChart` | 鎶樼嚎鍥� | `@/components/core/charts/art-line-chart` |
+| `ArtRingChart` | 鐜舰鍥� | `@/components/core/charts/art-ring-chart` |
+| `ArtRadarChart` | 闆疯揪鍥� | `@/components/core/charts/art-radar-chart` |
+| `ArtMapChart` | 鍦板浘鍥捐〃 | `@/components/core/charts/art-map-chart` |
+
+### 馃帹 **甯冨眬涓庡鑸�**
+| 缁勪欢 | 鐢ㄩ�� | 瀵煎叆璺緞 |
+|------|------|---------|
+| `ArtPageContent` | 椤甸潰鍐呭瀹瑰櫒 | `@/components/core/layouts/art-page-content` |
+| `ArtBreadcrumb` | 闈㈠寘灞戝鑸� | `@/components/core/layouts/art-breadcrumb` |
+| `ArtHeaderBar` | 椤靛ご宸ュ叿鏍� | `@/components/core/layouts/art-header-bar` |
+| `ArtWorkTab` | 澶氭爣绛鹃〉 | `@/components/core/layouts/art-work-tab` |
+| `ArtSidebarMenu` | 渚ц竟鏍忚彍鍗� | `@/components/core/layouts/art-menus/art-sidebar-menu` |
+| `ArtHorizontalMenu` | 姘村钩鑿滃崟 | `@/components/core/layouts/art-menus/art-horizontal-menu` |
+| `ArtFastEnter` | 蹇嵎鍏ュ彛 | `@/components/core/layouts/art-fast-enter` |
+
+### 馃敡 **宸ュ叿绫荤粍浠�**
+| 缁勪欢 | 鐢ㄩ�� | 瀵煎叆璺緞 |
+|------|------|---------|
+| `ArtExcelExport` | Excel 瀵煎嚭 | `@/components/core/forms/art-excel-export` |
+| `ArtExcelImport` | Excel 瀵煎叆 | `@/components/core/forms/art-excel-import` |
+| `ArtWangEditor` | 瀵屾枃鏈紪杈戝櫒 | `@/components/core/forms/art-wang-editor` |
+| `ArtVideoPlayer` | 瑙嗛鎾斁鍣� | `@/components/core/media/art-video-player` |
+| `ArtCutterImg` | 鍥剧墖瑁佸壀 | `@/components/core/media/art-cutter-img` |
+| `ArtNotification` | 閫氱煡涓績 | `@/components/core/layouts/art-notification` |
+
+### 馃幆 **鍩虹缁勪欢**
+| 缁勪欢 | 鐢ㄩ�� | 瀵煎叆璺緞 |
+|------|------|---------|
+| `ArtLogo` | 绯荤粺logo | `@/components/core/base/art-logo` |
+| `ArtSvgIcon` | SVG 鍥炬爣 | `@/components/core/base/art-svg-icon` |
+| `ArtBackToTop` | 杩斿洖椤堕儴 | `@/components/core/base/art-back-to-top` |
+| `ArtIconButton` | 鍥炬爣鎸夐挳 | `@/components/core/widget/art-icon-button` |
+
+## 浣跨敤鏂规硶
+
+### 鏂规硶 1锛氭悳绱㈢粍浠讹紙鎺ㄨ崘锛�
+
+褰撻渶瑕佹坊鍔犳煇涓姛鑳芥椂锛屼娇鐢� Python 鑴氭湰鎼滅储鐩稿叧缁勪欢锛�
+
+```bash
+# 鎼滅储鍏抽敭璇嶏紙鏀寔涓枃鍜岃嫳鏂囷級
+python skill/art-design-pro/scripts/search.py "琛ㄦ牸"
+
+# 鎼滅储琛ㄥ崟鐩稿叧缁勪欢
+python skill/art-design-pro/scripts/search.py "form"
+
+# 鎼滅储鍥捐〃
+python skill/art-design-pro/scripts/search.py "chart"
+
+# 鎼滅储鐗瑰畾鍒嗙被
+python skill/art-design-pro/scripts/search.py "table" --category tables
+```
+
+### 鏂规硶 2锛氱洿鎺ユ煡闃呯粍浠舵枃妗�
+
+鏌ョ湅瀹屾暣缁勪欢鍒楄〃锛�
+
+```bash
+python skill/art-design-pro/scripts/list.py
+```
+
+## 甯歌鍦烘櫙缁勪欢閫夋嫨
+
+### 鍦烘櫙 1锛氬垱寤� CRUD 鍒楄〃椤�
+
+**蹇呴�夌粍浠讹細**
+- `ArtTable` - 鏁版嵁琛ㄦ牸
+- `ArtSearchBar` - 鎼滅储鏍�
+- `ArtTableHeader` - 琛ㄦ牸宸ュ叿鏍忥紙鏂板銆佹壒閲忓垹闄ょ瓑锛�
+- `ArtPageContent` - 椤甸潰瀹瑰櫒
+
+**鈿狅笍 ArtSearchBar 蹇呴』浣跨敤 `v-model`锛�**
+```vue
+<ArtSearchBar
+ ref="searchBarRef"
+ v-model="searchForm"
+ :items="searchItems"
+ @search="handleSearch"
+ @reset="handleReset"
+/>
+
+<script setup>
+const searchBarRef = ref()
+const searchForm = ref({
+ name: '',
+ status: ''
+})
+
+const searchItems = [
+ {
+ key: 'name',
+ type: 'input',
+ label: '鍚嶇О',
+ props: { placeholder: '璇疯緭鍏ュ悕绉�', clearable: true }
+ },
+ {
+ key: 'status',
+ type: 'select',
+ label: '鐘舵��',
+ props: {
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+ }
+]
+
+const handleSearch = () => {
+ console.log('鎼滅储鍙傛暟:', searchForm.value)
+}
+
+const handleReset = () => {
+ searchForm.value = {
+ name: '',
+ status: ''
+ }
+}
+</script>
+```
+
+**鍙�夌粍浠讹細**
+- `ArtExcelExport` - 鏁版嵁瀵煎嚭
+- `ArtButtonTable` - 琛ㄦ牸琛屾搷浣滄寜閽�
+
+### 鍦烘櫙 2锛氬垱寤鸿〃鍗曢〉
+
+**蹇呴�夌粍浠讹細**
+- `ArtForm` - 琛ㄥ崟缁勪欢
+
+**鈿狅笍 ArtForm 蹇呴』浣跨敤 `:items` 鍜� `key`锛�**
+```vue
+<ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+/>
+
+<script setup>
+const formRef = ref()
+const formData = ref({
+ username: '',
+ status: ''
+})
+
+const formItems = [
+ {
+ key: 'username', // 鉁� 蹇呴』浣跨敤 key
+ label: '鐢ㄦ埛鍚�',
+ type: 'input',
+ props: { placeholder: '璇疯緭鍏ョ敤鎴峰悕' }
+ },
+ {
+ key: 'status',
+ label: '鐘舵��',
+ type: 'select',
+ props: {
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+ }
+]
+
+const formRules = {
+ username: [{ required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }]
+}
+</script>
+```
+
+**鉂� 閿欒鐢ㄦ硶锛堜笉瑕佷娇鐢級锛�**
+```vue
+<!-- 鉂� 涓嶈浣跨敤 schema -->
+<ArtForm :schema="formSchema" />
+
+<!-- 鉂� 涓嶈浣跨敤 field -->
+const formSchema = [
+ { field: 'username', label: '鐢ㄦ埛鍚�' } // 閿欒锛氬簲璇ユ槸 key
+]
+```
+
+**鈿狅笍 閲嶈锛氬湪 ElDialog 涓娇鐢� ArtForm**
+
+褰撳湪妯℃�佺獥锛圗lDialog锛変腑浣跨敤 ArtForm 鏃讹紝**蹇呴』闅愯棌鍐呴儴鎸夐挳**骞�**璁剧疆 span 灞炴��**锛�
+
+```vue
+<ElDialog v-model="dialogVisible" title="鏂板鐢ㄦ埛">
+ <ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ :rules="formRules"
+ :show-reset="false" <!-- 鉁� 蹇呴』璁剧疆 -->
+ :show-submit="false" <!-- 鉁� 蹇呴』璁剧疆 -->
+ label-width="120px"
+ />
+
+ <template #footer>
+ <ElButton @click="dialogVisible = false">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </template>
+</ElDialog>
+
+<script setup>
+const formItems = [
+ {
+ key: 'username',
+ label: '鐢ㄦ埛鍚�',
+ type: 'input',
+ span: 24, // 鉁� 蹇呴』璁剧疆锛氭ā鎬佺獥涓瘡涓瓧娈靛崰鎹暣琛�
+ props: { placeholder: '璇疯緭鍏ョ敤鎴峰悕' }
+ }
+]
+</script>
+```
+
+**閲嶈鎻愮ず锛�**
+
+1. **闅愯棌鍐呴儴鎸夐挳**锛歚:show-reset="false"` 鍜� `:show-submit="false"`
+2. **璁剧疆 span 灞炴��**锛氭瘡涓〃鍗曢」蹇呴』璁剧疆 `span: 24`锛堟暣琛屾樉绀猴級
+ - ArtForm 榛樿 `span: 6`锛堟瘡涓瓧娈靛崰 1/4 瀹藉害锛�
+ - 鍦� 600px 瀹界殑妯℃�佺獥涓紝涓嶈缃� span 浼氬鑷磋緭鍏ユ瀹藉害浠� 6px
+
+**鍘熷洜锛�** ArtForm 榛樿鏄剧ず閲嶇疆鍜屾彁浜ゆ寜閽紝鍦ㄦā鎬佺獥涓細涓� footer 涓殑鑷畾涔夋寜閽啿绐侊紱榛樿 span 鍊间笉閫傚悎妯℃�佺獥鍦烘櫙銆�
+
+**鍙�夌粍浠讹細**
+- `ArtDragVerify` - 婊戝潡楠岃瘉
+- `ArtWangEditor` - 瀵屾枃鏈紪杈�
+
+### 鍦烘櫙 3锛氬垱寤轰华琛ㄦ澘/缁熻椤�
+
+**蹇呴�夌粍浠讹細**
+- `ArtStatsCard` - 缁熻鍗$墖
+- `ArtLineChart` - 鎶樼嚎鍥撅紙鏁版嵁鍙鍖栵級
+- `ArtBarChart` - 鏌辩姸鍥撅紙鏁版嵁瀵规瘮锛�
+
+**鍙�夌粍浠讹細**
+- `ArtRingChart` - 鐜舰鍥撅紙鍗犳瘮鍒嗘瀽锛�
+- `ArtProgressCard` - 杩涘害灞曠ず
+
+**鈿狅笍 閲嶈锛氫华琛ㄦ澘甯冨眬鏍囧噯**
+
+浠〃鏉块〉闈㈠繀椤婚伒寰粺涓�鐨勫竷灞�瑙勮寖锛�
+
+```vue
+<template>
+ <!-- 鉁� 鏍瑰厓绱犲繀椤讳娇鐢� art-full-height 绫� -->
+ <div class="art-full-height">
+ <!-- 缁熻鍗$墖鍖哄煙锛氫娇鐢� ElRow 鍝嶅簲寮忓竷灞� -->
+ <ElRow :gutter="20" class="mb-5">
+ <ElCol :xs="24" :sm="12" :md="6" v-for="stat in stats" :key="stat.key">
+ <ArtStatsCard
+ :title="stat.title"
+ :count="stat.value"
+ :description="stat.description"
+ :icon="stat.icon"
+ :iconStyle="stat.iconStyle"
+ />
+ </ElCol>
+ </ElRow>
+
+ <!-- 鍥捐〃鍖哄煙锛氫娇鐢� ElCard 鍖呰9 -->
+ <ElRow :gutter="20">
+ <ElCol :xs="24" :md="12" :lg="12">
+ <ElCard class="art-table-card" shadow="never">
+ <template #header>
+ <div class="art-card-header">
+ <div class="title">
+ <h4>娴侀噺瓒嬪娍</h4>
+ <p>鏈�杩�7澶�</p>
+ </div>
+ </div>
+ </template>
+ <ArtLineChart
+ :data="chartData"
+ :xAxisData="xAxis"
+ :showLegend="true"
+ :showAreaColor="true"
+ />
+ </ElCard>
+ </ElCol>
+ </ElRow>
+ </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { ElRow, ElCol, ElCard } from 'element-plus'
+import ArtStatsCard from '@/components/core/cards/art-stats-card/index.vue'
+import ArtLineChart from '@/components/core/charts/art-line-chart/index.vue'
+
+const stats = ref([
+ {
+ key: 'total',
+ title: '鎬绘暟',
+ value: 100,
+ description: '绯荤粺鎬绘暟',
+ icon: 'ri:user-line',
+ iconStyle: 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
+ }
+])
+
+const chartData = ref([
+ { name: '娴侀噺', data: [120, 132, 101], smooth: true, showAreaColor: true }
+])
+</script>
+```
+
+**甯冨眬瑙勮寖璇存槑锛�**
+- 鉁� 鏍瑰厓绱犱娇鐢� `art-full-height` 绫伙紙鑷姩璁$畻椤甸潰鍓╀綑楂樺害锛�
+- 鉁� 缁熻鍗$墖浣跨敤 `ElRow` + `ElCol` 鍝嶅簲寮忓竷灞�
+- 鉁� 鍥捐〃鍖哄煙浣跨敤 `ElCard class="art-table-card" shadow="never"`
+- 鉁� 鍥捐〃鏍囬浣跨敤 ElCard 鐨� header slot
+- 鉂� 涓嶈浣跨敤鑷畾涔� padding锛堝 `p-5`锛�
+- 鉂� 涓嶈鍦� ElCol 涓婃坊鍔犻棿璺濈被锛堝 `mb-5`锛�
+
+**鈿狅笍 甯歌閿欒锛�**
+```vue
+<!-- 鉂� 閿欒锛氫娇鐢ㄧ粺璁″崱鐗囩粍浠讹紙ArtLineChartCard 鏄甫杩蜂綘鍥剧殑鍗$墖锛� -->
+import ArtLineChartCard from '@/components/core/cards/art-line-chart-card'
+
+<!-- 鉁� 姝g‘锛氫娇鐢ㄥ浘琛ㄧ粍浠讹紙ArtLineChart 鏄畬鏁寸殑鍥捐〃锛� -->
+import ArtLineChart from '@/components/core/charts/art-line-chart'
+```
+
+### 鍦烘櫙 4锛氬垱寤鸿鎯呴〉
+
+**蹇呴�夌粍浠讹細**
+- `ArtPageContent` - 椤甸潰瀹瑰櫒
+- `ArtBreadcrumb` - 闈㈠寘灞�
+
+**鍙�夌粍浠讹細**
+- `ArtTimelineListCard` - 鏃堕棿杞�
+- `ArtImageCard` - 鍥剧墖灞曠ず
+
+## 缁勪欢浣跨敤绀轰緥
+
+### ArtTable - 鏁版嵁琛ㄦ牸
+
+```vue
+<template>
+ <ElCard class="art-table-card" shadow="never">
+ <!-- 鈿狅笍 閲嶈锛欰rtTableHeader 闇�瑕侀�氳繃 v-model:columns 缁戝畾鍒楅厤缃� -->
+ <ArtTableHeader v-model:columns="columns" :loading="loading" />
+
+ <ArtTable
+ :data="tableData"
+ :columns="columns"
+ :pagination="pagination"
+ :loading="loading"
+ fit
+ @pagination:current-change="handlePageChange"
+ >
+ <!-- 鑷畾涔夊垪 -->
+ <template #status="{ row }">
+ <el-tag :type="row.status === 1 ? 'success' : 'danger'">
+ {{ row.status === 1 ? '鍚敤' : '绂佺敤' }}
+ </el-tag>
+ </template>
+
+ <!-- 鎿嶄綔鍒� -->
+ <template #action="{ row }">
+ <el-button link @click="handleEdit(row)">缂栬緫</el-button>
+ <el-button link type="danger" @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </ArtTable>
+ </ElCard>
+</template>
+
+<script setup>
+import ArtTable from '@/components/core/tables/art-table/index.vue'
+import ArtTableHeader from '@/components/core/tables/art-table-header/index.vue'
+
+const columns = [
+ { prop: 'name', label: '鍚嶇О' },
+ { prop: 'status', label: '鐘舵��', useSlot: true },
+ { prop: 'action', label: '鎿嶄綔', useSlot: true, width: 200 }
+]
+
+const tableData = ref([])
+const pagination = reactive({ current: 1, size: 10, total: 0 })
+</script>
+```
+
+**鈿狅笍 閲嶈娉ㄦ剰浜嬮」锛�**
+
+1. **鍒楄缃姛鑳�**锛氬鏋滈渶瑕佷娇鐢� `ArtTableHeader` 鐨勫垪璁剧疆鍔熻兘锛堟樉绀�/闅愯棌鍒楋級锛屽繀椤婚�氳繃 `v-model:columns="columns"` 灏嗗垪閰嶇疆浼犻�掔粰 `ArtTableHeader`
+2. **fit 灞炴��**锛氬缓璁湪 `ArtTable` 涓婃坊鍔� `fit` 灞炴�э紝纭繚琛ㄦ牸鍒楀鑷�傚簲瀹瑰櫒瀹藉害
+3. **columns 鏉ユ簮**锛歚columns` 蹇呴』鏄粠 `useTable` Hook 杩斿洖鐨勫搷搴斿紡鏁版嵁锛屾垨瀹氫箟涓哄搷搴斿紡寮曠敤
+4. **鈿狅笍 鍒楀閰嶇疆鍘熷垯锛堥噸瑕侊級**锛�
+ - 鉁� **浣跨敤 `width`**锛氬唴瀹归暱搴﹀浐瀹氱殑鍒楋紙鐘舵�併�佽鑹层�佹搷浣滃垪銆佺鍙g瓑锛�
+ - 鉁� **浣跨敤 `minWidth`**锛氬唴瀹归暱搴﹀彲鍙樼殑鍒楋紙鐢ㄦ埛鍚嶃�佸娉ㄣ�佸煙鍚嶃�佹椂闂寸瓑锛�
+ - 鉂� **閬垮厤鍏ㄩ儴浣跨敤鍥哄畾 `width`**锛氫細瀵艰嚧琛ㄦ牸鎬诲搴﹀浐瀹氾紝灞忓箷鏇村鏃跺彸渚у嚭鐜扮┖鐧�
+
+ **绀轰緥**锛�
+ ```js
+ // 鉁� 姝g‘锛氭贩鍚堜娇鐢� width 鍜� minWidth
+ const columns = [
+ { prop: 'username', label: '鐢ㄦ埛鍚�', minWidth: 120 }, // 鑷姩鎵╁睍
+ { prop: 'remark', label: '澶囨敞', minWidth: 150 }, // 鑷姩鎵╁睍
+ { prop: 'status', label: '鐘舵��', width: 80 }, // 鍥哄畾瀹藉害
+ { prop: 'action', label: '鎿嶄綔', width: 200, fixed: 'right' } // 鍥哄畾瀹藉害
+ ]
+
+ // 鉂� 閿欒锛氬叏閮ㄤ娇鐢ㄥ浐瀹� width
+ const columns = [
+ { prop: 'username', label: '鐢ㄦ埛鍚�', width: 150 }, // 浼氬鑷村彸渚х┖鐧�
+ { prop: 'remark', label: '澶囨敞', width: 200 },
+ { prop: 'status', label: '鐘舵��', width: 100 }
+ ]
+ ```
+
+### ArtForm - 琛ㄥ崟
+
+```vue
+<template>
+ <art-form
+ v-model="formData"
+ :items="formItems"
+ @submit="handleSubmit"
+ @reset="handleReset"
+ />
+</template>
+
+<script setup>
+import ArtForm from '@/components/core/forms/art-form/index.vue'
+
+const formData = ref({})
+
+const formItems = [
+ { key: 'name', label: '鍚嶇О', type: 'input', span: 12 },
+ { key: 'email', label: '閭', type: 'input', span: 12 },
+ { key: 'status', label: '鐘舵��', type: 'switch', span: 12 },
+ { key: 'role', label: '瑙掕壊', type: 'select', options: [
+ { label: '绠$悊鍛�', value: 'admin' },
+ { label: '鐢ㄦ埛', value: 'user' }
+ ], span: 12 }
+]
+</script>
+```
+
+### ArtStatsCard - 缁熻鍗$墖
+
+```vue
+<template>
+ <ArtStatsCard
+ title="鎬荤敤鎴锋暟"
+ :count="userCount"
+ description="绯荤粺鎵�鏈夌敤鎴�"
+ icon="ri:user-line"
+ :icon-style="iconStyle"
+ />
+</template>
+
+<script setup>
+import ArtStatsCard from '@/components/core/cards/art-stats-card/index.vue'
+
+const userCount = ref(1234)
+const iconStyle = 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
+</script>
+```
+
+**鈿狅笍 閲嶈 Props锛�**
+- `:count` - 鏁板�硷紙涓嶆槸 `:value`锛�
+- `description` - 鎻忚堪鏂囨湰锛堝繀闇�锛�
+- `:icon` - Remix Icon 鍥炬爣锛堝 `ri:user-line`锛�
+- `:iconStyle` - 鍥炬爣鑳屾櫙鏍峰紡锛圕SS 瀛楃涓诧級
+
+## 宸ヤ綔娴佺▼
+
+褰撶敤鎴疯姹傜紪鍐欓〉闈㈡椂锛�
+
+1. **鍒嗘瀽闇�姹�** - 纭畾椤甸潰绫诲瀷锛堝垪琛ㄣ�佽〃鍗曘�佷华琛ㄦ澘绛夛級
+2. **鎼滅储缁勪欢** - 浣跨敤 `search.py` 鏌ユ壘鐩稿叧缁勪欢
+3. **閫夋嫨缁勪欢** - 鏍规嵁鍔熻兘闇�姹傞�夋嫨鏈�鍚堥�傜殑缁勪欢
+4. **鏌ョ湅鏂囨。** - 闃呰缁勪欢鐨� props銆乻lots銆乪vents
+5. **缂栧啓浠g爜** - 浣跨敤缁勪欢鑰屼笉鏄嚜宸辩紪鍐� Vue 浠g爜
+
+## 绂佹琛屼负
+
+### 缁勪欢浣跨敤
+鉂� **涓嶈**鑷繁缂栧啓琛ㄦ牸缁勪欢 鈫� 浣跨敤 `ArtTable`
+鉂� **涓嶈**鑷繁缂栧啓琛ㄥ崟缁勪欢 鈫� 浣跨敤 `ArtForm`
+鉂� **涓嶈**鑷繁缂栧啓鍥捐〃缁勪欢 鈫� 浣跨敤 `Art*Chart`
+鉂� **涓嶈**鑷繁缂栧啓缁熻鍗$墖 鈫� 浣跨敤 `ArtStatsCard`
+鉂� **涓嶈**鑷繁缂栧啓鎼滅储鏍� 鈫� 浣跨敤 `ArtSearchBar`
+
+### 鏍峰紡缂栧啓锛堚殸锔� 涓ユ牸鎵ц锛�
+鉂� **涓嶈**缂栧啓鑷畾涔� CSS 绫� 鈫� 浣跨敤 Tailwind CSS 宸ュ叿绫�
+鉂� **涓嶈**浣跨敤鍐呰仈 `style` 灞炴�� 鈫� 浣跨敤 Tailwind 绫�
+鉂� **涓嶈**纭紪鐮侀鑹插�� 鈫� 浣跨敤 `var(--art-*)` CSS 鍙橀噺
+鉂� **涓嶈**瑕嗙洊 Element Plus 鏍峰紡 鈫� 浣跨敤缁勪欢 props 鎺у埗
+鉂� **涓嶈**缂栧啓 `<style scoped>` 鈫� 浣跨敤 Tailwind 宸ュ叿绫�
+鉂� **涓嶈**寮曞叆澶栭儴鏍峰紡搴� 鈫� 鍙敤 Tailwind + Element Plus
+
+**涓轰粈涔堣繖涔堜弗鏍硷紵**
+- 鉁� 淇濇寔 UI 椋庢牸缁熶竴
+- 鉁� 閬垮厤鏍峰紡鍐茬獊
+- 鉁� 鍑忓皯浠g爜浣撶Н
+- 鉁� 纭繚涓婚鍒囨崲姝e父
+- 鉁� 绗﹀悎椤圭洰璁捐瑙勮寖
+
+## 缁勪欢浣嶇疆
+
+鎵�鏈夌粍浠朵綅浜庯細`src/components/core/`
+
+## 鐩稿叧璧勬簮
+
+### 瀹樻柟鏂囨。
+- Art Design Pro 瀹樻柟鏂囨。锛歨ttps://www.artd.pro/docs/
+- Element Plus 鏂囨。锛歨ttps://element-plus.org/
+
+### 鏈湴鏂囨。
+
+**鏂囨。宸查噸缁勪负闈㈠悜闆嗘垚鐨勭粨鏋�**锛屾煡鐪嬪畬鏁寸储寮曪細`docs/00-index.md`
+
+#### 闆嗘垚鐩稿叧锛堟渶閲嶈锛�
+- `INTEGRATION_GUIDE.md` - **瀹屾暣闆嗘垚鎸囧崡**锛氬皢 Art Design Pro 闆嗘垚鍒板叾浠栭」鐩�
+- `templates/` - 閰嶇疆妯℃澘锛歷ite.config.js銆乸ackage.json銆�.env.example 绛�
+- `scripts/verify.py` - 闆嗘垚楠岃瘉宸ュ叿
+
+#### 蹇�熷紑濮�
+- `docs/getting-started/01-introduce.md` - 妗嗘灦浠嬬粛鍜岀壒鑹�
+- `docs/getting-started/02-quick-start.md` - 蹇�熷紑濮嬫寚鍗�
+- `docs/getting-started/03-must-read.md` - **蹇呰**锛氭帴鍙e鎺ャ�佺綉缁滆姹傘�佽彍鍗曢厤缃�
+- `docs/getting-started/04-standard.md` - 浠g爜瑙勮寖
+- `docs/getting-started/components-basics.md` - 缁勪欢鍜屽浘鏍囧熀纭�
+- `docs/getting-started/configuration-guide.md` - 绯荤粺閰嶇疆鎸囧崡锛堜富棰樸�佺幆澧冨彉閲忥級
+
+#### 鏍稿績姒傚康
+- `docs/core-concepts/project-structure.md` - 椤圭洰缁撴瀯
+- `docs/core-concepts/routing.md` - 璺敱鍜岃彍鍗曢厤缃�
+
+#### 缁勪欢鍜� Hooks
+- `docs/components/art-search-bar.md` - ArtSearchBar 缁勪欢鏂囨。
+- `docs/hooks/use-table.md` - useTable Hook 鏂囨。
+- `CHEATSHEET.md` - **缁勪欢閫熸煡琛�**锛氬叏閮� 56 涓粍浠剁殑瀵煎叆璺緞鍜岀敤娉�
+
+#### 杈呭姪鏂囨。
+- `FAQ.md` - 甯歌闂
+- `BEST_PRACTICES.md` - 鏈�浣冲疄璺�
+- `generator-guide.md` - 浠g爜鐢熸垚鍣ㄤ娇鐢ㄦ寚鍗�
+
+**浣跨敤鏂规硶**锛�
+鎵�鏈夋枃妗i兘鏄湰鍦� Markdown 鏂囦欢锛屽湪缂栧啓浠g爜鍓嶅厛鏌ラ槄鐩稿叧鏂囨。锛岀‘淇濋伒寰畼鏂硅鑼冦��
+
+
+
diff --git a/rsf-design/skill/art-design-pro/data/components.csv b/rsf-design/skill/art-design-pro/data/components.csv
new file mode 100644
index 0000000..d0de0f3
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/data/components.csv
@@ -0,0 +1,58 @@
+category,component,name_cn,name_en,description,import_path,props,slots,common_usage
+tables,ArtTable,鏁版嵁琛ㄦ牸,Data Table,鏀寔鍒嗛〉銆佹帓搴忋�佽嚜瀹氫箟鍒椼�佸搷搴斿紡楂樺害鐨勬暟鎹〃鏍�,@/components/core/tables/art-table,data,columns,pagination,loading,height,border,stripe,default,action,status,custom column,CRUD鍒楄〃椤�
+tables,ArtTableHeader,琛ㄦ牸澶撮儴宸ュ叿鏍�,Table Header Toolbar,琛ㄦ牸椤堕儴宸ュ叿鏍忥紙鏂板銆佹壒閲忓垹闄ゃ�佸鍑虹瓑锛�,@/components/core/tables/art-table-header,title,buttons,table-actions,default,鍒楄〃椤靛伐鍏锋爮
+forms,ArtForm,琛ㄥ崟缁勪欢,Form Component,鏀寔鍝嶅簲寮忋�佹牎楠屻�佸悇绉嶈〃鍗曢」绫诲瀷,@/components/core/forms/art-form,items,modelValue,span,gutter,labelPosition,showReset,showSubmit,default,琛ㄥ崟椤点�佺瓫閫夋潯浠�
+forms,ArtSearchBar,鎼滅储鏍�,Search Bar,琛ㄥ崟寮忔悳绱㈡爮锛岃嚜鍔ㄥ竷灞�,@/components/core/forms/art-search-bar,items,modelValue,span,search-button-text,default,鍒楄〃椤垫悳绱�
+forms,ArtButtonTable,琛ㄦ牸鎿嶄綔鎸夐挳缁�,Table Action Buttons,琛ㄦ牸琛岀殑鎿嶄綔鎸夐挳锛堢紪杈戙�佸垹闄ょ瓑锛�,@/components/core/forms/art-button-table,buttons,row,default,琛ㄦ牸鎿嶄綔鍒�
+forms,ArtButtonMore,鏇村鎿嶄綔鎸夐挳,More Actions Button,涓嬫媺寮忔洿澶氭搷浣滄寜閽�,@/components/core/forms/art-button-more,button,row,commands,default,琛ㄦ牸鎿嶄綔鍒楁孩鍑�
+forms,ArtExcelExport,Excel瀵煎嚭,Excel Export,鏁版嵁瀵煎嚭鍒癊xcel,@/components/core/forms/art-excel-export,data,filename,columns,default,鏁版嵁瀵煎嚭鍔熻兘
+forms,ArtExcelImport,Excel瀵煎叆,Excel Import,Excel鏁版嵁瀵煎叆,@/components/core/forms/art-excel-import,upload-url,template-url,accept,default,鎵归噺瀵煎叆鏁版嵁
+forms,ArtWangEditor,瀵屾枃鏈紪杈戝櫒,Rich Text Editor,鍩轰簬wangEditor鐨勫瘜鏂囨湰缂栬緫,@/components/core/forms/art-wang-editor,modelValue,height,placeholder,default,鏂囩珷鍐呭銆佸叕鍛�
+forms,ArtDragVerify,鎷栨嫿楠岃瘉,Drag Verify,婊戝潡楠岃瘉缁勪欢,@/components/core/forms/art-drag-verify,v-model,width,height,bar-size,default,琛ㄥ崟楠岃瘉
+cards,ArtStatsCard,缁熻鍗$墖,Stats Card,灞曠ず缁熻鏁版嵁鍜岃秼鍔�,@/components/core/cards/art-stats-card,title,value,icon,color,trend,default,浠〃鏉裤�佹暟鎹粺璁�
+cards,ArtBarChartCard,鏌辩姸鍥惧崱鐗�,Bar Chart Card,甯﹀浘琛ㄧ殑鍗$墖缁勪欢,@/components/core/cards/art-bar-chart-card,title,data,x-key,y-key,color,default,鏁版嵁鍙鍖�
+cards,ArtLineChartCard,鎶樼嚎鍥惧崱鐗�,Line Chart Card,甯︽姌绾垮浘鐨勫崱鐗�,@/components/core/cards/art-line-chart-card,title,data,x-key,y-key,color,default,瓒嬪娍灞曠ず
+cards,ArtDonutChartCard,鐜舰鍥惧崱鐗�,Donut Chart Card,甯︾幆褰㈠浘鐨勫崱鐗�,@/components/core/cards/art-donut-chart-card,title,data,name-key,value-key,default,鍗犳瘮鍒嗘瀽
+cards,ArtProgressCard,杩涘害鍗$墖,Progress Card,鏄剧ず杩涘害鐧惧垎姣�,@/components/core/cards/art-progress-card,title,percentage,color,status,default,浠诲姟杩涘害銆佸畬鎴愬害
+cards,ArtDataListCard,鏁版嵁鍒楄〃鍗$墖,Data List Card,鍒楄〃褰㈠紡鐨勬暟鎹睍绀�,@/components/core/cards/art-data-list-card,title,data,columns,default,绠�娲佹暟鎹睍绀�
+cards,ArtTimelineListCard,鏃堕棿杞村垪琛ㄥ崱鐗�,Timeline List Card,鏃堕棿杞村舰寮忕殑鏁版嵁鍒楄〃,@/components/core/cards/art-timeline-list-card,title,data,timestamp-key,content-key,default,鎿嶄綔鏃ュ織銆佸彉鏇磋褰�
+cards,ArtImageCard,鍥剧墖鍗$墖,Image Card,灞曠ず鍥剧墖鐨勫崱鐗�,@/components/core/cards/art-image-card,src,title,description,actions,default,鍥剧墖灞曠ず
+charts,ArtBarChart,鏌辩姸鍥�,Bar Chart,ECharts鏌辩姸鍥�,@/components/core/charts/art-bar-chart,data,x-key,y-key,color,default,鏁版嵁瀵规瘮
+charts,ArtLineChart,鎶樼嚎鍥�,Line Chart,ECharts鎶樼嚎鍥�,@/components/core/charts/art-line-chart,data,x-key,y-key,color,default,瓒嬪娍鍒嗘瀽
+charts,ArtRingChart,鐜舰鍥�,Ring Chart,ECharts鐜舰鍥�,@/components/core/charts/art-ring-chart,data,name-key,value-key,default,鍗犳瘮鍒嗘瀽
+charts,ArtRadarChart,闆疯揪鍥�,Radar Chart,ECharts闆疯揪鍥�,@/components/core/charts/art-radar-chart,data,indicators,default,澶氱淮瀵规瘮
+charts,ArtMapChart,鍦板浘鍥捐〃,Map Chart,ECharts鍦板浘鍥捐〃,@/components/core/charts/art-map-chart,data,map-key,value-key,default,鍦扮悊鏁版嵁
+charts,ArtDualBarCompareChart,鍙屾煴鐘跺姣斿浘,Dual Bar Compare,鍙岀郴鍒楁煴鐘跺浘瀵规瘮,@/components/core/charts/art-dual-bar-compare-chart,data,x-key,y1-key,y2-key,default,鏁版嵁瀵规瘮
+charts,ArtHBarChart,妯悜鏌辩姸鍥�,Horizontal Bar,妯悜鏌辩姸鍥�,@/components/core/charts/art-h-bar-chart,data,x-key,y-key,default,鎺掑悕灞曠ず
+charts,ArtKLineChart,K绾垮浘,K-Line Chart,ECharts K绾垮浘,@/components/core/charts/art-k-line-chart,data,date,values,default,閲戣瀺鏁版嵁
+charts,ArtScatterChart,鏁g偣鍥�,Scatter Chart,ECharts鏁g偣鍥�,@/components/core/charts/art-scatter-chart,data,x-key,y-key,default,鐩稿叧鎬у垎鏋�
+layouts,ArtPageContent,椤甸潰鍐呭瀹瑰櫒,Page Content,椤甸潰鍐呭瀹瑰櫒锛岃嚜鍔ㄨ绠楅珮搴�,@/components/core/layouts/art-page-content,default,all pages,鎵�鏈夐〉闈㈠鍣�
+layouts,ArtBreadcrumb,闈㈠寘灞戝鑸�,Breadcrumb,闈㈠寘灞戝鑸�,@/components/core/layouts/art-breadcrumb,routes,default,璇︽儏椤点�佸绾ч〉闈�
+layouts,ArtHeaderBar,椤靛ご宸ュ叿鏍�,Header Bar,椤甸潰澶撮儴宸ュ叿鏍�,@/components/core/layouts/art-header-bar,title,actions,default,椤甸潰椤堕儴
+layouts,ArtWorkTab,澶氭爣绛鹃〉,Work Tab,椤甸潰鏍囩椤靛垏鎹�,@/components/core/layouts/art-work-tab,default,澶氭爣绛鹃〉绠$悊
+layouts,ArtSidebarMenu,渚ц竟鏍忚彍鍗�,Sidebar Menu,宸︿晶鑿滃崟瀵艰埅,@/components/core/layouts/art-menus/art-sidebar-menu,default,涓昏彍鍗�
+layouts,ArtHorizontalMenu,姘村钩鑿滃崟,Horizontal Menu,椤堕儴姘村钩鑿滃崟,@/components/core/layouts/art-menus/art-horizontal-menu,default,椤堕儴瀵艰埅
+layouts,ArtMixedMenu,娣峰悎鑿滃崟,Mixed Menu,渚ц竟鏍�+椤堕儴娣峰悎鑿滃崟,@/components/core/layouts/art-menus/art-mixed-menu,default,涓昏彍鍗�
+layouts,ArtFastEnter,蹇嵎鍏ュ彛,Fast Entry,蹇嵎鍔熻兘鍏ュ彛,@/components/core/layouts/art-fast-enter,items,default,浠〃鏉裤�侀椤�
+layouts,ArtNotification,閫氱煡涓績,Notification Center,娑堟伅閫氱煡,@/components/core/layouts/art-notification,default,娑堟伅閫氱煡
+layouts,ArtGlobalComponent,鍏ㄥ眬缁勪欢,Global Component,鍏ㄥ眬缁勪欢瀹瑰櫒,@/components/core/layouts/art-global-component,default,鍏ㄥ眬鍔熻兘
+layouts,ArtGlobalSearch,鍏ㄥ眬鎼滅储,Global Search,鍏ㄥ眬鎼滅储鍔熻兘,@/components/core/layouts/art-global-search,default,鍏ㄥ眬鎼滅储
+layouts,ArtSettingsPanel,璁剧疆闈㈡澘,Settings Panel,璁剧疆鎶藉眽闈㈡澘,@/components/core/layouts/art-settings-panel,default,绯荤粺璁剧疆
+layouts,ArtScreenLock,灞忓箷閿�,Screen Lock,灞忓箷閿佸畾,@/components/core/layouts/art-screen-lock,default,瀹夊叏鍔熻兘
+layouts,ArtChatWindow,鑱婂ぉ绐楀彛,Chat Window,鑱婂ぉ绐楀彛缁勪欢,@/components/core/layouts/art-chat-window,default,鑱婂ぉ鍔熻兘
+layouts,ArtFireworksEffect,鐑熻姳鐗规晥,Fireworks Effect,鐑熻姳鍔ㄧ敾鏁堟灉,@/components/core/layouts/art-fireworks-effect,default,鑺傛棩鐗规晥
+media,ArtVideoPlayer,瑙嗛鎾斁鍣�,Video Player,瑙嗛鎾斁缁勪欢,@/components/core/media/art-video-player,src,poster,autoplay,default,瑙嗛鎾斁
+media,ArtCutterImg,鍥剧墖瑁佸壀,Image Cropper,鍥剧墖瑁佸壀缁勪欢,@/components/core/media/art-cutter-img,src,width,height,default,澶村儚涓婁紶銆佸浘鐗囪鍓�
+banners,ArtBasicBanner,鍩虹妯箙,Basic Banner,鍩虹妯箙缁勪欢,@/components/core/banners/art-basic-banner,title,description,actions,default,椤甸潰妯箙
+banners,ArtCardBanner,鍗$墖妯箙,Card Banner,鍗$墖寮忔í骞�,@/components/core/banners/art-card-banner,cards,default,棣栭〉妯箙
+text-effect,ArtCountTo,鏁板瓧鍔ㄧ敾,Count To,鏁板瓧婊氬姩鍔ㄧ敾,@/components/core/text-effect/art-count-to,start,end,duration,default,缁熻鏁板瓧鍔ㄧ敾
+text-effect,ArtTextScroll,鏂囨湰婊氬姩,Text Scroll,鏂囨湰婊氬姩鏁堟灉,@/components/core/text-effect/art-text-scroll,text,speed,default,鍏憡銆侀�氱煡
+text-effect,ArtFestivalTextScroll,鑺傛棩鏂囨湰婊氬姩,Festival Text Scroll,鑺傛棩涓婚鏂囨湰婊氬姩,@/components/core/text-effect/art-festival-text-scroll,text,speed,default,鑺傛棩鐗规晥
+base,ArtLogo,绯荤粺logo,System Logo,绯荤粺Logo缁勪欢,@/components/core/base/art-logo,size,default,鐧诲綍椤点�佷晶杈规爮
+base,ArtSvgIcon,SVG鍥炬爣,SVG Icon,SVG鍥炬爣缁勪欢,@/components/core/base/art-svg-icon,name,color,size,default,鍥炬爣灞曠ず
+base,ArtBackToTop,杩斿洖椤堕儴,Back to Top,杩斿洖椤堕儴鎸夐挳,@/components/core/base/art-back-to-top,default,闀块〉闈�
+widget,ArtIconButton,鍥炬爣鎸夐挳,Icon Button,鍥炬爣鎸夐挳缁勪欢,@/components/core/widget/art-icon-button,icon,color,size,default,蹇嵎鎿嶄綔
+others,ArtMenuRight,鍙抽敭鑿滃崟,Context Menu,鍙抽敭鑿滃崟,@/components/core/others/art-menu-right,items,default,琛ㄦ牸琛屾搷浣�
+others,ArtWatermark,姘村嵃,Watermark,椤甸潰姘村嵃,@/components/core/others/art-watermark,text,default,瀹夊叏闃叉姢
+theme,ArtThemeSvg,涓婚SVG,Theme SVG,璁㏒VG鍥剧墖璺熼殢涓婚鐨勭粍浠�,@/components/core/theme/theme-svg,size,themeColor,src,default,涓婚鐩稿叧SVG
+
diff --git a/rsf-design/skill/art-design-pro/docs/00-index.md b/rsf-design/skill/art-design-pro/docs/00-index.md
new file mode 100644
index 0000000..4e8bd61
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/00-index.md
@@ -0,0 +1,100 @@
+# Art Design Pro 鏂囨。绱㈠紩
+
+**鏈湴鍖栫姸鎬�**: 鉁� 100%
+
+鏈洰褰曞寘鍚� Art Design Pro 鐨勬枃妗o紝宸查噸缁勪负闈㈠悜闆嗘垚鐨勭粨鏋勩��
+
+---
+
+## 馃摎 蹇�熷紑濮�
+
+| 鏂囦欢 | 鏍囬 | 璇存槑 |
+|------|------|------|
+| [01-introduce.md](getting-started/01-introduce.md) | 浠嬬粛 | 妗嗘灦浠嬬粛鍜屾牳蹇冪壒鑹� |
+| [02-quick-start.md](getting-started/02-quick-start.md) | 蹇�熷紑濮� | 瀹夎鍜岃繍琛屾寚鍗� |
+| [03-must-read.md](getting-started/03-must-read.md) | 蹇呰鏂囨。 | 鎺ュ彛瀵规帴銆佺綉缁滆姹傘�佽彍鍗曢厤缃� |
+| [04-standard.md](getting-started/04-standard.md) | 瑙勮寖 | 浠g爜瑙勮寖鍜屾彁浜よ鑼� |
+
+## 馃敡 閰嶇疆鍜岄泦鎴�
+
+| 鏂囦欢 | 鏍囬 | 璇存槑 |
+|------|------|------|
+| [components-basics.md](getting-started/components-basics.md) | 缁勪欢鍜屽浘鏍囧熀纭� | Element Plus銆佺郴缁熺粍浠躲�佸浘鏍囩郴缁� |
+| [configuration-guide.md](getting-started/configuration-guide.md) | 绯荤粺閰嶇疆鎸囧崡 | 涓婚閰嶇疆銆佺幆澧冨彉閲忋�佸叏灞�璁剧疆 |
+| [style-guide.md](getting-started/style-guide.md) | **鏍峰紡瑙勮寖锛堥噸瑕侊級** | Tailwind CSS銆丆SS 鍙橀噺銆佸搷搴斿紡璁捐 |
+
+## 馃彈锔� 鏍稿績姒傚康
+
+| 鏂囦欢 | 鏍囬 | 璇存槑 |
+|------|------|------|
+| [project-structure.md](core-concepts/project-structure.md) | 椤圭洰缁撴瀯 | 鐩綍缁撴瀯鍜屾枃浠惰鏄� |
+| [routing.md](core-concepts/routing.md) | 璺敱鍜岃彍鍗� | 璺敱閰嶇疆鍜岃彍鍗曠敓鎴� |
+| [permission.md](core-concepts/permission.md) | 鏉冮檺绠$悊 | 鍓嶇/鍚庣鏉冮檺妯″紡銆佽鑹查厤缃� |
+
+## 馃З 缁勪欢
+
+| 鏂囦欢 | 鏍囬 | 璇存槑 |
+|------|------|------|
+| [art-form.md](components/art-form.md) | **ArtForm** | **琛ㄥ崟缁勪欢瀹屾暣鏂囨。锛堟柊澧烇級** |
+| [art-search-bar.md](components/art-search-bar.md) | ArtSearchBar | 鎼滅储鏍忕粍浠跺畬鏁存枃妗� |
+
+## 馃獫 Hooks
+
+| 鏂囦欢 | 鏍囬 | 璇存槑 |
+|------|------|------|
+| [use-table.md](hooks/use-table.md) | UseTable | 琛ㄦ牸 Hook 鏂囨。 |
+
+## 馃幆 绀轰緥
+
+| 鐩綍 | 璇存槑 |
+|------|------|
+| [examples/](examples/) | 浣跨敤绀轰緥 |
+
+---
+
+## 馃摉 杈呭姪鏂囨。
+
+| 鏂囦欢 | 鏍囬 | 璇存槑 |
+|------|------|------|
+| [FAQ.md](../FAQ.md) | 甯歌闂 | 甯歌闂瑙g瓟 |
+| [BEST_PRACTICES.md](../BEST_PRACTICES.md) | 鏈�浣冲疄璺� | 鏈�浣冲疄璺垫寚鍗� |
+| [CHEATSHEET.md](../CHEATSHEET.md) | 閫熸煡琛� | 缁勪欢閫熸煡琛� |
+| [generator-guide.md](generator-guide.md) | 浠g爜鐢熸垚鍣� | 浠g爜鐢熸垚鍣ㄤ娇鐢ㄦ寚鍗� |
+| [INTEGRATION_GUIDE.md](../INTEGRATION_GUIDE.md) | 闆嗘垚鎸囧崡 | 闆嗘垚鍒板叾浠栭」鐩殑瀹屾暣鎸囧崡 |
+
+---
+
+## 馃幆 浣跨敤寤鸿
+
+### 闆嗘垚鍒板叾浠栭」鐩�
+1. **棣栧厛闃呰** [INTEGRATION_GUIDE.md](../INTEGRATION_GUIDE.md) - 瀹屾暣鐨勯泦鎴愭寚鍗�
+2. **閰嶇疆妯℃澘** - 鍙傝�� `../templates/` 鐩綍涓殑閰嶇疆鏂囦欢
+3. **楠岃瘉闆嗘垚** - 杩愯 `python ../scripts/verify.py`
+
+### 鍦� Art Design Pro 椤圭洰涓紑鍙�
+1. **寮�鍙戝墠蹇呰**锛歔03-must-read.md](getting-started/03-must-read.md)
+2. **浜嗚В椤圭洰**锛歔project-structure.md](core-concepts/project-structure.md)
+3. **閬靛惊瑙勮寖**锛歔04-standard.md](getting-started/04-standard.md)
+
+### 缁勪欢浣跨敤鍙傝��
+- 琛ㄥ崟缁勪欢锛歔art-form.md](components/art-form.md) 猸�
+- 鎼滅储鏍忥細[art-search-bar.md](components/art-search-bar.md)
+- 琛ㄦ牸 Hook锛歔use-table.md](hooks/use-table.md)
+
+---
+
+## 鉁� 鏈湴鍖栦紭鍔�
+
+- **闆剁綉缁滀緷璧�** - 鎵�鏈夋枃妗f湰鍦颁繚瀛�
+- **闆跺够瑙夐闄�** - 鍐呭鏉ヨ嚜瀹樻柟鏂囨。鍜屽疄闄呬唬鐮�
+- **蹇�熻闂�** - 鏃犻渶绛夊緟缃戠粶鍔犺浇
+- **姘镐箙鍙敤** - 涓嶅彈 URL 鍙樺寲褰卞搷
+
+---
+
+**鏇存柊鏃ユ湡**: 2026-03-04
+**鐗堟湰**: v2.2.1+
+**瀹樻柟鏂囨。**: https://www.artd.pro/docs/
+
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/components/art-form.md b/rsf-design/skill/art-design-pro/docs/components/art-form.md
new file mode 100644
index 0000000..6780f16
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/components/art-form.md
@@ -0,0 +1,632 @@
+# ArtForm 琛ㄥ崟缁勪欢 | Art Design Pro
+
+## 姒傝堪
+
+ArtForm 鏄竴涓姛鑳藉己澶х殑琛ㄥ崟缁勪欢锛屽熀浜� Element Plus 灏佽锛屾敮鎸佸弻鍚戠粦瀹氥�佽〃鍗曢獙璇併�佸搷搴斿紡甯冨眬绛夌壒鎬с��
+
+## 鐗规��
+
+- **鍙屽悜缁戝畾鏀寔** - 浣跨敤 `defineModel`锛屾敮鎸� `v-model` 鍜� `:model` 涓ょ缁戝畾鏂瑰紡
+- **琛ㄥ崟楠岃瘉** - 瀹屾暣鐨勮〃鍗曢獙璇佹敮鎸侊紝涓� Element Plus Form 瑙勫垯鍏煎
+- **鍝嶅簲寮忓竷灞�** - 鍩轰簬 24 鏍呮牸绯荤粺锛岃嚜閫傚簲涓嶅悓灞忓箷灏哄
+- **澶氱鎺т欢绫诲瀷** - 鏀寔 input銆乶umber銆乻elect銆乻witch銆乨ate 绛� 20+ 绉嶈〃鍗曟帶浠�
+- **鍔ㄦ�佹帶鍒�** - 鏀寔鍔ㄦ�佹樉绀洪殣钘忚〃鍗曢」
+- **鎸夐挳鎺у埗** - 鍙厤缃樉绀�/闅愯棌鎻愪氦鍜岄噸缃寜閽�
+
+## 婧愮爜瀹氫箟
+
+```js
+interface FormProps {
+ items: FormItem[] // 琛ㄥ崟椤归厤缃暟缁�
+ span?: number // 姣忓垪鐨勫搴︼紙鍩轰簬 24 鏍煎竷灞�锛�
+ gutter?: number // 琛ㄥ崟鎺т欢闂撮殭
+ labelPosition?: 'left' | 'right' | 'top' // 鏍囩浣嶇疆
+ labelWidth?: string | number // 鏂囧瓧瀹藉害锛堥┘宄板懡鍚嶏級
+ buttonLeftLimit?: number // 鎸夐挳闈犲乏瀵归綈闄愬埗
+ showReset?: boolean // 鏄惁鏄剧ず閲嶇疆鎸夐挳
+ showSubmit?: boolean // 鏄惁鏄剧ず鎻愪氦鎸夐挳
+ disabledSubmit?: boolean // 鏄惁绂佺敤鎻愪氦鎸夐挳
+}
+
+const modelValue = defineModel({ default: {} })
+```
+
+## 鏁版嵁缁戝畾鏂瑰紡
+
+### 鏂瑰紡 1锛歷-model 鍙屽悜缁戝畾锛堟帹鑽愮敤浜庨厤缃〉闈級
+
+```vue
+<template>
+ <ArtForm
+ v-model="formData"
+ :items="formItems"
+ :show-reset="false"
+ :show-submit="false"
+ labelWidth="200px"
+ />
+ <div class="flex justify-end">
+ <ElButton type="primary" @click="handleSave">淇濆瓨</ElButton>
+ </div>
+</template>
+
+<script setup>
+const formData = ref({
+ username: '',
+ status: '1'
+})
+
+const formItems = [
+ { key: 'username', label: '鐢ㄦ埛鍚�', type: 'input' },
+ { key: 'status', label: '鐘舵��', type: 'select', props: { options: [...] } }
+]
+</script>
+```
+
+**閫傜敤鍦烘櫙锛�**
+- 閰嶇疆椤甸潰锛堢洿鎺ョ紪杈戯紝瀹炴椂鍚屾锛�
+- 绠�鍗曡〃鍗曪紙涓嶉渶瑕佹墜鍔ㄦ帶鍒舵彁浜ゆ椂鏈猴級
+- 闇�瑕佺嫭绔嬩繚瀛樻寜閽殑鍦烘櫙
+
+### 鏂瑰紡 2锛�:model 鍗曞悜缁戝畾锛堟帹鑽愮敤浜庡脊绐楄〃鍗曪級
+
+```vue
+<template>
+ <ElDialog v-model="dialogVisible" title="鏂板鐢ㄦ埛">
+ <ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ :rules="formRules"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <ElButton @click="dialogVisible = false">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+const formRef = ref()
+const formData = ref({})
+
+const handleSubmit = async () => {
+ // 鎵嬪姩鎺у埗鎻愪氦閫昏緫
+ await formRef.value.validate()
+ // 鎻愪氦鏁版嵁...
+}
+</script>
+```
+
+**鈿狅笍 閲嶈锛氬湪 ElDialog 涓娇鐢� ArtForm 鏃讹紝蹇呴』璁剧疆锛�**
+- `:show-reset="false"` - 闅愯棌 ArtForm 鍐呴儴鐨勯噸缃寜閽�
+- `:show-submit="false"` - 闅愯棌 ArtForm 鍐呴儴鐨勬彁浜ゆ寜閽�
+
+**鍘熷洜锛�** ElDialog 鐨� footer 鎻掓Ы宸茬粡鏈夎嚜瀹氫箟鎸夐挳锛屽鏋滀笉闅愯棌 ArtForm 鍐呴儴鎸夐挳锛屼細鍑虹幇涓ゅ鎸夐挳瀵艰嚧鏍峰紡閿欎贡銆�
+
+**閫傜敤鍦烘櫙锛�**
+- 寮圭獥琛ㄥ崟锛堥渶瑕佹墜鍔ㄦ帶鍒舵彁浜ゆ椂鏈猴級
+- 闇�瑕佽〃鍗曢獙璇佺殑鍦烘櫙
+- 闇�瑕佸湪鎻愪氦鍓嶈繘琛岄澶栧鐞嗙殑鍦烘櫙
+
+## 琛ㄥ崟椤归厤缃�
+
+### 鍩虹閰嶇疆
+
+```js
+interface FormItem {
+ key: string // 琛ㄥ崟椤瑰敮涓�鏍囪瘑锛堝繀濉級
+ label: string | Component // 鏍囩鏂囨湰鎴栬嚜瀹氫箟娓叉煋鍑芥暟
+ type?: string // 琛ㄥ崟椤圭被鍨�
+ span?: number // 鏍呮牸鍗犱綅鏍兼暟锛�0-24锛�
+ labelWidth?: string | number // 鏍囩瀹藉害锛堣鐩栧叏灞�璁剧疆锛�
+ hidden?: boolean // 鏄惁闅愯棌璇ヨ〃鍗曢」
+ props?: Object // 浼犻�掔粰琛ㄥ崟椤圭粍浠剁殑灞炴��
+ placeholder?: string // 鍗犱綅绗︽枃鏈�
+}
+```
+
+### 鏀寔鐨勮〃鍗曟帶浠剁被鍨�
+
+| type 鍊� | 璇存槑 | 缁勪欢 | props 绀轰緥 |
+|--------|------|------|-----------|
+| `input` | 杈撳叆妗� | ElInput | `{ placeholder: '璇疯緭鍏�', clearable: true }` |
+| `number` | 鏁板瓧杈撳叆妗� | ElInputNumber | `{ min: 0, max: 100, step: 1 }` |
+| `select` | 涓嬫媺閫夋嫨 | ElSelect | `{ options: [{ label: '閫夐」1', value: '1' }] }` |
+| `switch` | 寮�鍏� | ElSwitch | `{ }` |
+| `checkbox` | 澶嶉�夋 | ElCheckbox | `{ }` |
+| `checkboxgroup` | 澶嶉�夋缁� | ElCheckboxGroup | `{ options: [...] }` |
+| `radiogroup` | 鍗曢�夋缁� | ElRadioGroup | `{ options: [...] }` |
+| `date` | 鏃ユ湡閫夋嫨 | ElDatePicker | `{ type: 'date', valueFormat: 'YYYY-MM-DD' }` |
+| `daterange` | 鏃ユ湡鑼冨洿 | ElDatePicker | `{ type: 'daterange' }` |
+| `datetime` | 鏃ユ湡鏃堕棿 | ElDatePicker | `{ type: 'datetime', valueFormat: 'YYYY-MM-DD HH:mm:ss' }` |
+| `timepicker` | 鏃堕棿閫夋嫨 | ElTimePicker | `{ valueFormat: 'HH:mm:ss' }` |
+| `cascader` | 绾ц仈閫夋嫨 | ElCascader | `{ options: [...] }` |
+| `treeselect` | 鏍戦�夋嫨 | ElTreeSelect | `{ data: [...] }` |
+| `slider` | 婊戝潡 | ElSlider | `{ max: 100 }` |
+| `rate` | 璇勫垎 | ElRate | `{ max: 5 }` |
+
+### 鍔ㄦ�佹樉绀洪殣钘�
+
+```vue
+<script setup>
+const showAdvanced = ref(false)
+
+const formItems = computed(() => [
+ { key: 'username', label: '鐢ㄦ埛鍚�', type: 'input' },
+ {
+ key: 'advanced',
+ label: '楂樼骇閫夐」',
+ type: 'input',
+ hidden: !showAdvanced.value // 鍔ㄦ�佹帶鍒�
+ }
+])
+</script>
+```
+
+### 鑷畾涔夌粍浠�
+
+```vue
+<script setup>
+import { h } from 'vue'
+import CustomComponent from './CustomComponent.vue'
+
+const formItems = [
+ {
+ key: 'custom',
+ label: '鑷畾涔夌粍浠�',
+ type: () => h(CustomComponent, {
+ prop1: 'value1',
+ onCustomEvent: handleCustomEvent
+ })
+ }
+]
+</script>
+```
+
+## 琛ㄥ崟楠岃瘉
+
+```vue
+<template>
+ <ArtForm
+ ref="formRef"
+ v-model="formData"
+ :items="formItems"
+ :rules="formRules"
+ />
+</template>
+
+<script setup>
+const formRules = {
+ username: [
+ { required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' },
+ { min: 3, max: 20, message: '闀垮害鍦� 3 鍒� 20 涓瓧绗�', trigger: 'blur' }
+ ],
+ email: [
+ { required: true, message: '璇疯緭鍏ラ偖绠�', trigger: 'blur' },
+ { type: 'email', message: '璇疯緭鍏ユ纭殑閭鍦板潃', trigger: 'blur' }
+ ]
+}
+
+const handleSubmit = async () => {
+ try {
+ await formRef.value.validate()
+ console.log('楠岃瘉閫氳繃')
+ } catch (error) {
+ console.log('楠岃瘉澶辫触')
+ }
+}
+</script>
+```
+
+## 鍛藉悕瑙勮寖
+
+### Props 鍛藉悕
+
+| Prop | 绫诲瀷 | 榛樿鍊� | 璇存槑 |
+|------|------|--------|------|
+| `labelWidth` | `string \| number` | `'70px'` | 浣跨敤椹煎嘲鍛藉悕 |
+| `labelPosition` | `'left' \| 'right' \| 'top'` | `'right'` | 鏍囩浣嶇疆 |
+| `showReset` | `boolean` | `true` | 鏄惁鏄剧ず閲嶇疆鎸夐挳 |
+| `showSubmit` | `boolean` | `true` | 鏄惁鏄剧ず鎻愪氦鎸夐挳 |
+
+**娉ㄦ剰锛�** 铏界劧 Vue 3 鏀寔鐭í绾垮懡鍚嶏紙濡� `label-width`锛夛紝浣嗕负浜嗕繚鎸佷竴鑷存�э紝寤鸿浣跨敤椹煎嘲鍛藉悕 `labelWidth`銆�
+
+## 甯冨眬閰嶇疆
+
+### 鏍呮牸甯冨眬
+
+```vue
+<ArtForm
+ v-model="formData"
+ :items="formItems"
+ :span="8" // 姣忎釜琛ㄥ崟椤瑰崰鎹� 8 鏍硷紙姣忚 3 涓級
+ :gutter="16" // 琛ㄥ崟椤逛箣闂寸殑闂磋窛
+/>
+```
+
+### 鍝嶅簲寮忓竷灞�
+
+缁勪欢浼氳嚜鍔ㄩ�傞厤涓嶅悓灞忓箷灏哄锛�
+- **绉诲姩绔�**锛氭瘡琛屾樉绀� 1 涓〃鍗曢」
+- **骞虫澘**锛氭瘡琛屾樉绀� 2 涓〃鍗曢」
+- **妗岄潰绔�**锛氭牴鎹� `span` 灞炴�ф帶鍒�
+
+## 瀹屾暣绀轰緥
+
+### 绀轰緥 1锛氶厤缃〉闈紙浣跨敤 v-model锛�
+
+```vue
+<template>
+ <div class="config-page art-full-height">
+ <ElCard class="art-table-card" shadow="never">
+ <ArtForm
+ v-model="config"
+ :items="configItems"
+ :show-reset="false"
+ :show-submit="false"
+ labelWidth="200px"
+ />
+ <div class="flex justify-end">
+ <ElButton type="primary" :loading="saving" @click="handleSave">
+ 淇濆瓨閰嶇疆
+ </ElButton>
+ </div>
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import ArtForm from '@/components/core/forms/art-form/index.vue'
+
+const config = ref({
+ bridge_port: 8384,
+ http_port: 8081,
+ ip_limit: 0,
+ log_level: 'info'
+})
+
+const configItems = [
+ {
+ key: 'bridge_port',
+ label: '妗ユ帴绔彛',
+ type: 'number',
+ props: { min: 1024, max: 65535 },
+ span: 12
+ },
+ {
+ key: 'http_port',
+ label: 'HTTP 绔彛',
+ type: 'number',
+ props: { min: 1024, max: 65535 },
+ span: 12
+ },
+ {
+ key: 'log_level',
+ label: '鏃ュ織绾у埆',
+ type: 'select',
+ props: {
+ options: [
+ { label: 'Debug', value: 'debug' },
+ { label: 'Info', value: 'info' },
+ { label: 'Warn', value: 'warn' },
+ { label: 'Error', value: 'error' }
+ ]
+ },
+ span: 12
+ }
+]
+
+const saving = ref(false)
+
+const handleSave = async () => {
+ saving.value = true
+ try {
+ // 淇濆瓨閰嶇疆...
+ ElMessage.success('淇濆瓨鎴愬姛')
+ } finally {
+ saving.value = false
+ }
+}
+</script>
+```
+
+### 绀轰緥 2锛氬脊绐楄〃鍗曪紙浣跨敤 :model锛�
+
+```vue
+<template>
+ <ElDialog
+ v-model="dialogVisible"
+ :title="dialogMode === 'create' ? '鏂板鐢ㄦ埛' : '缂栬緫鐢ㄦ埛'"
+ width="600px"
+ >
+ <ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ :rules="formRules"
+ :show-reset="false"
+ :show-submit="false"
+ />
+
+ <template #footer>
+ <ElButton @click="dialogVisible = false">鍙栨秷</ElButton>
+ <ElButton type="primary" :loading="submitLoading" @click="handleSubmit">
+ 纭畾
+ </ElButton>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import ArtForm from '@/components/core/forms/art-form/index.vue'
+
+const dialogVisible = ref(false)
+const dialogMode = ref<'create' | 'edit'>('create')
+const formRef = ref<FormInstance>()
+const submitLoading = ref(false)
+
+const formData = ref({
+ username: '',
+ email: '',
+ status: '1'
+})
+
+const formRules: FormRules = {
+ username: [
+ { required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }
+ ],
+ email: [
+ { required: true, message: '璇疯緭鍏ラ偖绠�', trigger: 'blur' },
+ { type: 'email', message: '璇疯緭鍏ユ纭殑閭鍦板潃', trigger: 'blur' }
+ ]
+}
+
+const formItems = [
+ {
+ key: 'username',
+ label: '鐢ㄦ埛鍚�',
+ type: 'input',
+ span: 24, // 鈿狅笍 閲嶈锛氭ā鎬佺獥涓繀椤昏缃� span: 24锛堟暣琛屾樉绀猴級
+ props: { placeholder: '璇疯緭鍏ョ敤鎴峰悕' }
+ },
+ {
+ key: 'email',
+ label: '閭',
+ type: 'input',
+ span: 24, // 鏁磋鏄剧ず
+ props: { placeholder: '璇疯緭鍏ラ偖绠�' }
+ },
+ {
+ key: 'status',
+ label: '鐘舵��',
+ type: 'select',
+ span: 24, // 鏁磋鏄剧ず
+ props: {
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+ }
+]
+
+const handleSubmit = async () => {
+ try {
+ await formRef.value?.validate()
+ submitLoading.value = true
+ // 鎻愪氦閫昏緫...
+ ElMessage.success('鎻愪氦鎴愬姛')
+ dialogVisible.value = false
+ } catch (error) {
+ console.log('楠岃瘉澶辫触')
+ } finally {
+ submitLoading.value = false
+ }
+}
+</script>
+```
+
+**鈿狅笍 閲嶈鎻愮ず锛�**
+
+1. **闅愯棌鍐呴儴鎸夐挳**锛氬湪 ElDialog 涓娇鐢ㄦ椂锛屽繀椤昏缃細
+ - `:show-reset="false"` - 闅愯棌 ArtForm 鍐呴儴鐨勯噸缃寜閽�
+ - `:show-submit="false"` - 闅愯棌 ArtForm 鍐呴儴鐨勬彁浜ゆ寜閽�
+
+2. **璁剧疆 span 灞炴��**锛氬湪妯℃�佺獥涓紝姣忎釜琛ㄥ崟椤�**蹇呴』璁剧疆 `span: 24`**锛堟暣琛屾樉绀猴級
+ - ArtForm 榛樿 `span: 6`锛堟瘡涓瓧娈靛崰 1/4 瀹藉害锛�
+ - 鍦� 600px 瀹界殑妯℃�佺獥涓紝涓嶈缃� span 浼氬鑷磋緭鍏ユ瀹藉害鏋佺獎锛堜粎 6px锛�
+
+3. **甯冨眬寤鸿**锛�
+ - 妯℃�佺獥琛ㄥ崟锛�**鎵�鏈夊瓧娈佃缃� `span: 24`**锛堟暣琛屾樉绀猴級
+ - 閰嶇疆椤甸潰琛ㄥ崟锛氬彲浣跨敤 `span: 12`锛堝崐琛岋級鎴� `span: 24`锛堟暣琛岋級
+
+## API 鍙傝��
+
+### Props
+
+| 鍙傛暟 | 璇存槑 | 绫诲瀷 | 榛樿鍊� |
+|------|------|------|--------|
+| modelValue | 琛ㄥ崟鏁版嵁瀵硅薄锛堟敮鎸� v-model锛� | `Object` | `{}` |
+| items | 琛ㄥ崟椤归厤缃暟缁� | `FormItem[]` | `[]` |
+| span | 姣忓垪鐨勫搴︼紙鍩轰簬 24 鏍煎竷灞�锛� | `number` | `6` |
+| gutter | 琛ㄥ崟鎺т欢闂撮殭 | `number` | `12` |
+| labelPosition | 鏍囩浣嶇疆 | `'left' \| 'right' \| 'top'` | `'right'` |
+| labelWidth | 鏍囩瀹藉害 | `string \| number` | `'70px'` |
+| buttonLeftLimit | 鎸夐挳闈犲乏瀵归綈闄愬埗 | `number` | `2` |
+| showReset | 鏄惁鏄剧ず閲嶇疆鎸夐挳 | `boolean` | `true` |
+| showSubmit | 鏄惁鏄剧ず鎻愪氦鎸夐挳 | `boolean` | `true` |
+| disabledSubmit | 鏄惁绂佺敤鎻愪氦鎸夐挳 | `boolean` | `false` |
+
+### Events
+
+| 浜嬩欢鍚� | 璇存槑 | 鍙傛暟 |
+|--------|------|------|
+| reset | 鐐瑰嚮閲嶇疆鎸夐挳鏃惰Е鍙� | - |
+| submit | 鐐瑰嚮鎻愪氦鎸夐挳鏃惰Е鍙� | - |
+
+### Methods
+
+| 鏂规硶鍚� | 璇存槑 | 鍙傛暟 |
+|--------|------|------|
+| validate | 楠岃瘉琛ㄥ崟 | `() => Promise<boolean>` |
+| resetFields | 閲嶇疆琛ㄥ崟 | `() => void` |
+| clearValidate | 娓呴櫎楠岃瘉 | `() => void` |
+
+## 鏈�浣冲疄璺�
+
+### 1. 鏍规嵁鍦烘櫙閫夋嫨缁戝畾鏂瑰紡
+
+| 鍦烘櫙 | 鎺ㄨ崘鏂瑰紡 | 鍘熷洜 |
+|------|---------|------|
+| 閰嶇疆椤甸潰 | `v-model` | 鐩存帴缂栬緫锛屽疄鏃跺悓姝� |
+| 寮圭獥琛ㄥ崟 | `:model` | 鎵嬪姩鎺у埗鎻愪氦鏃舵満 |
+| 鎼滅储琛ㄥ崟 | `v-model` | 绠�鍖栦唬鐮侊紝鑷姩鍚屾 |
+
+### 2. 缁熶竴浣跨敤椹煎嘲鍛藉悕
+
+```vue
+<!-- 鉁� 鎺ㄨ崘 -->
+<ArtForm labelWidth="200px" />
+
+<!-- 鉂� 涓嶆帹鑽愶紙铏界劧鏈夋晥锛� -->
+<ArtForm label-width="200px" />
+```
+
+### 3. 鍚堢悊浣跨敤鏍呮牸甯冨眬
+
+```js
+// 鉁� 鎺ㄨ崘锛氭牴鎹唴瀹归暱搴﹁缃� span
+const formItems = [
+ { key: 'username', label: '鐢ㄦ埛鍚�', span: 12 }, // 鍗婅
+ { key: 'email', label: '閭', span: 12 }, // 鍗婅
+ { key: 'remark', label: '澶囨敞', span: 24 } // 鏁磋
+]
+
+// 鉂� 涓嶆帹鑽愶細鎵�鏈夊瓧娈甸兘鍗犳嵁鏁磋
+const formItems = [
+ { key: 'username', label: '鐢ㄦ埛鍚�', span: 24 },
+ { key: 'email', label: '閭', span: 24 }
+]
+```
+
+### 4. 浣跨敤 computed 瀹炵幇鍔ㄦ�佽〃鍗�
+
+```js
+const formItems = computed(() => [
+ { key: 'username', label: '鐢ㄦ埛鍚�', type: 'input' },
+ {
+ key: 'password',
+ label: '瀵嗙爜',
+ type: 'input',
+ hidden: dialogMode.value !== 'create' // 缂栬緫鏃朵笉鏄剧ず瀵嗙爜
+ }
+])
+```
+
+## 甯歌闂
+
+### Q0: 鍦� ElDialog 涓娇鐢� ArtForm 鏃跺嚭鐜颁袱濂楁寜閽�庝箞鍔烇紵
+
+**A:** 杩欐槸 ArtForm 鐨勫父瑙侀棶棰樸�傚湪妯℃�佺獥涓娇鐢ㄦ椂锛�**蹇呴』闅愯棌 ArtForm 鍐呴儴鐨勬寜閽�**锛�
+
+```vue
+<!-- 鉂� 閿欒锛氫細鍑虹幇涓ゅ鎸夐挳 -->
+<ElDialog v-model="dialogVisible" title="鏂板鐢ㄦ埛">
+ <ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ />
+ <template #footer>
+ <ElButton>鍙栨秷</ElButton>
+ <ElButton>纭畾</ElButton>
+ </template>
+</ElDialog>
+
+<!-- 鉁� 姝g‘锛氶殣钘� ArtForm 鍐呴儴鎸夐挳 -->
+<ElDialog v-model="dialogVisible" title="鏂板鐢ㄦ埛">
+ <ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ :show-reset="false"
+ :show-submit="false"
+ />
+ <template #footer>
+ <ElButton>鍙栨秷</ElButton>
+ <ElButton>纭畾</ElButton>
+ </template>
+</ElDialog>
+```
+
+**鍘熷洜锛�** ArtForm 榛樿鏄剧ず閲嶇疆鍜屾彁浜ゆ寜閽紙`showReset: true`, `showSubmit: true`锛夛紝鍦ㄦā鎬佺獥涓細涓� footer 涓殑鑷畾涔夋寜閽啿绐併��
+
+### Q1: v-model 鍜� :model 鏈変粈涔堝尯鍒紵
+
+**A:**
+- `v-model`锛氬弻鍚戠粦瀹氾紝琛ㄥ崟鏁版嵁淇敼鑷姩鍚屾鍒扮埗缁勪欢
+- `:model`锛氬崟鍚戠粦瀹氾紝闇�瑕佹墜鍔ㄥ鐞嗘暟鎹悓姝�
+
+鏍规嵁鍦烘櫙閫夋嫨锛�
+- 閰嶇疆椤甸潰锛氫娇鐢� `v-model`
+- 寮圭獥琛ㄥ崟锛氫娇鐢� `:model` + 鎵嬪姩鎻愪氦
+
+### Q2: 濡備綍鍔ㄦ�佹帶鍒惰〃鍗曢」鏄剧ず锛�
+
+**A:** 浣跨敤 `hidden` 灞炴�э細
+
+```js
+const formItems = computed(() => [
+ { key: 'field1', label: '瀛楁1', hidden: !showField1.value },
+ { key: 'field2', label: '瀛楁2' }
+])
+```
+
+### Q3: 濡備綍鑷畾涔夎〃鍗曢」锛�
+
+**A:** 浣跨敤 `render` 鍑芥暟鎴栨彃妲斤細
+
+```js
+// 鏂瑰紡 1锛氫娇鐢� render 鍑芥暟
+import { h } from 'vue'
+import CustomComponent from './CustomComponent.vue'
+
+{
+ key: 'custom',
+ label: '鑷畾涔�',
+ type: () => h(CustomComponent, { prop: 'value' })
+}
+
+// 鏂瑰紡 2锛氫娇鐢ㄦ彃妲�
+<ArtForm v-model="formData" :items="formItems">
+ <template #customField="{ item, modelValue }">
+ <CustomComponent v-model="modelValue[item.key]" />
+ </template>
+</ArtForm>
+```
+
+## 鐩稿叧鏂囨。
+
+- [ArtSearchBar 缁勪欢](./art-search-bar.md)
+- [useTable Hook](../hooks/use-table.md)
+- [CRUD 椤甸潰绀轰緥](../examples/templates/crud-page.md)
+- [Element Plus Form 鏂囨。](https://element-plus.org/zh-CN/component/form.html)
+
+## 瀹樻柟鏂囨。
+
+- [ArtForm 瀹樻柟鏂囨。](https://www.artd.pro/docs/zh/guide/components/art-form.html)
+
+---
+
+**鏈�鍚庢洿鏂�**锛�2026-03-04
+**缁存姢鑰�**锛欰rt Design Pro Skill Team
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/components/art-search-bar.md b/rsf-design/skill/art-design-pro/docs/components/art-search-bar.md
new file mode 100644
index 0000000..5349016
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/components/art-search-bar.md
@@ -0,0 +1,577 @@
+# ArtSearchBar 鎼滅储鏍忕粍浠� | Art Design Pro
+
+鏉ユ簮锛歨ttps://www.artd.pro/docs/zh/guide/components/art-search-bar.html
+
+## 姒傝堪
+
+涓�涓姛鑳藉己澶с�侀珮搴﹀彲閰嶇疆鐨勮〃鍗曟悳绱㈢粍浠讹紝鏀寔澶氱琛ㄥ崟鎺т欢绫诲瀷銆佸姩鎬佹樉绀洪殣钘忋�佽〃鍗曢獙璇佺瓑鐗规�с��
+
+## 鐗规��
+
+- **澶氱琛ㄥ崟鎺т欢** - 鏀寔杈撳叆妗嗐�侀�夋嫨鍣ㄣ�佹棩鏈熼�夋嫨鍣ㄣ�佺骇鑱旈�夋嫨鍣ㄧ瓑 20+ 绉嶈〃鍗曟帶浠�
+- **楂樺害鍙厤缃�** - 鏀寔鑷畾涔夊竷灞�銆佹爣绛句綅缃�侀棿璺濈瓑
+- **鍝嶅簲寮忚璁�** - 鑷�傚簲涓嶅悓灞忓箷灏哄
+- **鎻掓Ы鏀寔** - 鏀寔鑷畾涔夌粍浠跺拰鎻掓Ы娓叉煋
+- **琛ㄥ崟楠岃瘉** - 瀹屾暣鐨勮〃鍗曢獙璇佹敮鎸�
+- **鍔ㄦ�佹帶鍒�** - 鏀寔鍔ㄦ�佹樉绀洪殣钘忚〃鍗曢」
+
+## 鍩虹鐢ㄦ硶
+
+```vue
+<template>
+ <ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+</template>
+
+<script setup>
+const formData = ref({
+ name: '',
+ status: ''
+})
+
+const formItems = [
+ {
+ label: '鐢ㄦ埛鍚�',
+ key: 'name',
+ type: 'input',
+ placeholder: '璇疯緭鍏ョ敤鎴峰悕'
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+ }
+]
+
+const handleSearch = () => {
+ console.log('鎼滅储鍙傛暟:', formData.value)
+}
+
+const handleReset = () => {
+ console.log('閲嶇疆琛ㄥ崟')
+}
+</script>
+```
+
+## 鏀寔鐨勮〃鍗曟帶浠剁被鍨�
+
+### 杈撳叆绫绘帶浠�
+
+```javascript
+// 鏅�氳緭鍏ユ
+{
+ label: '鐢ㄦ埛鍚�',
+ key: 'name',
+ type: 'input',
+ placeholder: '璇疯緭鍏ョ敤鎴峰悕'
+}
+
+// 鏁板瓧杈撳叆妗�
+{
+ label: '骞撮緞',
+ key: 'age',
+ type: 'number',
+ props: {
+ min: 0,
+ max: 120
+ }
+}
+
+// 澶氳鏂囨湰
+{
+ label: '澶囨敞',
+ key: 'remark',
+ type: 'input',
+ props: {
+ type: 'textarea',
+ rows: 3
+ }
+}
+```
+
+### 閫夋嫨绫绘帶浠�
+
+```javascript
+// 涓嬫媺閫夋嫨
+{
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+}
+
+// 绾ц仈閫夋嫨鍣�
+{
+ label: '鍦板尯',
+ key: 'region',
+ type: 'cascader',
+ props: {
+ options: cascaderOptions,
+ props: { multiple: true }
+ }
+}
+
+// 鏍戦�夋嫨鍣�
+{
+ label: '閮ㄩ棬',
+ key: 'department',
+ type: 'treeselect',
+ props: {
+ data: treeData,
+ multiple: true,
+ showCheckbox: true
+ }
+}
+```
+
+### 鏃ユ湡鏃堕棿鎺т欢
+
+```javascript
+// 鏃ユ湡閫夋嫨
+{
+ label: '鍒涘缓鏃ユ湡',
+ key: 'createDate',
+ type: 'datetime',
+ props: {
+ type: 'date',
+ valueFormat: 'YYYY-MM-DD'
+ }
+}
+
+// 鏃ユ湡鑼冨洿
+{
+ label: '鏃堕棿鑼冨洿',
+ key: 'dateRange',
+ type: 'datetime',
+ props: {
+ type: 'daterange',
+ rangeSeparator: '鑷�',
+ startPlaceholder: '寮�濮嬫棩鏈�',
+ endPlaceholder: '缁撴潫鏃ユ湡'
+ }
+}
+
+// 鏃堕棿閫夋嫨鍣�
+{
+ label: '鏃堕棿',
+ key: 'time',
+ type: 'timepicker',
+ props: {
+ valueFormat: 'HH:mm:ss'
+ }
+}
+```
+
+### 鍏朵粬鎺т欢
+
+```javascript
+// 寮�鍏�
+{
+ label: '鏄惁鍚敤',
+ key: 'enabled',
+ type: 'switch'
+}
+
+// 鍗曢�夋缁�
+{
+ label: '鎬у埆',
+ key: 'gender',
+ type: 'radiogroup',
+ props: {
+ options: [
+ { label: '鐢�', value: '1' },
+ { label: '濂�', value: '2' }
+ ]
+ }
+}
+
+// 澶嶉�夋缁�
+{
+ label: '鍏磋叮鐖卞ソ',
+ key: 'hobbies',
+ type: 'checkboxgroup',
+ props: {
+ options: [
+ { label: '璇讳功', value: 'reading' },
+ { label: '杩愬姩', value: 'sports' }
+ ]
+ }
+}
+
+// 璇勫垎
+{
+ label: '璇勫垎',
+ key: 'rating',
+ type: 'rate'
+}
+
+// 婊戝潡
+{
+ label: '浠锋牸鍖洪棿',
+ key: 'priceRange',
+ type: 'slider',
+ props: {
+ range: true,
+ max: 1000
+ }
+}
+```
+
+## 鑷畾涔夌粍浠�
+
+### 浣跨敤娓叉煋鍑芥暟
+
+```javascript
+import { h } from 'vue'
+import CustomComponent from './CustomComponent.vue'
+
+{
+ label: '鑷畾涔夌粍浠�',
+ key: 'custom',
+ type: () => h(CustomComponent, {
+ prop1: 'value1',
+ onCustomEvent: handleCustomEvent
+ })
+}
+```
+
+### 浣跨敤鎻掓Ы
+
+```vue
+<template>
+ <ArtSearchBar v-model="formData" :items="formItems">
+ <template #customSlot="{ item, modelValue }">
+ <el-input
+ v-model="modelValue[item.key]"
+ placeholder="鎴戞槸鎻掓Ы娓叉煋鐨勭粍浠�"
+ />
+ </template>
+ </ArtSearchBar>
+</template>
+
+<script setup>
+const formItems = [
+ {
+ label: '鑷畾涔夋彃妲�',
+ key: 'customSlot',
+ type: 'input' // 杩欓噷鐨則ype浼氳鎻掓Ы瑕嗙洊
+ }
+]
+</script>
+```
+
+## 琛ㄥ崟楠岃瘉
+
+```vue
+<template>
+ <ArtSearchBar
+ ref="searchBarRef"
+ v-model="formData"
+ :items="formItems"
+ :rules="rules"
+ @search="handleSearch"
+ />
+</template>
+
+<script setup>
+const searchBarRef = ref()
+
+const rules = {
+ name: [
+ { required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }
+ ],
+ phone: [
+ { required: true, message: '璇疯緭鍏ユ墜鏈哄彿', trigger: 'blur' },
+ { pattern: /^1[3456789]\d{9}$/, message: '璇疯緭鍏ユ纭殑鎵嬫満鍙�', trigger: 'blur' }
+ ]
+}
+
+const handleSearch = async () => {
+ try {
+ await searchBarRef.value.validate()
+ console.log('楠岃瘉閫氳繃锛屾墽琛屾悳绱�')
+ } catch (error) {
+ console.log('楠岃瘉澶辫触')
+ }
+}
+</script>
+```
+
+## 鍔ㄦ�佹帶鍒�
+
+### 鍔ㄦ�佹樉绀洪殣钘�
+
+```javascript
+const formItems = computed(() => [
+ {
+ label: '鐢ㄦ埛鍚�',
+ key: 'name',
+ type: 'input'
+ },
+ {
+ label: '楂樼骇閫夐」',
+ key: 'advanced',
+ type: 'input',
+ hidden: !showAdvanced.value // 鍔ㄦ�佹帶鍒舵樉绀洪殣钘�
+ }
+])
+```
+
+### 鍔ㄦ�佹洿鏂伴厤缃�
+
+```javascript
+const userNameItem = ref({
+ label: '鐢ㄦ埛鍚�',
+ key: 'name',
+ type: 'input',
+ placeholder: '璇疯緭鍏ョ敤鎴峰悕'
+})
+
+// 鍔ㄦ�佷慨鏀归厤缃�
+const updateUserNameConfig = () => {
+ userNameItem.value = {
+ ...userNameItem.value,
+ label: '鏄电О',
+ placeholder: '璇疯緭鍏ユ樀绉�'
+ }
+}
+```
+
+## 甯冨眬閰嶇疆
+
+### 鏍呮牸甯冨眬
+
+```vue
+<ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ :span="8"
+ :gutter="16"
+/>
+```
+
+### 鏍囩閰嶇疆
+
+```vue
+<ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ label-position="top"
+ :label-width="120"
+/>
+```
+
+### 鍝嶅簲寮忓竷灞�
+
+缁勪欢浼氳嚜鍔ㄩ�傞厤涓嶅悓灞忓箷灏哄锛�
+- **绉诲姩绔�**锛氭瘡琛屾樉绀� 1 涓〃鍗曢」
+- **骞虫澘**锛氭瘡琛屾樉绀� 2 涓〃鍗曢」
+- **妗岄潰绔�**锛氭牴鎹� `span` 灞炴�ф帶鍒舵瘡琛屾樉绀虹殑琛ㄥ崟椤规暟閲�
+
+## API
+
+### Props
+
+| 鍙傛暟 | 璇存槑 | 绫诲瀷 | 榛樿鍊� |
+| --- | --- | --- | --- |
+| modelValue | 琛ㄥ崟鏁版嵁瀵硅薄 | `Object` | `{}` |
+| items | 琛ㄥ崟椤归厤缃暟缁� | `SearchFormItem[]` | `[]` |
+| span | 姣忎釜琛ㄥ崟椤瑰崰鎹殑鏍呮牸鏁� | `number` | `6` |
+| gutter | 鏍呮牸闂撮殧 | `number` | `12` |
+| labelPosition | 鏍囩浣嶇疆 | `'left' \| 'right' \| 'top'` | `'right'` |
+| labelWidth | 鏍囩瀹藉害 | `string \| number` | `'70px'` |
+| defaultExpanded | 榛樿鏄惁灞曞紑 | `boolean` | `false` |
+| showExpand | 鏄惁鏄剧ず灞曞紑鏀惰捣鎸夐挳 | `boolean` | `true` |
+| showReset | 鏄惁鏄剧ず閲嶇疆鎸夐挳 | `boolean` | `true` |
+| showSearch | 鏄惁鏄剧ず鎼滅储鎸夐挳 | `boolean` | `true` |
+| disabledSearch | 鏄惁绂佺敤鎼滅储鎸夐挳 | `boolean` | `false` |
+
+### SearchFormItem 閰嶇疆
+
+| 鍙傛暟 | 璇存槑 | 绫诲瀷 | 榛樿鍊� |
+| --- | --- | --- | --- |
+| key | 琛ㄥ崟椤瑰敮涓�鏍囪瘑 | `string` | - |
+| label | 鏍囩鏂囨湰 | `string` | - |
+| type | 琛ㄥ崟椤圭被鍨� | `string \| (() => VNode)` | `'input'` |
+| hidden | 鏄惁闅愯棌 | `boolean` | `false` |
+| span | 鏍呮牸鍗犱綅鏍兼暟 | `number` | - |
+| labelWidth | 鏍囩瀹藉害 | `string \| number` | - |
+| placeholder | 鍗犱綅绗� | `string` | - |
+| props | 浼犻�掔粰缁勪欢鐨勫睘鎬� | `Object` | - |
+| slots | 鎻掓Ы閰嶇疆 | `Record<string, () => any>` | - |
+
+### Events
+
+| 浜嬩欢鍚� | 璇存槑 | 鍙傛暟 |
+| --- | --- | --- |
+| search | 鐐瑰嚮鎼滅储鎸夐挳鏃惰Е鍙� | - |
+| reset | 鐐瑰嚮閲嶇疆鎸夐挳鏃惰Е鍙� | - |
+
+### Methods
+
+| 鏂规硶鍚� | 璇存槑 | 鍙傛暟 |
+| --- | --- | --- |
+| validate | 楠岃瘉琛ㄥ崟 | `() => Promise<boolean>` |
+| reset | 閲嶇疆琛ㄥ崟 | `() => void` |
+
+### Slots
+
+| 鎻掓Ы鍚� | 璇存槑 | 鍙傛暟 |
+| --- | --- | --- |
+| [key] | 鑷畾涔夎〃鍗曢」鍐呭 | `{ item: SearchFormItem, modelValue: Object }` |
+
+## 娉ㄦ剰浜嬮」
+
+1. **琛ㄥ崟椤� key 鍊煎繀椤诲敮涓�**锛岀敤浜庤〃鍗曟暟鎹粦瀹氬拰楠岃瘉
+2. **props 灞炴�т細鐩存帴浼犻�掔粰瀵瑰簲鐨� Element Plus 缁勪欢**锛岃鍙傝�� Element Plus 瀹樻柟鏂囨。
+3. **琛ㄥ崟楠岃瘉瑙勫垯鏍煎紡涓� Element Plus Form 缁勪欢涓�鑷�**
+
+## 瀹屾暣绀轰緥
+
+```vue
+<template>
+ <div class="search-example">
+ <ArtSearchBar
+ ref="searchBarRef"
+ v-model="formData"
+ :items="formItems"
+ :rules="rules"
+ :defaultExpanded="true"
+ :labelWidth="100"
+ labelPosition="right"
+ :span="6"
+ :gutter="16"
+ @search="handleSearch"
+ @reset="handleReset"
+ >
+ <template #customSlot>
+ <el-input
+ v-model="formData.customSlot"
+ placeholder="鎴戞槸鎻掓Ы娓叉煋鐨勭粍浠�"
+ />
+ </template>
+ </ArtSearchBar>
+
+ <div class="result">
+ <h3>鎼滅储缁撴灉锛�</h3>
+ <pre>{{ JSON.stringify(formData, null, 2) }}</pre>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+const searchBarRef = ref()
+
+const formData = ref({
+ name: '',
+ phone: '',
+ status: '',
+ dateRange: [],
+ customSlot: ''
+})
+
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }],
+ phone: [
+ { required: true, message: '璇疯緭鍏ユ墜鏈哄彿', trigger: 'blur' },
+ { pattern: /^1[3456789]\d{9}$/, message: '璇疯緭鍏ユ纭殑鎵嬫満鍙�', trigger: 'blur' }
+ ]
+}
+
+const formItems = [
+ {
+ label: '鐢ㄦ埛鍚�',
+ key: 'name',
+ type: 'input',
+ placeholder: '璇疯緭鍏ョ敤鎴峰悕',
+ props: { clearable: true }
+ },
+ {
+ label: '鎵嬫満鍙�',
+ key: 'phone',
+ type: 'input',
+ placeholder: '璇疯緭鍏ユ墜鏈哄彿',
+ props: { maxlength: 11 }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+ },
+ {
+ label: '鏃ユ湡鑼冨洿',
+ key: 'dateRange',
+ type: 'datetime',
+ props: {
+ type: 'daterange',
+ rangeSeparator: '鑷�',
+ startPlaceholder: '寮�濮嬫棩鏈�',
+ endPlaceholder: '缁撴潫鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD'
+ }
+ },
+ {
+ label: '鑷畾涔夋彃妲�',
+ key: 'customSlot',
+ type: 'input'
+ }
+]
+
+const handleSearch = async () => {
+ try {
+ await searchBarRef.value.validate()
+ console.log('鎼滅储鍙傛暟:', formData.value)
+ } catch (error) {
+ console.log('琛ㄥ崟楠岃瘉澶辫触')
+ }
+}
+
+const handleReset = () => {
+ console.log('閲嶇疆琛ㄥ崟')
+}
+</script>
+
+<style scoped>
+.search-example {
+ padding: 20px;
+}
+
+.result {
+ margin-top: 20px;
+ padding: 16px;
+ background-color: #f5f5f5;
+ border-radius: 4px;
+}
+
+.result pre {
+ margin: 0;
+ font-size: 12px;
+}
+</style>
+```
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/core-concepts/permission.md b/rsf-design/skill/art-design-pro/docs/core-concepts/permission.md
new file mode 100644
index 0000000..0c5cc5c
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/core-concepts/permission.md
@@ -0,0 +1,199 @@
+# 14 Permission
+> 鏉ユ簮锛�14 Permission
+
+鏈郴缁熸敮鎸佷袱绉嶆潈闄愭帶鍒舵ā寮忥紝鍩轰簬鐢ㄦ埛瑙掕壊鎴栬彍鍗曞垪琛ㄥ姩鎬佺鐞嗛〉闈㈣闂拰鎸夐挳鏄剧ず鏉冮檺銆�
+## 鏉冮檺鎺у埗妯″紡鈥�
+### 姒傝堪鈥�
+绯荤粺鎻愪緵浠ヤ笅涓ょ鏉冮檺鎺у埗妯″紡锛�
+- 鍩轰簬瑙掕壊锛氶�氳繃鎺ュ彛鑾峰彇鐢ㄦ埛瑙掕壊锛屾帶鍒堕〉闈㈣闂拰鎸夐挳鏄剧ず鏉冮檺銆�
+- 鍩轰簬鑿滃崟锛氶�氳繃鎺ュ彛鑾峰彇鑿滃崟鍒楄〃锛屼緷鎹彍鍗曠粨鏋勬帶鍒堕〉闈㈣闂拰鎸夐挳鏉冮檺銆�
+### 閰嶇疆鏂瑰紡鈥�
+鏉冮檺鎺у埗妯″紡閫氳繃鏍圭洰褰曚笅鐨�.env鏂囦欢閰嶇疆銆備慨鏀筕ITE_ACCESS_MODE鐨勫�煎彲鍒囨崲妯″紡锛�
+`.env``VITE_ACCESS_MODE`- frontend锛氬墠绔帶鍒舵ā寮忥紝鍩轰簬鍚庣杩斿洖鐨勮鑹叉爣璇嗚繘琛屾潈闄愭帶鍒躲��
+- backend锛氬悗绔帶鍒舵ā寮忥紝鍩轰簬鍚庣杩斿洖鐨勮彍鍗曞垪琛ㄨ繘琛屾潈闄愭帶鍒躲��
+```
+# 鏉冮檺鎺у埗妯″紡锛坒rontend | backend锛�
+VITE_ACCESS_MODE=frontend
+```
+## 鍓嶇鎺у埗妯″紡鈥�
+### 鍘熺悊鈥�
+鍓嶇缁存姢鑿滃崟鍒楄〃銆傜敤鎴风櫥褰曞悗锛屾帴鍙h繑鍥炶鑹叉爣璇嗭紙濡俁_SUPER锛夈�傚墠绔牴鎹鑹查亶鍘嗚彍鍗曞垪琛紝鑻ヨ彍鍗曠殑roles瀛楁鍖呭惈璇ヨ鑹诧紝鍒欏厑璁歌闂搴旇矾鐢便�傝嫢鏈缃畆oles锛屽垯榛樿鎵�鏈夌敤鎴峰彲璁块棶銆�
+`R_SUPER``roles``roles`### 閰嶇疆绀轰緥鈥�
+鑿滃崟閰嶇疆鏂囦欢浣嶄簬锛�/src/router/routes/asyncRoutes.ts
+`/src/router/routes/asyncRoutes.ts````
+[
+ {
+ id: 4,
+ path: "/system",
+ name: "System",
+ component: "/index/index",
+ meta: {
+ title: "menus.system.title",
+ icon: "ri:user-3-line",
+ keepAlive: false,
+ },
+ children: [
+ // 浠� R_SUPER 鍜� R_ADMIN 瑙掕壊鍙闂�
+ {
+ id: 41,
+ path: "user",
+ name: "User",
+ component: "/system/user",
+ meta: {
+ title: "menus.system.user",
+ keepAlive: true,
+ roles: ["R_SUPER", "R_ADMIN"],
+ },
+ },
+ // 鏈缃� roles锛屾墍鏈夌敤鎴峰彲璁块棶
+ {
+ id: 42,
+ path: "role",
+ name: "Role",
+ component: "/system/role",
+ meta: {
+ title: "menus.system.role",
+ keepAlive: true,
+ },
+ },
+ ],
+ },
+];
+```
+### 娉ㄦ剰浜嬮」鈥�
+- 纭繚鎺ュ彛杩斿洖鐨勮鑹叉爣璇嗕笌璺敱琛ㄧ殑roles瀛楁鍖归厤锛屽惁鍒欑敤鎴锋棤娉曡闂彈闄愰〉闈€��
+## 鍚庣鎺у埗妯″紡鈥�
+### 鍘熺悊鈥�
+鍚庣鐢熸垚鑿滃崟鍒楄〃銆傜敤鎴风櫥褰曞悗锛屾帴鍙h繑鍥炶彍鍗曟暟鎹紝鍓嶇鏍¢獙鍚庡姩鎬佹敞鍐岃矾鐢憋紝瀹炵幇鏉冮檺鎺у埗銆�
+### 鏁版嵁缁撴瀯鈥�
+鑿滃崟鏁版嵁缁撴瀯瀹氫箟浣嶄簬锛�/src/router/routes/asyncRoutes.ts
+`/src/router/routes/asyncRoutes.ts````
+[
+ {
+ id: 4,
+ path: "/system",
+ name: "System",
+ component: "/index/index",
+ meta: {
+ title: "menus.system.title",
+ icon: "ri:user-3-line",
+ keepAlive: false,
+ },
+ children: [
+ {
+ id: 41,
+ path: "user",
+ name: "User",
+ component: "/system/user",
+ meta: {
+ title: "menus.system.user",
+ keepAlive: true,
+ },
+ },
+ {
+ id: 42,
+ path: "role",
+ name: "Role",
+ component: "/system/role",
+ meta: {
+ title: "menus.system.role",
+ keepAlive: true,
+ },
+ },
+ ],
+ },
+];
+```
+### 娉ㄦ剰浜嬮」鈥�
+- 鍚庣杩斿洖鐨勮彍鍗曟暟鎹粨鏋勫繀椤讳笌鍓嶇瀹氫箟涓�鑷达紝鍚﹀垯鍙兘瀵艰嚧璺敱娉ㄥ唽澶辫触銆�
+## 鍓嶅悗绔帶鍒舵ā寮忓姣斺��
+- 鍓嶇鎺у埗妯″紡锛氶�傜敤浜庤鑹插浐瀹氱殑绯荤粺銆傚悗绔鑹插彉鏇撮渶鍚屾鏇存柊鍓嶇璺敱閰嶇疆銆傚疄鐜扮畝鍗曪紝閫傚悎灏忓瀷椤圭洰銆�
+- 鍚庣鎺у埗妯″紡锛氶�傜敤浜庢潈闄愬鏉傜殑绯荤粺銆傚悗绔繑鍥炲畬鏁磋彍鍗曞垪琛紝鍓嶇鍔ㄦ�佹敞鍐岃矾鐢便�傛洿鐏垫椿锛屼絾闇�纭繚鍓嶅悗绔暟鎹粨鏋勪竴鑷淬��
+## 鎸夐挳鏉冮檺鎺у埗鈥�
+鎸夐挳鏉冮檺鎺у埗鏀寔绮剧粏鍖栫鐞嗭紝閫氳繃鐢ㄦ埛瑙掕壊鎴栨帴鍙h繑鍥炵殑鏉冮檺鐮佸姩鎬佹帶鍒舵寜閽樉绀恒��
+### 鏉冮檺鐮佲��
+鏉冮檺鐮侀�傜敤浜庡墠绔拰鍚庣鎺у埗妯″紡锛�
+- 鍓嶇鎺у埗妯″紡锛氱櫥褰曟帴鍙i渶杩斿洖鏉冮檺鐮佸垪琛ㄣ��
+- 鍚庣鎺у埗妯″紡锛氳彍鍗曞垪琛ㄩ渶鍖呭惈authList瀛楁锛屽畾涔夋寜閽潈闄愩��
+#### 閰嶇疆绀轰緥锛堝悗绔帶鍒舵ā寮忥級鈥�
+```
+[
+ {
+ id: 44,
+ path: "menu",
+ name: "Menus",
+ component: "/system/menu",
+ meta: {
+ title: "menus.system.menu",
+ keepAlive: true,
+ authList: [
+ { id: 441, title: "鏂板", authMark: "add" },
+ { id: 442, title: "缂栬緫", authMark: "edit" },
+ ],
+ },
+ },
+];
+```
+#### 浣跨敤鏂瑰紡鈥�
+閫氳繃绯荤粺鎻愪緵鐨刪asAuth鏂规硶鎺у埗鎸夐挳鏄剧ず锛�
+`hasAuth````
+import { useAuth } from "@/composables/useAuth";
+const { hasAuth } = useAuth();
+```
+```
+<ElButton v-if="hasAuth('add')">娣诲姞</ElButton>
+```
+### 鑷畾涔夋寚浠わ紙v-auth锛夆��
+鍦ㄥ悗绔帶鍒舵ā寮忎笅锛屽彲閫氳繃鑷畾涔夋寚浠-auth鍩轰簬authList鐨刟uthMark鎺у埗鎸夐挳鏄剧ず銆�
+`v-auth``authList``authMark`#### 閰嶇疆绀轰緥鈥�
+```
+[
+ {
+ id: 44,
+ path: "menu",
+ name: "Menus",
+ component: "/system/menu",
+ meta: {
+ title: "menus.system.menu",
+ keepAlive: true,
+ authList: [
+ { id: 441, title: "鏂板", authMark: "add" },
+ { id: 442, title: "缂栬緫", authMark: "edit" },
+ { id: 443, title: "鍒犻櫎", authMark: "delete" },
+ ],
+ },
+ },
+];
+```
+#### 浣跨敤鏂瑰紡鈥�
+```
+<ElButton v-auth="'add'">娣诲姞</ElButton>
+```
+### 鑷畾涔夋寚浠わ紙v-roles锛夆��
+鍙熀浜庣敤鎴蜂俊鎭帴鍙d腑杩斿洖鐨剅oles杩涜鏉冮檺鎺у埗銆�
+`roles`#### 鐢ㄦ埛鎺ュ彛鈥�
+```
+{
+ "userId": "1",
+ "userName": "Super",
+ "roles": [
+ "R_SUPER"
+ ],
+ "buttons": [
+ "B_CODE1",
+ "B_CODE2",
+ "B_CODE3"
+ ]
+}
+```
+#### 浣跨敤绀轰緥鈥�
+```
+ <el-button v-roles="['R_SUPER', 'R_ADMIN']">鎸夐挳</el-button>
+ <el-button v-roles="'R_ADMIN'">鎸夐挳</el-button>
+```
+## 娉ㄦ剰浜嬮」鈥�
+- 纭繚鐧诲綍鎺ュ彛杩斿洖鐨勮鑹叉垨鏉冮檺鐮佷笌璺敱琛ㄩ厤缃竴鑷淬��
+- 鍚庣鎺у埗妯″紡涓嬶紝鑿滃崟鏁版嵁闇�涓ユ牸閬靛惊鍓嶇瀹氫箟鐨勭粨鏋勩��
+- 娴嬭瘯鏉冮檺鎺у埗鏃讹紝楠岃瘉涓嶅悓瑙掕壊鐢ㄦ埛鐨勯〉闈㈠拰鎸夐挳鏄剧ず鏄惁绗﹀悎棰勬湡銆�
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/core-concepts/project-structure.md b/rsf-design/skill/art-design-pro/docs/core-concepts/project-structure.md
new file mode 100644
index 0000000..ba10208
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/core-concepts/project-structure.md
@@ -0,0 +1,75 @@
+# 椤圭洰缁撴瀯 | Art Design Pro
+
+鏉ユ簮锛歨ttps://www.artd.pro/docs/zh/guide/essentials/project-introduce.html
+
+```
+鈹溾攢鈹� src
+鈹� 鈹溾攢鈹� api # API 鎺ュ彛鐩稿叧浠g爜
+鈹� 鈹� 鈹溾攢鈹� articleApi.ts # 鏂囩珷鐩稿叧鐨� API 鎺ュ彛瀹氫箟
+鈹� 鈹� 鈹溾攢鈹� menuApi.ts # 鑿滃崟鐩稿叧鐨� API 鎺ュ彛瀹氫箟
+鈹� 鈹� 鈹溾攢鈹� modules # API 妯″潡鍖栫洰褰�
+鈹� 鈹� 鈹斺攢鈹� usersApi.ts # 鐢ㄦ埛鐩稿叧鐨� API 鎺ュ彛瀹氫箟
+鈹� 鈹溾攢鈹� App.vue # Vue 鏍圭粍浠�
+鈹� 鈹溾攢鈹� assets # 闈欐�佽祫婧愮洰褰�
+鈹� 鈹� 鈹溾攢鈹� fonts # 瀛椾綋鏂囦欢
+鈹� 鈹� 鈹溾攢鈹� icons # 鍥炬爣鏂囦欢
+鈹� 鈹� 鈹溾攢鈹� img # 鍥剧墖璧勬簮
+鈹� 鈹� 鈹溾攢鈹� styles # 鍏ㄥ眬 CSS/SCSS 鏍峰紡鏂囦欢
+鈹� 鈹� 鈹斺攢鈹� svg # SVG 鍥炬爣璧勬簮
+鈹� 鈹溾攢鈹� components # 缁勪欢鐩綍
+鈹� 鈹� 鈹溾攢鈹� core # 绯荤粺缁勪欢锛圓rt Design Pro 缁勪欢搴擄級
+鈹� 鈹� 鈹斺攢鈹� custom # 鑷畾涔夌粍浠讹紙寮�鍙戣�呯粍浠跺簱锛�
+鈹� 鈹溾攢鈹� composables # Vue 3 Composable 鍑芥暟
+鈹� 鈹� 鈹溾攢鈹� useAuth.ts # 璁よ瘉鐩稿叧閫昏緫
+鈹� 鈹� 鈹溾攢鈹� useChart.ts # 鍥捐〃鐩稿叧閫昏緫
+鈹� 鈹� 鈹溾攢鈹� useCommon.ts # 閫氱敤鐨� Composable 鍑芥暟
+鈹� 鈹� 鈹斺攢鈹� useTheme.ts # 涓婚鍒囨崲閫昏緫
+鈹� 鈹溾攢鈹� config # 椤圭洰閰嶇疆鐩綍
+鈹� 鈹� 鈹溾攢鈹� assets # 闈欐�佽祫婧愰厤缃�
+鈹� 鈹� 鈹斺攢鈹� index.ts # 鍏ㄥ眬閰嶇疆鏂囦欢
+鈹� 鈹溾攢鈹� directives # Vue 鑷畾涔夋寚浠�
+鈹� 鈹� 鈹溾攢鈹� highlight.ts # 楂樹寒鎸囦护
+鈹� 鈹� 鈹溾攢鈹� permission.ts # 鏉冮檺鎸囦护
+鈹� 鈹� 鈹斺攢鈹� ripple.ts # 娉㈢汗鏁堟灉鎸囦护
+鈹� 鈹溾攢鈹� enums # 鏋氫妇瀹氫箟
+鈹� 鈹溾攢鈹� locales # 鍥介檯鍖栵紙i18n锛夎祫婧�
+鈹� 鈹溾攢鈹� main.ts # 椤圭洰涓诲叆鍙f枃浠�
+鈹� 鈹溾攢鈹� mock # Mock 鏁版嵁鐩綍
+鈹� 鈹溾攢鈹� router # Vue Router 璺敱鐩稿叧浠g爜
+鈹� 鈹� 鈹溾攢鈹� guards # 璺敱瀹堝崼
+鈹� 鈹� 鈹溾攢鈹� routes # 璺敱瀹氫箟
+鈹� 鈹� 鈹斺攢鈹� utils # 璺敱宸ュ叿鍑芥暟
+鈹� 鈹溾攢鈹� store # Pinia 鐘舵�佺鐞�
+鈹� 鈹� 鈹斺攢鈹� modules # 鍒嗘ā鍧楃殑鐘舵�佺鐞�
+鈹� 鈹溾攢鈹� types # 鍙�夌殑缁撴瀯璇存槑鐩綍锛堝綋鍓� JS 鐗堥粯璁や笉淇濈暀锛�
+鈹� 鈹溾攢鈹� utils # 宸ュ叿鍑芥暟鐩綍
+鈹� 鈹� 鈹溾攢鈹� browser # 娴忚鍣ㄧ浉鍏冲伐鍏�
+鈹� 鈹� 鈹溾攢鈹� constants # 甯搁噺瀹氫箟
+鈹� 鈹� 鈹溾攢鈹� http # HTTP 璇锋眰宸ュ叿
+鈹� 鈹� 鈹溾攢鈹� navigation # 瀵艰埅鐩稿叧宸ュ叿
+鈹� 鈹� 鈹斺攢鈹� storage # 瀛樺偍鐩稿叧宸ュ叿
+鈹� 鈹斺攢鈹� views # 椤甸潰缁勪欢鐩綍
+鈹斺攢鈹� vite.config.js # Vite 閰嶇疆鏂囦欢
+```
+
+## 鏍稿績鐩綍璇存槑
+
+### components/core/
+杩欐槸 Art Design Pro 鐨勬牳蹇冪粍浠跺簱锛屽寘鍚墍鏈夊彲澶嶇敤鐨勪笟鍔$粍浠讹細
+- **琛ㄦ牸**锛欰rtTable, ArtTableHeader
+- **琛ㄥ崟**锛欰rtForm, ArtSearchBar, ArtButtonTable
+- **鍥捐〃**锛欰rtStatsCard, ArtLineChart, ArtBarChart
+- **甯冨眬**锛欰rtPageContent, ArtBreadcrumb, ArtHeaderBar
+
+### views/
+椤甸潰缁勪欢鐩綍锛屾墍鏈変笟鍔¢〉闈㈤兘搴旇鏀惧湪杩欓噷銆�
+
+### router/routes/
+- **staticRoutes.ts**: 闈欐�佽矾鐢憋紙鐧诲綍銆�404绛夛級
+- **asyncRoutes.ts**: 鍔ㄦ�佽矾鐢憋紙涓氬姟椤甸潰锛�
+
+### config/
+椤圭洰閰嶇疆锛屽寘鎷郴缁熷悕绉般�佷富棰橀厤缃瓑銆�
+
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/core-concepts/routing.md b/rsf-design/skill/art-design-pro/docs/core-concepts/routing.md
new file mode 100644
index 0000000..a2c4294
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/core-concepts/routing.md
@@ -0,0 +1,149 @@
+# 璺敱鍜岃彍鍗� | Art Design Pro
+
+鏉ユ簮锛歨ttps://www.artd.pro/docs/zh/guide/essentials/route.html
+
+## 璺敱绫诲瀷
+
+椤圭洰涓殑璺敱鍒嗕负涓ょ被锛�**闈欐�佽矾鐢�** 鍜� **鍔ㄦ�佽矾鐢�**銆�
+
+- **闈欐�佽矾鐢�**锛氭棤闇�鏉冮檺鍗冲彲璁块棶鐨勫熀纭�椤甸潰璺敱锛堢櫥褰曢〉銆�404绛夛級
+- **鍔ㄦ�佽矾鐢�**锛氶渶瑕佹潈闄愭帶鍒剁殑涓氬姟椤甸潰璺敱锛堢敤鎴风鐞嗐�佽彍鍗曠鐞嗙瓑锛�
+
+## 闈欐�佽矾鐢�
+
+閰嶇疆浣嶇疆锛歚src/router/routes/staticRoutes.ts`
+
+```js
+export const staticRoutes: AppRouteRecordRaw[] = [
+ {
+ path: RoutesAlias.Login,
+ name: "Login",
+ component: () => import("@views/auth/login/index.vue"),
+ meta: { title: "menus.login.title", isHideTab: true, setTheme: true },
+ },
+ {
+ path: "/exception",
+ component: Home,
+ name: "Exception",
+ children: [
+ {
+ path: RoutesAlias.Exception404,
+ name: "Exception404",
+ component: () => import("@views/exception/404/index.vue"),
+ meta: { title: "404" },
+ }
+ ]
+ }
+];
+```
+
+## 鍔ㄦ�佽矾鐢�
+
+閰嶇疆浣嶇疆锛歚src/router/routes/asyncRoutes.ts`
+
+```js
+export const asyncRoutes: AppRouteRecord[] = [
+ {
+ name: "Dashboard",
+ path: "/dashboard/",
+ component: RoutesAlias.Layout,
+ meta: {
+ title: "menus.dashboard.title",
+ icon: "",
+ },
+ children: [
+ {
+ path: "console",
+ name: "Console",
+ component: RoutesAlias.Dashboard,
+ meta: {
+ title: "menus.dashboard.console",
+ keepAlive: false,
+ fixedTab: true,
+ },
+ }
+ ],
+ }
+];
+```
+
+## 璺敱鍏冧俊鎭紙meta锛�
+
+```js
+meta: {
+ title: string; // 璺敱鏍囬
+ icon?: string; // 璺敱鍥炬爣
+ showBadge?: boolean; // 鏄惁鏄剧ず寰界珷
+ showTextBadge?: string; // 鏂囨湰寰界珷
+ isHide?: boolean; // 鏄惁鍦ㄨ彍鍗曚腑闅愯棌
+ isHideTab?: boolean; // 鏄惁鍦ㄦ爣绛鹃〉涓殣钘�
+ link?: string; // 澶栭儴閾炬帴
+ isIframe?: boolean; // 鏄惁涓� iframe
+ keepAlive?: boolean; // 鏄惁缂撳瓨
+ roles?: string[]; // 瑙掕壊鏉冮檺
+ fixedTab?: boolean; // 鏄惁鍥哄畾鏍囩椤�
+ isFullPage?: boolean; // 鏄惁涓哄叏灞忛〉闈�
+ activePath?: string; // 婵�娲荤殑鑿滃崟璺緞
+}
+```
+
+## 鏂板缓椤甸潰姝ラ
+
+### 1. 鍒涘缓椤甸潰鏂囦欢
+
+鍦� `/src/views/` 鐩綍涓嬪垱寤洪〉闈細
+
+```vue
+<template>
+ <div class="page-content">
+ <h1>test page</h1>
+ </div>
+</template>
+```
+
+**娉ㄦ剰**锛氫娇鐢� `class="page-content"` 璁╅〉闈㈤珮搴﹀崰婊″睆骞曞墿浣欓珮搴︺��
+
+### 2. 娉ㄥ唽璺敱
+
+鍦� `src/router/routes/asyncRoutes.ts` 涓坊鍔犺矾鐢遍厤缃細
+
+```js
+// 涓�绾ц矾鐢�
+{
+ path: "/test/index",
+ name: "Test",
+ component: "/test/index",
+ meta: {
+ title: "娴嬭瘯椤�",
+ keepAlive: true,
+ },
+}
+
+// 澶氱骇璺敱
+{
+ name: "Form",
+ path: "/form/",
+ component: RoutesAlias.Layout,
+ meta: {
+ title: "琛ㄥ崟",
+ icon: "",
+ },
+ children: [
+ {
+ path: "basic",
+ name: "Basic",
+ component: "/form/basic",
+ meta: {
+ title: "鍩虹琛ㄥ崟",
+ keepAlive: true,
+ },
+ }
+ ],
+}
+```
+
+### 3. 璁块棶椤甸潰
+
+璁块棶 `http://localhost:3006/form/basic` 鍗冲彲鏌ョ湅鏂板缓鐨勯〉闈€��
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/examples/forms/search-bar.md b/rsf-design/skill/art-design-pro/docs/examples/forms/search-bar.md
new file mode 100644
index 0000000..c4c034f
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/examples/forms/search-bar.md
@@ -0,0 +1,302 @@
+# 鎼滅储鏍忎娇鐢ㄧず渚�
+
+## 姒傝堪
+
+`ArtSearchBar` 鏄� Art Design Pro 鐨勬悳绱㈡爮缁勪欢锛屾敮鎸佸姩鎬佽〃鍗曢」銆佹姌鍙犲睍寮�銆佽〃鍗曟牎楠岀瓑鍔熻兘銆�
+
+## 鍩虹鐢ㄦ硶
+
+```vue
+<template>
+ <div class="search-page">
+ <ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+ </div>
+</template>
+
+<script setup>
+ import type { SearchFormItem } from '@/components/core/forms/art-search-bar/index.vue'
+
+ const formData = ref({
+ username: '',
+ email: '',
+ status: ''
+ })
+
+ // 琛ㄥ崟椤归厤缃�
+ const formItems: SearchFormItem[] = [
+ {
+ prop: 'username',
+ label: '鐢ㄦ埛鍚�',
+ component: 'ElInput',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ敤鎴峰悕',
+ clearable: true
+ }
+ },
+ {
+ prop: 'email',
+ label: '閭',
+ component: 'ElInput',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ偖绠�',
+ clearable: true
+ }
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ component: 'ElSelect',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ clearable: true,
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+ }
+ ]
+
+ const handleSearch = (params) => {
+ console.log('鎼滅储鍙傛暟:', params)
+ // 鎵ц鎼滅储閫昏緫
+ }
+
+ const handleReset = () => {
+ console.log('閲嶇疆琛ㄥ崟')
+ }
+</script>
+```
+
+## 楂樼骇閰嶇疆
+
+### 榛樿灞曞紑
+
+```vue
+<ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ :defaultExpanded="true"
+ @search="handleSearch"
+/>
+```
+
+### 鑷畾涔夋爣绛惧搴�
+
+```vue
+<ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ :labelWidth="120"
+ @search="handleSearch"
+/>
+```
+
+### 璁剧疆涓�琛屾樉绀虹殑缁勪欢鏁�
+
+```vue
+<ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ :span="8"
+ @search="handleSearch"
+/>
+```
+
+### 琛ㄥ崟鏍¢獙
+
+```vue
+<ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+ :rules="rules"
+ @search="handleSearch"
+/>
+
+<script setup>
+ const rules = {
+ username: [
+ { required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }
+ ],
+ email: [
+ { type: 'email', message: '璇疯緭鍏ユ纭殑閭鏍煎紡', trigger: 'blur' }
+ ]
+ }
+</script>
+```
+
+## 琛ㄥ崟椤归厤缃被鍨�
+
+```js
+interface SearchFormItem {
+ prop: string // 瀛楁鍚�
+ label: string // 鏍囩鏂囨湰
+ component: string // 缁勪欢鍚嶇О锛堝 'ElInput'锛�
+ componentProps?: Object // 缁勪欢 props
+ span?: number // 鏍呮牸鍗犱綅鏍兼暟
+ itemProps?: Object // FormItem props
+}
+```
+
+## 鏀寔鐨勭粍浠�
+
+- `ElInput` - 杈撳叆妗�
+- `ElSelect` - 涓嬫媺閫夋嫨
+- `ElDatePicker` - 鏃ユ湡閫夋嫨
+- `ElTimePicker` - 鏃堕棿閫夋嫨
+- `ElCascader` - 绾ц仈閫夋嫨
+- `ElSwitch` - 寮�鍏�
+
+## 鍔ㄦ�佹搷浣�
+
+### 娣诲姞琛ㄥ崟椤�
+
+```js
+const addFormItem = () => {
+ formItems.value.push({
+ prop: 'newField',
+ label: '鏂板瓧娈�',
+ component: 'ElInput',
+ componentProps: {
+ placeholder: '璇疯緭鍏�'
+ }
+ })
+}
+```
+
+### 淇敼琛ㄥ崟椤�
+
+```js
+const updateFormItem = () => {
+ const index = formItems.value.findIndex(item => item.prop === 'username')
+ if (index !== -1) {
+ formItems.value[index].label = '鐢ㄦ埛鍚嶏紙宸蹭慨鏀癸級'
+ }
+}
+```
+
+### 鍒犻櫎琛ㄥ崟椤�
+
+```js
+const deleteFormItem = () => {
+ const index = formItems.value.findIndex(item => item.prop === 'email')
+ if (index !== -1) {
+ formItems.value.splice(index, 1)
+ }
+}
+```
+
+## 鎻掓Ы浣跨敤
+
+```vue
+<ArtSearchBar
+ v-model="formData"
+ :items="formItems"
+>
+ <template #slots>
+ <ElInput v-model="formData.custom" placeholder="鑷畾涔夌粍浠�" />
+ </template>
+</ArtSearchBar>
+```
+
+## 瀹為檯搴旂敤绀轰緥
+
+### 缁撳悎 useTable 浣跨敤
+
+```vue
+<template>
+ <div class="user-page art-full-height">
+ <!-- 鎼滅储鏍� -->
+ <ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ @search="handleSearch"
+ @reset="handleReset"
+ />
+
+ <!-- 琛ㄦ牸 -->
+ <ElCard class="art-table-card" shadow="never">
+ <ArtTable
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { useTable } from '@/hooks/core/useTable'
+
+ const searchForm = ref({
+ username: '',
+ status: ''
+ })
+
+ const searchItems = [
+ {
+ prop: 'username',
+ label: '鐢ㄦ埛鍚�',
+ component: 'ElInput'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ component: 'ElSelect',
+ componentProps: {
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ }
+ }
+ ]
+
+ const {
+ data,
+ columns,
+ pagination,
+ getData,
+ searchParams
+ } = useTable({
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20
+ },
+ columnsFactory: () => [...]
+ }
+ })
+
+ const handleSearch = (params) => {
+ Object.assign(searchParams, params)
+ getData()
+ }
+
+ const handleReset = () => {
+ searchForm.value = {
+ username: '',
+ status: ''
+ }
+ }
+</script>
+```
+
+## 鐩稿叧缁勪欢
+
+- [缁勪欢閫熸煡琛╙(../../cheatsheet.md)
+- [ArtSearchBar 缁勪欢鏂囨。](../../17-art-search-bar.md)
+- [琛ㄥ崟闆嗗悎绀轰緥](./form-collection.md)
+
+## 瀹屾暣绀轰緥浣嶇疆
+
+`src/views/examples/forms/search-bar.vue`
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/examples/index.md b/rsf-design/skill/art-design-pro/docs/examples/index.md
new file mode 100644
index 0000000..55b6026
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/examples/index.md
@@ -0,0 +1,197 @@
+# Art Design Pro 浣跨敤绀轰緥
+
+鏈洰褰曞寘鍚粠 HCG 椤圭洰涓彁鍙栫殑鐪熷疄浣跨敤妗堜緥锛屽府鍔╀綘蹇�熶笂鎵� Art Design Pro 缁勪欢搴撱��
+
+## 馃摎 绀轰緥鐩綍
+
+### 琛ㄦ牸绀轰緥 ([tables/](./tables/))
+
+| 绀轰緥 | 璇存槑 | 闅惧害 |
+|------|------|------|
+| [鍩虹琛ㄦ牸](./tables/basic-table.md) | 鏈�绠�鍗曠殑琛ㄦ牸浣跨敤绀轰緥 | 猸� |
+| [楂樼骇琛ㄦ牸](./tables/advanced-table.md) | 鍖呭惈鑷畾涔夊垪銆佹帓搴忋�佺瓫閫夌瓑鍔熻兘 | 猸愨瓙猸� |
+| [鏍戝舰琛ㄦ牸](./tables/tree-table.md) | 鏍戝舰缁撴瀯鏁版嵁灞曠ず | 猸愨瓙 |
+
+### 琛ㄥ崟绀轰緥 ([forms/](./forms/))
+
+| 绀轰緥 | 璇存槑 | 闅惧害 |
+|------|------|------|
+| [鎼滅储鏍廬(./forms/search-bar.md) | ArtSearchBar 缁勪欢瀹屾暣浣跨敤鎸囧崡 | 猸愨瓙 |
+| [琛ㄥ崟闆嗗悎](./forms/form-collection.md) | 鍚勭琛ㄥ崟缁勪欢鐨勭粍鍚堜娇鐢� | 猸愨瓙猸� |
+
+### 鏉冮檺鎺у埗绀轰緥 ([permission/](./permission/))
+
+| 绀轰緥 | 璇存槑 | 闅惧害 |
+|------|------|------|
+| [鎸夐挳鏉冮檺](./permission/button-permission.md) | v-roles 鍜� v-auth 鎸囦护浣跨敤 | 猸愨瓙 |
+| [瑙掕壊鎺у埗](./permission/role-control.md) | 鍩轰簬瑙掕壊鐨勬潈闄愮鐞� | 猸愨瓙猸� |
+| [椤甸潰鍙鎬(./permission/page-visibility.md) | 椤甸潰绾у埆鐨勬潈闄愭帶鍒� | 猸愨瓙 |
+
+### 椤甸潰妯℃澘 ([templates/](./templates/))
+
+| 妯℃澘 | 璇存槑 | 闅惧害 |
+|------|------|------|
+| [CRUD 椤甸潰](./templates/crud-page.md) | 瀹屾暣鐨勫鍒犳敼鏌ラ〉闈㈢ず渚� | 猸愨瓙猸� |
+| [浠〃鏉縘(./templates/dashboard.md) | 鏁版嵁缁熻鍜屽浘琛ㄥ睍绀洪〉闈� | 猸愨瓙猸� |
+| [鍒楄〃椤礭(./templates/list-page.md) | 绠�鍗曠殑鏁版嵁鍒楄〃椤甸潰 | 猸愨瓙 |
+
+## 馃殌 蹇�熷紑濮�
+
+### 1. 鏌ョ湅鍩虹绀轰緥
+
+濡傛灉浣犳槸绗竴娆′娇鐢� Art Design Pro锛屽缓璁寜浠ヤ笅椤哄簭闃呰锛�
+
+1. [鍩虹琛ㄦ牸](./tables/basic-table.md) - 浜嗚В鏈�绠�鍗曠殑琛ㄦ牸浣跨敤
+2. [鎼滅储鏍廬(./forms/search-bar.md) - 瀛︿範鎼滅储鍔熻兘瀹炵幇
+3. [CRUD 椤甸潰](./templates/crud-page.md) - 鏌ョ湅瀹屾暣鍔熻兘缁勫悎
+
+### 2. 浣跨敤浠g爜鐢熸垚鍣�
+
+浣跨敤浠g爜鐢熸垚鍣ㄥ揩閫熷垱寤洪〉闈㈡ā鏉匡細
+
+```bash
+# 鐢熸垚 CRUD 椤甸潰
+python skill/art-design-pro/scripts/generate.py \
+ crud \
+ --name "User" \
+ --path "system/user" \
+ --fields "username,email,phone,status"
+```
+
+璇﹁锛歔浠g爜鐢熸垚鍣ㄤ娇鐢ㄦ寚鍗梋(../generator-guide.md)
+
+### 3. 鍙傝�冨疄闄呴」鐩唬鐮�
+
+鎵�鏈夌ず渚嬮兘鎻愬彇鑷疄闄呴」鐩紝浣犲彲浠ュ湪浠ヤ笅浣嶇疆鎵惧埌瀹屾暣浠g爜锛�
+
+- 琛ㄦ牸绀轰緥锛歚src/views/examples/tables/`
+- 琛ㄥ崟绀轰緥锛歚src/views/examples/forms/`
+- 鏉冮檺绀轰緥锛歚src/views/examples/permission/`
+- 绯荤粺椤甸潰锛歚src/views/system/`
+
+## 馃摉 甯歌妯″紡
+
+### 妯″紡 1锛氭悳绱� + 琛ㄦ牸
+
+```vue
+<template>
+ <div class="page art-full-height">
+ <!-- 鎼滅储鏍� -->
+ <ArtSearchBar v-model="searchForm" @search="handleSearch" />
+
+ <!-- 琛ㄦ牸 -->
+ <ElCard class="art-table-card">
+ <ArtTable :data="data" :columns="columns" />
+ </ElCard>
+ </div>
+</template>
+```
+
+### 妯″紡 2锛氭悳绱� + 琛ㄦ牸 + 寮圭獥
+
+```vue
+<template>
+ <div class="page art-full-height">
+ <ArtSearchBar v-model="searchForm" @search="handleSearch" />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader>
+ <template #left>
+ <ElButton @click="showDialog">鏂板</ElButton>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable :data="data" :columns="columns" />
+
+ <ElDialog v-model="dialogVisible">
+ <ElForm>...</ElForm>
+ </ElDialog>
+ </ElCard>
+ </div>
+</template>
+```
+
+### 妯″紡 3锛氭潈闄愭帶鍒�
+
+```vue
+<template>
+ <ElButton v-auth="'user:create'">鏂板</ElButton>
+ <ElButton v-roles="['admin']">绠$悊</ElButton>
+</template>
+```
+
+## 馃幆 瀛︿範璺緞
+
+### 鍒濈骇锛�1-2 澶╋級
+
+- [ ] 闃呰鍩虹琛ㄦ牸绀轰緥
+- [ ] 闃呰鎼滅储鏍忕ず渚�
+- [ ] 鍒涘缓涓�涓畝鍗曠殑鍒楄〃椤甸潰
+
+### 涓骇锛�3-5 澶╋級
+
+- [ ] 瀛︿範瀹屾暣鐨� CRUD 椤甸潰
+- [ ] 鎺屾彙鑷畾涔夊垪娓叉煋
+- [ ] 瀹炵幇鏉冮檺鎺у埗
+
+### 楂樼骇锛�5-7 澶╋級
+
+- [ ] 瀛︿範楂樼骇琛ㄦ牸鍔熻兘
+- [ ] 鎺屾彙琛ㄥ崟鏍¢獙
+- [ ] 瀹炵幇澶嶆潅涓氬姟閫昏緫
+
+## 馃挕 鏈�浣冲疄璺�
+
+### 1. 浣跨敤 useTable Hook
+
+`useTable` Hook 鎻愪緵浜嗚〃鏍肩殑瀹屾暣鍔熻兘锛屾帹鑽愭墍鏈夎〃鏍奸〉闈娇鐢細
+
+```js
+const { data, columns, loading, pagination } = useTable({...})
+```
+
+### 2. 浣跨敤 ArtSearchBar
+
+`ArtSearchBar` 缁勪欢灏佽浜嗘悳绱㈡爮鐨勯�氱敤閫昏緫锛�
+
+```vue
+<ArtSearchBar v-model="formData" :items="formItems" />
+```
+
+### 3. 鏉冮檺鎺у埗
+
+浣跨敤 `v-auth` 鍜� `v-roles` 鎸囦护鎺у埗鎸夐挳鏉冮檺锛�
+
+```vue
+<ElButton v-auth="'user:create'">鏂板</ElButton>
+```
+
+### 4. 鏍峰紡绫�
+
+浣跨敤 Art Design Pro 鎻愪緵鐨勬牱寮忕被锛�
+
+- `art-full-height`: 鑷姩璁$畻鍓╀綑楂樺害
+- `art-table-card`: 琛ㄦ牸鍗$墖鏍峰紡
+- `flex-c`: Flex 鍨傜洿灞呬腑
+
+## 馃摎 鐩稿叧鏂囨。
+
+- [缁勪欢鏂囨。](../components/)
+- [Hook 鏂囨。](../hooks/)
+- [瀹樻柟鏂囨。](https://www.artd.pro/docs/zh/guide/)
+- [浠g爜鐢熸垚鍣╙(../generator-guide.md)
+
+## 馃 璐$尞绀轰緥
+
+濡傛灉浣犳湁濂界殑绀轰緥鎯宠鍒嗕韩锛岃锛�
+
+1. 灏嗕唬鐮佹彁浜ゅ埌 `src/views/examples/`
+2. 鍦ㄦ湰鐩綍娣诲姞瀵瑰簲鐨勬枃妗�
+3. 鏇存柊绱㈠紩鏂囦欢
+
+---
+
+**鏈�鍚庢洿鏂�**锛�2026-03-03
+**缁存姢鑰�**锛欰rt Design Pro Skill Team
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/examples/permission/button-permission.md b/rsf-design/skill/art-design-pro/docs/examples/permission/button-permission.md
new file mode 100644
index 0000000..a58bd02
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/examples/permission/button-permission.md
@@ -0,0 +1,306 @@
+# 鏉冮檺鎺у埗浣跨敤绀轰緥
+
+## 姒傝堪
+
+Art Design Pro 鎻愪緵浜嗗畬鍠勭殑鏉冮檺鎺у埗绯荤粺锛屾敮鎸佸熀浜庤鑹诧紙`v-roles`锛夊拰鍩轰簬鏉冮檺鐮侊紙`v-auth`锛夌殑鎸夐挳绾у埆鏉冮檺鎺у埗銆�
+
+## 鏉冮檺鎸囦护
+
+### v-roles - 鍩轰簬瑙掕壊鐨勬潈闄愭帶鍒�
+
+鏍规嵁鐢ㄦ埛瑙掕壊鎺у埗鍏冪礌鏄剧ず锛�
+
+```vue
+<template>
+ <div>
+ <!-- 鍙湁 admin 瑙掕壊鍙 -->
+ <ElButton v-roles="['admin']">绠$悊鍛樻搷浣�</ElButton>
+
+ <!-- admin 鎴� editor 瑙掕壊鍙 -->
+ <ElButton v-roles="['admin', 'editor']">缂栬緫鎿嶄綔</ElButton>
+
+ <!-- 澶氫釜瑙掕壊閮藉叿澶囨椂鎵嶅彲瑙侊紙AND 閫昏緫锛� -->
+ <ElButton v-roles="['admin', 'editor']" :mode="'and'">
+ 楂樼骇鎿嶄綔
+ </ElButton>
+ </div>
+</template>
+```
+
+### v-auth - 鍩轰簬鏉冮檺鐮佺殑鎺у埗
+
+鏍规嵁鐢ㄦ埛鏉冮檺鐮佹帶鍒跺厓绱犳樉绀猴細
+
+```vue
+<template>
+ <div>
+ <!-- 鍏峰鐗瑰畾鏉冮檺鐮佸彲瑙� -->
+ <ElButton v-auth="'user:create'">鏂板鐢ㄦ埛</ElButton>
+ <ElButton v-auth="'user:edit'">缂栬緫鐢ㄦ埛</ElButton>
+ <ElButton v-auth="'user:delete'">鍒犻櫎鐢ㄦ埛</ElButton>
+
+ <!-- 澶氫釜鏉冮檺鐮侀兘鍏峰鏃舵墠鍙锛圓ND 閫昏緫锛� -->
+ <ElButton v-auth="['user:create', 'user:edit']" :mode="'and'">
+ 瀹屾暣鎿嶄綔
+ </ElButton>
+ </div>
+</template>
+```
+
+## 瀹為檯搴旂敤绀轰緥
+
+### 琛ㄦ牸鎿嶄綔鍒楁潈闄愭帶鍒�
+
+```vue
+<template>
+ <ArtTable
+ :data="data"
+ :columns="columns"
+ >
+ <template #operation="{ row }">
+ <ElSpace>
+ <!-- 鏌ョ湅鏉冮檺 - 鎵�鏈夌敤鎴峰彲瑙� -->
+ <ElButton
+ type="primary"
+ size="small"
+ @click="handleView(row)"
+ >
+ 鏌ョ湅
+ </ElButton>
+
+ <!-- 缂栬緫鏉冮檺 - admin 鍜� editor 鍙 -->
+ <ElButton
+ v-roles="['admin', 'editor']"
+ type="warning"
+ size="small"
+ @click="handleEdit(row)"
+ >
+ 缂栬緫
+ </ElButton>
+
+ <!-- 鍒犻櫎鏉冮檺 - 鍙湁 admin 鍙 -->
+ <ElButton
+ v-roles="['admin']"
+ type="danger"
+ size="small"
+ @click="handleDelete(row)"
+ >
+ 鍒犻櫎
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTable>
+</template>
+```
+
+### 椤甸潰澶撮儴鎸夐挳鏉冮檺鎺у埗
+
+```vue
+<template>
+ <div class="user-page art-full-height">
+ <ElCard class="art-table-card" shadow="never">
+ <!-- 琛ㄦ牸澶撮儴 -->
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading">
+ <template #left>
+ <ElSpace wrap>
+ <!-- 鏂板鏉冮檺 -->
+ <ElButton
+ v-auth="'user:create'"
+ type="primary"
+ @click="handleAdd"
+ >
+ 鏂板鐢ㄦ埛
+ </ElButton>
+
+ <!-- 鎵归噺鍒犻櫎鏉冮檺 -->
+ <ElButton
+ v-auth="'user:delete:batch'"
+ type="danger"
+ :disabled="selectedRows.length === 0"
+ @click="handleBatchDelete"
+ >
+ 鎵归噺鍒犻櫎
+ </ElButton>
+
+ <!-- 瀵煎嚭鏉冮檺 -->
+ <ElButton
+ v-auth="'user:export'"
+ @click="handleExport"
+ >
+ 瀵煎嚭鏁版嵁
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <!-- 琛ㄦ牸 -->
+ <ArtTable
+ :data="data"
+ :columns="columns"
+ @selection-change="handleSelectionChange"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { useTable } from '@/hooks/core/useTable'
+
+ const selectedRows = ref([])
+
+ const { data, columns } = useTable({...})
+
+ const handleSelectionChange = (selection: any[]) => {
+ selectedRows.value = selection
+ }
+
+ // 鍚勭鎿嶄綔鏂规硶...
+</script>
+```
+
+### 鑷畾涔夋潈闄愭鏌�
+
+浣跨敤 `useAuth` Hook 杩涜缂栫▼寮忔潈闄愭鏌ワ細
+
+```vue
+<script setup>
+ import { useAuth } from '@/hooks/core/useAuth'
+
+ const { hasRole, hasAuth, hasRoles, hasAuths } = useAuth()
+
+ // 妫�鏌ュ崟涓鑹�
+ const isAdmin = hasRole('admin')
+
+ // 妫�鏌ュ涓鑹诧紙OR 閫昏緫锛�
+ const canEdit = hasRoles(['admin', 'editor'])
+
+ // 妫�鏌ュ崟涓潈闄愮爜
+ const canCreate = hasAuth('user:create')
+
+ // 妫�鏌ュ涓潈闄愮爜锛圤R 閫昏緫锛�
+ const canManage = hasAuths(['user:create', 'user:edit'])
+
+ // 浣跨敤绀轰緥
+ const handleOperation = () => {
+ if (!canManage) {
+ ElMessage.error('鏉冮檺涓嶈冻')
+ return
+ }
+ // 鎵ц鎿嶄綔
+ }
+</script>
+```
+
+## 鏉冮檺閰嶇疆
+
+### 瑙掕壊瀹氫箟
+
+鍦ㄧ敤鎴锋暟鎹粨鏋勪腑瀹氫箟瑙掕壊锛�
+
+```js
+interface User {
+ id: string
+ username: string
+ roles: string[] // 鐢ㄦ埛瑙掕壊鍒楄〃
+ permissions: string[] // 鐢ㄦ埛鏉冮檺鐮佸垪琛�
+}
+```
+
+### 鏉冮檺鐮侀厤缃�
+
+```js
+// 甯歌鏉冮檺鐮佸懡鍚嶈鑼�
+const permissionCodes = {
+ // 鐢ㄦ埛绠$悊
+ 'user:view': '鏌ョ湅鐢ㄦ埛',
+ 'user:create': '鍒涘缓鐢ㄦ埛',
+ 'user:edit': '缂栬緫鐢ㄦ埛',
+ 'user:delete': '鍒犻櫎鐢ㄦ埛',
+ 'user:export': '瀵煎嚭鐢ㄦ埛',
+
+ // 瑙掕壊绠$悊
+ 'role:view': '鏌ョ湅瑙掕壊',
+ 'role:create': '鍒涘缓瑙掕壊',
+ 'role:edit': '缂栬緫瑙掕壊',
+ 'role:delete': '鍒犻櫎瑙掕壊',
+
+ // 绯荤粺璁剧疆
+ 'system:view': '鏌ョ湅绯荤粺璁剧疆',
+ 'system:edit': '缂栬緫绯荤粺璁剧疆'
+}
+```
+
+## 鎸囦护鍙傛暟璇存槑
+
+### v-roles 鍙傛暟
+
+| 鍙傛暟 | 绫诲瀷 | 榛樿鍊� | 璇存槑 |
+|------|------|--------|------|
+| value | string\|string[] | - | 瑙掕壊浠g爜 |
+| mode | 'or'\|'and' | 'or' | 澶氳鑹插垽鏂�昏緫 |
+
+### v-auth 鍙傛暟
+
+| 鍙傛暟 | 绫诲瀷 | 榛樿鍊� | 璇存槑 |
+|------|------|--------|------|
+| value | string\|string[] | - | 鏉冮檺鐮� |
+| mode | 'or'\|'and' | 'or' | 澶氭潈闄愮爜鍒ゆ柇閫昏緫 |
+
+## 鏈�浣冲疄璺�
+
+### 1. 鏉冮檺绮掑害璁捐
+
+```js
+// 鉁� 鎺ㄨ崘锛氱粏绮掑害鏉冮檺鎺у埗
+<ElButton v-auth="'user:create'">鏂板</ElButton>
+<ElButton v-auth="'user:edit'">缂栬緫</ElButton>
+<ElButton v-auth="'user:delete'">鍒犻櫎</ElButton>
+
+// 鉂� 涓嶆帹鑽愶細绮楃矑搴︽潈闄愭帶鍒�
+<ElButton v-auth="'user:manage'">绠$悊</ElButton>
+```
+
+### 2. 鍓嶅悗绔潈闄愰獙璇�
+
+鍓嶇鏉冮檺鎺у埗鍙槸涓轰簡鐢ㄦ埛浣撻獙锛岀湡姝g殑鏉冮檺楠岃瘉蹇呴』鍦ㄥ悗绔繘琛岋細
+
+```js
+// 鍓嶇锛氭帶鍒� UI 鏄剧ず
+<ElButton v-auth="'user:delete'" @click="deleteUser">鍒犻櫎</ElButton>
+
+// 鍚庣锛氶獙璇佹潈闄�
+async function deleteUser(userId: string) {
+ // 妫�鏌ョ敤鎴锋槸鍚︽湁鍒犻櫎鏉冮檺
+ if (!hasPermission('user:delete')) {
+ throw new Error('鏉冮檺涓嶈冻')
+ }
+
+ // 鎵ц鍒犻櫎鎿嶄綔
+ await api.deleteUser(userId)
+}
+```
+
+### 3. 鏉冮檺缂撳瓨
+
+浣跨敤缂撳瓨鎻愬崌鏉冮檺妫�鏌ユ�ц兘锛�
+
+```js
+const { hasAuth } = useAuth()
+
+// 缂撳瓨鏉冮檺妫�鏌ョ粨鏋�
+const canDelete = computed(() => hasAuth('user:delete'))
+```
+
+## 鐩稿叧鏂囨。
+
+- [鏉冮檺绯荤粺鏂囨。](../../14-permission.md)
+- [useAuth Hook 鏂囨。](https://www.artd.pro/docs/zh/guide/hooks/use-auth.html)
+- [瑙掕壊绠$悊绀轰緥](./role-control.md)
+
+## 瀹屾暣绀轰緥浣嶇疆
+
+- `src/views/examples/permission/button-auth/index.vue`
+- `src/views/examples/permission/page-visibility/index.vue`
+- `src/views/examples/permission/switch-role/index.vue`
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/examples/tables/basic-table.md b/rsf-design/skill/art-design-pro/docs/examples/tables/basic-table.md
new file mode 100644
index 0000000..5b92895
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/examples/tables/basic-table.md
@@ -0,0 +1,180 @@
+# 鍩虹琛ㄦ牸浣跨敤绀轰緥
+
+## 姒傝堪
+
+杩欐槸涓�涓渶绠�鍗曠殑琛ㄦ牸浣跨敤绀轰緥锛屽睍绀轰簡濡備綍浣跨敤 `useTable` Hook 蹇�熷垱寤轰竴涓暟鎹〃鏍笺��
+
+## 瀹屾暣浠g爜
+
+```vue
+<!-- 鍩虹琛ㄦ牸 -->
+<template>
+ <div class="user-page art-full-height">
+ <ElCard class="art-table-card" shadow="never">
+ <!-- 琛ㄦ牸澶撮儴锛堝彲閫夛紝鎻愪緵鍒锋柊銆佸垪璁剧疆绛夊姛鑳斤級 -->
+ <ArtTableHeader v-model:columns="columns" :loading="loading" />
+
+ <!-- 琛ㄦ牸 -->
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ fit
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ </ArtTable>
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchGetUserList } from '@/api/system-manage'
+
+ defineOptions({ name: 'BasicTable' })
+
+ const {
+ data,
+ columns,
+ loading,
+ pagination,
+ handleSizeChange,
+ handleCurrentChange
+ } = useTable({
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20
+ },
+ columnsFactory: () => [
+ {
+ prop: 'id',
+ label: 'ID'
+ },
+ {
+ prop: 'nickName',
+ label: '鏄电О'
+ },
+ {
+ prop: 'userGender',
+ label: '鎬у埆',
+ sortable: true,
+ formatter: (row) => row.userGender || '鏈煡'
+ },
+ {
+ prop: 'userPhone',
+ label: '鎵嬫満鍙�'
+ },
+ {
+ prop: 'userEmail',
+ label: '閭'
+ }
+ ]
+ }
+ })
+</script>
+```
+
+## 鍏抽敭鐐硅鏄�
+
+### 1. useTable Hook
+
+`useTable` 鏄� Art Design Pro 鐨勬牳蹇� Hook锛屾彁渚涗簡琛ㄦ牸鐨勫畬鏁村姛鑳斤細
+
+```js
+const {
+ data, // 琛ㄦ牸鏁版嵁
+ columns, // 鍒楅厤缃�
+ loading, // 鍔犺浇鐘舵��
+ pagination, // 鍒嗛〉閰嶇疆
+ handleSizeChange, // 姣忛〉鏁伴噺鍙樺寲澶勭悊
+ handleCurrentChange // 褰撳墠椤靛彉鍖栧鐞�
+} = useTable({...})
+```
+
+### 2. 鍒楅厤缃�
+
+閫氳繃 `columnsFactory` 瀹氫箟琛ㄦ牸鍒楋細
+
+```js
+columnsFactory: () => [
+ {
+ prop: 'id', // 鏁版嵁瀛楁鍚�
+ label: 'ID' // 鍒楁爣棰�
+ },
+ {
+ prop: 'userGender',
+ label: '鎬у埆',
+ sortable: true, // 鍚敤鎺掑簭
+ formatter: (row) => // 鑷畾涔夋牸寮忓寲
+ row.userGender || '鏈煡'
+ }
+]
+```
+
+### 3. 鏍峰紡绫�
+
+- `art-full-height`: 鑷姩璁$畻椤甸潰鍓╀綑楂樺害
+- `art-table-card`: 绗﹀悎绯荤粺鏍峰紡鐨勮〃鏍煎崱鐗囷紝鑷姩鎾戞弧鍓╀綑楂樺害
+
+## 甯哥敤閰嶇疆
+
+### 娣诲姞搴忓彿鍒�
+
+```js
+columnsFactory: () => [
+ { type: 'index', width: 60, label: '搴忓彿' },
+ // ... 鍏朵粬鍒�
+]
+```
+
+### 娣诲姞澶嶉�夋鍒�
+
+```js
+columnsFactory: () => [
+ { type: 'selection' },
+ // ... 鍏朵粬鍒�
+]
+```
+
+### 鑷畾涔夊垪娓叉煋
+
+浣跨敤 `formatter` 鍑芥暟鑷畾涔夊垪鍐呭锛�
+
+```js
+{
+ prop: 'status',
+ label: '鐘舵��',
+ formatter: (row) => {
+ return h(ElTag, { type: 'success' }, () => '鍚敤')
+ }
+}
+```
+
+## 鍒嗛〉閰嶇疆
+
+`useTable` 鑷姩澶勭悊鍒嗛〉锛岄粯璁ら厤缃細
+
+```js
+// 榛樿鍒嗛〉鍙傛暟
+apiParams: {
+ current: 1, // 褰撳墠椤�
+ size: 20 // 姣忛〉鏁伴噺
+}
+```
+
+## 鐩稿叧缁勪欢
+
+- [缁勪欢閫熸煡琛╙(../../cheatsheet.md)
+- [useTable Hook 鏂囨。](../../16-use-table.md)
+- [楂樼骇琛ㄦ牸绀轰緥](./advanced-table.md)
+- [鏍戝舰琛ㄦ牸绀轰緥](./tree-table.md)
+
+## 瀹屾暣绀轰緥浣嶇疆
+
+`src/views/examples/tables/basic.vue`
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/examples/templates/crud-page.md b/rsf-design/skill/art-design-pro/docs/examples/templates/crud-page.md
new file mode 100644
index 0000000..f1139d8
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/examples/templates/crud-page.md
@@ -0,0 +1,350 @@
+# 瀹屾暣 CRUD 椤甸潰绀轰緥
+
+## 姒傝堪
+
+杩欐槸涓�涓畬鏁寸殑鐢ㄦ埛绠$悊 CRUD 椤甸潰绀轰緥锛屽睍绀轰簡 Art Design Pro 鐨勬牳蹇冨姛鑳界粍鍚堜娇鐢ㄣ��
+
+## 椤甸潰缁撴瀯
+
+```
+system/user/
+鈹溾攢鈹� index.vue # 涓婚〉闈�
+鈹溾攢鈹� modules/
+鈹� 鈹溾攢鈹� user-search.vue # 鎼滅储鏍忕粍浠�
+鈹� 鈹斺攢鈹� user-dialog.vue # 寮圭獥缁勪欢
+```
+
+## 涓婚〉闈唬鐮�
+
+```vue
+<!-- 鐢ㄦ埛绠$悊椤甸潰 -->
+<template>
+ <div class="user-page art-full-height">
+ <!-- 鎼滅储鏍� -->
+ <UserSearch
+ v-model="searchForm"
+ @search="handleSearch"
+ @reset="resetSearchParams"
+ />
+
+ <ElCard class="art-table-card" shadow="never">
+ <!-- 琛ㄦ牸澶撮儴 -->
+ <ArtTableHeader
+ v-model:columns="columnChecks"
+ :loading="loading"
+ @refresh="refreshData"
+ >
+ <template #left>
+ <ElSpace wrap>
+ <ElButton
+ v-auth="'user:create'"
+ type="primary"
+ @click="showDialog('add')"
+ v-ripple
+ >
+ 鏂板鐢ㄦ埛
+ </ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <!-- 琛ㄦ牸 -->
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ />
+
+ <!-- 鐢ㄦ埛寮圭獥 -->
+ <UserDialog
+ v-model:visible="dialogVisible"
+ :type="dialogType"
+ :user-data="currentUserData"
+ @submit="handleDialogSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchGetUserList } from '@/api/system-manage'
+ import UserSearch from './modules/user-search.vue'
+ import UserDialog from './modules/user-dialog.vue'
+ import { ElTag, ElMessageBox, ElImage } from 'element-plus'
+ import { DialogType } from '@/types'
+
+ defineOptions({ name: 'User' })
+
+ type UserListItem = Api.SystemManage.UserListItem
+
+ // 寮圭獥鐩稿叧
+ const dialogType = ref<DialogType>('add')
+ const dialogVisible = ref(false)
+ const currentUserData = ref<Partial<UserListItem>>({})
+
+ // 閫変腑琛�
+ const selectedRows = ref<UserListItem[]>([])
+
+ // 鎼滅储琛ㄥ崟
+ const searchForm = ref({
+ userName: undefined,
+ userGender: undefined,
+ userPhone: undefined,
+ userEmail: undefined,
+ status: '1'
+ })
+
+ // 鐢ㄦ埛鐘舵�侀厤缃�
+ const USER_STATUS_CONFIG = {
+ '1': { type: 'success' as const, text: '鍦ㄧ嚎' },
+ '2': { type: 'info' as const, text: '绂荤嚎' },
+ '3': { type: 'warning' as const, text: '寮傚父' },
+ '4': { type: 'danger' as const, text: '娉ㄩ攢' }
+ } as const
+
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ searchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20,
+ ...searchForm.value
+ },
+ columnsFactory: () => [
+ { type: 'selection' },
+ { type: 'index', width: 60, label: '搴忓彿' },
+ {
+ prop: 'userInfo',
+ label: '鐢ㄦ埛淇℃伅',
+ width: 280,
+ formatter: (row) => {
+ return h('div', { class: 'user flex-c' }, [
+ h(ElImage, {
+ class: 'size-9.5 rounded-md',
+ src: row.avatar,
+ previewSrcList: [row.avatar],
+ previewTeleported: true
+ }),
+ h('div', { class: 'ml-2' }, [
+ h('p', { class: 'user-name' }, row.userName),
+ h('p', { class: 'email' }, row.userEmail)
+ ])
+ ])
+ }
+ },
+ {
+ prop: 'userGender',
+ label: '鎬у埆',
+ sortable: true,
+ formatter: (row) => row.userGender
+ },
+ { prop: 'userPhone', label: '鎵嬫満鍙�' },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ formatter: (row) => {
+ const statusConfig = USER_STATUS_CONFIG[row.status as keyof typeof USER_STATUS_CONFIG]
+ return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
+ }
+ },
+ {
+ prop: 'createTime',
+ label: '鍒涘缓鏃ユ湡',
+ sortable: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => showDialog('edit', row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => deleteUser(row)
+ })
+ ])
+ }
+ ]
+ }
+ })
+
+ /**
+ * 鎼滅储澶勭悊
+ */
+ const handleSearch = (params) => {
+ Object.assign(searchParams, params)
+ getData()
+ }
+
+ /**
+ * 鏄剧ず鐢ㄦ埛寮圭獥
+ */
+ const showDialog = (type: DialogType, row?: UserListItem): void => {
+ dialogType.value = type
+ currentUserData.value = row || {}
+ nextTick(() => {
+ dialogVisible.value = true
+ })
+ }
+
+ /**
+ * 鍒犻櫎鐢ㄦ埛
+ */
+ const deleteUser = (row: UserListItem): void => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ鐢ㄦ埛鍚楋紵', '鍒犻櫎鐢ㄦ埛', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'error'
+ }).then(async () => {
+ // 璋冪敤鍒犻櫎 API
+ // await deleteUserApi(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ refreshData()
+ })
+ }
+
+ /**
+ * 澶勭悊寮圭獥鎻愪氦浜嬩欢
+ */
+ const handleDialogSubmit = async () => {
+ dialogVisible.value = false
+ currentUserData.value = {}
+ refreshData()
+ }
+
+ /**
+ * 澶勭悊琛ㄦ牸琛岄�夋嫨鍙樺寲
+ */
+ const handleSelectionChange = (selection: UserListItem[]): void => {
+ selectedRows.value = selection
+ }
+</script>
+```
+
+## 鍏抽敭鍔熻兘鐐�
+
+### 1. useTable Hook 闆嗘垚
+
+```js
+const {
+ columns, // 鍒楅厤缃�
+ columnChecks, // 鍒楁樉绀烘帶鍒�
+ data, // 琛ㄦ牸鏁版嵁
+ loading, // 鍔犺浇鐘舵��
+ pagination, // 鍒嗛〉淇℃伅
+ getData, // 鑾峰彇鏁版嵁
+ searchParams, // 鎼滅储鍙傛暟
+ resetSearchParams, // 閲嶇疆鎼滅储
+ handleSizeChange, // 姣忛〉鏁伴噺鍙樺寲
+ handleCurrentChange, // 褰撳墠椤靛彉鍖�
+ refreshData // 鍒锋柊鏁版嵁
+} = useTable({...})
+```
+
+### 2. 鑷畾涔夊垪娓叉煋
+
+浣跨敤 `formatter` 鍑芥暟瀹炵幇澶嶆潅鐨勫垪娓叉煋锛�
+
+```js
+{
+ prop: 'userInfo',
+ label: '鐢ㄦ埛淇℃伅',
+ formatter: (row) => {
+ return h('div', { class: 'user flex-c' }, [
+ h(ElImage, { src: row.avatar }),
+ h('div', { class: 'ml-2' }, [
+ h('p', row.userName),
+ h('p', row.userEmail)
+ ])
+ ])
+ }
+}
+```
+
+### 3. 鏉冮檺鎺у埗
+
+```vue
+<ElButton
+ v-auth="'user:create'"
+ type="primary"
+ @click="showDialog('add')"
+>
+ 鏂板鐢ㄦ埛
+</ElButton>
+```
+
+### 4. 鎼滅储鏍忛泦鎴�
+
+```vue
+<UserSearch
+ v-model="searchForm"
+ @search="handleSearch"
+ @reset="resetSearchParams"
+/>
+```
+
+### 5. 寮圭獥琛ㄥ崟
+
+```vue
+<UserDialog
+ v-model:visible="dialogVisible"
+ :type="dialogType"
+ :user-data="currentUserData"
+ @submit="handleDialogSubmit"
+/>
+```
+
+## 鏍峰紡璇存槑
+
+- `art-full-height`: 鑷姩璁$畻椤甸潰鍓╀綑楂樺害
+- `art-table-card`: 绗﹀悎绯荤粺鏍峰紡鐨勮〃鏍煎崱鐗囷紝鑷姩鎾戞弧鍓╀綑楂樺害
+- `flex-c`: Flex 甯冨眬锛屽瀭鐩村眳涓�
+- `size-9.5`: 鍥哄畾灏哄锛堢害 38px锛�
+
+## 鐩稿叧缁勪欢
+
+- [缁勪欢閫熸煡琛╙(../../cheatsheet.md)
+- [useTable Hook 鏂囨。](../../16-use-table.md)
+- [ArtSearchBar 缁勪欢鏂囨。](../../17-art-search-bar.md)
+
+## 瀹屾暣绀轰緥浣嶇疆
+
+`src/views/system/user/index.vue`
+
+## 浣跨敤浠g爜鐢熸垚鍣�
+
+鍙互浣跨敤浠g爜鐢熸垚鍣ㄥ揩閫熷垱寤虹被浼奸〉闈細
+
+```bash
+python skill/art-design-pro/scripts/generate.py \
+ crud \
+ --name "User" \
+ --path "system/user" \
+ --fields "username,email,phone,status"
+```
+
+璇﹁ [浠g爜鐢熸垚鍣ㄤ娇鐢ㄦ寚鍗梋(../../generator-guide.md)銆�
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/generator-guide.md b/rsf-design/skill/art-design-pro/docs/generator-guide.md
new file mode 100644
index 0000000..a26b7dd
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/generator-guide.md
@@ -0,0 +1,378 @@
+# Art Design Pro 浠g爜鐢熸垚鍣ㄤ娇鐢ㄦ寚鍗�
+
+> **閲嶈鏇存柊 (2026-03-04)**: 浠g爜鐢熸垚鍣ㄥ凡鏇存柊涓轰娇鐢� ArtForm 缁勪欢鍜屾纭殑 ArtSearchBar 鐢ㄦ硶銆�
+
+## 馃摉 绠�浠�
+
+浠g爜鐢熸垚鍣ㄦ槸 Art Design Pro Skill 鐨勬牳蹇冨伐鍏蜂箣涓�锛屽彲浠ヨ嚜鍔ㄧ敓鎴愭爣鍑嗙殑 Vue3 椤甸潰浠g爜锛屽ぇ骞呮彁鍗囧紑鍙戞晥鐜囥��
+
+## 鉁� 鏈�鏂版洿鏂�
+
+### v2.3.0 (2026-03-04)
+
+- 鉁� **淇 ArtSearchBar 鐢ㄦ硶** - 浣跨敤 `v-model` + `:items` + `@search` + `@reset`
+- 鉁� **寮圭獥浣跨敤 ArtForm** - 鏇挎崲鍘熸潵鐨� `ElForm`锛屼娇鐢� `ArtForm` + `:model`
+- 鉁� **娣诲姞琛ㄥ崟椤归厤缃敓鎴�** - 鑷姩鐢熸垚 `searchItems` 鍜� `formItems` 閰嶇疆
+- 鉁� **鏀寔澶氱鏁版嵁绫诲瀷** - 鑷姩璇嗗埆 `number`銆乣boolean`銆乣date` 绛夌被鍨�
+- 鉁� **淇瀵煎叆璇彞** - 娣诲姞 `FormInstance` 绫诲瀷瀵煎叆
+
+### 鐢熸垚浠g爜鐗圭偣
+
+**鎼滅储鏍忕粍浠讹細**
+```vue
+<ArtSearchBar
+ v-model="searchForm"
+ :items="searchItems"
+ @search="handleSearch"
+ @reset="handleReset"
+/>
+```
+
+**寮圭獥琛ㄥ崟锛�**
+```vue
+<ElDialog v-model="dialogVisible" :title="dialogTitle">
+ <ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ :rules="rules"
+ label-width="120px"
+ />
+ <template #footer>
+ <ElButton @click="handleClose">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+ </template>
+</ElDialog>
+```
+
+---
+
+## 馃幆 鏀寔鐨勯〉闈㈢被鍨�
+
+### 1. CRUD 鍒楄〃椤碉紙`crud`锛�
+
+鐢熸垚瀹屾暣鐨勫鍒犳敼鏌ラ〉闈紝鍖呭惈锛�
+- 鉁� 涓婚〉闈紙index.vue锛�
+- 鉁� 鎼滅储鏍忕粍浠讹紙modules/xxx-search.vue锛�
+- 鉁� 寮圭獥缁勪欢锛坢odules/xxx-dialog.vue锛�
+- 鉁� 闆嗘垚 useTable Hook
+- 鉁� 闆嗘垚 ArtSearchBar 鍜� ArtTable 缁勪欢
+- 鉁� 瀹屾暣鐨勭被鍨嬪畾涔�
+
+**浣跨敤绀轰緥**锛�
+
+```bash
+# 鐢熸垚鐢ㄦ埛绠$悊椤甸潰
+python skill/art-design-pro/scripts/generate.py crud \
+ --name "User" \
+ --path "system/user" \
+ --fields "username,email,phone,status"
+
+# 鐢熸垚浜у搧绠$悊椤甸潰
+python skill/art-design-pro/scripts/generate.py crud \
+ --name "Product" \
+ --path "product/list" \
+ --fields "name,price,stock,category,status"
+```
+
+**鐢熸垚鐨勬枃浠剁粨鏋�**锛�
+
+```
+src/views/system/user/
+鈹溾攢鈹� index.vue # 涓婚〉闈�
+鈹斺攢鈹� modules/
+ 鈹溾攢鈹� user-search.vue # 鎼滅储鏍忕粍浠�
+ 鈹斺攢鈹� user-dialog.vue # 寮圭獥缁勪欢
+```
+
+**鐗规��**锛�
+- 鑷姩鐢熸垚琛ㄦ牸鍒楅厤缃�
+- 鑷姩鐢熸垚鎼滅储琛ㄥ崟
+- 鑷姩鐢熸垚寮圭獥琛ㄥ崟
+- 鍖呭惈瀹屾暣鐨勭被鍨嬪畾涔�
+- 闆嗘垚 Art Design Pro 缁勪欢
+- 绗﹀悎椤圭洰瑙勮寖
+
+---
+
+### 2. 鍩虹琛ㄦ牸椤碉紙`table`锛�
+
+鐢熸垚绠�鍗曠殑鏁版嵁灞曠ず琛ㄦ牸椤甸潰锛岄�傜敤浜庯細
+- 鍙鏁版嵁鍒楄〃
+- 涓嶉渶瑕佸鏉傛搷浣滅殑椤甸潰
+- 鏁版嵁灞曠ず鍜屽鍑�
+
+**浣跨敤绀轰緥**锛�
+
+```bash
+# 鐢熸垚璁㈠崟鍒楄〃椤甸潰
+python skill/art-design-pro/scripts/generate.py table \
+ --name "Order" \
+ --path "order/list" \
+ --fields "order_no,customer,total,status,created_at"
+
+# 鐢熸垚鏃ュ織鍒楄〃椤甸潰
+python skill/art-design-pro/scripts/generate.py table \
+ --name "Log" \
+ --path "system/logs" \
+ --fields "id,level,message,created_at"
+```
+
+**鐗规��**锛�
+- 绠�娲佺殑椤甸潰缁撴瀯
+- 鍙寘鍚〃鏍煎睍绀�
+- 鏀寔鍒嗛〉
+- 闆嗘垚 useTable Hook
+
+---
+
+### 3. 浠〃鏉块〉闈紙`dashboard`锛�
+
+鐢熸垚鏁版嵁缁熻鍜屽浘琛ㄥ睍绀洪〉闈紝鍖呭惈锛�
+- 缁熻鍗$墖锛圓rtStatsCard锛�
+- 鍥捐〃缁勪欢锛圓rtLineChart銆丄rtBarChart 绛夛級
+- 鍝嶅簲寮忓竷灞�
+
+**浣跨敤绀轰緥**锛�
+
+```bash
+# 鐢熸垚鏁版嵁鍒嗘瀽浠〃鏉�
+python skill/art-design-pro/scripts/generate.py dashboard \
+ --name "Analytics" \
+ --path "dashboard/analytics" \
+ --charts "line,bar,pie"
+
+# 鐢熸垚閿�鍞粺璁′华琛ㄦ澘
+python skill/art-design-pro/scripts/generate.py dashboard \
+ --name "Sales" \
+ --path "dashboard/sales" \
+ --charts "line,bar"
+```
+
+**鏀寔鐨勫浘琛ㄧ被鍨�**锛�
+- `line` - 鎶樼嚎鍥撅紙ArtLineChart锛�
+- `bar` - 鏌辩姸鍥撅紙ArtBarChart锛�
+- `pie` - 楗煎浘锛圓rtRingChart锛�
+- `radar` - 闆疯揪鍥撅紙ArtRadarChart锛�
+
+---
+
+## 馃敡 鍙傛暟璇存槑
+
+### 閫氱敤鍙傛暟
+
+| 鍙傛暟 | 蹇呭~ | 璇存槑 | 绀轰緥 |
+|------|------|------|------|
+| `--name` | 鉁� | 瀹炰綋鍚嶇О锛堣嫳鏂囷級 | `User`, `Product`, `Order` |
+| `--path` | 鉁� | 椤甸潰璺緞锛堢浉瀵逛簬 views锛� | `system/user`, `product/list` |
+| `--fields` | 鉁�* | 瀛楁鍒楄〃锛堥�楀彿鍒嗛殧锛� | `username,email,status` |
+| `--charts` | 鉂� | 鍥捐〃绫诲瀷锛堜粎 dashboard锛� | `line,bar,pie` |
+| `--output` | 鉂� | 杈撳嚭鐩綍 | `/path/to/output` |
+
+* `--fields` 瀵� `crud` 鍜� `table` 绫诲瀷鏄繀濉殑锛屽 `dashboard` 绫诲瀷涓嶉渶瑕併��
+
+### 瀛楁绫诲瀷瀹氫箟
+
+鏀寔鍦ㄥ瓧娈靛悕鍚庢寚瀹氱被鍨嬶紙浣跨敤 `:` 鍒嗛殧锛夛細
+
+```bash
+# 榛樿涓� string 绫诲瀷
+python generate.py crud --name "User" --fields "username,email"
+
+# 鎸囧畾绫诲瀷
+python generate.py crud --name "User" --fields "username:string,age:number,active:boolean"
+```
+
+鏀寔鐨勭被鍨嬶細
+- `string` - 瀛楃涓诧紙榛樿锛�
+- `number` - 鏁板瓧
+- `boolean` - 甯冨皵鍊�
+- `date` - 鏃ユ湡
+
+---
+
+## 馃搵 宸ヤ綔娴佺▼
+
+### 鏍囧噯寮�鍙戞祦绋�
+
+1. **鐢熸垚浠g爜**
+ ```bash
+ python skill/art-design-pro/scripts/generate.py crud \
+ --name "User" \
+ --path "system/user" \
+ --fields "username,email,status"
+ ```
+
+2. **澶嶅埗浠g爜鍒伴」鐩�**
+ - 灏嗚緭鍑虹殑浠g爜淇濆瓨鍒板搴旀枃浠�
+ - 鍒涘缓鐩綍缁撴瀯锛歚src/views/system/user/`
+
+3. **璋冩暣浠g爜**
+ - 鏇挎崲 `yourApiFunction` 涓哄疄闄呯殑 API 鍑芥暟
+ - 鏇存柊绫诲瀷瀹氫箟锛坄Api.YourModule.UserItem`锛�
+ - 璋冩暣瀛楁鏄犲皠鍜屾牎楠岃鍒�
+ - 娣诲姞涓氬姟閫昏緫
+
+4. **娴嬭瘯椤甸潰**
+ - 璁块棶椤甸潰楠岃瘉鍔熻兘
+ - 娴嬭瘯澧炲垹鏀规煡鎿嶄綔
+ - 妫�鏌ユ牱寮忓拰鍝嶅簲寮�
+
+---
+
+## 馃挕 鏈�浣冲疄璺�
+
+### 1. 瀛楁鍛藉悕瑙勮寖
+
+浣跨敤鑻辨枃鍛藉悕锛屼唬鐮佺敓鎴愬櫒浼氳嚜鍔ㄧ敓鎴愪腑鏂囨爣绛撅細
+
+```bash
+# 鎺ㄨ崘鍛藉悕
+--fields "username,email,phone,status,created_at"
+
+# 鑷姩鐢熸垚鐨勬爣绛�
+# username 鈫� 鐢ㄦ埛鍚�
+# email 鈫� 閭
+# phone 鈫� 鎵嬫満鍙�
+# status 鈫� 鐘舵��
+# created_at 鈫� 鍒涘缓鏃堕棿
+```
+
+### 2. 瀛楁鏁伴噺鎺у埗
+
+- **鎼滅储瀛楁**锛氬缓璁� 3-5 涓紙鏈�澶氭樉绀哄墠 5 涓級
+- **琛ㄦ牸鍒�**锛氬缓璁� 5-8 涓紙鏈�澶氭樉绀哄墠 8 涓級
+- **琛ㄥ崟瀛楁**锛氫笉闄愬埗锛屾寜闇�娣诲姞
+
+### 3. 鍥捐〃鎼厤
+
+```bash
+# 鍒嗘瀽绫讳华琛ㄦ澘锛堣秼鍔垮垎鏋愶級
+--charts "line,bar"
+
+# 姒傝绫讳华琛ㄦ澘锛堟暟鎹垎甯冿級
+--charts "pie,radar"
+
+# 缁煎悎浠〃鏉�
+--charts "line,bar,pie,radar"
+```
+
+---
+
+## 馃悰 甯歌闂
+
+### Q1: 鐢熸垚鐨勪唬鐮侀渶瑕佹墜鍔ㄨ皟鏁村摢浜涘湴鏂癸紵
+
+**A**锛氫富瑕侀渶瑕佽皟鏁达細
+1. API 鍑芥暟锛歚yourApiFunction` 鈫� 瀹為檯鐨� API 鍑芥暟
+2. 绫诲瀷瀹氫箟锛歚Api.YourModule.UserItem` 鈫� 瀹為檯鐨勭被鍨嬪畾涔�
+3. 瀛楁鏍¢獙瑙勫垯锛氬湪寮圭獥缁勪欢鐨� `rules` 瀵硅薄涓坊鍔�
+4. 涓氬姟閫昏緫锛氬垹闄ゃ�佹彁浜ょ瓑鎿嶄綔鐨勫疄闄呭疄鐜�
+
+### Q2: 濡備綍娣诲姞鑷畾涔夊垪锛�
+
+**A**锛氬湪鐢熸垚浠g爜鍚庯紝淇敼 `columnsFactory` 涓殑鍒楅厤缃細
+
+```js
+{ prop: 'status', label: '鐘舵��',
+ formatter: (row) => {
+ return h(ElTag, { type: row.status === '1' ? 'success' : 'danger' },
+ () => row.status === '1' ? '鍚敤' : '绂佺敤')
+ }
+}
+```
+
+### Q3: 濡備綍闆嗘垚鐪熷疄鐨� API锛�
+
+**A**锛氫慨鏀� `useTable` 涓殑 `apiFn` 鍙傛暟锛�
+
+```js
+// 淇敼鍓�
+apiFn: yourApiFunction
+
+// 淇敼鍚�
+import { fetchGetUserList } from '@/api/system-manage'
+
+apiFn: fetchGetUserList
+```
+
+### Q4: 鐢熸垚鐨勪唬鐮佷笉绗﹀悎椤圭洰瑙勮寖锛�
+
+**A**锛氫唬鐮佺敓鎴愬櫒鐢熸垚鐨勪唬鐮佺鍚� Art Design Pro 椤圭洰瑙勮寖锛屽鏋滈渶瑕佽皟鏁达細
+1. 淇敼鐢熸垚鍣ㄦā鏉匡紙`scripts/generate.py`锛�
+2. 鎴栬�呯敓鎴愪唬鐮佸悗鎵嬪姩璋冩暣
+
+---
+
+## 馃殌 楂樼骇鐢ㄦ硶
+
+### 1. 鎵归噺鐢熸垚椤甸潰
+
+浣跨敤 shell 鑴氭湰鎵归噺鐢熸垚澶氫釜椤甸潰锛�
+
+```bash
+#!/bin/bash
+# batch-generate.sh
+
+pages=(
+ "User:system/user:username,email,phone,status"
+ "Role:system/role:name,code,description"
+ "Permission:system/permission:name,code,type"
+)
+
+for page in "${pages[@]}"; do
+ IFS=':' read -r name path fields <<< "$page"
+ python skill/art-design-pro/scripts/generate.py crud \
+ --name "$name" \
+ --path "$path" \
+ --fields "$fields"
+done
+```
+
+### 2. 鑷畾涔夋ā鏉�
+
+淇敼 `scripts/generate.py` 涓殑妯℃澘瀛楃涓诧紝鑷畾涔夌敓鎴愮殑浠g爜椋庢牸銆�
+
+### 3. 闆嗘垚鍒伴」鐩剼鏈�
+
+鍦ㄩ」鐩殑 `package.json` 涓坊鍔犲揩鎹峰懡浠わ細
+
+```json
+{
+ "scripts": {
+ "generate:page": "python skill/art-design-pro/scripts/generate.py"
+ }
+}
+```
+
+浣跨敤锛�
+```bash
+npm run generate:page crud --name "User" --path "system/user" --fields "username,email"
+```
+
+---
+
+## 馃搳 鏁堢巼瀵规瘮
+
+| 鎿嶄綔 | 鎵嬪姩缂栧啓 | 浣跨敤鐢熸垚鍣� | 鏁堢巼鎻愬崌 |
+|------|----------|------------|----------|
+| CRUD 椤甸潰锛�3 涓枃浠讹級 | 60-90 鍒嗛挓 | 5-10 鍒嗛挓 | **600%-900%** |
+| 鍩虹琛ㄦ牸椤� | 20-30 鍒嗛挓 | 2-5 鍒嗛挓 | **400%-500%** |
+| 浠〃鏉块〉 | 40-60 鍒嗛挓 | 5-10 鍒嗛挓 | **400%-500%** |
+
+---
+
+## 馃摎 鐩稿叧鏂囨。
+
+- [useTable Hook 鏂囨。](https://www.artd.pro/docs/zh/guide/hooks/use-table.html)
+- [ArtTable 缁勪欢鏂囨。](https://www.artd.pro/docs/zh/guide/components/art-table.html)
+- [ArtSearchBar 缁勪欢鏂囨。](https://www.artd.pro/docs/zh/guide/components/art-search-bar.html)
+- [椤圭洰绀轰緥椤甸潰](src/views/)
+
+---
+
+**鏈�鍚庢洿鏂�**锛�2026-03-03
+**缁存姢鑰�**锛欰rt Design Pro Skill Team
+
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/getting-started/01-introduce.md b/rsf-design/skill/art-design-pro/docs/getting-started/01-introduce.md
new file mode 100644
index 0000000..434dda4
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/getting-started/01-introduce.md
@@ -0,0 +1,56 @@
+# 浠嬬粛 | Art Design Pro
+
+鏉ユ簮锛歨ttps://www.artd.pro/docs/zh/guide/introduce.html
+
+## 鍏充簬 Art Design Pro
+
+浣滀负涓�鍚嶅紑鍙戣�咃紝鎴戝彂鐜颁紶缁熺郴缁熷湪鐢ㄦ埛浣撻獙鍜岃瑙夎璁′笂涓嶈兘瀹屽叏婊¤冻闇�姹傘�傝繖璁╂垜寮�濮嬫�濊�冿紝濡備綍璁捐涓�娆炬棦绮捐嚧缇庤鍙堣兘鎻愪緵椤虹晠鎿嶄綔浣撻獙鐨勫悗鍙扮郴缁熴��
+
+缁忚繃鍙嶅鎬濊�冿紝鎴戝喅瀹氭帹鍑� Art Design Pro 鈥斺�� 瀹冧笉浠呰瀺姹囦簡鏈�鏂扮殑鎶�鏈紝鏇寸鎸佺潃瀵瑰畬缇庣敤鎴蜂綋楠屽拰瑙嗚璁捐鐨勮拷姹傘��
+
+## 瑙e喅鐨勯棶棰�
+
+### 瑙嗚鐤插姵涓庝箯鍛�
+
+闀挎椂闂撮潰瀵瑰喎鍐板啺鐨勭晫闈紝闅惧厤浼氳浜烘劅鍒扮柌鎯�傛湰妯℃澘鍔涙眰閫氳繃绮惧績璁捐鐨勯厤鑹层�佹帓鐗堝拰鍔ㄦ晥锛岃姣忎竴娆$偣鍑汇�佹瘡涓�娆℃粴鍔ㄩ兘鍍忔槸涓�娈电編濂界殑浜掑姩浣撻獙銆�
+
+### 鐢ㄦ埛浣撻獙涓嶄匠
+
+绠$悊绯荤粺涓嶄粎瑕佺湅寰楄垝鏈嶏紝鏇磋鐢ㄥ緱椤烘墜銆傛垜甯屾湜閫氳繃绠�娲佺洿瑙傜殑甯冨眬鍜屾祦鐣呰嚜鐒剁殑浜や簰锛岃鐢ㄦ埛鑳借繀閫熸壘鍒版墍闇�鍔熻兘锛屼韩鍙楀埌涓�绉嶄汉鎬у寲鐨勫叧鎬�銆�
+
+### 閲嶅閫犺疆瀛愶紝寮�鍙戞垚鏈珮
+
+鎴戦兘鏇句负浠庨浂寮�濮嬭璁′竴濂楀畬鏁寸殑鍓嶇鐣岄潰鑰屽ご鐤笺�傝繖涓ā鏉挎彁渚涗簡涓�鏁村缁忚繃鍙嶅鎵撶(鐨� UI 缁勪欢鍜岃璁¤鑼冿紝璁╀綘鍙互蹇�熸惌寤哄嚭鏃㈢編瑙傚張瀹炵敤鐨勫悗鍙扮鐞嗙郴缁熴��
+
+## 鏍稿績鐗硅壊
+
+### 缇庡涓庡疄鐢ㄥ苟閲�
+
+鎴戠浉淇℃妧鏈拰鑹烘湳鍙互骞惰偐鍚岃銆傛ā鏉块噰鐢ㄤ簡娴佽鑰屼笉娴じ鐨勮璁¢鏍硷紝姣忎竴澶勭粏鑺傞兘缁忚繃绮惧績鎵撶(锛屽姏姹傝鐣岄潰鏃㈣祻蹇冩偊鐩紝鍙堜笉浼氬枾瀹惧ず涓汇��
+
+### 鎯呮劅鍖栫殑鐢ㄦ埛浣撻獙
+
+姣忎竴涓氦浜掑姩鏁堬紝姣忎竴澶勬寜閽弽棣堬紝閮芥槸涓轰簡璁╃敤鎴峰湪鎿嶄綔鏃舵劅鍙楀埌娓╂殩涓庝汉鎬у寲銆�
+
+### 妯″潡鍖栥�佺伒娲诲畾鍒�
+
+鎴戞彁渚涗簡楂樺害妯″潡鍖栫殑璁捐锛屽悇涓粍浠堕兘鍙互鏍规嵁瀹為檯闇�姹傝嚜鐢辫皟鏁淬��
+
+### 鍏ㄥ搷搴斿紡璁捐
+
+妯℃澘涓嶄粎閫傞厤鍚勭灞忓箷灏哄锛屾洿閫氳繃绮剧粏鐨勮璁¤涓嶅悓璁惧涓嬬殑鐢ㄦ埛閮借兘鑾峰緱涓�鑷磋�屼紭璐ㄧ殑浣撻獙銆�
+
+### 鎷掔粷杩囧害灏佽
+
+鎴戝潥鎸佺畝娲佸拰閫忔槑鐨勫師鍒欙紝閬垮厤瀵圭郴缁熻繘琛岃繃搴﹀皝瑁呫�備綘鍙互杞绘澗淇敼鍜屾墿灞曚换鎰忛儴鍒嗐��
+
+## 鎶�鏈爤
+
+- **寮�鍙戞鏋�**锛歏ue3銆丣avaScript銆乂ite銆丒lement-Plus銆丼css
+- **浠g爜瑙勮寖**锛欵slint銆丳rettier銆丼tylelint銆丠usky銆丩int-staged銆乧z-git
+
+## 娴忚鍣ㄦ敮鎸�
+
+Chrome銆丒dge銆丗irefox銆丼afari銆丱pera 绛夌幇浠f祻瑙堝櫒銆�
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/getting-started/02-quick-start.md b/rsf-design/skill/art-design-pro/docs/getting-started/02-quick-start.md
new file mode 100644
index 0000000..06b5316
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/getting-started/02-quick-start.md
@@ -0,0 +1,55 @@
+# 蹇�熷紑濮� | Art Design Pro
+
+鏉ユ簮锛歨ttps://www.artd.pro/docs/zh/guide/quick-start.html
+
+## 鍑嗗宸ヤ綔
+
+### 鐜瑕佹眰
+
+纭繚 Node.js 婊¤冻浠ヤ笅瑕佹眰锛�
+- Node.js 20.19.0 鍙婁互涓婄増鏈�
+
+## 涓嬭浇婧愮爜
+
+**GitHub:**
+```bash
+git clone https://github.com/Daymychen/art-design-pro
+```
+
+**Gitee:**
+```bash
+git clone https://gitee.com/lingchen163/art-design-pro
+```
+
+## 鍚姩椤圭洰
+
+鏈」鐩娇鐢� pnpm 宸ュ叿瀹夎渚濊禆锛屾帹鑽愪娇鐢� pnpm
+
+### 1. 瀹夎 pnpm
+
+```bash
+npm install -g pnpm
+# 鎴栬��
+yarn global add pnpm
+```
+
+### 2. 瀹夎渚濊禆
+
+```bash
+pnpm install
+```
+
+濡傛灉 pnpm install 瀹夎澶辫触锛屽皾璇曚娇鐢ㄤ笅闈㈢殑鍛戒护锛�
+```bash
+pnpm install --ignore-scripts
+```
+
+### 3. 杩愯椤圭洰
+
+```bash
+pnpm dev
+```
+
+椤圭洰鍚姩鍚庝細鑷姩鎵撳紑娴忚鍣ㄨ繍琛岋紝榛樿璁块棶鍦板潃锛歨ttp://localhost:3006
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/getting-started/03-must-read.md b/rsf-design/skill/art-design-pro/docs/getting-started/03-must-read.md
new file mode 100644
index 0000000..035c3fe
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/getting-started/03-must-read.md
@@ -0,0 +1,88 @@
+# 寮�鍙戝繀璇绘枃妗� | Art Design Pro
+
+鏉ユ簮锛歨ttps://www.artd.pro/docs/zh/guide/must-read.html
+
+## 鎺ュ彛瀵规帴
+
+榛樿杩斿洖浠ヤ笅鏍煎紡锛屽闇�淇敼璇峰埌 `src/typings/http.d.ts` 鏂囦欢淇敼锛�
+
+```js
+interface BaseResponse<T = unknown> {
+ code: number; // 鐘舵�佺爜
+ msg: string; // 娑堟伅
+ data: T; // 鏁版嵁
+}
+```
+
+## 缃戠粶璇锋眰
+
+榛樿杩斿洖 data 涓殑鏁版嵁鑰屼笉鏄暣涓搷搴斾綋锛�
+
+```js
+try {
+ const { token, refreshToken } = await fetchLogin({
+ userName: username,
+ password,
+ });
+} catch (error) {
+ if (error instanceof HttpError) {
+ // 杩欓噷鍙互鏍规嵁鐘舵�佺爜杩涜涓嶅悓鐨勫鐞�
+ }
+}
+```
+
+## 鑿滃崟鏁版嵁锛坅syncRoutes.ts锛�
+
+- `RoutesAlias.Layout` 鎸囧悜鐨勬槸甯冨眬瀹瑰櫒
+- 鍚庣杩斿洖鐨勮彍鍗曟暟鎹腑锛宑omponent 瀛楁闇�瑕佹寚鍚� `/index/index`
+- `roles` 瀛楁鐢ㄤ簬鍓嶇鎺у埗妯″紡
+
+**鍓嶇妯″紡**锛氶�氳繃鑾峰彇鐢ㄦ埛淇℃伅鎺ュ彛杩斿洖鐨� roles 璺熻彍鍗曟暟鎹� asyncRoutes 涓殑 roles 杩涜瀵规瘮瀹炵幇鑿滃崟杩囨护
+
+**鍚庣妯″紡**锛氱洿鎺ラ�氳繃鎺ュ彛杩斿洖瀵瑰簲瑙掕壊鐨勮彍鍗曞嵆鍙紝涓嶉渶瑕佽繑鍥� `roles` 瀛楁
+
+绀轰緥锛�
+```js
+{
+ name: 'Dashboard',
+ path: '/dashboard',
+ component: RoutesAlias.Layout,
+ meta: {
+ title: 'menus.dashboard.title',
+ icon: '',
+ roles: ['R_SUPER', 'R_ADMIN'] // 鍓嶇妯″紡闇�瑕�
+ },
+ children: [
+ {
+ path: 'console',
+ name: 'Console',
+ component: RoutesAlias.Dashboard,
+ meta: {
+ title: 'menus.dashboard.console',
+ keepAlive: false,
+ fixedTab: true
+ }
+ }
+ ]
+}
+```
+
+## 鎵撳寘澶у皬璇存槑
+
+- **瀹屾暣鐗堥」鐩�**锛氱害 10MB
+- **绮剧畝鐗堥」鐩�**锛氱害 5MB
+
+椤圭洰榛樿寮�鍚� gzip 鍘嬬缉锛屽洜姝や細棰濆鐢熸垚 .gz 鏂囦欢锛�
+
+鍏抽棴 gzip 鏃讹紝瀹為檯鎵撳寘浣撶Н绾� 4.5MB
+
+寮�鍚� gzip 鍚庯紝浜х墿浣撶Н鏇村皬锛堟祻瑙堝櫒璇锋眰鏃朵細浼樺厛鍔犺浇 .gz 鏂囦欢锛�
+
+**杩涗竴姝ヤ紭鍖栨柟妗�**锛�
+
+鑻ュ浣撶Н鏈夋洿楂樿姹傦紝鍙�氳繃浠ヤ笅鏂瑰紡浼樺寲锛屽彲杞绘槗闄嶈嚦 3.5MB 宸﹀彸锛�
+- 绮剧畝鎴栨浛鎹㈠浘鏍囧簱
+- 绉婚櫎闈炲繀瑕佸浘鐗囪祫婧�
+- 鍑忓皯绗笁鏂瑰簱渚濊禆锛屾垨鏇挎崲涓烘洿杞婚噺鐨勬柟妗�
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/getting-started/04-standard.md b/rsf-design/skill/art-design-pro/docs/getting-started/04-standard.md
new file mode 100644
index 0000000..9ac5e95
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/getting-started/04-standard.md
@@ -0,0 +1,106 @@
+# 瑙勮寖 | Art Design Pro
+
+鏉ユ簮锛歨ttps://www.artd.pro/docs/zh/guide/project/standard.html
+
+## 浠g爜瑙勮寖宸ュ叿
+
+- **Eslint**: JS 浠g爜妫�鏌�
+- **Prettier**: 浠g爜鏍煎紡鍖�
+- **Stylelint**: CSS 浠g爜妫�鏌�
+- **Commitlint**: Git 鎻愪氦淇℃伅妫�鏌�
+- **Husky**: Git 閽╁瓙宸ュ叿
+- **Lint-staged**: Git 鎻愪氦鍓嶈繍琛屼唬鐮佹牎楠�
+- **cz-git**: 鍙鍖栨彁浜ゅ伐鍏�
+
+## 鑷姩鍖�
+
+浠g爜鎻愪氦浼氳嚜鍔ㄦ墽琛岄厤缃ソ鐨勬枃浠讹紝鑷姩瀹屾垚浠g爜鏍¢獙鍜屾牸寮忓寲銆�
+
+`package.json` 閰嶇疆锛�
+
+```json
+{
+ "lint-staged": {
+ "*.{js,ts}": [
+ "eslint --fix",
+ "prettier --write"
+ ],
+ "*.{cjs,json}": [
+ "prettier --write"
+ ],
+ "*.{vue,html}": [
+ "eslint --fix",
+ "prettier --write",
+ "stylelint --fix"
+ ],
+ "*.{scss,css}": [
+ "stylelint --fix",
+ "prettier --write"
+ ],
+ "*.md": [
+ "prettier --write"
+ ]
+ }
+}
+```
+
+## 甯哥敤鍛戒护
+
+```bash
+# 妫�鏌ラ」鐩腑鐨� js 璇硶
+pnpm lint
+
+# 淇椤圭洰涓� js 璇硶閿欒
+pnpm fix
+
+# 浣跨敤 Prettier 鏍煎紡鍖栨墍鏈夋枃浠�
+pnpm lint:prettier
+
+# 浣跨敤 Stylelint 妫�鏌ュ拰淇鏍峰紡
+pnpm lint:stylelint
+
+# 杩愯 lint-staged 妫�鏌ユ殏瀛樻枃浠�
+pnpm lint:lint-staged
+
+# 璁剧疆 Husky Git 閽╁瓙
+pnpm prepare
+
+# 浣跨敤 Commitizen 瑙勮寖鍖栨彁浜ゆ秷鎭�
+pnpm commit
+```
+
+## 鎻愪氦瑙勮寖
+
+### 鎻愪氦绫诲瀷
+
+```bash
+feat // 鏂板鍔熻兘
+fix // 淇缂洪櫡
+docs // 鏂囨。鍙樻洿
+style // 浠g爜鏍煎紡锛堜笉褰卞搷鍔熻兘锛�
+refactor // 浠g爜閲嶆瀯锛堜笉鍖呮嫭 bug 淇銆佸姛鑳芥柊澧烇級
+perf // 鎬ц兘浼樺寲
+test // 娣诲姞鐤忔紡娴嬭瘯鎴栧凡鏈夋祴璇曟敼鍔�
+build // 鏋勫缓娴佺▼銆佸閮ㄤ緷璧栧彉鏇�
+ci // 淇敼 CI 閰嶇疆銆佽剼鏈�
+revert // 鍥炴粴 commit
+chore // 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鐨勬洿鏀�
+wip // 瀵规瀯寤鸿繃绋嬫垨杈呭姪宸ュ叿鐨勬洿鏀�
+```
+
+### 鎻愪氦浠g爜娴佺▼
+
+```bash
+git add .
+pnpm commit
+# 閫夋嫨鎻愪氦绫诲瀷锛屽~鍐欐彁浜や俊鎭�
+git push
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **涓嶈璺宠繃 Husky 閽╁瓙**锛氫娇鐢� `pnpm commit` 鑰屼笉鏄� `git commit`
+2. **鎻愪氦鍓嶈嚜鍔ㄦ牸寮忓寲**锛歀int-staged 浼氳嚜鍔ㄦ牸寮忓寲鏆傚瓨鐨勬枃浠�
+3. **閬靛惊鎻愪氦瑙勮寖**锛氫娇鐢� Commitizen 閫夋嫨姝g‘鐨勬彁浜ょ被鍨�
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/getting-started/components-basics.md b/rsf-design/skill/art-design-pro/docs/getting-started/components-basics.md
new file mode 100644
index 0000000..6ae7fa6
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/getting-started/components-basics.md
@@ -0,0 +1,137 @@
+# 缁勪欢鍜屽浘鏍囧熀纭� | Art Design Pro
+
+鏈枃妗d粙缁� Art Design Pro 涓粍浠跺簱鍜屽浘鏍囩殑浣跨敤鏂规硶銆�
+
+## Element Plus 缁勪欢搴�
+
+椤圭洰浣跨敤 Element Plus 浣滀负鍩虹 UI 缁勪欢搴擄紝瑕嗙洊浜� 80% 鐨勫父鐢ㄧ粍浠堕渶姹傘��
+
+### 鑷姩瀵煎叆閰嶇疆
+
+椤圭洰閫氳繃 `unplugin-vue-components` 瀹炵幇缁勪欢鎸夐渶鑷姩瀵煎叆锛�
+
+```js
+// vite.config.js
+Components({
+ resolvers: [
+ ElementPlusResolver(),
+ ],
+ dts: 'src/types/import/components.d.ts'
+})
+```
+
+### 鍙敤缁勪欢
+
+Element Plus 鎻愪緵锛�
+- **鍩虹缁勪欢**锛氭寜閽�佽〃鍗曘�佽〃鏍肩瓑
+- **鍙嶉缁勪欢**锛氬脊绐椼�佹秷鎭彁绀虹瓑
+- **甯冨眬缁勪欢**锛氬鍣ㄣ�佹爡鏍肩郴缁熺瓑
+
+**瀹樻柟鏂囨。**锛歨ttps://element-plus.org/
+
+### 鏍峰紡瀹氬埗
+
+鏍峰紡鏂囦欢璺緞锛歚src/assets/styles/el-ui.scss`
+
+```scss
+@forward 'element-plus/theme-chalk/src/common/var.scss' with (
+ $colors: (
+ 'primary': (
+ 'base': #409EFF,
+ ),
+ ),
+);
+```
+
+## 绯荤粺鍐呯疆缁勪欢
+
+### 鍥炬爣鐩稿叧
+- **鍥炬爣閫夋嫨鍣�** - 鍙鍖栧浘鏍囬�夋嫨宸ュ叿
+
+### 濯掍綋澶勭悊
+- **鍥惧儚瑁佸壀** - 鍥剧墖涓婁紶鍜岃鍓粍浠�
+- **瑙嗛鎾斁鍣�** - 瑙嗛鎾斁缁勪欢
+
+### 鏁版嵁澶勭悊
+- **Excel 瀵煎叆瀵煎嚭** - 鏁版嵁瀵煎叆瀵煎嚭鍔熻兘
+- **鏁板瓧婊氬姩** - 缁熻鏁板瓧鍔ㄧ敾
+
+### 缂栬緫鍣�
+- **瀵屾枃鏈紪杈戝櫒** - 鍩轰簬 wangEditor
+
+### 鐗规晥缁勪欢
+- **姘村嵃** - 椤甸潰姘村嵃缁勪欢
+- **鍙抽敭鑿滃崟** - 鑷畾涔夊彸閿彍鍗�
+- **鏂囧瓧婊氬姩** - 鏂囧瓧婊氬姩鏁堟灉
+- **绀艰姳鏁堟灉** - 鑺傛棩鐗规晥
+
+### 鍏朵粬
+- **浜岀淮鐮�** - 浜岀淮鐮佺敓鎴愬櫒
+- **鎷栨嫿** - 鎷栨嫿鎺掑簭鍔熻兘
+
+## 鍥炬爣绯荤粺
+
+椤圭洰鍐呯疆 600+ 鍥炬爣锛屾弧瓒冲ぇ閮ㄥ垎鍥炬爣闇�姹傘��
+
+### 鍥炬爣搴撶洰褰�
+
+鍥炬爣璧勬簮鐩綍锛歚src/assets/icons/system`
+
+### 浣跨敤鏂瑰紡
+
+#### Unicode 鏂瑰紡
+
+```html
+<i class="iconfont-sys"></i>
+```
+
+#### Font class 鏂瑰紡
+
+```html
+<i class="iconfont-sys iconsys-gou"></i>
+```
+
+### 鍥炬爣鎵╁睍
+
+濡傞渶娣诲姞鑷畾涔夊浘鏍囷紝鍙闂郴缁熷浘鏍囧簱杩涜鎵╁睍銆�
+
+**娉ㄦ剰**锛氱郴缁熷浘鏍囦娇鐢� `iconfont-sys` 绫诲悕锛岃�岄潪 `iconfont`锛屾柟渚跨敤鎴锋墿灞曘��
+
+## 绯荤粺妯℃澘
+
+### 鍗$墖妯℃澘
+- 缁熻鍗$墖
+- 鏁版嵁灞曠ず鍗$墖
+- 鍥捐〃鍗$墖
+
+### 妯箙妯℃澘
+- 鍩虹妯箙
+- 鍗$墖妯箙
+
+### 鍥捐〃妯℃澘
+- 鏌辩姸鍥�
+- 鎶樼嚎鍥�
+- 楗煎浘
+- 闆疯揪鍥�
+- 鍦板浘鍥捐〃
+
+### 鍏朵粬妯℃澘
+- 鍦板浘椤甸潰
+- 鑱婂ぉ椤甸潰
+- 鏃ュ巻椤甸潰
+- 瀹氫环椤甸潰
+
+## 鏈�浣冲疄璺�
+
+1. **浼樺厛浣跨敤绯荤粺缁勪欢** - 绯荤粺缁勪欢宸茬粡杩囦紭鍖栧拰娴嬭瘯
+2. **鍙傝�冨畼鏂规枃妗�** - Element Plus 瀹樻柟鏂囨。鎻愪緵瀹屾暣 API
+3. **缁熶竴鏍峰紡** - 淇濇寔鏁翠釜椤圭洰鐨勮瑙変竴鑷存��
+4. **鎸夐渶瀵煎叆** - 浣跨敤鑷姩瀵煎叆鍑忓皯鎵撳寘浣撶Н
+
+## 鐩稿叧鏂囨。
+
+- [Element Plus 瀹樻柟鏂囨。](https://element-plus.org/)
+- [涓婚閰嶇疆](./configuration-guide.md#涓婚閰嶇疆)
+- [鐜鍙橀噺](./configuration-guide.md#鐜鍙橀噺)
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/getting-started/configuration-guide.md b/rsf-design/skill/art-design-pro/docs/getting-started/configuration-guide.md
new file mode 100644
index 0000000..ce161bb
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/getting-started/configuration-guide.md
@@ -0,0 +1,286 @@
+# 绯荤粺閰嶇疆鎸囧崡 | Art Design Pro
+
+鏈枃妗d粙缁� Art Design Pro 鐨勫悇椤圭郴缁熼厤缃紝鍖呮嫭涓婚銆佺幆澧冨彉閲忓拰鍏ㄥ眬璁剧疆銆�
+
+## 鐜鍙橀噺閰嶇疆
+
+### 鐜鏂囦欢璇存槑
+
+椤圭洰鏍圭洰褰曚笅鐨勭幆澧冨彉閲忔枃浠讹細
+- `.env` - 閫傜敤浜庢墍鏈夌幆澧�
+- `.env.development` - 浠呴�傜敤浜庡紑鍙戠幆澧�
+- `.env.production` - 浠呴�傜敤浜庣敓浜х幆澧�
+
+### 鑷畾涔夌幆澧冨彉閲�
+
+鑷畾涔夌幆澧冨彉閲忓繀椤讳互 `VITE_` 寮�澶淬��
+
+**鍦ㄤ唬鐮佷腑璁块棶**锛�
+```js
+console.log(import.meta.env.VITE_PORT);
+```
+
+### 鐜閰嶇疆绀轰緥
+
+#### .env锛堥�氱敤閰嶇疆锛�
+
+```bash
+# 鐗堟湰鍙�
+VITE_VERSION = 2.4.1.1
+
+# 绔彛鍙�
+VITE_PORT = 3006
+
+# 缃戠珯鍦板潃鍓嶇紑
+VITE_BASE_URL = /art-design-pro/
+
+# API 鍦板潃鍓嶇紑
+VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+
+# 鏉冮檺妯″紡锛坒rontend | backend锛�
+VITE_ACCESS_MODE = frontend
+
+# 璺ㄥ煙璇锋眰鏃舵槸鍚︽惡甯� Cookie
+VITE_WITH_CREDENTIALS = false
+
+# 鏄惁鎵撳紑璺敱淇℃伅
+VITE_OPEN_ROUTE_INFO = false
+
+# 閿佸睆鍔犲瘑瀵嗛挜
+VITE_LOCK_ENCRYPT_KEY = jfsfjk1938jfj
+```
+
+#### .env.development锛堝紑鍙戠幆澧冿級
+
+```bash
+# 缃戠珯鍦板潃鍓嶇紑
+VITE_BASE_URL = /
+
+# API 璇锋眰鍩虹璺緞锛堝紑鍙戠幆澧冮�氬父涓轰唬鐞嗗墠缂�锛�
+VITE_API_URL = /api
+
+# 鏈湴寮�鍙戜唬鐞嗙殑鐩爣鍚庣鍦板潃
+VITE_API_PROXY_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+
+# Delete console
+VITE_DROP_CONSOLE = false
+```
+
+#### .env.production锛堢敓浜х幆澧冿級
+
+```bash
+# 缃戠珯鍦板潃鍓嶇紑
+VITE_BASE_URL = /art-design-pro/
+
+# API 鍦板潃鍓嶇紑
+VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
+
+# Delete console
+VITE_DROP_CONSOLE = true
+```
+
+## 涓婚閰嶇疆
+
+### CSS 涓婚鍙橀噺
+
+CSS 鍙橀噺鍖呮嫭涓婚棰滆壊銆佽儗鏅鑹层�佹枃瀛楅鑹层�佽竟妗嗛鑹层�侀槾褰辩瓑锛岃兘鑷�傚簲 Light 鍜� Dark 妯″紡銆�
+
+**閰嶇疆鏂囦欢**锛歚src/assets/styles/variables.scss`
+
+### 浣跨敤 CSS 鍙橀噺
+
+```scss
+// 鏂囧瓧棰滆壊
+color: var(--art-gray-100);
+color: var(--art-gray-900);
+
+// 杈规
+border: 1px solid var(--art-border-color);
+border: 1px solid var(--art-border-dashed-color);
+
+// 鑳屾櫙棰滆壊
+background-color: var(--art-main-bg-color);
+
+// 闃村奖
+box-shadow: var(--art-box-shadow);
+box-shadow: var(--art-box-shadow-xs);
+
+// 浣跨敤甯﹂�忔槑搴︾殑棰滆壊
+color: rgba(var(--art-gray-800-rgb), 0.6);
+
+// 涓婚鑹�
+color: var(--main-color);
+background-color: var(--el-color-primary-light-1);
+```
+
+### Light 涓婚鍙橀噺
+
+```scss
+:root {
+ // Theme color
+ --art-primary: 93, 135, 255;
+ --art-secondary: 73, 190, 255;
+ --art-error: 250, 137, 107;
+ --art-info: 83, 155, 255;
+ --art-success: 19, 222, 185;
+ --art-warning: 255, 174, 31;
+ --art-danger: 255, 77, 79;
+
+ // Background color
+ --art-gray-100: #f9f9f9;
+ --art-gray-200: #f1f1f4;
+ --art-gray-300: #dbdfe9;
+ --art-gray-400: #c4cada;
+ --art-gray-500: #99a1b7;
+ --art-gray-600: #78829d;
+ --art-gray-700: #4b5675;
+ --art-gray-800: #252f4a;
+ --art-gray-900: #071437;
+
+ // Border
+ --art-border-color: #eaebf1;
+ --art-border-dashed-color: #dbdfe9;
+
+ // Shadow
+ --art-box-shadow-xs: 0 0.1rem 0.75rem 0.25rem rgba(0, 0, 0, 0.05);
+ --art-box-shadow-sm: 0 0.1rem 1rem 0.25rem rgba(0, 0, 0, 0.05);
+ --art-box-shadow: 0 0.5rem 1.5rem 0.5rem rgba(0, 0, 0, 0.075);
+ --art-box-shadow-lg: 0 1rem 2rem 1rem rgba(0, 0, 0, 0.1);
+
+ // Background
+ --art-bg-color: #fafbfc;
+ --art-main-bg-color: #ffffff;
+}
+```
+
+### Dark 涓婚鍙橀噺
+
+```scss
+html.dark {
+ // Theme color
+ --art-primary: 93, 135, 255;
+
+ // Background color
+ --art-gray-100: #1b1c22;
+ --art-gray-200: #26272f;
+ --art-gray-300: #363843;
+ --art-gray-400: #464852;
+ --art-gray-500: #636674;
+ --art-gray-600: #808290;
+ --art-gray-700: #9a9cae;
+ --art-gray-800: #b5b7c8;
+ --art-gray-900: #f5f5f5;
+
+ // Border
+ --art-border-color: #26272f;
+ --art-border-dashed-color: #363843;
+
+ // Background
+ --art-bg-color: #070707;
+ --art-main-bg-color: #161618;
+}
+```
+
+### 濯掍綋鏌ヨ锛堣澶囧昂瀵革級
+
+```scss
+$device-notebook: 1600px; // notebook
+$device-ipad-pro: 1180px; // ipad pro
+$device-ipad: 800px; // ipad
+$device-ipad-vertical: 900px; // ipad-绔栧睆
+$device-phone: 500px; // mobile
+```
+
+## 绯荤粺鍏ㄥ眬閰嶇疆
+
+### 绯荤粺 Logo 閰嶇疆
+
+**閰嶇疆鏂囦欢**锛歚src/components/core/base/ArtLogo.vue`
+
+```vue
+<template>
+ <div class="art-logo">
+ <img :style="logoStyle" src="@imgs/common/logo.png" alt="logo" />
+ </div>
+</template>
+```
+
+濡傞渶鏇存崲 Logo锛屽彧闇�淇敼鍥剧墖璧勬簮璺緞鍗冲彲銆�
+
+### 绯荤粺鍚嶇О閰嶇疆
+
+**閰嶇疆鏂囦欢**锛歚src/config/index.ts`
+
+```js
+const appConfig: SystemConfig = {
+ systemInfo: {
+ name: "Art Design Pro", // 绯荤粺鍚嶇О
+ },
+};
+```
+
+### 鍏ㄥ眬閰嶇疆璇︾粏
+
+**閰嶇疆鏂囦欢璺緞**锛歚src/config/setting.ts`
+
+```js
+const appConfig: SystemConfig = {
+ // 绯荤粺淇℃伅
+ systemInfo: {
+ name: "Art Design Pro",
+ },
+
+ // 绯荤粺涓婚鍒楄〃
+ settingThemeList: [
+ {
+ name: "Light",
+ theme: SystemThemeEnum.LIGHT,
+ color: ["#fff", "#fff"],
+ },
+ {
+ name: "Dark",
+ theme: SystemThemeEnum.DARK,
+ color: ["#22252A"],
+ },
+ {
+ name: "System",
+ theme: SystemThemeEnum.AUTO,
+ color: ["#fff", "#22252A"],
+ },
+ ],
+
+ // 鑿滃崟甯冨眬鍒楄〃
+ menuLayoutList: [
+ { name: "Left", value: MenuTypeEnum.LEFT },
+ { name: "Top", value: MenuTypeEnum.TOP },
+ { name: "Mixed", value: MenuTypeEnum.TOP_LEFT },
+ { name: "Dual Column", value: MenuTypeEnum.DUAL_MENU },
+ ],
+
+ // 绯荤粺涓昏壊
+ systemMainColor: [
+ "#5D87FF", // 涓昏壊
+ "#B48DF3", // 绱壊
+ "#1D84FF", // 钃濊壊
+ "#60C041", // 缁胯壊
+ "#38C0FC", // 闈掕壊
+ "#F9901F", // 姗欒壊
+ "#FF80C8", // 绮夎壊
+ ] as const,
+
+ // 绯荤粺鍏朵粬椤归粯璁ら厤缃�
+ systemSetting: {
+ defaultMenuWidth: 240, // 鑿滃崟瀹藉害
+ defaultCustomRadius: "0.75", // 鑷畾涔夊渾瑙�
+ defaultTabStyle: "tab-default", // 鏍囩鏍峰紡
+ },
+};
+```
+
+## 鐩稿叧鏂囨。
+
+- [缁勪欢鍜屽浘鏍囧熀纭�](./components-basics.md)
+- [椤圭洰缁撴瀯](../core-concepts/project-structure.md)
+- [闆嗘垚鎸囧崡](../../INTEGRATION_GUIDE.md)
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/getting-started/style-guide.md b/rsf-design/skill/art-design-pro/docs/getting-started/style-guide.md
new file mode 100644
index 0000000..1277cd8
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/getting-started/style-guide.md
@@ -0,0 +1,355 @@
+# 鏍峰紡瑙勮寖 | Art Design Pro
+
+**鈿狅笍 閲嶈锛氫弗鏍兼墽琛屾湰瑙勮寖锛岀‘淇� UI 缁熶竴鍜岄伩鍏嶆牱寮忓啿绐�**
+
+## 馃搵 鏍稿績鍘熷垯
+
+1. **浼樺厛浣跨敤 Tailwind CSS 宸ュ叿绫�**
+2. **浣跨敤椤圭洰瀹氫箟鐨� CSS 鍙橀噺**
+3. **绂佹缂栧啓鑷畾涔夋牱寮�**
+4. **淇濇寔鍝嶅簲寮忚璁�**
+
+---
+
+## 馃帹 Tailwind CSS 宸ュ叿绫�
+
+### 甯冨眬
+
+```vue
+<!-- 鉁� 鎺ㄨ崘 -->
+<div class="flex flex-wrap gap-4">
+<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
+<div class="flex items-center justify-between">
+
+<!-- 鉂� 绂佹 -->
+<div class="my-layout" style="display: flex; gap: 16px;">
+```
+
+### 闂磋窛
+
+```vue
+<!-- 鉁� 鎺ㄨ崘 -->
+<div class="p-4"> padding: 1rem (16px) </div>
+<div class="px-4 py-2"> 姘村钩鍜屽瀭鐩� </div>
+<div class="m-4"> margin: 1rem </div>
+<div class="mt-2 mb-4"> 涓婁笅杈硅窛 </div>
+<div class="gap-4"> flex/grid gap </div>
+
+<!-- Tailwind 闂磋窛琛� -->
+<!-- p-0: 0, p-1: 4px, p-2: 8px, p-3: 12px, p-4: 16px, p-5: 20px, p-6: 24px -->
+<!-- px/x: 姘村钩, py/y: 鍨傜洿, m: margin, p: padding -->
+```
+
+### 灏哄
+
+```vue
+<!-- 鉁� 鎺ㄨ崘 -->
+<div class="w-full"> width: 100% </div>
+<div class="w-1/2"> width: 50% </div>
+<div class="w-64"> width: 16rem (256px) </div>
+<div class="h-screen"> height: 100vh </div>
+
+<!-- 鍝嶅簲寮忓搴� -->
+<div class="w-full md:w-1/2 lg:w-1/3">
+```
+
+### 鏂囨湰
+
+```vue
+<!-- 鉁� 鎺ㄨ崘 -->
+<div class="text-sm"> 灏忓瓧浣� </div>
+<div class="text-base"> 姝e父瀛椾綋 </div>
+<div class="text-lg"> 澶у瓧浣� </div>
+<div class="font-bold"> 绮椾綋 </div>
+<div class="text-center"> 灞呬腑瀵归綈 </div>
+```
+
+### 棰滆壊锛堜娇鐢� CSS 鍙橀噺锛�
+
+```vue
+<!-- 鉁� 鎺ㄨ崘锛氫娇鐢� CSS 鍙橀噺 -->
+<div class="text-[var(--art-gray-800)]">
+<div style="color: var(--art-gray-800)">
+
+<!-- 鉂� 绂佹锛氱‖缂栫爜棰滆壊 -->
+<div class="text-[#333]">
+<div style="color: #333">
+```
+
+---
+
+## 馃帹 CSS 鍙橀噺
+
+### 棰滆壊鍙橀噺
+
+```scss
+// 鏂囧瓧棰滆壊
+color: var(--art-gray-100); // 娴呰壊
+color: var(--art-gray-800); // 娣辫壊
+color: var(--art-primary); // 涓昏壊
+
+// 鑳屾櫙棰滆壊
+background-color: var(--art-bg-color);
+background-color: var(--art-main-bg-color);
+
+// 杈规棰滆壊
+border: 1px solid var(--art-border-color);
+border: 1px solid var(--art-border-dashed-color);
+
+// 闃村奖
+box-shadow: var(--art-box-shadow);
+box-shadow: var(--art-box-shadow-sm);
+```
+
+### 涓婚棰滆壊
+
+```scss
+// 涓婚鑹�
+color: var(--main-color);
+background-color: var(--el-color-primary-light-1); // 鏈�娣�
+background-color: var(--el-color-primary-light-9); // 鏈�娴�
+```
+
+---
+
+## 馃摫 鍝嶅簲寮忚璁�
+
+### 鏂偣绯荤粺
+
+```vue
+<!-- 鉁� 鎺ㄨ崘锛氫娇鐢� Tailwind 鍝嶅簲寮忓墠缂� -->
+<div class="px-4 md:px-6 lg:px-8">
+<div class="w-full md:w-1/2 lg:w-1/3">
+
+<!-- 绉诲姩绔紭鍏堬細榛樿鏍峰紡鏄Щ鍔ㄧ -->
+<div class="flex-col md:flex-row">
+ <!-- 绉诲姩绔細鍨傜洿鎺掑垪 -->
+ <!-- 涓瓑鍙婁互涓婏細姘村钩鎺掑垪 -->
+</div>
+```
+
+### 鏂偣琛�
+
+| 鍓嶇紑 | 灞忓箷 | 瀹藉害 | 璁惧 |
+|------|------|------|------|
+| (鏃�) | 榛樿 | 0px+ | 绉诲姩绔� |
+| `sm:` | 灏忓睆 | 640px+ | 鎵嬫満妯睆 |
+| `md:` | 涓睆 | 768px+ | 骞虫澘绔栧睆 |
+| `lg:` | 澶у睆 | 1024px+ | 骞虫澘妯睆 |
+| `xl:` | 瓒呭ぇ灞� | 1280px+ | 绗旇鏈� |
+| `2xl:` | 瓒呰秴澶� | 1536px+ | 妗岄潰 |
+
+### 鍝嶅簲寮忕ず渚�
+
+```vue
+<!-- 瀹瑰櫒锛氱Щ鍔ㄧ鍏ㄥ锛屽钩鏉夸竴鍗婏紝妗岄潰涓夊垎涔嬩竴 -->
+<div class="w-full md:w-1/2 lg:w-1/3">
+
+<!-- 甯冨眬锛氱Щ鍔ㄧ鍨傜洿锛屾闈㈡按骞� -->
+<div class="flex flex-col md:flex-row gap-4">
+
+<!-- 鏄剧ず锛氱Щ鍔ㄧ闅愯棌锛屾闈㈡樉绀� -->
+<div class="hidden md:block">
+
+<!-- 闂磋窛锛氱Щ鍔ㄧ灏忥紝妗岄潰澶� -->
+<div class="p-2 md:p-4 lg:p-6">
+```
+
+---
+
+## 鉂� 绂佹琛屼负
+
+### 缁濆绂佹
+
+1. **鉂� 缂栧啓鑷畾涔� CSS 绫�**
+ ```vue
+ <!-- 鉂� 绂佹 -->
+ <style scoped>
+ .my-custom-class {
+ padding: 16px;
+ color: red;
+ }
+ </style>
+ ```
+
+2. **鉂� 浣跨敤鍐呰仈 style锛堥櫎闈炲姩鎬佺粦瀹氾級**
+ ```vue
+ <!-- 鉂� 绂佹 -->
+ <div style="padding: 16px; color: red;">
+
+ <!-- 鉁� 鍏佽锛氬姩鎬佺粦瀹� -->
+ <div :style="{ color: dynamicColor }">
+ ```
+
+3. **鉂� 纭紪鐮侀鑹插��**
+ ```scss
+ /* 鉂� 绂佹 */
+ color: #333;
+ background: #fff;
+ border: 1px solid #ddd;
+
+ /* 鉁� 鎺ㄨ崘 */
+ color: var(--art-gray-800);
+ background: var(--art-main-bg-color);
+ border: 1px solid var(--art-border-color);
+ ```
+
+4. **鉂� 瑕嗙洊 Element Plus 榛樿鏍峰紡**
+ ```vue
+ <!-- 鉂� 绂佹锛氳鐩栫粍浠舵牱寮� -->
+ <style>
+ .el-button {
+ background: red !important;
+ }
+ </style>
+
+ <!-- 鉁� 鎺ㄨ崘锛氫娇鐢ㄧ粍浠� props -->
+ <el-button type="danger">鍒犻櫎</el-button>
+ ```
+
+5. **鉂� 寮曞叆澶栭儴鏍峰紡搴�**
+ ```vue
+ <!-- 鉂� 绂佹锛氶櫎 Tailwind 鍜� Element Plus 澶� -->
+ <link href="bootstrap.css" rel="stylesheet">
+ ```
+
+6. **鉂� 缂栧啓 !important**
+ ```scss
+ /* 鉂� 绂佹锛堥櫎闈炰慨澶嶇粍浠跺簱闂锛� */
+ .my-class {
+ color: red !important;
+ }
+ ```
+
+---
+
+## 鉁� 鎺ㄨ崘鍋氭硶
+
+### 1. 浣跨敤 Tailwind 宸ュ叿绫�
+
+```vue
+<template>
+ <div class="p-4 flex flex-wrap gap-4 border border-gray-200 rounded">
+ <div class="flex-1 min-w-0">
+ <div class="text-sm font-medium text-gray-700">鏍囬</div>
+ <div class="mt-2 text-gray-600">鍐呭</div>
+ </div>
+ </div>
+</template>
+```
+
+### 2. 浣跨敤缁勪欢 props 鎺у埗鏍峰紡
+
+```vue
+<template>
+ <!-- 鉁� 浣跨敤 Element Plus 鐨� props -->
+ <el-button type="primary" size="large" round>
+ 鎸夐挳
+ </el-button>
+
+ <!-- 鉁� 浣跨敤 Art Design Pro 缁勪欢鐨� props -->
+ <art-table :border="true" :stripe="true">
+ </art-table>
+</template>
+```
+
+### 3. 鍔ㄦ�佹牱寮忕粦瀹�
+
+```vue
+<template>
+ <!-- 鉁� 鍔ㄦ�佹牱寮忎娇鐢� CSS 鍙橀噺 -->
+ <div :style="{ color: statusColor }">
+ </div>
+
+ <!-- 鉁� 鍔ㄦ�� class -->
+ <div :class="['base-class', { 'active': isActive }]">
+ </div>
+</template>
+
+<script setup>
+const statusColor = computed(() => {
+ return props.status === 'success'
+ ? 'var(--art-success)'
+ : 'var(--art-error)'
+})
+</script>
+```
+
+### 4. 鍝嶅簲寮忓竷灞�
+
+```vue
+<template>
+ <!-- 鉁� 绉诲姩绔紭鍏� -->
+ <div class="flex flex-col md:flex-row gap-4 p-4">
+ <div class="w-full md:w-1/2 lg:w-1/3">
+ 绉诲姩绔叏瀹斤紝骞虫澘涓�鍗婏紝妗岄潰涓夊垎涔嬩竴
+ </div>
+ </div>
+</template>
+```
+
+---
+
+## 馃帹 甯哥敤 Tailwind 绫婚�熸煡
+
+### 甯冨眬
+- `flex`, `flex-col`, `flex-wrap`
+- `items-center`, `justify-between`
+- `gap-2`, `gap-4`, `gap-6`
+
+### 闂磋窛
+- `p-2` ~ `p-6`: padding
+- `px-4`, `py-2`: 姘村钩/鍨傜洿 padding
+- `m-2` ~ `m-6`: margin
+- `mt-2`, `mb-4`: 涓婁笅 margin
+
+### 灏哄
+- `w-full`: width 100%
+- `w-1/2`, `w-1/3`: 鐧惧垎姣斿搴�
+- `h-screen`: height 100vh
+- `max-w-full`: 鏈�澶у搴� 100%
+
+### 鏂囨湰
+- `text-sm`, `text-base`, `text-lg`: 瀛椾綋澶у皬
+- `font-bold`, `font-medium`: 瀛楅噸
+- `text-center`, `text-right`: 瀵归綈
+
+### 鏄剧ず
+- `hidden`: 闅愯棌
+- `block`, `inline-block`: 鏄剧ず绫诲瀷
+- `md:block`: 涓瓑鍙婁互涓婃樉绀�
+
+---
+
+## 馃摑 浠g爜瀹℃煡娓呭崟
+
+鍦ㄦ彁浜や唬鐮佸墠锛岀‘璁わ細
+
+- [ ] 鎵�鏈夋牱寮忎娇鐢� Tailwind CSS 宸ュ叿绫�
+- [ ] 棰滆壊浣跨敤 CSS 鍙橀噺锛坄var(--art-*)`锛�
+- [ ] 娌℃湁鑷畾涔� `<style scoped>` 鍧�
+- [ ] 娌℃湁鍐呰仈 `style` 灞炴�э紙闄ら潪鍔ㄦ�佺粦瀹氾級
+- [ ] 鍝嶅簲寮忎娇鐢ㄦ柇鐐瑰墠缂�锛坄md:`, `lg:` 绛夛級
+- [ ] 娌℃湁纭紪鐮侀鑹插��
+- [ ] 娌℃湁瑕嗙洊 Element Plus 鏍峰紡
+- [ ] 浣跨敤缁勪欢 props 鑰岄潪鏍峰紡瑕嗙洊
+
+---
+
+**杩濆弽鏈鑼冨彲鑳藉鑷�**锛�
+- 鉂� UI 涓嶇粺涓�
+- 鉂� 鏍峰紡鍐茬獊
+- 鉂� 涓婚鍒囨崲澶辨晥
+- 鉂� 鍝嶅簲寮忓竷灞�閿欒
+- 鉂� 浠g爜瀹℃煡涓嶉�氳繃
+
+---
+
+**閬靛惊鏈鑼冪殑濂藉**锛�
+- 鉁� 淇濇寔璁捐涓�鑷存��
+- 鉁� 鍑忓皯鏍峰紡鍐茬獊
+- 鉁� 浠g爜鏇存槗缁存姢
+- 鉁� 涓婚鑷姩閫傞厤
+- 鉁� 鍝嶅簲寮忓紑绠卞嵆鐢�
+
+
diff --git a/rsf-design/skill/art-design-pro/docs/hooks/use-table.md b/rsf-design/skill/art-design-pro/docs/hooks/use-table.md
new file mode 100644
index 0000000..e9ac4f3
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/docs/hooks/use-table.md
@@ -0,0 +1,551 @@
+# useTable Hook 鏂囨。
+
+## 姒傝堪
+
+`useTable` 鏄� Art Design Pro 鐨勬牳蹇� Hook锛屾彁渚涗簡琛ㄦ牸鏁版嵁绠$悊銆佸垎椤点�佹悳绱€�佸埛鏂扮瓑瀹屾暣鍔熻兘銆�
+
+## 鍩虹鐢ㄦ硶
+
+```js
+import { useTable } from '@/hooks/core/useTable'
+
+const {
+ data, // 琛ㄦ牸鏁版嵁
+ columns, // 鍒楅厤缃�
+ loading, // 鍔犺浇鐘舵��
+ pagination, // 鍒嗛〉淇℃伅
+ getData, // 鑾峰彇鏁版嵁
+ searchParams, // 鎼滅储鍙傛暟
+ resetSearchParams, // 閲嶇疆鎼滅储鍙傛暟
+ handleSizeChange, // 姣忛〉鏁伴噺鍙樺寲
+ handleCurrentChange, // 褰撳墠椤靛彉鍖�
+ refreshData // 鍒锋柊鏁版嵁
+} = useTable({
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20
+ },
+ columnsFactory: () => [...]
+ }
+})
+```
+
+## 閰嶇疆鍙傛暟
+
+### core 閰嶇疆
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| apiFn | Function | 鉁� | API 璇锋眰鍑芥暟 |
+| apiParams | Object | 鉁� | API 璇锋眰鍙傛暟 |
+| columnsFactory | Function | 鉁� | 鍒楅厤缃伐鍘傚嚱鏁� |
+| paginationKey | Object | 鉂� | 鍒嗛〉瀛楁鏄犲皠 |
+
+### transform 閰嶇疆锛堟暟鎹浆鎹級
+
+| 鍙傛暟 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| dataTransformer | Function | 鏁版嵁杞崲鍣� |
+| responseTransformer | Function | 鍝嶅簲杞崲鍣� |
+
+## API 鍑芥暟瑕佹眰
+
+`apiFn` 蹇呴』杩斿洖涓�涓� Promise锛屾帴鏀跺垎椤靛弬鏁帮細
+
+```js
+// API 鍑芥暟绀轰緥
+export async function fetchGetUserList(params: {
+ current: number
+ size: number
+ [key: string]: any
+}) {
+ return request.get<ApiResponseType<UserListItem[]>('/api/users', {
+ params
+ })
+}
+
+// 鍝嶅簲鏍煎紡瑕佹眰
+interface ApiResponseType<T> {
+ current: number // 褰撳墠椤�
+ size: number // 姣忛〉鏁伴噺
+ total: number // 鎬昏褰曟暟
+ records: T[] // 鏁版嵁鍒楄〃
+}
+```
+
+## 鍒楅厤缃�
+
+### 鍩虹鍒楅厤缃�
+
+```js
+columnsFactory: () => [
+ {
+ prop: 'id', // 鏁版嵁瀛楁鍚�
+ label: 'ID', // 鍒楁爣棰�
+ width: 100, // 鍒楀搴︼紙鍥哄畾锛�
+ sortable: true // 鏄惁鍙帓搴�
+ }
+]
+```
+
+### 鈿狅笍 鍒楀閰嶇疆鍘熷垯锛堥噸瑕侊級
+
+**鍘熷垯**锛氶伩鍏嶆墍鏈夊垪閮戒娇鐢ㄥ浐瀹� `width`锛屽惁鍒欎細瀵艰嚧琛ㄦ牸鎬诲搴﹀浐瀹氾紝灞忓箷鏇村鏃跺彸渚у嚭鐜扮┖鐧姐��
+
+| 鍒楃被鍨� | 鎺ㄨ崘閰嶇疆 | 璇存槑 | 绀轰緥 |
+|--------|----------|------|------|
+| **鍥哄畾鍐呭鍒�** | `width: 80-120` | 鐘舵�併�佽鑹层�佺鍙g瓑 | `{ width: 80 }` |
+| **鎿嶄綔鍒�** | `width: 150-260` | 缂栬緫銆佸垹闄ょ瓑鎿嶄綔鎸夐挳 | `{ width: 200, fixed: 'right' }` |
+| **鍙彉鍐呭鍒�** | `minWidth: 100-200` | 鐢ㄦ埛鍚嶃�佸娉ㄣ�佸煙鍚嶇瓑 | `{ minWidth: 120 }` |
+| **鏃堕棿鏃ユ湡鍒�** | `minWidth: 150-180` | 鍒涘缓鏃堕棿銆佹洿鏂版椂闂寸瓑 | `{ minWidth: 160 }` |
+
+**鉁� 姝g‘绀轰緥**锛�
+```js
+columnsFactory: () => [
+ { prop: 'username', label: '鐢ㄦ埛鍚�', minWidth: 120 }, // 鑷姩鎵╁睍
+ { prop: 'remark', label: '澶囨敞', minWidth: 150 }, // 鑷姩鎵╁睍
+ { prop: 'status', label: '鐘舵��', width: 80 }, // 鍥哄畾瀹藉害
+ { prop: 'created_at', label: '鍒涘缓鏃堕棿', minWidth: 160 }, // 鑷姩鎵╁睍
+ { prop: 'action', label: '鎿嶄綔', width: 200, fixed: 'right' } // 鍥哄畾瀹藉害
+]
+```
+
+**鉂� 閿欒绀轰緥**锛�
+```js
+// 鉂� 鎵�鏈夊垪閮戒娇鐢ㄥ浐瀹� width - 浼氬鑷村彸渚х┖鐧�
+columnsFactory: () => [
+ { prop: 'username', label: '鐢ㄦ埛鍚�', width: 150 },
+ { prop: 'remark', label: '澶囨敞', width: 200 },
+ { prop: 'status', label: '鐘舵��', width: 100 }
+]
+```
+
+### 鑷畾涔夊垪娓叉煋
+
+浣跨敤 `formatter` 鍑芥暟鑷畾涔夊垪鍐呭锛�
+
+```js
+{
+ prop: 'status',
+ label: '鐘舵��',
+ formatter: (row: UserItem, column: any, value: any, index: number) => {
+ return h(ElTag, { type: 'success' }, () => '鍚敤')
+ }
+}
+```
+
+### 澶嶆潅鍒楁覆鏌�
+
+缁勫悎澶氫釜缁勪欢锛�
+
+```js
+{
+ prop: 'userInfo',
+ label: '鐢ㄦ埛淇℃伅',
+ width: 280,
+ formatter: (row) => {
+ return h('div', { class: 'user flex-c' }, [
+ h(ElImage, {
+ class: 'size-9.5 rounded-md',
+ src: row.avatar,
+ previewSrcList: [row.avatar],
+ previewTeleported: true
+ }),
+ h('div', { class: 'ml-2' }, [
+ h('p', { class: 'user-name' }, row.userName),
+ h('p', { class: 'email' }, row.userEmail)
+ ])
+ ])
+ }
+}
+```
+
+## 鐗规畩鍒楃被鍨�
+
+### 閫夋嫨鍒�
+
+```js
+{ type: 'selection' }
+```
+
+### 搴忓彿鍒�
+
+```js
+{
+ type: 'index',
+ width: 60,
+ label: '搴忓彿'
+}
+```
+
+### 鎿嶄綔鍒�
+
+```js
+{
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEdit(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDelete(row)
+ })
+ ])
+}
+```
+
+## 鍒嗛〉閰嶇疆
+
+### 榛樿鍒嗛〉瀛楁
+
+```js
+// 榛樿鍒嗛〉鍙傛暟鍚�
+apiParams: {
+ current: 1, // 褰撳墠椤�
+ size: 20 // 姣忛〉鏁伴噺
+}
+
+// 榛樿鍝嶅簲瀛楁鍚�
+{
+ current: number // 褰撳墠椤�
+ size: number // 姣忛〉鏁伴噺
+ total: number // 鎬昏褰曟暟
+}
+```
+
+### 鑷畾涔夊垎椤靛瓧娈垫槧灏�
+
+濡傛灉 API 瀛楁鍚嶄笉鍚岋紝鍙互鑷畾涔夋槧灏勶細
+
+```js
+useTable({
+ core: {
+ apiFn: fetchList,
+ apiParams: {
+ current: 1,
+ size: 20
+ },
+ paginationKey: {
+ current: 'pageNum', // 璇锋眰鍙傛暟涓殑褰撳墠椤靛瓧娈�
+ size: 'pageSize' // 璇锋眰鍙傛暟涓殑姣忛〉鏁伴噺瀛楁
+ }
+ }
+})
+```
+
+## 鏁版嵁杞崲
+
+### 鏁版嵁杞崲鍣�
+
+杞崲 API 杩斿洖鐨勬暟鎹細
+
+```js
+useTable({
+ core: {...},
+ transform: {
+ dataTransformer: (records) => {
+ return records.map(item => ({
+ ...item,
+ avatar: item.avatar || '/default-avatar.png'
+ }))
+ }
+ }
+})
+```
+
+### 鍝嶅簲杞崲鍣�
+
+杞崲鏁翠釜鍝嶅簲锛�
+
+```js
+useTable({
+ core: {...},
+ transform: {
+ responseTransformer: (response) => {
+ return {
+ current: response.data.page,
+ size: response.data.pageSize,
+ total: response.data.total,
+ records: response.data.list
+ }
+ }
+ }
+})
+```
+
+## 鎼滅储鍔熻兘
+
+### 鎼滅储鍙傛暟
+
+```js
+const searchForm = ref({
+ username: '',
+ status: '1'
+})
+
+const { searchParams, getData } = useTable({
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20,
+ ...searchForm.value
+ },
+ columnsFactory: () => [...]
+ }
+})
+
+// 鎵ц鎼滅储
+const handleSearch = (params) => {
+ Object.assign(searchParams, params)
+ getData()
+}
+```
+
+### 閲嶇疆鎼滅储
+
+```js
+const resetSearchParams = () => {
+ searchParams.value = {
+ username: '',
+ status: '1'
+ }
+ getData()
+}
+```
+
+## 鍒锋柊鏁版嵁
+
+```js
+const refreshData = () => {
+ getData()
+}
+```
+
+## 瀹屾暣绀轰緥
+
+### 鍩虹琛ㄦ牸
+
+```js
+const { data, columns, loading, pagination } = useTable({
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20
+ },
+ columnsFactory: () => [
+ { type: 'selection' },
+ { type: 'index', width: 60, label: '搴忓彿' },
+ { prop: 'id', label: 'ID' },
+ { prop: 'username', label: '鐢ㄦ埛鍚�' },
+ { prop: 'email', label: '閭' }
+ ]
+ }
+})
+```
+
+### 甯︽悳绱㈢殑琛ㄦ牸
+
+```vue
+<template>
+ <div class="user-page art-full-height">
+ <ArtSearchBar v-model="searchForm" @search="handleSearch" />
+
+ <ElCard class="art-table-card">
+ <ArtTableHeader v-model:columns="columns" :loading="loading" />
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ fit
+ @pagination:current-change="handleCurrentChange"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import { useTable } from '@/hooks/core/useTable'
+
+ const searchForm = ref({
+ username: '',
+ status: '1'
+ })
+
+ const {
+ data,
+ columns,
+ loading,
+ pagination,
+ searchParams,
+ getData,
+ handleCurrentChange
+ } = useTable({
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20,
+ ...searchForm.value
+ },
+ columnsFactory: () => [...]
+ }
+ })
+
+ const handleSearch = (params) => {
+ Object.assign(searchParams, params)
+ getData()
+ }
+</script>
+```
+
+## API 鍙傝��
+
+### 杩斿洖鍊�
+
+| 灞炴�� | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| data | Ref<T[]> | 琛ㄦ牸鏁版嵁 |
+| columns | Ref<Column[]> | 鍒楅厤缃� |
+| loading | Ref<boolean> | 鍔犺浇鐘舵�� |
+| pagination | Ref<Pagination> | 鍒嗛〉淇℃伅 |
+| getData | Function | 鑾峰彇鏁版嵁鏂规硶 |
+| searchParams | Ref<Object> | 鎼滅储鍙傛暟 |
+| resetSearchParams | Function | 閲嶇疆鎼滅储鍙傛暟 |
+| handleSizeChange | Function | 姣忛〉鏁伴噺鍙樺寲澶勭悊 |
+| handleCurrentChange | Function | 褰撳墠椤靛彉鍖栧鐞� |
+| refreshData | Function | 鍒锋柊鏁版嵁 |
+
+### Pagination 绫诲瀷
+
+```js
+interface Pagination {
+ current: number // 褰撳墠椤�
+ size: number // 姣忛〉鏁伴噺
+ total: number // 鎬昏褰曟暟
+}
+```
+
+## 鏈�浣冲疄璺�
+
+### 1. 浣跨敤 columnsFactory
+
+浣跨敤宸ュ巶鍑芥暟纭繚姣忔鑾峰彇鏈�鏂扮殑鍒楅厤缃細
+
+```js
+// 鉁� 鎺ㄨ崘
+columnsFactory: () => [
+ { prop: 'id', label: 'ID' }
+]
+
+// 鉂� 涓嶆帹鑽�
+columns: [
+ { prop: 'id', label: 'ID' }
+]
+```
+
+### 2. 鍒楅厤缃彁鍙�
+
+灏嗗鏉傜殑鍒楅厤缃彁鍙栧埌鍗曠嫭鐨勫嚱鏁帮細
+
+```js
+const getUserColumns = () => [
+ { type: 'selection' },
+ { type: 'index', width: 60, label: '搴忓彿' },
+ // ... 鍏朵粬鍒�
+]
+
+useTable({
+ core: {
+ columnsFactory: getUserColumns
+ }
+})
+```
+
+### 3. 绫诲瀷瀹氫箟
+
+涓鸿〃鏍兼暟鎹畾涔夌被鍨嬶細
+
+```js
+type UserListItem = Api.SystemManage.UserListItem
+
+const { data } = useTable<UserListItem>({...})
+```
+
+### 4. 鍒楀閰嶇疆瑙勮寖
+
+鈿狅笍 **閲嶈**锛氶伒寰垪瀹介厤缃師鍒欙紝閬垮厤琛ㄦ牸鍙充晶鍑虹幇绌虹櫧锛�
+
+- 鍥哄畾鍐呭鍒椾娇鐢� `width`锛氱姸鎬併�佽鑹层�佹搷浣滃垪
+- 鍙彉鍐呭鍒椾娇鐢� `minWidth`锛氱敤鎴峰悕銆佸娉ㄣ�佸煙鍚嶃�佹椂闂�
+- 鍙傝�冧笂鏂�"鍒楅厤缃�"閮ㄥ垎鐨勮缁嗚鏄�
+
+## 甯歌闂
+
+### Q: 濡備綍瀹炵幇鑷畾涔夊垎椤碉紵
+
+A: 閫氳繃 `paginationKey` 閰嶇疆鑷畾涔夊瓧娈垫槧灏勶細
+
+```js
+paginationKey: {
+ current: 'page',
+ size: 'pageSize'
+}
+```
+
+### Q: 濡備綍澶勭悊涓嶈鍒欑殑鏁版嵁缁撴瀯锛�
+
+A: 浣跨敤 `dataTransformer` 鎴� `responseTransformer`锛�
+
+```js
+transform: {
+ responseTransformer: (res) => {
+ return {
+ current: res.page,
+ size: res.pageSize,
+ total: res.total,
+ records: res.data.list
+ }
+ }
+}
+```
+
+### Q: 濡備綍瀹炵幇鍒楃殑鏄剧ず/闅愯棌锛�
+
+A: 灏� `columns` 閫氳繃 `v-model:columns` 浼犻�掔粰 `ArtTableHeader`锛�
+
+```vue
+<ArtTableHeader v-model:columns="columns" />
+<ArtTable :columns="columns" />
+```
+
+**閲嶈鎻愮ず**锛�
+- `ArtTableHeader` 鍜� `ArtTable` 蹇呴』浣跨敤鍚屼竴涓� `columns` 寮曠敤
+- `columns` 蹇呴』鏄粠 `useTable` 杩斿洖鐨勫搷搴斿紡鏁版嵁
+- 涓嶈鍦� `ArtTable` 涓婃坊鍔� `fit` 灞炴�т互澶栫殑鍏朵粬甯冨眬鐩稿叧灞炴��
+
+## 鐩稿叧鏂囨。
+
+- [ArtTable 缁勪欢](./components/art-table.md)
+- [鍩虹琛ㄦ牸绀轰緥](./examples/tables/basic-table.md)
+- [CRUD 椤甸潰绀轰緥](./examples/templates/crud-page.md)
+
+## 瀹樻柟鏂囨。
+
+- [useTable 瀹樻柟鏂囨。](https://www.artd.pro/docs/zh/guide/hooks/use-table.html)
+
+---
+
+**鏈�鍚庢洿鏂�**锛�2026-03-03
+
+
diff --git a/rsf-design/skill/art-design-pro/scripts/generate.py b/rsf-design/skill/art-design-pro/scripts/generate.py
new file mode 100644
index 0000000..43667e3
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/scripts/generate.py
@@ -0,0 +1,816 @@
+#!/usr/bin/env python3
+"""
+Art Design Pro 浠g爜鐢熸垚鍣�
+
+鑷姩鐢熸垚鏍囧噯鐨� Vue3 椤甸潰浠g爜锛屽寘鎷細
+- CRUD 鍒楄〃椤碉紙甯︽悳绱€�佸脊绐椼�佹搷浣滐級
+- 鍩虹琛ㄦ牸椤碉紙绠�鍗曟暟鎹睍绀猴級
+- 浠〃鏉块〉闈紙鏁版嵁缁熻鍜屽浘琛級
+
+浣跨敤绀轰緥锛�
+ # 鐢熸垚 CRUD 鍒楄〃椤�
+ python generate.py crud --name "User" --path "system/user" --fields "username,email,status"
+
+ # 鐢熸垚鍩虹琛ㄦ牸椤�
+ python generate.py table --name "Product" --path "product/list" --fields "name,price,stock"
+
+ # 鐢熸垚浠〃鏉块〉闈�
+ python generate.py dashboard --name "Analytics" --path "dashboard/analytics" --charts "line,bar"
+"""
+
+import argparse
+import sys
+from typing import List, Dict
+
+if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(encoding="utf-8")
+if hasattr(sys.stderr, "reconfigure"):
+ sys.stderr.reconfigure(encoding="utf-8")
+
+
+# ====================================
+# 妯℃澘瀹氫箟
+# ====================================
+
+CRUD_MAIN_TEMPLATE = """<!-- {name_pascal}绠$悊椤甸潰 -->
+<!-- 浣跨敤 Art Design Pro 缁勪欢搴撳揩閫熸瀯寤� CRUD 椤甸潰 -->
+<template>
+ <div class="{name_lower}-page art-full-height">
+ <!-- 鎼滅储鏍� -->
+ <{name_pascal}Search v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></{name_pascal}Search>
+
+ <ElCard class="art-table-card" shadow="never">
+ <!-- 琛ㄦ牸澶撮儴 -->
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton type="primary" @click="showDialog('add')" v-ripple>鏂板{name_pascal}</ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <!-- 琛ㄦ牸 -->
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ </ArtTable>
+
+ <!-- {name_pascal}寮圭獥 -->
+ <{name_pascal}Dialog
+ v-model:visible="dialogVisible"
+ :type="dialogType"
+ :data="currentData"
+ @submit="handleDialogSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import {{ useTable }} from '@/hooks/core/useTable'
+ import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+ import {name_pascal}Search from './modules/{name_lower}-search.vue'
+ import {name_pascal}Dialog from './modules/{name_lower}-dialog.vue'
+
+ defineOptions({{ name: '{name_pascal}' }})
+
+ // 寮圭獥鐩稿叧
+ const dialogType = ref('add')
+ const dialogVisible = ref(false)
+ const currentData = ref({{}})
+
+ // 鎼滅储琛ㄥ崟
+ const searchForm = ref({{
+{search_fields}
+ }})
+
+ const {{
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ }} = useTable({{
+ core: {{
+ apiFn: yourApiFunction, // TODO: 鏇挎崲涓哄疄闄呯殑 API 鍑芥暟
+ apiParams: {{
+ current: 1,
+ size: 20,
+ ...searchForm.value
+ }},
+ columnsFactory: () => [
+{table_columns}
+ ]
+ }}
+ }})
+
+ /**
+ * 鎼滅储澶勭悊
+ */
+ const handleSearch = (params) => {{
+ replaceSearchParams(params)
+ getData()
+ }}
+
+ /**
+ * 鏄剧ず寮圭獥
+ */
+ const showDialog = (type, row) => {{
+ dialogType.value = type
+ currentData.value = row || {{}}
+ nextTick(() => {{
+ dialogVisible.value = true
+ }})
+ }}
+
+ const deleteItem = (row) => {{
+ console.log('鍒犻櫎鏁版嵁:', row)
+ }}
+
+ /**
+ * 澶勭悊寮圭獥鎻愪氦
+ */
+ const handleDialogSubmit = async () => {{
+ try {{
+ dialogVisible.value = false
+ currentData.value = {{}}
+ refreshData()
+ }} catch (error) {{
+ console.error('鎻愪氦澶辫触:', error)
+ }}
+ }}
+</script>
+"""
+
+CRUD_SEARCH_TEMPLATE = """<!-- {name_pascal}鎼滅储鏍� -->
+<template>
+ <ArtSearchBar
+ ref="searchBarRef"
+ v-model="formData"
+ :items="formItems"
+ :rules="rules"
+ @reset="handleReset"
+ @search="handleSearch"
+ >
+ </ArtSearchBar>
+</template>
+
+<script setup>
+ defineOptions({{ name: '{name_pascal}Search' }})
+
+ const props = defineProps({{
+ modelValue: {{ required: true }}
+ }})
+
+ const emit = defineEmits(['update:modelValue', 'search', 'reset'])
+ const searchBarRef = ref()
+ const rules = {{}}
+ const formData = computed({{
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+ }})
+ const formItems = computed(() => [
+{search_items_config}
+ ])
+
+ const handleReset = () => {{
+ emit('reset')
+ }}
+
+ const handleSearch = async (params) => {{
+ await searchBarRef.value?.validate()
+ emit('search', params)
+ }}
+</script>
+"""
+
+CRUD_DIALOG_TEMPLATE = """<!-- {name_pascal}寮圭獥 -->
+<template>
+ <ElDialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="600px"
+ :before-close="handleClose"
+ >
+ <ArtForm
+ ref="formRef"
+ :items="formItems"
+ :model="formData"
+ :rules="rules"
+ :show-reset="false"
+ :show-submit="false"
+ label-width="120px"
+ />
+
+ <template #footer>
+ <ElButton @click="handleClose">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit" :loading="loading">纭畾</ElButton>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+
+ defineOptions({{ name: '{name_pascal}Dialog' }})
+
+ const props = defineProps({{
+ visible: {{ required: true }},
+ type: {{ required: true }},
+ data: {{ required: false }}
+ }})
+
+ const emit = defineEmits(['update:visible', 'submit'])
+
+ const formRef = ref()
+ const loading = ref(false)
+
+ const dialogVisible = computed({{
+ get: () => props.visible,
+ set: (val) => emit('update:visible', val)
+ }})
+
+ const dialogTitle = computed(() => {{
+ return props.type === 'add' ? '鏂板{name_pascal}' : '缂栬緫{name_pascal}'
+ }})
+
+ const formData = ref({{
+{dialog_field_defaults}
+ }})
+
+ const rules = {{
+ // TODO: 娣诲姞鏍¢獙瑙勫垯
+ }}
+
+ // 琛ㄥ崟椤归厤缃�
+ const formItems = [
+{form_items_config}
+ ]
+
+ watch(() => props.data, (newData) => {{
+ if (newData && Object.keys(newData).length > 0) {{
+ Object.assign(formData.value, newData)
+ }}
+ }}, {{ immediate: true }})
+
+ const handleClose = () => {{
+ formRef.value?.resetFields()
+ dialogVisible.value = false
+ }}
+
+ const handleSubmit = async () => {{
+ try {{
+ await formRef.value?.validate()
+ loading.value = true
+ // TODO: 璋冪敤 API 鎻愪氦鏁版嵁
+ emit('submit')
+ }} catch (error) {{
+ console.error('楠岃瘉澶辫触:', error)
+ }} finally {{
+ loading.value = false
+ }}
+ }}
+</script>
+"""
+
+TABLE_TEMPLATE = """<!-- {name_pascal}鍒楄〃 -->
+<template>
+ <div class="{name_lower}-page art-full-height">
+ <ElCard class="art-table-card" shadow="never">
+ <!-- 琛ㄦ牸 -->
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ </ArtTable>
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import {{ useTable }} from '@/hooks/core/useTable'
+
+ defineOptions({{ name: '{name_pascal}' }})
+
+ const {{
+ data,
+ columns,
+ loading,
+ pagination,
+ handleSizeChange,
+ handleCurrentChange
+ }} = useTable({{
+ core: {{
+ apiFn: yourApiFunction, // TODO: 鏇挎崲涓哄疄闄呯殑 API 鍑芥暟
+ apiParams: {{
+ current: 1,
+ size: 20
+ }},
+ columnsFactory: () => [
+{table_columns}
+ ]
+ }}
+ }})
+</script>
+"""
+
+DASHBOARD_TEMPLATE = """<!-- {name_pascal}浠〃鏉� -->
+<template>
+ <div class="art-full-height">
+ <!-- 缁熻鍗$墖 -->
+ <ElRow :gutter="20" class="mb-5">
+ <ElCol :xs="24" :sm="12" :md="6" v-for="stat in stats" :key="stat.key">
+ <ArtStatsCard
+ :title="stat.title"
+ :count="stat.value"
+ :description="stat.description"
+ :icon="stat.icon"
+ :iconStyle="stat.iconStyle"
+ />
+ </ElCol>
+ </ElRow>
+
+ <!-- 鍥捐〃鍖哄煙 -->
+ <ElRow :gutter="20">
+{chart_sections}
+ </ElRow>
+ </div>
+</template>
+
+<script setup>
+ import {{ ref, onMounted }} from 'vue'
+ import {{ ElRow, ElCol, ElCard }} from 'element-plus'
+ import ArtStatsCard from '@/components/core/cards/art-stats-card/index.vue'
+
+ defineOptions({{ name: '{name_pascal}' }})
+
+ // 缁熻鍗$墖鏁版嵁
+ const stats = ref([
+ {{
+ key: 'total',
+ title: '鎬荤敤鎴锋暟',
+ value: 1234,
+ description: '绯荤粺鎵�鏈夌敤鎴�',
+ icon: 'ri:user-line',
+ iconStyle: 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
+ }},
+ {{
+ key: 'active',
+ title: '娲昏穬鐢ㄦ埛',
+ value: 567,
+ description: '褰撳墠鍦ㄧ嚎鐢ㄦ埛',
+ icon: 'ri:user-follow-line',
+ iconStyle: 'background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
+ }},
+ {{
+ key: 'new',
+ title: '鏂板鐢ㄦ埛',
+ value: 89,
+ description: '浠婃棩鏂板',
+ icon: 'ri:user-add-line',
+ iconStyle: 'background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
+ }},
+ {{
+ key: 'rate',
+ title: '杞寲鐜�',
+ value: 23,
+ description: '杞寲鐧惧垎姣�',
+ icon: 'ri:percent-line',
+ iconStyle: 'background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'
+ }}
+ ])
+
+ // 鍥捐〃鏁版嵁
+ const chartData = ref([
+ {{ name: '鏁版嵁1', data: [120, 132, 101, 134, 90, 230, 210] }},
+ {{ name: '鏁版嵁2', data: [220, 182, 191, 234, 290, 330, 310] }}
+ ])
+
+ const xAxis = ref(['鍛ㄤ竴', '鍛ㄤ簩', '鍛ㄤ笁', '鍛ㄥ洓', '鍛ㄤ簲', '鍛ㄥ叚', '鍛ㄦ棩'])
+
+ // TODO: 鑾峰彇瀹為檯鏁版嵁
+ const fetchDashboardData = async () => {{
+ // 璋冪敤 API 鑾峰彇鏁版嵁
+ }}
+
+ onMounted(() => {{
+ fetchDashboardData()
+ }})
+</script>
+"""
+
+
+# ====================================
+# 鐢熸垚鍣ㄧ被
+# ====================================
+
+class CodeGenerator:
+ """浠g爜鐢熸垚鍣ㄥ熀绫�"""
+
+ def __init__(self, args):
+ self.args = args
+ self.name = args.name
+ self.path = args.path
+ self.fields = self._parse_fields(args.fields) if hasattr(args, 'fields') and args.fields else []
+
+ def _parse_fields(self, fields_str: str) -> List[Dict[str, str]]:
+ """瑙f瀽瀛楁鍙傛暟"""
+ if not fields_str:
+ return []
+
+ fields = []
+ for field in fields_str.split(','):
+ field = field.strip()
+ if ':' in field:
+ name, ftype = field.split(':', 1)
+ fields.append({'name': name.strip(), 'type': ftype.strip()})
+ else:
+ fields.append({'name': field, 'type': 'string'})
+ return fields
+
+ def _to_camel_case(self, snake_str: str) -> str:
+ """杞崲涓洪┘宄板懡鍚�"""
+ components = snake_str.split('_')
+ return components[0] + ''.join(x.title() for x in components[1:])
+
+ def _to_pascal_case(self, snake_str: str) -> str:
+ """杞崲涓哄笗鏂崱鍛藉悕锛堥瀛楁瘝澶у啓锛�"""
+ components = snake_str.split('_')
+ return ''.join(x.title() for x in components)
+
+ def _generate_field_label(self, field: Dict[str, str]) -> str:
+ """鐢熸垚瀛楁鏍囩"""
+ name = field['name']
+ labels = {
+ 'username': '鐢ㄦ埛鍚�',
+ 'email': '閭',
+ 'phone': '鎵嬫満鍙�',
+ 'status': '鐘舵��',
+ 'name': '鍚嶇О',
+ 'created_at': '鍒涘缓鏃堕棿',
+ 'updated_at': '鏇存柊鏃堕棿',
+ 'price': '浠锋牸',
+ 'stock': '搴撳瓨',
+ 'description': '鎻忚堪'
+ }
+ return labels.get(name, self._to_pascal_case(name))
+
+ def generate(self):
+ """鐢熸垚浠g爜锛堝瓙绫诲疄鐜帮級"""
+ raise NotImplementedError
+
+
+class CrudGenerator(CodeGenerator):
+ """CRUD 鍒楄〃椤电敓鎴愬櫒"""
+
+ def generate(self):
+ """鐢熸垚 CRUD 鍒楄〃椤典唬鐮�"""
+ name_lower = self.name.lower()
+ name_camel = self._to_camel_case(name_lower)
+ name_pascal = self._to_pascal_case(self.name)
+
+ # 鐢熸垚鍚勯儴鍒嗕唬鐮�
+ search_fields = self._generate_search_fields()
+ table_columns = self._generate_table_columns()
+
+ # 涓婚〉闈�
+ main_code = CRUD_MAIN_TEMPLATE.format(
+ name_pascal=name_pascal,
+ name_lower=name_lower,
+ name_camel=name_camel,
+ search_fields=search_fields,
+ table_columns=table_columns
+ )
+
+ # 鎼滅储鏍忕粍浠�
+ search_items_config = self._generate_search_items_config()
+
+ search_code = CRUD_SEARCH_TEMPLATE.format(
+ name_pascal=name_pascal,
+ search_items_config=search_items_config
+ )
+
+ # 寮圭獥缁勪欢
+ dialog_field_defaults = self._generate_dialog_fields()
+ form_items_config = self._generate_form_items_config()
+
+ dialog_code = CRUD_DIALOG_TEMPLATE.format(
+ name_pascal=name_pascal,
+ dialog_field_defaults=dialog_field_defaults,
+ form_items_config=form_items_config
+ )
+
+ return {
+ 'main': main_code,
+ 'search': search_code,
+ 'dialog': dialog_code
+ }
+
+ def _generate_search_fields(self) -> str:
+ """鐢熸垚鎼滅储琛ㄥ崟瀛楁"""
+ if not self.fields:
+ return ' // 娣诲姞鎼滅储瀛楁'
+
+ lines = []
+ for field in self.fields[:5]:
+ name = field['name']
+ lines.append(f" {name}: undefined,")
+ return '\n'.join(lines)
+
+ def _generate_table_columns(self) -> str:
+ """鐢熸垚琛ㄦ牸鍒楅厤缃�"""
+ lines = []
+ lines.append(" { type: 'selection' },")
+ lines.append(" { type: 'index', width: 60, label: '搴忓彿' },")
+
+ for field in self.fields[:8]:
+ name = field['name']
+ label = self._generate_field_label(field)
+ lines.append(f" {{ prop: '{name}', label: '{label}' }},")
+
+ # 鎿嶄綔鍒�
+ lines.append(""" {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => showDialog('edit', row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => deleteItem(row)
+ })
+ ])
+ }""")
+
+ return '\n'.join(lines)
+
+ def _generate_form_items(self) -> str:
+ """鐢熸垚琛ㄥ崟椤癸紙宸插簾寮冿紝淇濈暀鐢ㄤ簬鍏煎锛�"""
+ return ''
+
+ def _generate_search_items_config(self) -> str:
+ """鐢熸垚 ArtSearchBar 鐨� items 閰嶇疆"""
+ if not self.fields:
+ return ' // TODO: 娣诲姞鎼滅储瀛楁閰嶇疆'
+
+ lines = []
+ for field in self.fields[:5]:
+ name = field['name']
+ label = self._generate_field_label(field)
+ ftype = field.get('type', 'string')
+
+ # 鏍规嵁绫诲瀷鐢熸垚涓嶅悓鐨勯厤缃�
+ if ftype == 'boolean':
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "select", props: {{ options: [{{ label: "鏄�", value: "true" }}, {{ label: "鍚�", value: "false" }}] }} }},')
+ elif name in ['status', 'type']:
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "select", props: {{ options: [{{ label: "鍚敤", value: "1" }}, {{ label: "绂佺敤", value: "0" }}] }} }},')
+ else:
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "input", props: {{ placeholder: "璇疯緭鍏label}", clearable: true }} }},')
+
+ return '\n'.join(lines)
+
+ def _generate_form_items_config(self) -> str:
+ """鐢熸垚 ArtForm 鐨� items 閰嶇疆"""
+ if not self.fields:
+ return ' // TODO: 娣诲姞琛ㄥ崟瀛楁閰嶇疆'
+
+ lines = []
+ for field in self.fields:
+ name = field['name']
+ label = self._generate_field_label(field)
+ ftype = field.get('type', 'string')
+
+ # 鏍规嵁绫诲瀷鐢熸垚涓嶅悓鐨勯厤缃紙寮圭獥琛ㄥ崟浣跨敤 span: 24锛�
+ if ftype == 'number':
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "number", span: 24, props: {{ placeholder: "璇疯緭鍏label}" }} }},')
+ elif ftype == 'boolean':
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "switch", span: 24 }},')
+ elif ftype == 'date':
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "date", span: 24, props: {{ valueFormat: "YYYY-MM-DD" }} }},')
+ elif name in ['status', 'type']:
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "select", span: 24, props: {{ options: [{{ label: "鍚敤", value: "1" }}, {{ label: "绂佺敤", value: "0" }}] }} }},')
+ else:
+ lines.append(f' {{ key: "{name}", label: "{label}", type: "input", span: 24, props: {{ placeholder: "璇疯緭鍏label}" }} }},')
+
+ return '\n'.join(lines)
+
+ def _generate_interface_fields(self) -> str:
+ """鐢熸垚鎺ュ彛瀛楁"""
+ if not self.fields:
+ return ' // 瀹氫箟瀛楁绫诲瀷'
+
+ lines = []
+ for field in self.fields[:5]:
+ name = field['name']
+ ftype = field.get('type', 'string')
+ lines.append(f" {name}?: {ftype}")
+ return '\n'.join(lines)
+
+ def _generate_dialog_fields(self) -> str:
+ """鐢熸垚寮圭獥琛ㄥ崟瀛楁"""
+ if not self.fields:
+ return ' // 瀹氫箟瀛楁'
+
+ lines = []
+ for field in self.fields:
+ name = field['name']
+ lines.append(f" {name}: '',")
+ return '\n'.join(lines)
+
+
+class TableGenerator(CodeGenerator):
+ """鍩虹琛ㄦ牸椤电敓鎴愬櫒"""
+
+ def generate(self):
+ """鐢熸垚鍩虹琛ㄦ牸椤典唬鐮�"""
+ name_lower = self.name.lower()
+ name_pascal = self._to_pascal_case(self.name)
+
+ table_columns = self._generate_columns()
+
+ code = TABLE_TEMPLATE.format(
+ name_pascal=name_pascal,
+ name_lower=name_lower,
+ table_columns=table_columns
+ )
+
+ return {'main': code}
+
+ def _generate_columns(self) -> str:
+ """鐢熸垚琛ㄦ牸鍒�"""
+ if not self.fields:
+ return " { prop: 'id', label: 'ID' }\n // 娣诲姞鏇村鍒�"
+
+ lines = []
+ for field in self.fields[:10]:
+ name = field['name']
+ label = self._generate_field_label(field)
+ lines.append(f" {{ prop: '{name}', label: '{label}' }},")
+
+ return '\n'.join(lines)
+
+
+class DashboardGenerator(CodeGenerator):
+ """浠〃鏉块〉闈㈢敓鎴愬櫒"""
+
+ def generate(self):
+ """鐢熸垚浠〃鏉块〉浠g爜"""
+ name_lower = self.name.lower()
+ name_pascal = self._to_pascal_case(self.name)
+
+ charts = self.args.charts.split(',') if hasattr(self.args, 'charts') and self.args.charts else ['line']
+ chart_sections = self._generate_chart_sections(charts)
+
+ code = DASHBOARD_TEMPLATE.format(
+ name_pascal=name_pascal,
+ name_lower=name_lower,
+ chart_sections=chart_sections
+ )
+
+ return {'main': code}
+
+ def _generate_chart_sections(self, charts: List[str]) -> str:
+ """鐢熸垚鍥捐〃鍖哄潡"""
+ sections = []
+
+ chart_types = {
+ 'line': ('ArtLineChart', '娴侀噺瓒嬪娍'),
+ 'bar': ('ArtBarChart', '鏁版嵁瀵规瘮'),
+ 'pie': ('ArtRingChart', '鍗犳瘮鍒嗘瀽'),
+ 'radar': ('ArtRadarChart', '澶氱淮鍒嗘瀽')
+ }
+
+ for chart in charts[:4]:
+ chart = chart.strip()
+ if chart in chart_types:
+ component, title = chart_types[chart]
+ sections.append(f' <ElCol :xs="24" :md="12" :lg="12">')
+ sections.append(' <ElCard class="art-table-card" shadow="never">')
+ sections.append(' <template #header>')
+ sections.append(' <div class="art-card-header">')
+ sections.append(' <div class="title">')
+ sections.append(f' <h4>{title}</h4>')
+ sections.append(' <p>鏁版嵁姒傝</p>')
+ sections.append(' </div>')
+ sections.append(' </div>')
+ sections.append(' </template>')
+ sections.append(f' <{component} :data="chartData" :xAxisData="xAxis" height="300px" :showLegend="true" />')
+ sections.append(' </ElCard>')
+ sections.append(' </ElCol>')
+
+ return '\n'.join(sections) if sections else ' <!-- 娣诲姞鍥捐〃 -->'
+
+
+# ====================================
+# 涓诲嚱鏁�
+# ====================================
+
+def main():
+ """涓诲嚱鏁�"""
+ parser = argparse.ArgumentParser(
+ description='Art Design Pro 浠g爜鐢熸垚鍣�',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog='''
+浣跨敤绀轰緥锛�
+ # 鐢熸垚 CRUD 鍒楄〃椤�
+ python generate.py crud --name "User" --path "system/user" --fields "username,email,status"
+
+ # 鐢熸垚鍩虹琛ㄦ牸椤�
+ python generate.py table --name "Product" --path "product/list" --fields "name,price,stock"
+
+ # 鐢熸垚浠〃鏉块〉
+ python generate.py dashboard --name "Analytics" --path "dashboard/analytics" --charts "line,bar"
+ '''
+ )
+
+ subparsers = parser.add_subparsers(dest='type', help='椤甸潰绫诲瀷')
+
+ # CRUD 椤甸潰鐢熸垚鍣�
+ crud_parser = subparsers.add_parser('crud', help='鐢熸垚 CRUD 鍒楄〃椤�')
+ crud_parser.add_argument('--name', required=True, help='瀹炰綋鍚嶇О锛堝 User銆丳roduct锛�')
+ crud_parser.add_argument('--path', required=True, help='椤甸潰璺緞锛堝 system/user锛�')
+ crud_parser.add_argument('--fields', required=True, help='瀛楁鍒楄〃锛堥�楀彿鍒嗛殧锛屽 username,email,status锛�')
+
+ # 琛ㄦ牸椤甸潰鐢熸垚鍣�
+ table_parser = subparsers.add_parser('table', help='鐢熸垚鍩虹琛ㄦ牸椤�')
+ table_parser.add_argument('--name', required=True, help='瀹炰綋鍚嶇О')
+ table_parser.add_argument('--path', required=True, help='椤甸潰璺緞')
+ table_parser.add_argument('--fields', required=True, help='瀛楁鍒楄〃')
+
+ # 浠〃鏉块〉闈㈢敓鎴愬櫒
+ dashboard_parser = subparsers.add_parser('dashboard', help='鐢熸垚浠〃鏉块〉')
+ dashboard_parser.add_argument('--name', required=True, help='浠〃鏉垮悕绉�')
+ dashboard_parser.add_argument('--path', required=True, help='椤甸潰璺緞')
+ dashboard_parser.add_argument('--charts', help='鍥捐〃绫诲瀷锛堥�楀彿鍒嗛殧锛歭ine,bar,pie,radar锛�')
+
+ args = parser.parse_args()
+
+ if not args.type:
+ parser.print_help()
+ sys.exit(1)
+
+ # 鍒涘缓鐢熸垚鍣�
+ generators = {
+ 'crud': CrudGenerator,
+ 'table': TableGenerator,
+ 'dashboard': DashboardGenerator
+ }
+
+ generator_class = generators.get(args.type)
+ if not generator_class:
+ print(f'鉂� 閿欒锛氫笉鏀寔鐨勯〉闈㈢被鍨� "{args.type}"')
+ sys.exit(1)
+
+ generator = generator_class(args)
+ result = generator.generate()
+
+ # 杈撳嚭鐢熸垚鐨勪唬鐮�
+ if args.type == 'crud':
+ print('=' * 80)
+ print('馃搫 涓婚〉闈� (index.vue):')
+ print('=' * 80)
+ print(result['main'])
+ print('\n')
+ print('=' * 80)
+ print('馃搫 鎼滅储鏍忕粍浠� (modules/{}-search.vue):'.format(generator.name.lower()))
+ print('=' * 80)
+ print(result['search'])
+ print('\n')
+ print('=' * 80)
+ print('馃搫 寮圭獥缁勪欢 (modules/{}-dialog.vue):'.format(generator.name.lower()))
+ print('=' * 80)
+ print(result['dialog'])
+ else:
+ print('=' * 80)
+ print(f'馃搫 鐢熸垚鐨� {args.type} 椤甸潰:')
+ print('=' * 80)
+ print(result['main'])
+
+ print('\n鉁� 浠g爜鐢熸垚瀹屾垚锛�')
+ print(f'馃挕 鎻愮ず锛氳灏嗙敓鎴愮殑浠g爜澶嶅埗鍒板搴旂洰褰曪紝骞舵牴鎹疄闄呴渶姹傝皟鏁�')
+ print(f'馃搨 鐩爣鐩綍: src/views/{args.path}/')
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/rsf-design/skill/art-design-pro/scripts/init.py b/rsf-design/skill/art-design-pro/scripts/init.py
new file mode 100644
index 0000000..5a44b62
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/scripts/init.py
@@ -0,0 +1,423 @@
+#!/usr/bin/env python3
+"""
+Art Design Pro Skill 鍒濆鍖栭厤缃�
+
+杩愯姝よ剼鏈互閫傞厤褰撳墠椤圭洰閰嶇疆锛�
+ python init.py
+
+鍔熻兘锛�
+ - 鑷姩妫�娴嬮」鐩被鍨嬶紙瀹屾暣 ADP / 閮ㄥ垎闆嗘垚 / 鏈泦鎴愶級
+ - 鑷姩妫�娴嬮」鐩厤缃�
+ - 鐢熸垚 project-config.json
+"""
+
+import json
+import sys
+from pathlib import Path
+
+if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(encoding="utf-8")
+if hasattr(sys.stderr, "reconfigure"):
+ sys.stderr.reconfigure(encoding="utf-8")
+
+
+class ProjectDetector:
+ """椤圭洰绫诲瀷妫�娴嬪櫒"""
+
+ def __init__(self, project_root):
+ self.project_root = Path(project_root)
+
+ def detect_project_type(self):
+ """妫�娴嬮」鐩被鍨�"""
+ print("馃攳 妫�娴嬮」鐩被鍨�...")
+
+ # 妫�鏌� 1: 鏄惁鏄畬鏁� Art Design Pro 椤圭洰
+ if self._is_full_adp_project():
+ return "full_adp"
+
+ # 妫�鏌� 2: 鏄惁閮ㄥ垎闆嗘垚
+ if self._is_partial_integration():
+ return "partial"
+
+ # 妫�鏌� 3: 鏈泦鎴�
+ return "none"
+
+ def _is_full_adp_project(self):
+ """妫�娴嬫槸鍚︽槸瀹屾暣鐨� Art Design Pro 椤圭洰"""
+ checks = []
+
+ # 妫�鏌ュ叧閿洰褰�
+ checks.append((self.project_root / "src" / "components" / "core").exists())
+ checks.append((self.project_root / "src" / "hooks" / "core").exists())
+ checks.append((self.project_root / "src" / "views" / "examples").exists())
+ checks.append((self.project_root / "src" / "views" / "system").exists())
+
+ # 妫�鏌� README 鎴� package.json
+ readme = self.project_root / "README.md"
+ if readme.exists():
+ with open(readme, encoding="utf-8", errors="ignore") as f:
+ content = f.read()
+ checks.append("Art Design Pro" in content or "art-design-pro" in content)
+
+ # 鑷冲皯 5 椤规鏌ラ�氳繃鎵嶈涓烘槸瀹屾暣椤圭洰
+ return sum(checks) >= 5
+
+ def _is_partial_integration(self):
+ """妫�娴嬫槸鍚﹂儴鍒嗛泦鎴�"""
+ # 鑷冲皯鏈夌粍浠剁洰褰�
+ components_dir = self.project_root / "src" / "components" / "core"
+ if not components_dir.exists():
+ return False
+
+ # 妫�鏌ユ槸鍚︽湁瓒冲澶氱殑缁勪欢锛堥�掑綊鏌ユ壘鎵�鏈夊瓙鐩綍锛�
+ components = list(components_dir.glob("**/index.vue"))
+ return len(components) >= 30
+
+ def check_dependencies(self):
+ """妫�鏌ヤ緷璧栧畨瑁�"""
+ print("\n馃摝 妫�鏌ヤ緷璧�...")
+
+ package_json = self.project_root / "package.json"
+ if not package_json.exists():
+ return {"installed": False, "missing": ["package.json 涓嶅瓨鍦�"]}
+
+ with open(package_json, encoding="utf-8", errors="ignore") as f:
+ try:
+ deps = json.load(f)
+ except json.JSONDecodeError:
+ return {"installed": False, "missing": ["package.json 鏍煎紡閿欒"]}
+
+ required = [
+ "vue", "vue-router", "pinia", "element-plus", "axios", "echarts"
+ ]
+
+ missing = []
+ for dep in required:
+ if dep not in deps.get("dependencies", {}):
+ missing.append(dep)
+
+ if missing:
+ return {"installed": False, "missing": missing}
+ else:
+ return {"installed": True, "missing": []}
+
+ def check_build_config(self):
+ """妫�鏌ユ瀯寤洪厤缃�"""
+ print("\n鈿欙笍 妫�鏌ユ瀯寤洪厤缃�...")
+
+ vite_config = None
+ for candidate in ["vite.config.js", "vite.config.mjs", "vite.config.cjs", "vite.config.js"]:
+ config_path = self.project_root / candidate
+ if config_path.exists():
+ vite_config = config_path
+ break
+
+ if not vite_config:
+ return {"configured": False, "issues": ["vite.config.js 涓嶅瓨鍦�"]}
+
+ with open(vite_config, encoding="utf-8", errors="ignore") as f:
+ content = f.read()
+
+ issues = []
+
+ # 妫�鏌ュ繀闇�鐨勬彃浠�
+ if "unplugin-auto-import" not in content:
+ issues.append("缂哄皯 unplugin-auto-import")
+ if "unplugin-vue-components" not in content:
+ issues.append("缂哄皯 unplugin-vue-components")
+ if "ElementPlusResolver" not in content:
+ issues.append("缂哄皯 ElementPlusResolver")
+ if "'@/'" not in content and '"@/"' not in content:
+ issues.append("缂哄皯璺緞鍒悕閰嶇疆")
+
+ if issues:
+ return {"configured": False, "issues": issues}
+ else:
+ return {"configured": True, "issues": []}
+
+ def check_components(self):
+ """妫�鏌ョ粍浠跺畨瑁�"""
+ print("\n馃З 妫�鏌ョ粍浠�...")
+
+ components_dir = self.project_root / "src" / "components" / "core"
+ if not components_dir.exists():
+ return {"installed": False, "count": 0, "status": "鐩綍涓嶅瓨鍦�"}
+
+ components = list(components_dir.glob("**/index.vue"))
+ count = len(components)
+
+ if count == 0:
+ return {"installed": False, "count": 0, "status": "鏃犵粍浠�"}
+ elif count < 50:
+ return {"installed": False, "count": count, "status": "涓嶅畬鏁�"}
+ else:
+ return {"installed": True, "count": count, "status": "瀹屾暣"}
+
+ def generate_integration_report(self):
+ """鐢熸垚闆嗘垚鎶ュ憡"""
+ print("\n" + "="*60)
+ print("馃搳 Art Design Pro 闆嗘垚鐘舵�佹姤鍛�")
+ print("="*60)
+
+ project_type = self.detect_project_type()
+
+ if project_type == "full_adp":
+ print("\n鉁� 妫�娴嬬粨鏋滐細瀹屾暣鐨� Art Design Pro 椤圭洰")
+ print("\n馃帀 鎵�鏈夌粍浠跺凡灏辩华锛佷綘鍙互锛�")
+ print(" 1. 浣跨敤 search.py 鏌ユ壘缁勪欢")
+ print(" python skill/art-design-pro/scripts/search.py table")
+ print(" 2. 浣跨敤 generate.py 鐢熸垚浠g爜")
+ print(" python skill/art-design-pro/scripts/generate.py --help")
+ print(" 3. 鏌ョ湅 INTEGRATION_GUIDE.md 浜嗚В闆嗘垚璇︽儏")
+ print(" 4. 鏌ョ湅 docs/ 鐩綍涓殑鏂囨。")
+
+ return True
+
+ elif project_type == "partial":
+ print("\n鈿狅笍 妫�娴嬬粨鏋滐細閮ㄥ垎闆嗘垚 Art Design Pro")
+
+ # 璇︾粏妫�鏌�
+ deps = self.check_dependencies()
+ config = self.check_build_config()
+ components = self.check_components()
+
+ print("\n鍙戠幇鐨勯棶棰橈細")
+
+ if not deps["installed"]:
+ print(f"\n鉂� 渚濊禆闂:")
+ for dep in deps["missing"][:3]:
+ print(f" - {dep}")
+ print(" 淇: pnpm add " + " ".join(deps["missing"]))
+
+ if not config["configured"]:
+ print(f"\n鉂� 閰嶇疆闂:")
+ for issue in config["issues"]:
+ print(f" - {issue}")
+ print(" 淇: 鍙傝�� templates/vite.config.js.template")
+
+ if not components["installed"]:
+ print(f"\n鈿狅笍 缁勪欢闂: {components['status']}锛坽components['count']} 涓級")
+ print(" 寤鸿: 妫�鏌ョ粍浠剁洰褰曟槸鍚﹀畬鏁�")
+
+ print("\n馃挕 涓嬩竴姝ユ搷浣滐細")
+ print(" 1. 淇涓婅堪闂")
+ print(" 2. 杩愯楠岃瘉鑴氭湰: python skill/art-design-pro/scripts/verify.py")
+ print(" 3. 鏌ョ湅闆嗘垚鎸囧崡: INTEGRATION_GUIDE.md")
+
+ return False
+
+ else: # project_type == "none"
+ print("\n鉂� 妫�娴嬬粨鏋滐細鏈泦鎴� Art Design Pro")
+
+ print("\n馃挕 浣犻渶瑕佸厛闆嗘垚 Art Design Pro锛�")
+ print("\n 鎺ㄨ崘鏂瑰紡锛氬厠闅嗗畬鏁撮」鐩�")
+ print(" git clone https://github.com/Daymychen/art-design-pro.git your-project")
+ print(" cd your-project")
+ print(" pnpm install")
+ print(" pnpm dev")
+
+ print("\n 鎴栨煡鐪嬮泦鎴愭寚鍗�:")
+ print(" 馃搫 INTEGRATION_GUIDE.md - 瀹屾暣鐨勯泦鎴愭楠�")
+
+ return False
+
+
+# ==================== 鍘熸湁鍔熻兘淇濇寔涓嶅彉 ====================
+
+def get_config_path():
+ """鑾峰彇閰嶇疆鏂囦欢璺緞"""
+ return Path(__file__).parent.parent / "project-config.json"
+
+
+def load_config():
+ """鍔犺浇褰撳墠閰嶇疆"""
+ config_path = get_config_path()
+ if config_path.exists():
+ with open(config_path, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ return None
+
+
+def save_config(config):
+ """淇濆瓨閰嶇疆"""
+ config_path = get_config_path()
+ with open(config_path, 'w', encoding='utf-8') as f:
+ json.dump(config, f, indent=2, ensure_ascii=False)
+ print(f"鉁� 閰嶇疆宸蹭繚瀛樺埌: {config_path}")
+
+
+def auto_detect_project():
+ """鑷姩妫�娴嬮」鐩厤缃�"""
+ skill_path = Path(__file__).parent.parent
+ project_root = skill_path.parent.parent
+
+ config = {
+ "project": {
+ "name": project_root.name,
+ "root": str(project_root)
+ },
+ "paths": {},
+ "artDesignPro": {},
+ "generation": {}
+ }
+
+ # 妫�娴嬪墠绔洰褰�
+ possible_src = [
+ project_root / "frontend" / "src",
+ project_root / "src",
+ project_root / "client" / "src",
+ ]
+
+ src_dir = None
+ for path in possible_src:
+ if path.exists():
+ src_dir = path
+ break
+
+ if src_dir:
+ config["paths"]["src"] = str(src_dir.relative_to(project_root))
+ config["paths"]["views"] = f"{src_dir.relative_to(project_root)}/views"
+ config["paths"]["components"] = f"{src_dir.relative_to(project_root)}/components"
+ config["paths"]["router"] = f"{src_dir.relative_to(project_root)}/router"
+ config["paths"]["api"] = f"{src_dir.relative_to(project_root)}/api"
+
+ # 妫�娴� Art Design Pro 缁勪欢
+ core_components = src_dir / "components" / "core"
+ if core_components.exists():
+ config["artDesignPro"]["componentsPath"] = f"{src_dir.relative_to(project_root)}/components/core"
+
+ # 妫�娴嬬ず渚嬬洰褰�
+ examples = src_dir / "views" / "examples"
+ if examples.exists():
+ config["artDesignPro"]["examplesPath"] = f"{src_dir.relative_to(project_root)}/views/examples"
+
+ # 妫�娴嬬郴缁熺洰褰�
+ system = src_dir / "views" / "system"
+ if system.exists():
+ config["artDesignPro"]["systemPath"] = f"{src_dir.relative_to(project_root)}/views/system"
+
+ config["generation"]["outputPath"] = f"{src_dir.relative_to(project_root)}/views"
+ config["generation"]["useJavaScript"] = False
+ config["generation"]["useJavaScript"] = True
+ config["generation"]["useCompositionAPI"] = True
+
+ return config
+
+
+def interactive_config():
+ """浜や簰寮忛厤缃�"""
+ print("馃殌 Art Design Pro Skill 鍒濆鍖栭厤缃甛n")
+ print("璇峰洖绛斾互涓嬮棶棰樹互閰嶇疆 skill锛堢洿鎺ュ洖杞︿娇鐢ㄩ粯璁ゅ�硷級:\n")
+
+ config = {
+ "project": {
+ "name": input("椤圭洰鍚嶇О: ").strip() or "your-project"
+ },
+ "paths": {},
+ "artDesignPro": {},
+ "generation": {
+ "useJavaScript": False,
+ "useJavaScript": True,
+ "useCompositionAPI": True
+ }
+ }
+
+ # 璺緞閰嶇疆
+ src = input(f"婧愪唬鐮佺洰褰� [src]: ").strip() or "src"
+ config["paths"]["src"] = src
+ config["paths"]["views"] = f"{src}/views"
+ config["paths"]["components"] = f"{src}/components"
+ config["paths"]["router"] = f"{src}/router"
+ config["paths"]["api"] = f"{src}/api"
+
+ # Art Design Pro 閰嶇疆
+ comp_path = input(f"Art Design Pro 缁勪欢璺緞 [src/components/core]: ").strip() or "src/components/core"
+ config["artDesignPro"]["componentsPath"] = comp_path
+
+ examples_path = input(f"绀轰緥椤甸潰璺緞 [src/views/examples]: ").strip() or "src/views/examples"
+ config["artDesignPro"]["examplesPath"] = examples_path
+
+ # 浠g爜鐢熸垚閰嶇疆
+ output_path = input(f"鐢熸垚浠g爜杈撳嚭璺緞 [src/views]: ").strip() or "src/views"
+ config["generation"]["outputPath"] = output_path
+
+ return config
+
+
+def main():
+ """涓诲嚱鏁�"""
+ import argparse
+
+ parser = argparse.ArgumentParser(description='Art Design Pro Skill 鍒濆鍖栭厤缃�')
+ parser.add_argument('--check', '-c', action='store_true', help='妫�鏌ラ」鐩泦鎴愮姸鎬�')
+ parser.add_argument('--auto', '-a', action='store_true', help='鑷姩妫�娴嬪苟鐢熸垚閰嶇疆')
+ args = parser.parse_args()
+
+ # 濡傛灉鏄鏌ユā寮�
+ if args.check:
+ skill_path = Path(__file__).parent.parent
+ project_root = skill_path.parent.parent
+
+ detector = ProjectDetector(project_root)
+ detector.generate_integration_report()
+ return
+
+ print("=" * 60)
+ print("Art Design Pro Skill - 鍒濆鍖栭厤缃�")
+ print("=" * 60)
+ print()
+
+ # 妫�鏌ユ槸鍚﹀凡鏈夐厤缃�
+ existing_config = load_config()
+ if existing_config and not args.auto:
+ print(f"馃搵 褰撳墠閰嶇疆:")
+ print(json.dumps(existing_config, indent=2, ensure_ascii=False))
+ print()
+ choice = input("鏄惁閲嶆柊閰嶇疆? (y/N): ").strip().lower()
+ if choice != 'y':
+ print("鉁� 淇濇寔鐜版湁閰嶇疆")
+ print()
+ print("馃挕 鎻愮ず: 浣跨敤 --check 閫夐」妫�鏌ラ泦鎴愮姸鎬�")
+ return
+
+ # 閫夋嫨閰嶇疆鏂瑰紡
+ if args.auto:
+ print("\n馃攳 浣跨敤鑷姩妫�娴嬫ā寮�...")
+ choice = "1"
+ else:
+ print("\n閰嶇疆鏂瑰紡:")
+ print("1. 鑷姩妫�娴嬶紙鎺ㄨ崘锛�")
+ print("2. 鎵嬪姩閰嶇疆")
+ choice = input("\n璇烽�夋嫨 (1/2): ").strip() or "1"
+
+ if choice == "1":
+ print("\n馃攳 姝e湪鑷姩妫�娴嬮」鐩厤缃�...")
+ config = auto_detect_project()
+ print("鉁� 鑷姩妫�娴嬪畬鎴�")
+ else:
+ config = interactive_config()
+
+ # 淇濆瓨閰嶇疆
+ save_config(config)
+
+ # 鏄剧ず閰嶇疆鎽樿
+ print("\n" + "=" * 60)
+ print("閰嶇疆鎽樿:")
+ print("=" * 60)
+ print(f"椤圭洰鍚嶇О: {config['project']['name']}")
+ print(f"婧愪唬鐮佺洰褰�: {config['paths'].get('src', 'src')}")
+ print(f"缁勪欢璺緞: {config['artDesignPro'].get('componentsPath', 'src/components/core')}")
+ print(f"鐢熸垚杈撳嚭: {config['generation'].get('outputPath', 'src/views')}")
+ print()
+ print("鉁� 鍒濆鍖栧畬鎴愶紒鐜板湪鍙互浣跨敤 skill 浜嗐��")
+ print()
+ print("涓嬩竴姝�:")
+ print(" 1. 娴嬭瘯缁勪欢鎼滅储: python scripts/search.py table")
+ print(" 2. 鐢熸垚浠g爜: python scripts/generate.py crud --name 'User' --path 'system/user' --fields 'username,email'")
+ print(" 3. 妫�鏌ラ泦鎴愮姸鎬�: python scripts/verify.py")
+ print()
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/rsf-design/skill/art-design-pro/scripts/list.py b/rsf-design/skill/art-design-pro/scripts/list.py
new file mode 100644
index 0000000..8de80f9
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/scripts/list.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+"""
+Art Design Pro 缁勪欢鍒楄〃宸ュ叿
+
+鍒楀嚭鎵�鏈夊彲鐢ㄧ殑 Art Design Pro 缁勪欢
+"""
+
+import csv
+import sys
+from pathlib import Path
+
+if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(encoding="utf-8")
+if hasattr(sys.stderr, "reconfigure"):
+ sys.stderr.reconfigure(encoding="utf-8")
+
+COMPONENTS_DB = Path(__file__).parent.parent / "data" / "components.csv"
+
+
+def main():
+ """鍒楀嚭鎵�鏈夌粍浠�"""
+
+ with open(COMPONENTS_DB, "r", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+
+ # 鎸夊垎绫荤粍缁�
+ categories = {}
+ for row in reader:
+ cat = row["category"]
+ if cat not in categories:
+ categories[cat] = []
+ categories[cat].append(row)
+
+ print("\n" + "=" * 70)
+ print("馃摝 Art Design Pro 缁勪欢搴撳畬鏁村垪琛�".center(70))
+ print("=" * 70)
+
+ # 瀹氫箟鍒嗙被鏄剧ず椤哄簭鍜屼腑鏂囨爣棰�
+ category_titles = {
+ "tables": ("馃搳 琛ㄦ牸涓庢暟鎹睍绀�", "tables"),
+ "forms": ("馃摑 琛ㄥ崟涓庤緭鍏�", "forms"),
+ "cards": ("馃幋 鍗$墖缁勪欢", "cards"),
+ "charts": ("馃搱 鍥捐〃缁勪欢", "charts"),
+ "layouts": ("馃帹 甯冨眬涓庡鑸�", "layouts"),
+ "media": ("馃幀 濯掍綋缁勪欢", "media"),
+ "banners": ("馃帾 妯箙缁勪欢", "banners"),
+ "text-effect": ("鉁� 鏂囨湰鐗规晥", "text-effect"),
+ "base": ("馃敡 鍩虹缁勪欢", "base"),
+ "widget": ("馃幆 灏忛儴浠�", "widget"),
+ "others": ("馃攲 鍏朵粬缁勪欢", "others"),
+ }
+
+ # 鎸夊畾涔夐『搴忚緭鍑�
+ for cat_key, (title, _) in category_titles.items():
+ if cat_key not in categories:
+ continue
+
+ print(f"\n{title}")
+ print("-" * 70)
+
+ for comp in categories[cat_key]:
+ print(
+ f" 鈥� {comp['component']:<30} {comp['name_cn']:<20} {comp['name_en']}"
+ )
+
+ print("\n" + "=" * 70)
+ print(f"馃搳 鍏辫 {sum(len(comps) for comps in categories.values())} 涓粍浠�")
+ print("=" * 70 + "\n")
+
+ print("馃挕 浣跨敤鏂规硶:")
+ print(" python scripts/search.py \"鍏抽敭璇峔" # 鎼滅储缁勪欢")
+ print(" python scripts/search.py --list # 鏌ョ湅鎵�鏈夊垎绫籠n")
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/rsf-design/skill/art-design-pro/scripts/search.py b/rsf-design/skill/art-design-pro/scripts/search.py
new file mode 100644
index 0000000..214abff
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/scripts/search.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+"""
+Art Design Pro 缁勪欢鎼滅储宸ュ叿
+
+鐢ㄦ硶锛�
+ python search.py "琛ㄦ牸" # 鎼滅储涓枃鍏抽敭璇�
+ python search.py "table" # 鎼滅储鑻辨枃鍏抽敭璇�
+ python search.py "form" --category forms # 鎸夊垎绫绘悳绱�
+"""
+
+import csv
+import sys
+import argparse
+from pathlib import Path
+
+if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(encoding="utf-8")
+if hasattr(sys.stderr, "reconfigure"):
+ sys.stderr.reconfigure(encoding="utf-8")
+
+# 缁勪欢鏁版嵁搴撹矾寰�
+COMPONENTS_DB = Path(__file__).parent.parent / "data" / "components.csv"
+
+
+def load_components():
+ """鍔犺浇鎵�鏈夌粍浠�"""
+ components = []
+ with open(COMPONENTS_DB, "r", encoding="utf-8") as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ components.append(row)
+ return components
+
+
+def search_components(keyword: str, category: str = None):
+ """鎼滅储缁勪欢"""
+ components = load_components()
+ keyword = keyword.lower()
+
+ results = []
+
+ for comp in components:
+ # 濡傛灉鎸囧畾浜嗗垎绫伙紝鍏堣繃婊�
+ if category and comp["category"] != category:
+ continue
+
+ # 鎼滅储鑼冨洿锛氫腑鏂囧悕绉般�佽嫳鏂囧悕绉般�佹弿杩般�佸父瑙佺敤娉�
+ search_fields = [
+ comp.get("name_cn") or "",
+ comp.get("name_en") or "",
+ comp.get("description") or "",
+ comp.get("component") or "",
+ comp.get("common_usage") or "",
+ ]
+
+ # 浠讳綍瀛楁鍖归厤鍗冲懡涓�
+ if any(keyword in field.lower() for field in search_fields):
+ results.append(comp)
+
+ return results
+
+
+def format_component(comp: dict):
+ """鏍煎紡鍖栧崟涓粍浠朵俊鎭�"""
+ return f"""
+鈹屸攢 {comp['component']} ({comp['name_cn']})
+鈹�
+鈹� 馃摑 {comp['description']}
+鈹�
+鈹� 馃摝 瀵煎叆: {comp['import_path']}
+鈹�
+鈹� 馃敡 Props: {comp['props']}
+鈹� 馃幆 Slots: {comp['slots']}
+鈹�
+鈹� 馃挕 甯歌鍦烘櫙: {comp['common_usage']}
+鈹斺攢鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�"""
+
+
+def print_results(results: list):
+ """鎵撳嵃鎼滅储缁撴灉"""
+ if not results:
+ print("鉂� 鏈壘鍒板尮閰嶇殑缁勪欢")
+ print("\n馃挕 鎻愮ず锛氬皾璇曚娇鐢ㄦ洿閫氱敤鐨勫叧閿瘝")
+ print(" 渚嬪锛�'琛ㄦ牸'銆�'琛ㄥ崟'銆�'鍥捐〃'銆�'鍗$墖'銆�'甯冨眬'")
+ return
+
+ print(f"\n鉁� 鎵惧埌 {len(results)} 涓粍浠�:\n")
+
+ for i, comp in enumerate(results, 1):
+ print(f"{i}. {comp['component']} - {comp['name_cn']}")
+ print(f" {comp['description']}")
+ print(f" 鐢ㄩ��: {comp['common_usage']}")
+ print()
+
+ # 濡傛灉缁撴灉瓒呰繃5涓紝璇㈤棶鏄惁鏌ョ湅璇︽儏
+ if len(results) > 5:
+ print("馃挕 浣跨敤 --detail 鍙傛暟鏌ョ湅缁勪欢璇︾粏淇℃伅\n")
+ else:
+ print("\n璇︾粏淇℃伅:")
+ print("=" * 60)
+ for comp in results:
+ print(format_component(comp))
+ print()
+
+
+def print_detailed_results(results: list):
+ """鎵撳嵃璇︾粏鎼滅储缁撴灉"""
+ if not results:
+ print("鉂� 鏈壘鍒板尮閰嶇殑缁勪欢")
+ return
+
+ print(f"\n鉁� 鎵惧埌 {len(results)} 涓粍浠�:\n")
+ print("=" * 60)
+
+ for i, comp in enumerate(results, 1):
+ print(f"\n銆恵i}銆憑comp['component']} - {comp['name_cn']}")
+ print(format_component(comp))
+
+
+def list_all_categories():
+ """鍒楀嚭鎵�鏈夊垎绫�"""
+ components = load_components()
+ categories = sorted(set(c["category"] for c in components))
+
+ print("\n馃搧 缁勪欢鍒嗙被:\n")
+ for cat in categories:
+ count = sum(1 for c in components if c["category"] == cat)
+ print(f" 鈥� {cat}: {count} 涓粍浠�")
+
+ print("\n馃挕 浣跨敤 --category <鍒嗙被鍚�> 鎼滅储鐗瑰畾鍒嗙被鐨勭粍浠�")
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Art Design Pro 缁勪欢鎼滅储宸ュ叿",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+绀轰緥锛�
+ python search.py "琛ㄦ牸" # 鎼滅储涓枃鍏抽敭璇�
+ python search.py "table" # 鎼滅储鑻辨枃鍏抽敭璇�
+ python search.py "form" --category forms # 鎸夊垎绫绘悳绱�
+ python search.py --list # 鍒楀嚭鎵�鏈夊垎绫�
+ python search.py "chart" --detail # 鏌ョ湅璇︾粏淇℃伅
+ """
+ )
+
+ parser.add_argument("keyword", nargs="?", help="鎼滅储鍏抽敭璇�")
+ parser.add_argument("--category", "-c", help="鎸夊垎绫昏繃婊� (tables, forms, cards, charts, layouts, etc.)")
+ parser.add_argument("--detail", "-d", action="store_true", help="鏄剧ず璇︾粏淇℃伅")
+ parser.add_argument("--list", "-l", action="store_true", help="鍒楀嚭鎵�鏈夊垎绫�")
+
+ args = parser.parse_args()
+
+ # 鍒楀嚭鎵�鏈夊垎绫�
+ if args.list:
+ list_all_categories()
+ return
+
+ # 蹇呴』鎻愪緵鍏抽敭璇�
+ if not args.keyword:
+ parser.print_help()
+ return
+
+ # 鎼滅储缁勪欢
+ results = search_components(args.keyword, args.category)
+
+ # 鎵撳嵃缁撴灉
+ if args.detail:
+ print_detailed_results(results)
+ else:
+ print_results(results)
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/rsf-design/skill/art-design-pro/scripts/verify.py b/rsf-design/skill/art-design-pro/scripts/verify.py
new file mode 100644
index 0000000..b69f4f7
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/scripts/verify.py
@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+"""
+Art Design Pro 闆嗘垚楠岃瘉宸ュ叿
+
+鐢ㄦ硶:
+ python scripts/verify.py
+ python scripts/verify.py --verbose
+
+鍔熻兘:
+ 妫�鏌ラ」鐩殑 Art Design Pro 闆嗘垚鐘舵�侊紝鍖呮嫭渚濊禆銆侀厤缃�佺粍浠剁瓑
+"""
+
+import os
+import sys
+import json
+from pathlib import Path
+
+if hasattr(sys.stdout, "reconfigure"):
+ sys.stdout.reconfigure(encoding="utf-8")
+if hasattr(sys.stderr, "reconfigure"):
+ sys.stderr.reconfigure(encoding="utf-8")
+
+
+class IntegrationVerifier:
+ def __init__(self, project_root=None, verbose=False):
+ # 濡傛灉鏈寚瀹氶」鐩牴鐩綍锛屼娇鐢ㄥ綋鍓嶇洰褰�
+ self.project_root = Path(project_root) if project_root else Path.cwd()
+ self.verbose = verbose
+ self.issues = []
+ self.warnings = []
+ self.success = []
+
+ def log(self, message):
+ """鎵撳嵃鏃ュ織"""
+ print(message)
+
+ def check_dependencies(self):
+ """妫�鏌ヤ緷璧栧畨瑁呮儏鍐�"""
+ self.log("\n馃摝 妫�鏌ヤ緷璧�...")
+
+ package_json = self.project_root / 'package.json'
+ if not package_json.exists():
+ self.issues.append("鉂� package.json 涓嶅瓨鍦�")
+ self.log(" 鉂� package.json 涓嶅瓨鍦�")
+ return False
+
+ with open(package_json, encoding='utf-8', errors='ignore') as f:
+ try:
+ deps = json.load(f)
+ except json.JSONDecodeError:
+ self.issues.append("鉂� package.json 鏍煎紡閿欒")
+ self.log(" 鉂� package.json 鏍煎紡閿欒")
+ return False
+
+ # 蹇呴渶鐨勭敓浜т緷璧�
+ required_deps = [
+ 'vue', 'vue-router', 'pinia',
+ 'element-plus', 'axios', 'echarts'
+ ]
+
+ # 蹇呴渶鐨勫紑鍙戜緷璧�
+ required_dev_deps = [
+ 'vite',
+ 'unplugin-auto-import',
+ 'unplugin-vue-components',
+ 'unplugin-element-plus'
+ ]
+
+ missing = []
+
+ # 妫�鏌ョ敓浜т緷璧�
+ for dep in required_deps:
+ if dep not in deps.get('dependencies', {}):
+ missing.append(f"{dep} (dependencies)")
+
+ # 妫�鏌ュ紑鍙戜緷璧�
+ for dep in required_dev_deps:
+ if dep not in deps.get('devDependencies', {}):
+ missing.append(f"{dep} (devDependencies)")
+
+ if missing:
+ issue = f"鉂� 缂哄皯渚濊禆: {', '.join(missing[:3])}"
+ if len(missing) > 3:
+ issue += f" 绛� {len(missing)} 涓�"
+ self.issues.append(issue)
+ self.log(f" {issue}")
+ return False
+ else:
+ self.log(" 鉁� dependencies: 鎵�鏈夊繀闇�渚濊禆宸插畨瑁�")
+ self.success.append("dependencies")
+ return True
+
+ def check_vite_config(self):
+ """妫�鏌� Vite 閰嶇疆"""
+ self.log("\n鈿欙笍 妫�鏌� Vite 閰嶇疆...")
+
+ vite_config = None
+ for candidate in ['vite.config.js', 'vite.config.mjs', 'vite.config.cjs', 'vite.config.js']:
+ config_path = self.project_root / candidate
+ if config_path.exists():
+ vite_config = config_path
+ break
+
+ if not vite_config:
+ self.issues.append("鉂� vite.config.js 涓嶅瓨鍦�")
+ self.log(" 鉂� vite.config.js 涓嶅瓨鍦�")
+ return False
+
+ with open(vite_config, encoding='utf-8', errors='ignore') as f:
+ content = f.read()
+
+ # 妫�鏌ュ叧閿厤缃」
+ checks = {
+ 'unplugin-auto-import': 'unplugin-auto-import' in content,
+ 'unplugin-vue-components': 'unplugin-vue-components' in content,
+ 'ElementPlusResolver': 'ElementPlusResolver' in content,
+ 'path alias': ('@/' in content or "'@':" in content or '"@":' in content),
+ 'tailwindcss': 'tailwindcss' in content.lower()
+ }
+
+ issues = [name for name, passed in checks.items() if not passed]
+ if issues:
+ issue = f"鉂� build_config: 缂哄皯鎴栭厤缃敊璇�: {', '.join(issues)}"
+ self.issues.append(issue)
+ self.log(f" {issue}")
+
+ if self.verbose:
+ self.log(" 璇︾粏璇存槑:")
+ if 'unplugin-auto-import' in issues:
+ self.log(" - 缂哄皯 unplugin-auto-import 閰嶇疆")
+ if 'path alias' in issues:
+ self.log(" - 缂哄皯璺緞鍒悕閰嶇疆锛園/锛�")
+ self.log(" 鍙傝��: templates/vite.config.js.template")
+ return False
+ else:
+ self.log(" 鉁� build_config: Vite 閰嶇疆姝g‘")
+ self.success.append("build_config")
+ return True
+
+ def check_components(self):
+ """妫�鏌ョ粍浠跺畨瑁�"""
+ self.log("\n馃З 妫�鏌ョ粍浠�...")
+
+ components_dir = self.project_root / 'src' / 'components' / 'core'
+ if not components_dir.exists():
+ self.issues.append("鉂� components: src/components/core 鐩綍涓嶅瓨鍦�")
+ self.log(" 鉂� components: src/components/core 鐩綍涓嶅瓨鍦�")
+ self.log(" 寤鸿: 鍏堣繍琛� python scripts/init.py")
+ return False
+
+ # 缁熻缁勪欢鏁伴噺锛堥�掑綊鏌ユ壘鎵�鏈夊瓙鐩綍锛�
+ components = list(components_dir.glob('**/index.vue'))
+ component_count = len(components)
+
+ if component_count < 50:
+ warning = f"鈿狅笍 components: 鍙壘鍒� {component_count} 涓粍浠讹紙棰勬湡 56+锛�"
+ self.warnings.append(warning)
+ self.log(f" {warning}")
+
+ if self.verbose:
+ self.log(" 缁勪欢鍙兘涓嶅畬鏁达紝寤鸿閲嶆柊澶嶅埗缁勪欢鐩綍")
+ return False
+ else:
+ self.log(f" 鉁� components: 鎵惧埌 {component_count} 涓粍浠�")
+ self.success.append("components")
+ return True
+
+ def check_js_runtime_config(self):
+ """妫�鏌� JS 鐗堣嚜鍔ㄥ鍏ラ厤缃�"""
+ self.log("\n馃摑 妫�鏌� JS 杩愯鏃堕厤缃�...")
+
+ auto_import_config = self.project_root / '.auto-import.json'
+ if auto_import_config.exists():
+ self.log(" 鉁� auto-import: ESLint 鑷姩瀵煎叆閰嶇疆瀛樺湪")
+ self.success.append("auto-import")
+ return True
+
+ warning = "鈿狅笍 auto-import: 鏈壘鍒� .auto-import.json锛屽彲鑳藉皻鏈繍琛岃繃鏋勫缓鎴� dev"
+ self.warnings.append(warning)
+ self.log(f" {warning}")
+ return False
+
+ def run_all_checks(self):
+ """杩愯鎵�鏈夋鏌�"""
+ print("="*60)
+ print("馃攳 Art Design Pro 闆嗘垚楠岃瘉")
+ print("="*60)
+ print()
+
+ # 妫�鏌ラ」鐩牴鐩綍
+ self.log(f"馃搧 椤圭洰鐩綍: {self.project_root}")
+
+ # 鎵ц鍚勯」妫�鏌�
+ deps_ok = self.check_dependencies()
+ vite_ok = self.check_vite_config()
+ components_ok = self.check_components()
+ auto_import_ok = self.check_js_runtime_config()
+
+ print()
+ print("="*60)
+ print("馃搳 楠岃瘉瀹屾垚")
+ print("="*60)
+
+ # 杈撳嚭缁撴灉
+ all_good = not self.issues and not self.warnings
+
+ if self.success:
+ print(f"\n鉁� 閫氳繃鐨勬鏌� ({len(self.success)}):")
+ for item in self.success:
+ print(f" 鉁� {item}")
+
+ if self.warnings:
+ print(f"\n鈿狅笍 璀﹀憡 ({len(self.warnings)}):")
+ for warning in self.warnings:
+ print(f" {warning}")
+
+ if self.issues:
+ print(f"\n鉂� 鍙戠幇闂 ({len(self.issues)}):")
+ for issue in self.issues:
+ print(f" {issue}")
+
+ # 缁欏嚭淇寤鸿
+ if self.issues:
+ print("\n馃挕 淇寤鸿:")
+ if "package.json" in " ".join(self.issues):
+ print(" 1. 瀹夎缂哄け渚濊禆")
+ print(" pnpm add <缂哄け鐨勪緷璧�>")
+ if "vite.config.js" in " ".join(self.issues):
+ print(" 2. 鏇存柊 Vite 閰嶇疆")
+ print(" 鍙傝��: templates/vite.config.js.template")
+ if "components/core" in " ".join(self.issues):
+ print(" 3. 澶嶅埗 Art Design Pro 缁勪欢")
+ print(" 璇﹁: INTEGRATION_GUIDE.md")
+ print(" 4. 杩愯: python scripts/init.py")
+
+ # 杩斿洖閫�鍑虹爜
+ if all_good:
+ print("\n馃帀 鎵�鏈夋鏌ラ�氳繃锛佷綘鐨勯」鐩凡姝g‘闆嗘垚 Art Design Pro")
+ return 0
+ elif not self.issues:
+ print("\n鉁� 闆嗘垚鍩烘湰瀹屾垚锛屾湁涓�浜涘皬璀﹀憡浣嗕笉褰卞搷浣跨敤")
+ return 0
+ else:
+ print("\n鉂� 闆嗘垚瀛樺湪闂锛岃鎸夌収鎻愮ず淇")
+ return 1
+
+
+def main():
+ import argparse
+
+ parser = argparse.ArgumentParser(description='Art Design Pro 闆嗘垚楠岃瘉宸ュ叿')
+ parser.add_argument('--verbose', '-v', action='store_true', help='鏄剧ず璇︾粏杈撳嚭')
+ parser.add_argument('--project-root', '-p', type=str, help='椤圭洰鏍圭洰褰曪紙榛樿褰撳墠鐩綍锛�')
+
+ args = parser.parse_args()
+
+ verifier = IntegrationVerifier(
+ project_root=args.project_root,
+ verbose=args.verbose
+ )
+
+ sys.exit(verifier.run_all_checks())
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/rsf-design/skill/art-design-pro/templates/env/.env.example b/rsf-design/skill/art-design-pro/templates/env/.env.example
new file mode 100644
index 0000000..d1ebf46
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/templates/env/.env.example
@@ -0,0 +1,16 @@
+# 搴旂敤鍩虹璺緞
+VITE_BASE_URL=/
+
+# API 鍦板潃
+VITE_API_URL=/
+VITE_API_PROXY_URL=http://localhost:8080
+
+# 寮�鍙戞湇鍔″櫒绔彛
+VITE_PORT=3000
+
+# 搴旂敤鏍囬
+VITE_APP_TITLE=Your Application
+
+# 鐗堟湰鍙�
+VITE_VERSION=1.0.0
+
diff --git a/rsf-design/skill/art-design-pro/templates/package.json.template b/rsf-design/skill/art-design-pro/templates/package.json.template
new file mode 100644
index 0000000..16e2615
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/templates/package.json.template
@@ -0,0 +1,52 @@
+{
+ "name": "your-project",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "Your project description",
+ "author": "",
+ "license": "MIT",
+ "scripts": {
+ "dev": "vite --open",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext .js,.mjs,.cjs,.vue",
+ "lint:fix": "eslint . --ext .js,.mjs,.cjs,.vue --fix",
+ "format": "prettier --write \"**/*.{js,mjs,cjs,json,css,scss,vue,html,md}\""
+ },
+ "dependencies": {
+ "vue": "^3.5.21",
+ "vue-router": "^4.5.1",
+ "pinia": "^3.0.3",
+ "pinia-plugin-persistedstate": "^4.3.0",
+ "element-plus": "^2.11.2",
+ "@element-plus/icons-vue": "^2.3.2",
+ "@iconify/vue": "^5.0.0",
+ "@vueuse/core": "^13.9.0",
+ "axios": "^1.12.2",
+ "echarts": "^6.0.0",
+ "crypto-js": "^4.2.0",
+ "nprogress": "^0.2.0",
+ "mitt": "^3.0.1",
+ "file-saver": "^2.0.5",
+ "highlight.js": "^11.10.0",
+ "qrcode.vue": "^3.6.0",
+ "vue-draggable-plus": "^0.6.0",
+ "xlsx": "^0.18.5"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^6.0.1",
+ "vite": "^7.1.5",
+ "unplugin-auto-import": "^20.2.0",
+ "unplugin-vue-components": "^29.1.0",
+ "unplugin-element-plus": "^0.10.0",
+ "tailwindcss": "^4.1.14",
+ "@tailwindcss/vite": "^4.1.14",
+ "sass": "^1.77.0",
+ "@types/node": "^20.11.0"
+ },
+ "engines": {
+ "node": ">=20.19.0",
+ "pnpm": ">=8.8.0"
+ }
+}
+
diff --git a/rsf-design/skill/art-design-pro/templates/tailwind.config.js.template b/rsf-design/skill/art-design-pro/templates/tailwind.config.js.template
new file mode 100644
index 0000000..34384bb
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/templates/tailwind.config.js.template
@@ -0,0 +1,50 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ // Tailwind CSS 閰嶇疆
+ // 鎸囧畾鍖呭惈 Tailwind 鏍峰紡鐨勬枃浠惰矾寰�
+ content: [
+ "./index.html",
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
+ ],
+
+ // 涓婚鎵╁睍
+ theme: {
+ extend: {
+ // 棰滆壊鎵╁睍 - 浣跨敤 Element Plus 鐨� CSS 鍙橀噺
+ colors: {
+ primary: 'var(--el-color-primary)',
+ success: 'var(--el-color-success)',
+ warning: 'var(--el-color-warning)',
+ danger: 'var(--el-color-danger)',
+ error: 'var(--el-color-error)',
+ info: 'var(--el-color-info)',
+ },
+
+ // 灏哄鎵╁睍
+ spacing: {
+ '72': '18rem',
+ '84': '21rem',
+ '96': '24rem',
+ '128': '32rem',
+ },
+
+ // Z-index 灞傜骇
+ zIndex: {
+ '0': '0',
+ '10': '10',
+ '20': '20',
+ '30': '30',
+ '40': '40',
+ '50': '50',
+ '100': '100',
+ '1000': '1000',
+ '2000': '2000',
+ '3000': '3000',
+ }
+ },
+ },
+
+ // 鎻掍欢
+ plugins: [],
+}
+
diff --git a/rsf-design/skill/art-design-pro/templates/vite.config.js.template b/rsf-design/skill/art-design-pro/templates/vite.config.js.template
new file mode 100644
index 0000000..fae8b77
--- /dev/null
+++ b/rsf-design/skill/art-design-pro/templates/vite.config.js.template
@@ -0,0 +1,71 @@
+import path from 'path'
+import { fileURLToPath } from 'url'
+import { defineConfig, loadEnv } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import tailwindcss from '@tailwindcss/vite'
+import AutoImport from 'unplugin-auto-import/vite'
+import ElementPlus from 'unplugin-element-plus/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+
+export default ({ mode }) => {
+ const root = process.cwd()
+ const env = loadEnv(mode, root)
+ const { VITE_PORT = '3000', VITE_BASE_URL = '/', VITE_API_PROXY_URL = 'http://localhost:8080' } =
+ env
+
+ return defineConfig({
+ base: VITE_BASE_URL,
+ server: {
+ port: Number(VITE_PORT),
+ host: true,
+ proxy: {
+ '/api': {
+ target: VITE_API_PROXY_URL,
+ changeOrigin: 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')
+ }
+ },
+ plugins: [
+ vue(),
+ tailwindcss(),
+ AutoImport({
+ imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
+ dts: false,
+ resolvers: [ElementPlusResolver()],
+ eslintrc: {
+ enabled: true,
+ filepath: './.auto-import.json',
+ globalsPropValue: true
+ }
+ }),
+ Components({
+ dts: false,
+ resolvers: [ElementPlusResolver()]
+ }),
+ ElementPlus({
+ useSource: true
+ })
+ ],
+ build: {
+ target: 'es2015',
+ outDir: 'dist'
+ }
+ })
+}
+
+function resolvePath(targetPath) {
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), targetPath)
+}
+
diff --git a/rsf-design/src/App.vue b/rsf-design/src/App.vue
new file mode 100644
index 0000000..f6a6a92
--- /dev/null
+++ b/rsf-design/src/App.vue
@@ -0,0 +1,37 @@
+<template>
+ <ElConfigProvider
+ size="default"
+ :locale="locales[language]"
+ :z-index="3000"
+ :card="{
+ shadow: 'never'
+ }"
+ >
+ <RouterView></RouterView>
+ </ElConfigProvider>
+</template>
+
+<script setup>
+ import { useUserStore } from './store/modules/user'
+ import zh from 'element-plus/es/locale/lang/zh-cn'
+ import en from 'element-plus/es/locale/lang/en'
+ import { systemUpgrade } from './utils/sys'
+ import { toggleTransition } from './utils/ui/animation'
+ import { checkStorageCompatibility } from './utils/storage'
+ import { initializeTheme } from './hooks/core/useTheme'
+ const userStore = useUserStore()
+ const { language } = storeToRefs(userStore)
+ const locales = {
+ zh,
+ en
+ }
+ onBeforeMount(() => {
+ toggleTransition(true)
+ initializeTheme()
+ })
+ onMounted(() => {
+ checkStorageCompatibility()
+ toggleTransition(false)
+ systemUpgrade()
+ })
+</script>
diff --git a/rsf-design/src/api/auth.js b/rsf-design/src/api/auth.js
new file mode 100644
index 0000000..b7c9fbb
--- /dev/null
+++ b/rsf-design/src/api/auth.js
@@ -0,0 +1,19 @@
+import request from '@/utils/http'
+function fetchLogin(params) {
+ return request.post({
+ url: '/api/auth/login',
+ params
+ // showSuccessMessage: true // 鏄剧ず鎴愬姛娑堟伅
+ // showErrorMessage: false // 涓嶆樉绀洪敊璇秷鎭�
+ })
+}
+function fetchGetUserInfo() {
+ return request.get({
+ url: '/api/user/info'
+ // 鑷畾涔夎姹傚ご
+ // headers: {
+ // 'X-Custom-Header': 'your-custom-value'
+ // }
+ })
+}
+export { fetchGetUserInfo, fetchLogin }
diff --git a/rsf-design/src/api/system-manage.js b/rsf-design/src/api/system-manage.js
new file mode 100644
index 0000000..e5c5bc3
--- /dev/null
+++ b/rsf-design/src/api/system-manage.js
@@ -0,0 +1,19 @@
+import request from '@/utils/http'
+function fetchGetUserList(params) {
+ return request.get({
+ url: '/api/user/list',
+ params
+ })
+}
+function fetchGetRoleList(params) {
+ return request.get({
+ url: '/api/role/list',
+ params
+ })
+}
+function fetchGetMenuList() {
+ return request.get({
+ url: '/api/v3/system/menus/simple'
+ })
+}
+export { fetchGetMenuList, fetchGetRoleList, fetchGetUserList }
diff --git a/rsf-design/src/assets/images/avatar/avatar.webp b/rsf-design/src/assets/images/avatar/avatar.webp
new file mode 100644
index 0000000..bea307b
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar1.webp b/rsf-design/src/assets/images/avatar/avatar1.webp
new file mode 100644
index 0000000..68e256c
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar1.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar10.webp b/rsf-design/src/assets/images/avatar/avatar10.webp
new file mode 100644
index 0000000..a813d4c
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar10.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar2.webp b/rsf-design/src/assets/images/avatar/avatar2.webp
new file mode 100644
index 0000000..6716e3f
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar2.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar3.webp b/rsf-design/src/assets/images/avatar/avatar3.webp
new file mode 100644
index 0000000..7355ad4
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar3.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar4.webp b/rsf-design/src/assets/images/avatar/avatar4.webp
new file mode 100644
index 0000000..56a9549
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar4.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar5.webp b/rsf-design/src/assets/images/avatar/avatar5.webp
new file mode 100644
index 0000000..f78400c
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar5.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar6.webp b/rsf-design/src/assets/images/avatar/avatar6.webp
new file mode 100644
index 0000000..9771b78
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar6.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar7.webp b/rsf-design/src/assets/images/avatar/avatar7.webp
new file mode 100644
index 0000000..e5ef6fe
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar7.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar8.webp b/rsf-design/src/assets/images/avatar/avatar8.webp
new file mode 100644
index 0000000..b66e48f
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar8.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/avatar/avatar9.webp b/rsf-design/src/assets/images/avatar/avatar9.webp
new file mode 100644
index 0000000..7974139
--- /dev/null
+++ b/rsf-design/src/assets/images/avatar/avatar9.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/ceremony/hb.png b/rsf-design/src/assets/images/ceremony/hb.png
new file mode 100644
index 0000000..4103324
--- /dev/null
+++ b/rsf-design/src/assets/images/ceremony/hb.png
Binary files differ
diff --git a/rsf-design/src/assets/images/ceremony/sd.png b/rsf-design/src/assets/images/ceremony/sd.png
new file mode 100644
index 0000000..75ec838
--- /dev/null
+++ b/rsf-design/src/assets/images/ceremony/sd.png
Binary files differ
diff --git a/rsf-design/src/assets/images/ceremony/xc.png b/rsf-design/src/assets/images/ceremony/xc.png
new file mode 100644
index 0000000..9c7ab67
--- /dev/null
+++ b/rsf-design/src/assets/images/ceremony/xc.png
Binary files differ
diff --git a/rsf-design/src/assets/images/ceremony/yd.png b/rsf-design/src/assets/images/ceremony/yd.png
new file mode 100644
index 0000000..426912d
--- /dev/null
+++ b/rsf-design/src/assets/images/ceremony/yd.png
Binary files differ
diff --git a/rsf-design/src/assets/images/common/logo.webp b/rsf-design/src/assets/images/common/logo.webp
new file mode 100644
index 0000000..71542b5
--- /dev/null
+++ b/rsf-design/src/assets/images/common/logo.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/draw/draw1.png b/rsf-design/src/assets/images/draw/draw1.png
new file mode 100644
index 0000000..da5d87a
--- /dev/null
+++ b/rsf-design/src/assets/images/draw/draw1.png
Binary files differ
diff --git a/rsf-design/src/assets/images/favicon.ico b/rsf-design/src/assets/images/favicon.ico
new file mode 100644
index 0000000..21e7063
--- /dev/null
+++ b/rsf-design/src/assets/images/favicon.ico
Binary files differ
diff --git a/rsf-design/src/assets/images/lock/bg_dark.webp b/rsf-design/src/assets/images/lock/bg_dark.webp
new file mode 100644
index 0000000..1c33435
--- /dev/null
+++ b/rsf-design/src/assets/images/lock/bg_dark.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/lock/bg_light.webp b/rsf-design/src/assets/images/lock/bg_light.webp
new file mode 100644
index 0000000..00efbd9
--- /dev/null
+++ b/rsf-design/src/assets/images/lock/bg_light.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/login/lf_icon2.webp b/rsf-design/src/assets/images/login/lf_icon2.webp
new file mode 100644
index 0000000..5e4f3fd
--- /dev/null
+++ b/rsf-design/src/assets/images/login/lf_icon2.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/menu_layouts/dual_column.png b/rsf-design/src/assets/images/settings/menu_layouts/dual_column.png
new file mode 100644
index 0000000..9b868ca
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/menu_layouts/dual_column.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/menu_layouts/horizontal.png b/rsf-design/src/assets/images/settings/menu_layouts/horizontal.png
new file mode 100644
index 0000000..ca779bc
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/menu_layouts/horizontal.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/menu_layouts/mixed.png b/rsf-design/src/assets/images/settings/menu_layouts/mixed.png
new file mode 100644
index 0000000..c82b580
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/menu_layouts/mixed.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/menu_layouts/vertical.png b/rsf-design/src/assets/images/settings/menu_layouts/vertical.png
new file mode 100644
index 0000000..16e942b
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/menu_layouts/vertical.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/menu_styles/dark.png b/rsf-design/src/assets/images/settings/menu_styles/dark.png
new file mode 100644
index 0000000..e1653b7
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/menu_styles/dark.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/menu_styles/design.png b/rsf-design/src/assets/images/settings/menu_styles/design.png
new file mode 100644
index 0000000..7681aa8
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/menu_styles/design.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/menu_styles/light.png b/rsf-design/src/assets/images/settings/menu_styles/light.png
new file mode 100644
index 0000000..3007b99
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/menu_styles/light.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/theme_styles/dark.png b/rsf-design/src/assets/images/settings/theme_styles/dark.png
new file mode 100644
index 0000000..e8c6e44
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/theme_styles/dark.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/theme_styles/light.png b/rsf-design/src/assets/images/settings/theme_styles/light.png
new file mode 100644
index 0000000..6754238
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/theme_styles/light.png
Binary files differ
diff --git a/rsf-design/src/assets/images/settings/theme_styles/system.png b/rsf-design/src/assets/images/settings/theme_styles/system.png
new file mode 100644
index 0000000..6a6baa9
--- /dev/null
+++ b/rsf-design/src/assets/images/settings/theme_styles/system.png
Binary files differ
diff --git a/rsf-design/src/assets/images/svg/403.svg b/rsf-design/src/assets/images/svg/403.svg
new file mode 100644
index 0000000..68790ad
--- /dev/null
+++ b/rsf-design/src/assets/images/svg/403.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="94" y="34" width="212" height="233"><path d="M306 34H94v233h212V34Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M234.427 155.64h38.36V69.6h-38.36v86.04ZM113.326 155.64h121.1V69.6h-121.1v86.04Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.126 155.354h104.2v-72.95h-104.2v72.95ZM236.369 71.05s0 3.3 1.65 5.05c2.33 2.52 7.38-.2 7.38-.2s-1.75 5.15-1.55 10.19c.29 8.24 6.99 9.51 10 4.75 4.56 4.85 8.94-.29 9.52-2.62 4.27 4.76 9.32-.87 9.32-.87v-6.3l-23.99-12.13-12.33 2.13Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M234.429 155.641h-121.1l-15.93 32.11h121.1l15.93-32.11Z" fill="#fff"/><path d="M234.427 69.6h38.46v86.04M113.326 146.52V69.6h121.1M234.429 155.641l-15.93 32.11h-121.1l15.93-32.11h111.39" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M226.37 159.715H116.82l-12.04 23.86H215l11.37-23.86Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="m288.807 187.751-15.92-32.11h-38.46l16.02 32.11h38.36Z" fill="#fff"/><path d="m238.607 163.981 11.84 23.77h38.36l-15.92-32.11h-38.46" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M207.336 223.734c-3.69-13.77-15.44-23.86-29.33-23.86h-8.65s-27.09 14.94-27.09 33.27c0 18.34 25.44 33.18 25.44 33.18h10.4c13.79-.1 25.44-10.19 29.13-23.87 1.75-12.51 0-18.62.1-18.72Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M243.459 240.421c3.98 0 7.28-3.3 7.28-7.27 0-3.98-3.3-7.28-7.28-7.28h-31.08c-3.98 0-7.28 3.3-7.28 7.28 0 3.97 3.3 7.27 7.28 7.27h31.08Z" fill="#C7DEFF"/><path d="M210.342 223.737c-4.08-13.87-16.9-23.96-32.05-23.96H168.972s-29.62 14.94-29.62 33.37 27.87 33.37 27.87 33.37h11.27c15.05-.1 27.77-10.19 31.75-23.96" stroke="#071F4D"/><path d="M212.379 240.421c-3.98 0-7.28-3.3-7.28-7.27m0 0c0-3.98 3.3-7.28 7.28-7.28" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" fill="#006EFF"/><path d="M168.781 199.777c-18.45 0-33.41 14.94-33.41 33.37s14.96 33.37 33.41 33.37c18.45 0 33.4-14.94 33.4-33.37s-14.95-33.37-33.4-33.37Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M168.775 209.38c-13.14 0-23.79 10.64-23.79 23.77 0 13.12 10.65 23.76 23.79 23.76 13.14 0 23.8-10.64 23.8-23.76 0-13.13-10.66-23.77-23.8-23.77Z" fill="#00E4E5"/><path d="M162.174 223.736a17.48 17.48 0 0 1 14.76-8.05M159.455 231.982c.1-1.36.29-2.62.68-3.88" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M173.535 209.87c-1.55-.3-3.11-.49-4.76-.49-13.11 0-23.79 10.67-23.79 23.77 0 13.09 10.68 23.76 23.79 23.76 1.65 0 3.21-.19 4.76-.48-10.88-2.23-19.03-11.84-19.03-23.28 0-11.45 8.15-21.05 19.03-23.28Z" fill="#071F4D"/><path d="M219.957 225.774h23.6c4.08 0 7.38 3.3 7.38 7.37m0 0c0 4.08-3.3 7.37-7.38 7.37h-20.1M212.091 225.774h3.3" stroke="#071F4D"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" fill="#fff"/><path d="m248.894 34.485-.19 18.24c0 4.07-.39 5.23-2.14 6.79-8.15 6.88-10.97 9.02-9.22 12.9 1.45 3.2 6.79 2.23 9.61-1.55-.39 4.56-5.24 15.32-.58 18.04 4.37 2.52 6.89-3.49 6.89-3.49s.49 3.49 4.47 3.49c3.69 0 5.24-4.75 5.24-4.75s2.14 3.49 6.22 1.35c3.11-1.55 5.44-7.08 5.44-26.67v-24.35" stroke="#071F4D"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6l2.04-9.6Z" fill="#fff"/><path d="M255.307 75.71s-.39 5.43-2.04 9.6" stroke="#071F4D"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63l2.04-8.63Z" fill="#fff"/><path d="M264.921 75.323s-.68 5.24-2.04 8.63M147.801 34.485v34.92M121.775 34.485v34.92M102.546 204.724v13.97M102.546 222.379v.87M102.546 197.934v3.49M115.268 206.955v26.29M115.268 239.451v5.34M244.43 197.643v11.93M244.43 213.939v3.49M270.359 201.232v33.76M115.369 47.774h-13.6M94.486 47.774h3.4M241.516 47.774h-84.1M280.168 47.774h25.35" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m282.497 183.575-12.04-23.86h-27.29l11.36 23.86h27.97Z" fill="#00E4E5"/><path d="M234.427 134.88V69.6M234.427 140.412v7.66" stroke="#071F4D"/><path d="M220.831 228.684h16.99M240.934 228.684h2.43" stroke="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="m223.842 187.462 21.46-.2-10.97-20.66-10.49 20.86Z" fill="#071F4D"/></g></svg>
\ No newline at end of file
diff --git a/rsf-design/src/assets/images/svg/404.svg b/rsf-design/src/assets/images/svg/404.svg
new file mode 100644
index 0000000..48e1ca3
--- /dev/null
+++ b/rsf-design/src/assets/images/svg/404.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M290.7 38.4h-50.3v63.5h50.3V38.4Zm-16 47.5V54.4h-18.3v31.5h18.3ZM199 71.3V38.7h-16v48.6h32v14.6h16V38.7h-16v32.6h-16ZM331.7 87.3v14.6h16V38.7h-16v32.6h-16V38.7h-16v48.6h32Z" fill="#DEEBFC"/><path d="M324.3 119.5h24.1v-17.4h-24.1" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M218.5 120H69.4v11.5h130.5l18.6-11.5ZM231.5 131.5h117.2V120H231.5v11.5Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M348.4 119.5v-17.4h-24.1v17.4h24.1Z" fill="#071F4D"/><path d="M159.6 119.5h164.9V102H169.2M114.8 102H57.9v17.5h39.6" stroke="#071F4D"/><path d="M242.5 223.2V98.7c0-3.6 2.9-6.6 6.6-6.6m0 0c3.6 0 6.6 2.9 6.6 6.6v2.8M243.1 153.9h53.1M243.1 193.9h53.1M296.5 219.1V98.8c0-3.6 2.9-6.6 6.6-6.6 3.6 0 6.6 2.9 6.6 6.6v2.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m140.5 96.4 15.9 12.9 6.6-9.4-12.2-11.2-8.2 5.8-2.1 1.9Z" fill="#C7DEFF"/><path d="m170.2 93.5-4.2 4-3-2.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M150.2 87.1s3.7-5.3 8.1-3.9c2.7.8 1.7-1.6 1.7-1.6l-9.2-5.6-3.9 8.5 3.3 2.6Z" fill="#C7DEFF"/><path d="m148 85.5 13 12.1" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M179.202 87.001c.9-.1 1.7-.6 2.3-1.4.6-.6.9-1.4.8-1.9-.2-1.1-38.4-36-39.6-37.1-.7-.6-2-.7-3.3 0-.7.3-1.3 1-1.8 1.9 0 0-3 .4 1.3 11.1 4.3 10.6 6.6 19.1 6.6 19.1s-1.8 4.1-2.8 6.6c-.9 1.7 5.8 2.1 8.4-4.4 4.4.8 13.6 3.4 19 12.6v.1c1.1 1.9 1.9 1.2 2.4.9 3.2-2.4 7.2-5.8 6.7-7.5Z" fill="#00E4E5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M182.3 83.802c-.2-1.1-38.4-36-39.6-37.1-.7-.6-2-.7-3.3 0l42.2 39c.5-.7.8-1.4.7-1.9Z" fill="#071F4D"/><path d="M137.8 48.4 179.2 87M140.4 63.2l5.2-3.9M143.6 72.4s3.2-2.1 6.8-2.3M144.8 75.903c.6-.2 5.4-1.8 7.7-1.5M142.3 91.6l4.2-3.8M159.9 103.9l5-6.5" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m206.1 129.6 11.7 16.7 9-7.2-8.7-14.1-9.5 3.3-2.5 1.3Z" fill="#C7DEFF"/><path d="m235.4 134.9-5.2 2.7-2.2-3.2" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M217.9 123.3s4.9-4.1 8.8-1.6c2.4 1.5 2.1-1.1 2.1-1.1l-7.3-7.9-6.1 7.2 2.5 3.4Z" fill="#C7DEFF"/><path d="m216.2 121.1 9.2 15.2" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M237.397 136.399c3.7-1.4 8.5-3.7 8.5-5.4.8.2 1.8-.1 2.6-.7.7-.4 1.3-1 1.3-1.6.1-1.2-27.2-45.1-28.1-46.5-.5-.7-1.8-1.1-3.2-.8-.8.2-1.6.6-2.3 1.4 0 0-3-.4-1.8 11 1.3 11.4 1.2 20.2 1.2 20.2s-2.8 3.5-4.4 5.6c-1.3 1.4 5 3.5 9.3-2 4.1 1.9 12.2 6.9 14.9 17.2v.1c.5 2.1 1.5 1.7 2 1.5Z" fill="#00E4E5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M249.8 128.699c.1-1.2-27.2-45.1-28.1-46.5-.5-.7-1.8-1.1-3.2-.8l30 49c.6-.5 1.2-1.1 1.3-1.7Z" fill="#071F4D"/><path d="m216.5 82.6 29.4 48.4M214.9 97.6l6.1-2.3M215.5 107.395s3.7-1.2 7.2-.4M215.7 111.098c.6 0 5.7-.3 7.8.7M209.1 125.5l5-2.5M222.7 142.1l6.5-4.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M208.2 122c-2.5 1-29.9 15.6-47.1 28.8-17.2 13.2-27 23.3-30.3 30.3-1.8 3.6-2.4 4.4-2.4 4.4-1.3-3.4-3.6-10.1-3.9-12.4-.4-4.1 0-9 3.3-14.3 3.3-5.3 27.4-43.9 32.4-51.9-2.4-1.9-8.2-6.8-19.8-16.9-3.4 2.8-35.6 29.9-44.6 38.5-9 8.6-10.8 15.7-10.8 29.3v63.6s25.2 3.9 44.3 2.1c19.1-1.8 44.1-4.1 44.1-4.1s-2.6-29.6 2.9-36.1c5.4-6.5 41.6-34.2 45.5-37.5-3.8-7.5-9-15.8-13.6-23.8Z" fill="#006EFF"/><path d="M154.103 116.7c-9 14.4-23.8 38.1-26.3 42.1-3.3 5.3-3.7 10.2-3.3 14.3M85.1 215.7v-57.9c0-13.6 1.8-20.8 10.8-29.3 1.8-1.7 4.5-4.2 7.8-7.1M197.402 165.1c-9.5 7.6-18.6 15.2-21.1 18.2-5.4 6.5-2.9 36.1-2.9 36.1M123.4 218.3s4.2-30.3 7.5-37.2c3.3-6.9 13-17.1 30.3-30.3 10.5-8.1 24.9-16.6 35-22.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M173.1 256.4v-8h-68V264s14.2-5.5 31.1-3.8c29.1 2.9 36.8-3.8 36.8-3.8h.1ZM271.5 255.1h-31.1v9.2s6.5-5.5 14.2-3.8c13.4 2.9 16.9-3.8 16.9-3.8v-1.6Z" fill="#C7DEFF"/><path d="M105.2 245h51.9M240.7 251.7h30.6M165.6 245h7.2M56.7 219.6c10.4 0 17.4.5 23.2 1.1 11.4 1.3 18.4 3.1 39 3.1 31.1 0 31.1-4.3 62.2-4.3s31.2 4.3 51.5 4.3 27.7-5.8 51.5-5.8c15.1 0 20.4 5.8 51.5 5.8" stroke="#071F4D"/><path d="M284.1 218c16.2 0 21.5 5.8 51.5 5.8" stroke="#071F4D"/><path d="M159.5 203.5v6.2M202.4 146.7l-31.6 25.9c-7.1 5.9-11.3 14.7-11.3 23.9M206.7 143.2l2.7-2.3M138.098 118.6l-19.2 24c-7.5 9.4-11.3 21.3-10.6 33.3l2.4 38.5M143.6 111.7l3.2-4" stroke="#DEEBFC"/><path d="m141.8 178.4 6-1.1M94.5 140.6l5.4-4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M135.2 236.099c7.395-.581 13.632-1.202 19.262-1.762 19.569-1.948 31.803-3.166 59.838.562 24.429 3.262 31.821 1.804 43.661-.53 3.766-.743 7.982-1.574 13.339-2.37 14.976-2.226 25.902.28 35.11 2.393 4.442 1.019 8.485 1.946 12.39 2.207 12 .7 16.8-.3 16.8-.3v-9.6c-.1.04-4.95.99-16.8.3-3.972-.232-8.097-1.176-12.641-2.215-9.183-2.101-20.074-4.593-34.859-2.385-5.305.792-9.493 1.619-13.239 2.358-11.858 2.342-19.291 3.809-43.761.542-28.035-3.728-40.269-2.51-59.838-.562-5.63.56-11.867 1.181-19.262 1.762-15.1 1.2-36.9 1.3-60-2.2-10.8-1.6-18.7-1.1-18.7-1.1v9.6s7.9-.5 18.7 1.1c23.1 3.5 44.9 3.4 60 2.2Z" fill="#C7DEFF"/></svg>
\ No newline at end of file
diff --git a/rsf-design/src/assets/images/svg/500.svg b/rsf-design/src/assets/images/svg/500.svg
new file mode 100644
index 0000000..512429f
--- /dev/null
+++ b/rsf-design/src/assets/images/svg/500.svg
@@ -0,0 +1,5 @@
+<svg
+ viewBox="0 0 400 300"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="38" width="307" height="224"><path d="M353.3 38H47.5v223.8h305.8V38Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M299.2 200.6H61.6v5.1h240.3l-2.7-5.1Z" fill="#C7DEFF"/><path d="m308.9 185.8-6.5 20H183.7M332.3 127.6h10.6l-5 16.7-14.8-.1-7.2 21.1M328.8 127.4l13.6-39.6M307.6 166 337 84.7H180.6l-9.8 26.9h-10.5M296.6 196l4.3-11.8M157.2 149.2l6.4-17.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-34.8 95.8h136.4l34.7-95.8ZM169.9 166.2l5-13.6-5 13.6Z" fill="#fff"/><path d="m169.9 166.2 5-13.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.8 93.1H188.5l-4 11.7h135.8l4.5-11.7Z" fill="#006EFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M102.6 159.5h38.3l2.7 36.6h-38.4c-10.1 0-20.9-8.2-20.9-18.3 0-10.1 8.2-18.3 18.3-18.3Z" fill="#DEEBFC"/><path fill-rule="evenodd" clip-rule="evenodd" d="M84.3 174.102c2.5 3.4 10 5 17.9 2.8 16.6-6.5 23.8-3.9 23.8-3.9s.5-3.4 1.3-5c-5.8-3-15.4.3-26.1 3.1-10.7 2.8-15.8-2.5-15.8-2.5-.4 0-1.1 2.8-1.1 5.5Z" fill="#fff"/><path d="M96.5 194.2c-7.2-3.3-12.2-10.5-12.2-19m0 0c0-11.5 9.3-20.8 20.8-20.8h29.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8l14.5 19.8Zm-14.5-19.8c0-11.5 9.3-20.8 20.8-20.8l-20.8 20.8Zm20.8-20.8c11.5 0 20.8 9.3 20.8 20.8l-20.8-20.8Zm20.8 20.8c0 8.4-5 15.6-12.1 18.9l12.1-18.9Z" fill="#fff"/><path d="M140.3 195.1c-8.4-2.7-14.5-10.6-14.5-19.8m0 0c0-11.5 9.3-20.8 20.8-20.8m0 0c11.5 0 20.8 9.3 20.8 20.8m0 0c0 8.4-5 15.6-12.1 18.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.5 177.2c0-7.7-6.3-14-14-14s-14 6.3-14 14c0 5.8 3.5 10.8 8.6 12.9.1 0 5.8 1.6 10.7 0 5.3-1.7 8.7-7.1 8.7-12.9Z" fill="#00E4E5"/><path d="M140.5 190.1c-5.8-2.4-9.9-8.2-9.9-14.9 0-8.9 7.2-16.1 16.1-16.1 8.9 0 16.1 7.2 16.1 16.1 0 6.8-4.2 12.5-10.1 14.9M88.4 170.604c2.9 1.3 7.7 2.6 13.6.3 14.7-5.7 22.3-4.3 24.6-3.5M84.5 174.599s5.9 6.5 19 1.7c9.2-3.4 15.3-3.9 18.8-3.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M340.6 112.3h-55.2l-2.7 6.2H338l2.6-6.2Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M236.8 117.9c-16.13 0-29.2 13.07-29.2 29.2s13.07 29.2 29.2 29.2 29.2-13.07 29.2-29.2-13.07-29.2-29.2-29.2Z" fill="#00E4E5"/><path d="M265 123.3c13.1 13.1 13.1 34.4 0 47.6M306 205.9h19.2M61.7 205.9h32.9M181.2 196.2h115.2M47.5 205.9h10v-9.7h73.8" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M146.7 179.2c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M169.5 196.2c3.9 0 7.1 3.2 7.1 7.1 0 3.9-3.2 7.1-7.1 7.1H144c-2.1 0-3.9 1.7-3.9 3.9v1c0 2.1 1.7 3.9 3.9 3.9h48c5.1 0 9.2 4.1 9.2 9.2s-4.1 9.3-9.2 9.2h-33.8c-2.3 0-4.1 1.8-4.1 4.1s1.8 4.1 4.1 4.1h4.2c4.4 0 8 3.6 8 8s-3.6 8-8 8H111c-3.7 0-6.8-3-6.8-6.8 0-3.7 3-6.8 6.8-6.8h.3c2.3 0 4.1-1.8 4.1-4.1s-1.8-4.1-4.1-4.1H79c-4.5 0-8.1-3.6-8.1-8.1s3.6-8.1 8.1-8.1h37.7c2.1 0 3.9-1.7 3.9-3.9 0-2.1-1.7-3.9-3.9-3.9h-7.9c-4.4 0-7.9-3.5-7.9-7.9s3.5-7.9 7.9-7.9h30.4c2.2 0 3.9-1.8 3.9-3.9V187c0-1.9 1.6-3.5 3.5-3.5s3.5 1.6 3.5 3.5v5.3c0 2.2 1.8 3.9 3.9 3.9h15.5Z" fill="#006EFF"/><path d="m227.8 138.5 18.7 18.7M227.8 157.2l18.7-18.7" stroke="#fff" stroke-width="6"/><path fill-rule="evenodd" clip-rule="evenodd" d="M194.8 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8ZM202.9 96.9c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8Z" fill="#fff"/><path d="m291.7 184.3-1.6 4.6h-121M298.1 166.7l22.5-61.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m193 134.1 2.2-5.1h-19.4l-2.3 5.1H193ZM313.2 123.5l2.2-5.1h-24.5l-2.3 5.1h24.6Z" fill="#DEEBFC"/><path d="m164.5 159.2 19.8-54.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M199.6 119.8h-53.2l-4.4 9.3h53.2l4.4-9.3Z" fill="#00E4E5"/><path d="M151.3 129.1H142l4.4-9.3h16.9" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M353.3 169.4h-67.4l-4.8 12.2h67.3l4.9-12.2Z" fill="#006EFF"/><path d="M332.4 169.4h20.9l-4.9 12.2h-39.7M242.7 235.5v-4.8c0-3.8 3.1-7 7-7h20.2c3.8 0 7 3.1 7 7" stroke="#071F4D"/><path d="M261.1 235.5v-4.8c0-3.8 3.1-7 7-7h13.7c3.8 0 7 3.1 7 7v4.8M242.6 230.7h13.7M235.2 237.7h63.3M224 237.7h6.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M324.1 141.3H335l3.3-10.7h-10.2l-4 10.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M288.3 230.4c0-3.6-2.9-6.5-6.5-6.5h-14.2c-3.6 0-6.5 2.9-6.5 6.5v5.3h27.2v-5.3Z" fill="#071F4D"/><path d="M80.4 228.5H83M87.7 228.5h19.2M146.3 195.8v2c0 3.6-2.9 6.6-6.6 6.6H138M133.4 204.3h1.5M154 249.9h9.4" stroke="#DEEBFC"/><path d="m299.4 141.9 5.1-13.9" stroke="#071F4D"/></g></svg>
diff --git a/rsf-design/src/assets/images/svg/login_icon.svg b/rsf-design/src/assets/images/svg/login_icon.svg
new file mode 100644
index 0000000..4beb3ab
--- /dev/null
+++ b/rsf-design/src/assets/images/svg/login_icon.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="a" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="44" y="42" width="312" height="217"><path d="M355.3 42H44v216.9h311.3V42Z" fill="#fff"/></mask><g mask="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M288.2 248.4h25.1v-30h-25.1v30Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M304.498 238.199c-1.5-3.9-5.9-15.4-4-21.6-2.9.8-3.3.1-5-.1-1.7-.1 0 10.7 2.2 16.4 1.7 4.5 2.1 11.1 2.1 13.6h5.4c.2-1.9.3-5.5-.7-8.3Z" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6" fill="#fff"/><path d="M311.5 214.7v-1.6c0-.7-.6-1.3-1.3-1.3h-22.8c-.7 0-1.3.6-1.3 1.3v1.6M290.2 214.7h21.4c1 0 1.8.8 1.8 1.8v29" stroke="#071F4D" stroke-width="1.096"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" fill="#fff"/><path d="M284.3 245.6v-29c0-1 .8-1.8 1.8-1.8h1.6" stroke="#071F4D" stroke-width="1.096"/><path d="M295.402 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3M300.502 216.5c-.9 4.2-.4 9.7 2.8 17.5 2.4 5.9 1.9 10.2 1.8 12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m331 258.4-.3-5.2H88.5l-1.2 5.2H331Z" fill="#C7DEFF"/><path d="M252.9 248.7H331M216.6 258.4H331M47.1 139.3l-2.6 1.5 42.7 117.6h129.2v-6.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" fill="#fff"/><path d="m247.2 248.6-40.4-111.3H50.5l40.3 111.3h156.4Z" stroke="#071F4D"/><path d="m203.2 153.2 32.2 88.7H97.8l-32.3-88.7" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M72.2 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4ZM79.3 146.9c-.77 0-1.4.63-1.4 1.4 0 .77.63 1.4 1.4 1.4.77 0 1.4-.63 1.4-1.4 0-.77-.63-1.4-1.4-1.4Z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M263.5 171.2h80.3v-63.7h-80.3v63.7Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M290 143.9h-45.6l12.5 51.3H290v-51.3Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M286 117.4h-29.3v77.8h92.9v-67.6l-55.9.6-7.7-10.8Z" fill="#00E4E5"/><path d="m332.6 127.6-38.9.6-7.7-10.8h-11.7M308.9 195.2h45.9M250.3 195.2h28.5M287.3 195.2h12.3" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M130.5 211.4H186v-44h-55.5v44Z" fill="#C7DEFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M148.7 192.5h-31.6l8.7 35.5h22.9v-35.5Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M145.9 174.2h-20.2V228h64.1v-46.7l-38.6.4-5.3-7.5Z" fill="#006EFF"/><path d="m179 181.3-27.8.4-5.3-7.5h-7.7M176.2 201.7h19.2M163.2 210.7H195M172.1 228h-54.2M184.8 228h8.1M174.9 228h5.4" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="m293.2 155.7-6.4 6.3 15.3 15.3 22.7-22.6-6.4-6.4-16.3 16.3-8.9-8.9Z" fill="#fff"/><path d="M57.2 258.4h283.6M345.9 258.4h8.1M55.4 258.4h220.5M160.1 118.8l-1.2 2.7M156.7 127c-.3.8-.7 1.8-1.1 2.8M222 68.5c-1 .2-1.9.5-2.9.8M214.1 70.7c-5.8 1.9-11.3 4.4-16.5 7.4M195.4 79.5c-.9.5-1.7 1.1-2.5 1.6M314.2 98.5c-.6-.8-1.3-1.5-2-2.3M308.9 92.8c-4-4-8.3-7.6-13-10.8M293.9 80.7c-.8-.5-1.7-1.1-2.5-1.6" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.296 71.203c-3.6-1.5-18.5-2.9-21.8-1.9-1 5.8 4.9 13.5 4.9 13.5s6-9.9 16.9-11.6Z" fill="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M251.3 42.704c-6.5 6.7-7.8 13-8.8 19.3 24.4-1.1 36.3 13 42.8 20 3.2-9.1 7.8-23 7.2-29-7.1-6.4-20-11.7-41.2-10.3Z" fill="#C7DEFF"/><path d="M230 69.3c36.2-3.8 52 21.1 52 21.1s11.4-28.2 10.5-37.4c-7.3-6.5-23.3-12-45.6-10.1-9 6.3-15.6 18.7-16.9 26.4Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M161.604 70.7c-6 8.4-9.9 21.9-8.8 33.8 8.4 5.3 32.3 10.5 43.6 11.5 6.1-7.9 15.9-26 15.9-26s-32-4.8-50.7-19.3Z" fill="#C7DEFF"/><path d="M193.103 119.5c4.8-2.7 19.2-29.5 19.2-29.5s-35.8-5.4-53.7-21.8c-9.3 6.1-16.4 24.3-15 40.1 10.6 6.7 45.8 13.3 49.5 11.2Z" stroke="#071F4D"/><path fill-rule="evenodd" clip-rule="evenodd" d="M189.5 111.6c-3 5.2-5.7 7.2-9.8 6.6 12.2 2.6 13.5 1.2 15.6-1.1 2.2-2.4 4.2-6.6 4.2-6.6s-3.1 2.5-10 1.1Z" fill="#071F4D"/><path d="M331 251.8v6.6M77 165.4l-2.7-6.7h7.8M222.8 228.9l2.8 6.6h-7.9" stroke="#071F4D"/></g></svg>
\ No newline at end of file
diff --git a/rsf-design/src/assets/images/user/avatar.webp b/rsf-design/src/assets/images/user/avatar.webp
new file mode 100644
index 0000000..6d7234b
--- /dev/null
+++ b/rsf-design/src/assets/images/user/avatar.webp
Binary files differ
diff --git a/rsf-design/src/assets/images/user/bg.webp b/rsf-design/src/assets/images/user/bg.webp
new file mode 100644
index 0000000..762b22d
--- /dev/null
+++ b/rsf-design/src/assets/images/user/bg.webp
Binary files differ
diff --git a/rsf-design/src/assets/styles/core/app.scss b/rsf-design/src/assets/styles/core/app.scss
new file mode 100644
index 0000000..c0efeed
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/core/dark.scss b/rsf-design/src/assets/styles/core/dark.scss
new file mode 100644
index 0000000..c52abc3
--- /dev/null
+++ b/rsf-design/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;
+ }
+
+ /* 浠g爜鍧� */
+ .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/rsf-design/src/assets/styles/core/el-dark.scss b/rsf-design/src/assets/styles/core/el-dark.scss
new file mode 100644
index 0000000..8f81cdf
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/core/el-light.scss b/rsf-design/src/assets/styles/core/el-light.scss
new file mode 100644
index 0000000..ddf2bc5
--- /dev/null
+++ b/rsf-design/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
+// 鑷畾涔塃lement 浜壊涓婚
+
+@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/rsf-design/src/assets/styles/core/el-ui.scss b/rsf-design/src/assets/styles/core/el-ui.scss
new file mode 100644
index 0000000..44429a1
--- /dev/null
+++ b/rsf-design/src/assets/styles/core/el-ui.scss
@@ -0,0 +1,519 @@
+// 浼樺寲 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銆乨ropdown 鐨勪笁瑙�
+.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;
+ }
+}
diff --git a/rsf-design/src/assets/styles/core/md.scss b/rsf-design/src/assets/styles/core/md.scss
new file mode 100644
index 0000000..b22fdc2
--- /dev/null
+++ b/rsf-design/src/assets/styles/core/md.scss
@@ -0,0 +1,1036 @@
+/* 鏂囩珷鏍囬璁剧疆锛坔1-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/rsf-design/src/assets/styles/core/mixin.scss b/rsf-design/src/assets/styles/core/mixin.scss
new file mode 100644
index 0000000..db36888
--- /dev/null
+++ b/rsf-design/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;
+ }
+}
+
+//鑳屾櫙閫忔槑锛屾枃瀛椾笉閫忔槑銆傚吋瀹笽E8
+@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/rsf-design/src/assets/styles/core/reset.scss b/rsf-design/src/assets/styles/core/reset.scss
new file mode 100644
index 0000000..17a3bcf
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/core/router-transition.scss b/rsf-design/src/assets/styles/core/router-transition.scss
new file mode 100644
index 0000000..f47c741
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/core/tailwind.css b/rsf-design/src/assets/styles/core/tailwind.css
new file mode 100644
index 0000000..1a9e22c
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/core/theme-animation.scss b/rsf-design/src/assets/styles/core/theme-animation.scss
new file mode 100644
index 0000000..377b945
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/core/theme-change.scss b/rsf-design/src/assets/styles/core/theme-change.scss
new file mode 100644
index 0000000..5b640d2
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/custom/one-dark-pro.scss b/rsf-design/src/assets/styles/custom/one-dark-pro.scss
new file mode 100644
index 0000000..36bdf63
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/assets/styles/index.scss b/rsf-design/src/assets/styles/index.scss
new file mode 100644
index 0000000..cdc2ddc
--- /dev/null
+++ b/rsf-design/src/assets/styles/index.scss
@@ -0,0 +1,23 @@
+// 閲嶇疆榛樿鏍峰紡
+@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';
diff --git a/rsf-design/src/assets/svg/loading.js b/rsf-design/src/assets/svg/loading.js
new file mode 100644
index 0000000..be3d62b
--- /dev/null
+++ b/rsf-design/src/assets/svg/loading.js
@@ -0,0 +1,34 @@
+const fourDotsSpinnerSvg = `
+ <svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
+ <style>
+ .spinner {
+ transform-origin: 20px 20px;
+ animation: rotate 1.6s linear infinite;
+ }
+ .dot {
+ fill: var(--theme-color);
+ animation: fade 1.6s infinite;
+ }
+ .dot:nth-child(1) { animation-delay: 0s; }
+ .dot:nth-child(2) { animation-delay: 0.5s; }
+ .dot:nth-child(3) { animation-delay: 1s; }
+ .dot:nth-child(4) { animation-delay: 1.5s; }
+ @keyframes rotate {
+ 100% { transform: rotate(360deg); }
+ }
+ @keyframes fade {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+ }
+ </style>
+ <g class="spinner">
+ <circle class="dot" cx="20" cy="8" r="4"/>
+ <circle class="dot" cx="32" cy="20" r="4"/>
+ <circle class="dot" cx="20" cy="32" r="4"/>
+ <circle class="dot" cx="8" cy="20" r="4"/>
+ </g>
+ </svg>
+`;
+export {
+ fourDotsSpinnerSvg
+};
diff --git a/rsf-design/src/components/core/banners/art-basic-banner/index.vue b/rsf-design/src/components/core/banners/art-basic-banner/index.vue
new file mode 100644
index 0000000..e38ea6e
--- /dev/null
+++ b/rsf-design/src/components/core/banners/art-basic-banner/index.vue
@@ -0,0 +1,261 @@
+<!-- 鍩虹妯箙缁勪欢 -->
+<template>
+ <div
+ class="art-card basic-banner"
+ :class="[{ 'has-decoration': decoration }, boxStyle]"
+ :style="{ height }"
+ @click="emit('click')"
+ >
+ <!-- 娴佹槦鏁堟灉 -->
+ <div v-if="meteorConfig?.enabled && isDark" class="basic-banner__meteors">
+ <span
+ v-for="(meteor, index) in meteors"
+ :key="index"
+ class="meteor"
+ :style="{
+ top: '-60px',
+ left: `${meteor.x}%`,
+ animationDuration: `${meteor.speed}s`,
+ animationDelay: `${meteor.delay}s`
+ }"
+ ></span>
+ </div>
+
+ <div class="basic-banner__content">
+ <!-- title slot -->
+ <slot name="title">
+ <p v-if="title" class="basic-banner__title" :style="{ color: titleColor }">{{ title }}</p>
+ </slot>
+
+ <!-- subtitle slot -->
+ <slot name="subtitle">
+ <p v-if="subtitle" class="basic-banner__subtitle" :style="{ color: subtitleColor }">{{
+ subtitle
+ }}</p>
+ </slot>
+
+ <!-- button slot -->
+ <slot name="button">
+ <div
+ v-if="buttonConfig?.show"
+ class="basic-banner__button"
+ :style="{
+ backgroundColor: buttonColor,
+ color: buttonTextColor,
+ borderRadius: buttonRadius
+ }"
+ @click.stop="emit('buttonClick')"
+ >
+ {{ buttonConfig?.text }}
+ </div>
+ </slot>
+
+ <!-- default slot -->
+ <slot></slot>
+
+ <!-- background image -->
+ <img
+ v-if="imageConfig.src"
+ class="basic-banner__background-image"
+ :src="imageConfig.src"
+ :style="{ width: imageConfig.width, bottom: imageConfig.bottom, right: imageConfig.right }"
+ loading="lazy"
+ alt="鑳屾櫙鍥剧墖"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref, computed } from 'vue'
+ import { useSettingStore } from '@/store/modules/setting'
+ const settingStore = useSettingStore()
+ const { isDark } = storeToRefs(settingStore)
+ defineOptions({ name: 'ArtBasicBanner' })
+ const props = defineProps({
+ height: { required: false, default: '11rem' },
+ title: { required: false, default: '' },
+ subtitle: { required: false, default: '' },
+ titleColor: { required: false, default: 'white' },
+ subtitleColor: { required: false, default: 'white' },
+ boxStyle: { required: false, default: '!bg-theme/60' },
+ decoration: { required: false, default: true },
+ buttonConfig: {
+ required: false,
+ default: () => ({
+ show: true,
+ text: '鏌ョ湅',
+ color: '#fff',
+ textColor: '#333',
+ radius: '6px'
+ })
+ },
+ meteorConfig: { required: false, default: () => ({ enabled: false, count: 10 }) },
+ imageConfig: {
+ required: false,
+ default: () => ({ src: '', width: '12rem', bottom: '-3rem', right: '0' })
+ }
+ })
+ const emit = defineEmits(['click', 'buttonClick'])
+ const buttonColor = computed(() => props.buttonConfig?.color ?? '#fff')
+ const buttonTextColor = computed(() => props.buttonConfig?.textColor ?? '#333')
+ const buttonRadius = computed(() => props.buttonConfig?.radius ?? '6px')
+ const meteors = ref([])
+ onMounted(() => {
+ if (props.meteorConfig?.enabled) {
+ meteors.value = generateMeteors(props.meteorConfig?.count ?? 10)
+ }
+ })
+ function generateMeteors(count) {
+ const segmentWidth = 100 / count
+ return Array.from({ length: count }, (_, index) => {
+ const segmentStart = index * segmentWidth
+ const x = segmentStart + Math.random() * segmentWidth
+ const isSlow = Math.random() > 0.5
+ return {
+ x,
+ speed: isSlow ? 5 + Math.random() * 3 : 2 + Math.random() * 2,
+ delay: Math.random() * 5
+ }
+ })
+ }
+</script>
+
+<style lang="scss" scoped>
+ .basic-banner {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0 2rem;
+ overflow: hidden;
+ color: white;
+ border-radius: calc(var(--custom-radius) + 2px) !important;
+
+ &__content {
+ position: relative;
+ z-index: 1;
+ }
+
+ &__title {
+ margin: 0 0 0.5rem;
+ font-size: 1.5rem;
+ font-weight: 600;
+ }
+
+ &__subtitle {
+ position: relative;
+ z-index: 10;
+ margin: 0 0 1.5rem;
+ font-size: 0.9rem;
+ opacity: 0.9;
+ }
+
+ &__button {
+ box-sizing: border-box;
+ display: inline-block;
+ min-width: 80px;
+ height: var(--el-component-custom-height);
+ padding: 0 12px;
+ font-size: 14px;
+ line-height: var(--el-component-custom-height);
+ text-align: center;
+ cursor: pointer;
+ user-select: none;
+ transition: all 0.3s;
+
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+
+ &__background-image {
+ position: absolute;
+ right: 0;
+ bottom: -3rem;
+ z-index: 0;
+ width: 12rem;
+ }
+
+ &.has-decoration::after {
+ position: absolute;
+ right: -10%;
+ bottom: -20%;
+ width: 60%;
+ height: 140%;
+ content: '';
+ background: rgb(255 255 255 / 10%);
+ border-radius: 30%;
+ transform: rotate(-20deg);
+ }
+
+ &__meteors {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+
+ .meteor {
+ position: absolute;
+ width: 2px;
+ height: 60px;
+ background: linear-gradient(
+ to top,
+ rgb(255 255 255 / 40%),
+ rgb(255 255 255 / 10%),
+ transparent
+ );
+ opacity: 0;
+ transform-origin: top left;
+ animation-name: meteor-fall;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+
+ &::before {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ width: 2px;
+ height: 2px;
+ content: '';
+ background: rgb(255 255 255 / 50%);
+ }
+ }
+ }
+ }
+
+ @keyframes meteor-fall {
+ 0% {
+ opacity: 1;
+ transform: translate(0, -60px) rotate(-45deg);
+ }
+
+ 100% {
+ opacity: 0;
+ transform: translate(400px, 340px) rotate(-45deg);
+ }
+ }
+
+ @media (width <= 640px) {
+ .basic-banner {
+ box-sizing: border-box;
+ justify-content: flex-start;
+ padding: 16px;
+
+ &__title {
+ font-size: 1.4rem;
+ }
+
+ &__background-image {
+ display: none;
+ }
+
+ &.has-decoration::after {
+ display: none;
+ }
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/banners/art-card-banner/index.vue b/rsf-design/src/components/core/banners/art-card-banner/index.vue
new file mode 100644
index 0000000..0e16579
--- /dev/null
+++ b/rsf-design/src/components/core/banners/art-card-banner/index.vue
@@ -0,0 +1,71 @@
+<!-- 鍗$墖妯箙缁勪欢 -->
+<template>
+ <div class="art-card-sm flex-c flex-col pb-6" :style="{ height: height }">
+ <div class="flex-c flex-col gap-4 text-center">
+ <div class="w-45">
+ <img :src="image" :alt="title" class="w-full h-full object-contain" />
+ </div>
+ <div class="box-border px-4">
+ <p class="mb-2 text-lg font-semibold text-g-800">{{ title }}</p>
+ <p class="m-0 text-sm text-g-600">{{ description }}</p>
+ </div>
+ <div class="flex-c gap-3">
+ <div
+ v-if="cancelButton?.show"
+ class="inline-block h-9 px-3 text-sm/9 c-p select-none rounded-md border border-g-300"
+ :style="{
+ backgroundColor: cancelButton?.color,
+ color: cancelButton?.textColor
+ }"
+ @click="handleCancel"
+ >
+ {{ cancelButton?.text }}
+ </div>
+ <div
+ v-if="button?.show"
+ class="inline-block h-9 px-3 text-sm/9 c-p select-none rounded-md"
+ :style="{ backgroundColor: button?.color, color: button?.textColor }"
+ @click="handleClick"
+ >
+ {{ button?.text }}
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import defaultIcon from '@imgs/3d/icon1.webp'
+ defineOptions({ name: 'ArtCardBanner' })
+ defineProps({
+ height: { required: false, default: '24rem' },
+ image: { required: false, default: defaultIcon },
+ title: { required: false, default: '' },
+ description: { required: false, default: '' },
+ button: {
+ required: false,
+ default: () => ({
+ show: true,
+ text: '鏌ョ湅璇︽儏',
+ color: 'var(--theme-color)',
+ textColor: '#fff'
+ })
+ },
+ cancelButton: {
+ required: false,
+ default: () => ({
+ show: false,
+ text: '鍙栨秷',
+ color: '#f5f5f5',
+ textColor: '#666'
+ })
+ }
+ })
+ const emit = defineEmits(['click', 'cancel'])
+ const handleClick = () => {
+ emit('click')
+ }
+ const handleCancel = () => {
+ emit('cancel')
+ }
+</script>
diff --git a/rsf-design/src/components/core/base/art-back-to-top/index.vue b/rsf-design/src/components/core/base/art-back-to-top/index.vue
new file mode 100644
index 0000000..d6f7886
--- /dev/null
+++ b/rsf-design/src/components/core/base/art-back-to-top/index.vue
@@ -0,0 +1,36 @@
+<!-- 杩斿洖椤堕儴鎸夐挳 -->
+<template>
+ <Transition
+ enter-active-class="tad-300 ease-out"
+ leave-active-class="tad-200 ease-in"
+ enter-from-class="opacity-0 translate-y-2"
+ enter-to-class="opacity-100 translate-y-0"
+ leave-from-class="opacity-100 translate-y-0"
+ leave-to-class="opacity-0 translate-y-2"
+ >
+ <div
+ v-show="showButton"
+ class="fixed right-10 bottom-15 size-9.5 flex-cc c-p border border-g-300 rounded-md tad-300 hover:bg-g-200"
+ @click="scrollToTop"
+ >
+ <ArtSvgIcon icon="ri:arrow-up-wide-line" class="text-g-500 text-lg" />
+ </div>
+ </Transition>
+</template>
+
+<script setup>
+ import { useCommon } from '@/hooks/core/useCommon'
+ defineOptions({ name: 'ArtBackToTop' })
+ const { scrollToTop } = useCommon()
+ const showButton = ref(false)
+ const scrollThreshold = 300
+ onMounted(() => {
+ const scrollContainer = document.getElementById('app-main')
+ if (scrollContainer) {
+ const { y } = useScroll(scrollContainer)
+ watch(y, (newY) => {
+ showButton.value = newY > scrollThreshold
+ })
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/base/art-logo/index.vue b/rsf-design/src/components/core/base/art-logo/index.vue
new file mode 100644
index 0000000..c7b2a8f
--- /dev/null
+++ b/rsf-design/src/components/core/base/art-logo/index.vue
@@ -0,0 +1,14 @@
+<!-- 绯荤粺logo -->
+<template>
+ <div class="flex-cc">
+ <img :style="logoStyle" src="@imgs/common/logo.webp" alt="logo" class="w-full h-full" />
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtLogo' })
+ const props = defineProps({
+ size: { required: false, default: 36 }
+ })
+ const logoStyle = computed(() => ({ width: `${props.size}px` }))
+</script>
diff --git a/rsf-design/src/components/core/base/art-svg-icon/index.vue b/rsf-design/src/components/core/base/art-svg-icon/index.vue
new file mode 100644
index 0000000..7f2dc9e
--- /dev/null
+++ b/rsf-design/src/components/core/base/art-svg-icon/index.vue
@@ -0,0 +1,20 @@
+<!-- 鍥炬爣缁勪欢 -->
+<template>
+ <span v-if="icon" v-bind="containerAttrs">
+ <Icon :icon="icon" />
+ </span>
+</template>
+
+<script setup>
+ import { Icon } from '@iconify/vue/offline'
+ defineOptions({ name: 'ArtSvgIcon', inheritAttrs: false })
+ defineProps({
+ icon: { required: false }
+ })
+ const attrs = useAttrs()
+ const containerAttrs = computed(() => ({
+ ...attrs,
+ class: ['art-svg-icon inline-flex shrink-0', attrs.class].filter(Boolean).join(' '),
+ style: attrs.style
+ }))
+</script>
diff --git a/rsf-design/src/components/core/cards/art-bar-chart-card/index.vue b/rsf-design/src/components/core/cards/art-bar-chart-card/index.vue
new file mode 100644
index 0000000..39ca650
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-bar-chart-card/index.vue
@@ -0,0 +1,84 @@
+<!-- 鏌辩姸鍥惧崱鐗� -->
+<template>
+ <div class="art-card relative overflow-hidden" :style="{ height: `${height}rem` }">
+ <div class="mb-5 flex-b items-start px-5 pt-5">
+ <div>
+ <p class="m-0 text-2xl font-medium leading-tight text-g-900">
+ {{ value }}
+ </p>
+ <p class="mt-1 text-sm text-g-600">{{ label }}</p>
+ </div>
+ <div
+ class="text-sm font-medium text-danger"
+ :class="[percentage > 0 ? 'text-success' : '', isMiniChart ? 'absolute bottom-5' : '']"
+ >
+ {{ percentage > 0 ? '+' : '' }}{{ percentage }}%
+ </div>
+ <div v-if="date" class="absolute bottom-5 right-5 text-xs text-g-600">
+ {{ date }}
+ </div>
+ </div>
+ <div
+ ref="chartRef"
+ class="absolute bottom-0 left-0 right-0 mx-auto"
+ :class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
+ :style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
+ ></div>
+ </div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+
+ defineOptions({ name: 'ArtBarChartCard' })
+ const props = defineProps({
+ value: { required: false },
+ label: { required: false },
+ date: { required: false },
+ percentage: { required: false, default: 0 },
+ isMiniChart: { required: false, default: false },
+ height: { required: false, default: 11 },
+ barWidth: { required: false, default: '26%' },
+ color: { required: false },
+ chartData: { required: false, default: () => [] }
+ })
+ const { chartRef } = useChartComponent({
+ props: {
+ height: `${props.height}rem`,
+ loading: false,
+ isEmpty: !props.chartData?.length || props.chartData.every((val) => val === 0)
+ },
+ checkEmpty: () => !props.chartData?.length || props.chartData.every((val) => val === 0),
+ watchSources: [() => props.chartData, () => props.color, () => props.barWidth],
+ generateOptions: () => {
+ const computedColor = props.color || useChartOps().themeColor
+ return {
+ grid: {
+ top: 0,
+ right: 0,
+ bottom: 15,
+ left: 0
+ },
+ xAxis: {
+ type: 'category',
+ show: false
+ },
+ yAxis: {
+ type: 'value',
+ show: false
+ },
+ series: [
+ {
+ data: props.chartData,
+ type: 'bar',
+ barWidth: props.barWidth,
+ itemStyle: {
+ color: computedColor,
+ borderRadius: 2
+ }
+ }
+ ]
+ }
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/cards/art-data-list-card/index.vue b/rsf-design/src/components/core/cards/art-data-list-card/index.vue
new file mode 100644
index 0000000..215773d
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-data-list-card/index.vue
@@ -0,0 +1,43 @@
+<!-- 鏁版嵁鍒楄〃鍗$墖 -->
+<template>
+ <div class="art-card p-5">
+ <div class="pb-3.5">
+ <p class="text-lg font-medium">{{ title }}</p>
+ <p class="text-sm text-g-600">{{ subtitle }}</p>
+ </div>
+ <ElScrollbar :style="{ height: maxHeight }">
+ <div v-for="(item, index) in list" :key="index" class="flex-c py-3">
+ <div v-if="item.icon" class="flex-cc mr-3 size-10 rounded-lg" :class="item.class">
+ <ArtSvgIcon :icon="item.icon" class="text-xl" />
+ </div>
+ <div class="flex-1">
+ <div class="mb-1 text-sm">{{ item.title }}</div>
+ <div class="text-xs text-g-500">{{ item.status }}</div>
+ </div>
+ <div class="ml-3 text-xs text-g-500">{{ item.time }}</div>
+ </div>
+ </ElScrollbar>
+ <ElButton
+ class="mt-[25px] w-full text-center"
+ v-if="showMoreButton"
+ v-ripple
+ @click="handleMore"
+ >鏌ョ湅鏇村</ElButton
+ >
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtDataListCard' })
+ const ITEM_HEIGHT = 66
+ const props = defineProps({
+ list: { required: true },
+ title: { required: true },
+ subtitle: { required: false },
+ maxCount: { required: false, default: 5 },
+ showMoreButton: { required: false }
+ })
+ const maxHeight = computed(() => `${ITEM_HEIGHT * props.maxCount}px`)
+ const emit = defineEmits(['more'])
+ const handleMore = () => emit('more')
+</script>
diff --git a/rsf-design/src/components/core/cards/art-donut-chart-card/index.vue b/rsf-design/src/components/core/cards/art-donut-chart-card/index.vue
new file mode 100644
index 0000000..e1b1c76
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-donut-chart-card/index.vue
@@ -0,0 +1,102 @@
+<!-- 鐜瀷鍥惧崱鐗� -->
+<template>
+ <div class="art-card overflow-hidden" :style="{ height: `${height}rem` }">
+ <div class="flex box-border h-full p-5 pr-2">
+ <div class="flex w-full items-start gap-5">
+ <div class="flex-b h-full flex-1 flex-col">
+ <p class="m-0 text-xl font-medium leading-tight text-g-900">
+ {{ title }}
+ </p>
+ <div>
+ <p class="m-0 mt-2.5 text-xl font-medium leading-tight text-g-900">
+ {{ formatNumber(value) }}
+ </p>
+ <div
+ class="mt-1.5 text-xs font-medium"
+ :class="percentage > 0 ? 'text-success' : 'text-danger'"
+ >
+ {{ percentage > 0 ? '+' : '' }}{{ percentage }}%
+ <span v-if="percentageLabel">{{ percentageLabel }}</span>
+ </div>
+ </div>
+ <div class="mt-2 flex gap-4 text-xs text-g-600">
+ <div v-if="currentValue" class="flex-cc">
+ <div class="size-2 bg-theme/100 rounded mr-2"></div>
+ {{ currentValue }}
+ </div>
+ <div v-if="previousValue" class="flex-cc">
+ <div class="size-2 bg-g-400 rounded mr-2"></div>
+ {{ previousValue }}
+ </div>
+ </div>
+ </div>
+ <div class="flex-c h-full max-w-40 flex-1">
+ <div ref="chartRef" class="h-30 w-full"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+
+ defineOptions({ name: 'ArtDonutChartCard' })
+ const props = defineProps({
+ value: { required: true },
+ title: { required: true },
+ percentage: { required: true },
+ percentageLabel: { required: false },
+ currentValue: { required: false },
+ previousValue: { required: false },
+ height: { required: false, default: 9 },
+ color: { required: false },
+ radius: { required: false, default: () => ['70%', '90%'] },
+ data: { required: false, default: () => [0, 0] }
+ })
+ const formatNumber = (num) => {
+ return num.toLocaleString()
+ }
+ const { chartRef } = useChartComponent({
+ props: {
+ height: `${props.height}rem`,
+ loading: false,
+ isEmpty: props.data.every((val) => val === 0)
+ },
+ checkEmpty: () => props.data.every((val) => val === 0),
+ watchSources: [
+ () => props.data,
+ () => props.color,
+ () => props.radius,
+ () => props.currentValue,
+ () => props.previousValue
+ ],
+ generateOptions: () => {
+ const computedColor = props.color || useChartOps().themeColor
+ return {
+ series: [
+ {
+ type: 'pie',
+ radius: props.radius,
+ avoidLabelOverlap: false,
+ label: {
+ show: false
+ },
+ data: [
+ {
+ value: props.data[0],
+ name: props.currentValue,
+ itemStyle: { color: computedColor }
+ },
+ {
+ value: props.data[1],
+ name: props.previousValue,
+ itemStyle: { color: '#e6e8f7' }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/cards/art-image-card/index.vue b/rsf-design/src/components/core/cards/art-image-card/index.vue
new file mode 100644
index 0000000..36f924a
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-image-card/index.vue
@@ -0,0 +1,67 @@
+<!-- 鍥剧墖鍗$墖 -->
+<template>
+ <div class="w-full c-p" @click="handleClick">
+ <div class="art-card overflow-hidden">
+ <div class="relative w-full aspect-[16/10] overflow-hidden">
+ <ElImage
+ :src="props.imageUrl"
+ fit="cover"
+ loading="lazy"
+ class="w-full h-full transition-transform duration-300 ease-in-out hover:scale-105"
+ >
+ <template #placeholder>
+ <div class="flex-cc w-full h-full bg-[#f5f7fa]">
+ <ElIcon><Picture /></ElIcon>
+ </div>
+ </template>
+ </ElImage>
+ <div
+ class="absolute right-3.5 bottom-3.5 py-1 px-2 text-xs bg-g-200 rounded"
+ v-if="props.readTime"
+ >
+ {{ props.readTime }} 闃呰
+ </div>
+ </div>
+
+ <div class="p-4">
+ <div
+ class="inline-block py-0.5 px-2 mb-2 text-xs bg-g-300/70 rounded"
+ v-if="props.category"
+ >
+ {{ props.category }}
+ </div>
+ <p class="m-0 mb-3 text-base font-medium">{{ props.title }}</p>
+ <div class="flex-c gap-4 text-xs text-g-600">
+ <span class="flex-c gap-1" v-if="props.views">
+ <ElIcon class="text-base"><View /></ElIcon>
+ {{ props.views }}
+ </span>
+ <span class="flex-c gap-1" v-if="props.comments">
+ <ElIcon class="text-base"><ChatLineRound /></ElIcon>
+ {{ props.comments }}
+ </span>
+ <span>{{ props.date }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { Picture, View, ChatLineRound } from '@element-plus/icons-vue'
+
+ defineOptions({ name: 'ArtImageCard' })
+ const props = defineProps({
+ imageUrl: { required: false, default: '' },
+ title: { required: false, default: '' },
+ category: { required: false, default: '' },
+ readTime: { required: false, default: '' },
+ views: { required: false, default: 0 },
+ comments: { required: false, default: 0 },
+ date: { required: false, default: '' }
+ })
+ const emit = defineEmits(['click'])
+ const handleClick = () => {
+ emit('click', props)
+ }
+</script>
diff --git a/rsf-design/src/components/core/cards/art-line-chart-card/index.vue b/rsf-design/src/components/core/cards/art-line-chart-card/index.vue
new file mode 100644
index 0000000..084fe20
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-line-chart-card/index.vue
@@ -0,0 +1,109 @@
+<!-- 鎶樼嚎鍥惧崱鐗� -->
+<template>
+ <div class="art-card relative overflow-hidden" :style="{ height: `${height}rem` }">
+ <div class="mb-2.5 flex-b items-start p-5">
+ <div>
+ <p class="text-2xl font-medium leading-none">
+ {{ value }}
+ </p>
+ <p class="mt-1 text-sm text-g-500">{{ label }}</p>
+ </div>
+ <div
+ class="text-sm font-medium"
+ :class="[
+ percentage > 0 ? 'text-success' : 'text-danger',
+ isMiniChart ? 'absolute bottom-5' : ''
+ ]"
+ >
+ {{ percentage > 0 ? '+' : '' }}{{ percentage }}%
+ </div>
+ <div v-if="date" class="absolute bottom-5 right-5 text-xs text-g-500">
+ {{ date }}
+ </div>
+ </div>
+ <div
+ ref="chartRef"
+ class="absolute bottom-0 left-0 right-0 box-border w-full"
+ :class="isMiniChart ? '!absolute !top-5 !right-5 !bottom-auto !left-auto !h-15 !w-4/10' : ''"
+ :style="{ height: isMiniChart ? '60px' : `calc(${height}rem - 5rem)` }"
+ ></div>
+ </div>
+</template>
+
+<script setup>
+ import { graphic } from '@/plugins/echarts'
+ import { getCssVar, hexToRgba } from '@/utils/ui'
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+
+ defineOptions({ name: 'ArtLineChartCard' })
+ const props = defineProps({
+ value: { required: false },
+ label: { required: false },
+ date: { required: false },
+ percentage: { required: false, default: 0 },
+ isMiniChart: { required: false, default: false },
+ height: { required: false, default: 11 },
+ color: { required: false },
+ chartData: { required: false, default: () => [] },
+ showAreaColor: { required: false, default: false }
+ })
+ const { chartRef } = useChartComponent({
+ props: {
+ height: `${props.height}rem`,
+ loading: false,
+ isEmpty: !props.chartData?.length || props.chartData.every((val) => val === 0)
+ },
+ checkEmpty: () => !props.chartData?.length || props.chartData.every((val) => val === 0),
+ watchSources: [() => props.chartData, () => props.color, () => props.showAreaColor],
+ generateOptions: () => {
+ const computedColor = props.color || useChartOps().themeColor
+ return {
+ grid: {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+ xAxis: {
+ type: 'category',
+ show: false,
+ boundaryGap: false
+ },
+ yAxis: {
+ type: 'value',
+ show: false
+ },
+ series: [
+ {
+ data: props.chartData,
+ type: 'line',
+ smooth: true,
+ showSymbol: false,
+ lineStyle: {
+ width: 3,
+ color: computedColor
+ },
+ areaStyle: props.showAreaColor
+ ? {
+ color: new graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color: props.color
+ ? hexToRgba(props.color, 0.2).rgba
+ : hexToRgba(getCssVar('--el-color-primary'), 0.2).rgba
+ },
+ {
+ offset: 1,
+ color: props.color
+ ? hexToRgba(props.color, 0.01).rgba
+ : hexToRgba(getCssVar('--el-color-primary'), 0.01).rgba
+ }
+ ])
+ }
+ : void 0
+ }
+ ]
+ }
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/cards/art-progress-card/index.vue b/rsf-design/src/components/core/cards/art-progress-card/index.vue
new file mode 100644
index 0000000..dc6e616
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-progress-card/index.vue
@@ -0,0 +1,65 @@
+<!-- 杩涘害鏉″崱鐗� -->
+<template>
+ <div class="art-card h-32 flex flex-col justify-center px-5">
+ <div class="mb-3.5 flex-c" :style="{ justifyContent: icon ? 'space-between' : 'flex-start' }">
+ <div v-if="icon" class="size-11 flex-cc bg-g-300 text-xl rounded-lg" :class="iconStyle">
+ <ArtSvgIcon :icon="icon" class="text-2xl"></ArtSvgIcon>
+ </div>
+ <div>
+ <ArtCountTo
+ class="mb-1 block text-2xl font-semibold"
+ :target="percentage"
+ :duration="2000"
+ suffix="%"
+ :style="{ textAlign: icon ? 'right' : 'left' }"
+ />
+ <p class="text-sm text-g-500">{{ title }}</p>
+ </div>
+ </div>
+ <ElProgress
+ :percentage="currentPercentage"
+ :stroke-width="strokeWidth"
+ :show-text="false"
+ :color="color"
+ class="[&_.el-progress-bar__outer]:bg-[rgb(240_240_240)]"
+ />
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtProgressCard' })
+ const props = defineProps({
+ percentage: { required: true },
+ title: { required: false },
+ icon: { required: false },
+ iconStyle: { required: false },
+ strokeWidth: { required: false, default: 5 },
+ color: { required: false, default: '#67C23A' }
+ })
+ const animationDuration = 500
+ const currentPercentage = ref(0)
+ const animateProgress = () => {
+ const startTime = Date.now()
+ const startValue = currentPercentage.value
+ const endValue = props.percentage
+ const animate = () => {
+ const currentTime = Date.now()
+ const elapsed = currentTime - startTime
+ const progress = Math.min(elapsed / animationDuration, 1)
+ currentPercentage.value = startValue + (endValue - startValue) * progress
+ if (progress < 1) {
+ requestAnimationFrame(animate)
+ }
+ }
+ requestAnimationFrame(animate)
+ }
+ onMounted(() => {
+ animateProgress()
+ })
+ watch(
+ () => props.percentage,
+ () => {
+ animateProgress()
+ }
+ )
+</script>
diff --git a/rsf-design/src/components/core/cards/art-stats-card/index.vue b/rsf-design/src/components/core/cards/art-stats-card/index.vue
new file mode 100644
index 0000000..26aae1b
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-stats-card/index.vue
@@ -0,0 +1,51 @@
+<!-- 缁熻鍗$墖 -->
+<template>
+ <div
+ class="art-card h-32 flex-c px-5 transition-transform duration-200 hover:-translate-y-0.5"
+ :class="boxStyle"
+ >
+ <div v-if="icon" class="mr-4 size-11 flex-cc rounded-lg text-xl text-white" :class="iconStyle">
+ <ArtSvgIcon :icon="icon"></ArtSvgIcon>
+ </div>
+ <div class="flex-1">
+ <p class="m-0 text-lg font-medium" :style="{ color: textColor }" v-if="title">
+ {{ title }}
+ </p>
+ <ArtCountTo
+ class="m-0 text-2xl font-medium"
+ v-if="count !== undefined"
+ :target="count"
+ :duration="2000"
+ :decimals="decimals"
+ :separator="separator"
+ />
+ <p
+ class="mt-1 text-sm text-g-500 opacity-90"
+ :style="{ color: textColor }"
+ v-if="description"
+ >{{ description }}</p
+ >
+ </div>
+ <div v-if="showArrow">
+ <ArtSvgIcon icon="ri:arrow-right-s-line" class="text-xl text-g-500" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtStatsCard' })
+ defineProps({
+ icon: { required: false },
+ title: { required: false },
+ description: { required: false },
+ count: { required: false },
+ showArrow: { required: false, default: false },
+ boxStyle: { required: false },
+ iconStyle: { required: false },
+ textColor: { required: false },
+ iconSize: { required: false, default: 30 },
+ iconBgRadius: { required: false, default: 50 },
+ decimals: { required: false, default: 0 },
+ separator: { required: false, default: ',' }
+ })
+</script>
diff --git a/rsf-design/src/components/core/cards/art-timeline-list-card/index.vue b/rsf-design/src/components/core/cards/art-timeline-list-card/index.vue
new file mode 100644
index 0000000..1a13e83
--- /dev/null
+++ b/rsf-design/src/components/core/cards/art-timeline-list-card/index.vue
@@ -0,0 +1,41 @@
+<!-- 鏃堕棿杞村垪琛ㄥ崱鐗� -->
+<template>
+ <div class="art-card p-5">
+ <div class="pb-3.5">
+ <p class="text-lg font-medium">{{ title }}</p>
+ <p class="text-sm text-g-600">{{ subtitle }}</p>
+ </div>
+ <ElScrollbar :style="{ height: maxHeight }">
+ <ElTimeline class="!pl-0.5">
+ <ElTimelineItem
+ v-for="item in list"
+ :key="item.time"
+ :timestamp="item.time"
+ :placement="TIMELINE_PLACEMENT"
+ :color="item.status"
+ :center="true"
+ >
+ <div class="flex-c gap-3">
+ <div class="flex-c gap-2">
+ <span class="text-sm">{{ item.content }}</span>
+ <span v-if="item.code" class="text-sm text-theme"> #{{ item.code }} </span>
+ </div>
+ </div>
+ </ElTimelineItem>
+ </ElTimeline>
+ </ElScrollbar>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtTimelineListCard' })
+ const ITEM_HEIGHT = 65
+ const TIMELINE_PLACEMENT = 'top'
+ const props = defineProps({
+ list: { required: true },
+ title: { required: false, default: '' },
+ subtitle: { required: false, default: '' },
+ maxCount: { required: false, default: 5 }
+ })
+ const maxHeight = computed(() => `${ITEM_HEIGHT * props.maxCount}px`)
+</script>
diff --git a/rsf-design/src/components/core/charts/art-bar-chart/index.vue b/rsf-design/src/components/core/charts/art-bar-chart/index.vue
new file mode 100644
index 0000000..04aa680
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-bar-chart/index.vue
@@ -0,0 +1,157 @@
+<!-- 鏌辩姸鍥� -->
+<template>
+ <div ref="chartRef" :style="{ height: props.height }" v-loading="props.loading"> </div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ import { getCssVar } from '@/utils/ui'
+ import { graphic } from '@/plugins/echarts'
+ defineOptions({ name: 'ArtBarChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ borderRadius: { required: false, default: 4 },
+ data: { required: false, default: () => [0, 0, 0, 0, 0, 0, 0] },
+ xAxisData: { required: false, default: () => [] },
+ barWidth: { required: false, default: '40%' },
+ stack: { required: false, default: false },
+ showAxisLabel: { required: false, default: true },
+ showAxisLine: { required: false, default: true },
+ showSplitLine: { required: false, default: true },
+ showTooltip: { required: false, default: true },
+ showLegend: { required: false, default: false },
+ legendPosition: { required: false, default: 'bottom' }
+ })
+ const isMultipleData = computed(() => {
+ return (
+ Array.isArray(props.data) &&
+ props.data.length > 0 &&
+ typeof props.data[0] === 'object' &&
+ 'name' in props.data[0]
+ )
+ })
+ const getColor = (customColor, index) => {
+ if (customColor) return customColor
+ if (index !== void 0) {
+ return props.colors[index % props.colors.length]
+ }
+ return new graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color: getCssVar('--el-color-primary-light-4')
+ },
+ {
+ offset: 1,
+ color: getCssVar('--el-color-primary')
+ }
+ ])
+ }
+ const createGradientColor = (color) => {
+ return new graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color
+ },
+ {
+ offset: 1,
+ color
+ }
+ ])
+ }
+ const getBaseItemStyle = (color) => ({
+ borderRadius: props.borderRadius,
+ color: typeof color === 'string' ? createGradientColor(color) : color
+ })
+ const createSeriesItem = (config) => {
+ const animationConfig = getAnimationConfig()
+ return {
+ name: config.name,
+ data: config.data,
+ type: 'bar',
+ stack: config.stack,
+ itemStyle: getBaseItemStyle(config.color),
+ barWidth: config.barWidth || props.barWidth,
+ ...animationConfig
+ }
+ }
+ const {
+ chartRef,
+ getAxisLineStyle,
+ getAxisLabelStyle,
+ getAxisTickStyle,
+ getSplitLineStyle,
+ getAnimationConfig,
+ getTooltipStyle,
+ getLegendStyle,
+ getGridWithLegend
+ } = useChartComponent({
+ props,
+ checkEmpty: () => {
+ if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
+ const singleData = props.data
+ return !singleData.length || singleData.every((val) => val === 0)
+ }
+ if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
+ const multiData = props.data
+ return (
+ !multiData.length ||
+ multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
+ )
+ }
+ return true
+ },
+ watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
+ generateOptions: () => {
+ const options = {
+ grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
+ top: 15,
+ right: 0,
+ left: 0
+ }),
+ tooltip: props.showTooltip ? getTooltipStyle() : void 0,
+ xAxis: {
+ type: 'category',
+ data: props.xAxisData,
+ axisTick: getAxisTickStyle(),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ axisLabel: getAxisLabelStyle(props.showAxisLabel)
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ splitLine: getSplitLineStyle(props.showSplitLine)
+ }
+ }
+ if (props.showLegend && isMultipleData.value) {
+ options.legend = getLegendStyle(props.legendPosition)
+ }
+ if (isMultipleData.value) {
+ const multiData = props.data
+ options.series = multiData.map((item, index) => {
+ const computedColor = getColor(props.colors[index], index)
+ return createSeriesItem({
+ name: item.name,
+ data: item.data,
+ color: computedColor,
+ barWidth: item.barWidth,
+ stack: props.stack ? item.stack || 'total' : void 0
+ })
+ })
+ } else {
+ const singleData = props.data
+ const computedColor = getColor()
+ options.series = [
+ createSeriesItem({
+ data: singleData,
+ color: computedColor
+ })
+ ]
+ }
+ return options
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/charts/art-dual-bar-compare-chart/index.vue b/rsf-design/src/components/core/charts/art-dual-bar-compare-chart/index.vue
new file mode 100644
index 0000000..ff869b9
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-dual-bar-compare-chart/index.vue
@@ -0,0 +1,159 @@
+<!-- 鍙屽悜鍫嗗彔鏌辩姸鍥� -->
+<template>
+ <div ref="chartRef" :style="{ height: props.height }" v-loading="props.loading"> </div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ defineOptions({ name: 'ArtDualBarCompareChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ positiveData: { required: false, default: () => [] },
+ negativeData: { required: false, default: () => [] },
+ xAxisData: { required: false, default: () => [] },
+ positiveName: { required: false, default: '姝e悜鏁版嵁' },
+ negativeName: { required: false, default: '璐熷悜鏁版嵁' },
+ barWidth: { required: false, default: 16 },
+ yAxisMin: { required: false, default: -100 },
+ yAxisMax: { required: false, default: 100 },
+ showDataLabel: { required: false, default: false },
+ positiveBorderRadius: { required: false, default: () => [10, 10, 0, 0] },
+ negativeBorderRadius: { required: false, default: () => [0, 0, 10, 10] },
+ showAxisLabel: { required: false, default: true },
+ showAxisLine: { required: false, default: false },
+ showSplitLine: { required: false, default: false },
+ showTooltip: { required: false, default: true },
+ showLegend: { required: false, default: false },
+ legendPosition: { required: false, default: 'bottom' }
+ })
+ const createSeriesConfig = (config) => {
+ const { fontColor } = useChartOps()
+ const animationConfig = getAnimationConfig()
+ return {
+ name: config.name,
+ type: 'bar',
+ stack: 'total',
+ barWidth: props.barWidth,
+ barGap: '-100%',
+ data: config.data,
+ itemStyle: {
+ borderRadius: config.borderRadius,
+ color: props.colors[config.colorIndex]
+ },
+ label: {
+ show: props.showDataLabel,
+ position: config.labelPosition,
+ formatter: config.formatter || ((params) => String(params.value)),
+ color: fontColor,
+ fontSize: 12
+ },
+ ...animationConfig
+ }
+ }
+ const {
+ chartRef,
+ getAxisLineStyle,
+ getAxisLabelStyle,
+ getAxisTickStyle,
+ getSplitLineStyle,
+ getAnimationConfig,
+ getTooltipStyle,
+ getLegendStyle,
+ getGridWithLegend
+ } = useChartComponent({
+ props,
+ checkEmpty: () => {
+ return (
+ props.isEmpty ||
+ !props.positiveData.length ||
+ !props.negativeData.length ||
+ (props.positiveData.every((val) => val === 0) &&
+ props.negativeData.every((val) => val === 0))
+ )
+ },
+ watchSources: [
+ () => props.positiveData,
+ () => props.negativeData,
+ () => props.xAxisData,
+ () => props.colors
+ ],
+ generateOptions: () => {
+ const processedNegativeData = props.negativeData.map((val) => (val > 0 ? -val : val))
+ const gridConfig = {
+ top: props.showLegend ? 50 : 20,
+ right: 0,
+ left: 0,
+ bottom: 0,
+ // 澧炲姞搴曢儴闂磋窛
+ containLabel: true
+ }
+ const options = {
+ backgroundColor: 'transparent',
+ animation: true,
+ animationDuration: 1e3,
+ animationEasing: 'cubicOut',
+ grid: getGridWithLegend(props.showLegend, props.legendPosition, gridConfig),
+ // 浼樺寲鐨勬彁绀烘閰嶇疆
+ tooltip: props.showTooltip
+ ? {
+ ...getTooltipStyle(),
+ trigger: 'axis',
+ axisPointer: {
+ type: 'none'
+ // 鍘婚櫎鎸囩ず绾�
+ }
+ }
+ : void 0,
+ // 鍥句緥閰嶇疆
+ legend: props.showLegend
+ ? {
+ ...getLegendStyle(props.legendPosition),
+ data: [props.negativeName, props.positiveName]
+ }
+ : void 0,
+ // X杞撮厤缃�
+ xAxis: {
+ type: 'category',
+ data: props.xAxisData,
+ axisTick: getAxisTickStyle(),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ boundaryGap: true
+ },
+ // Y杞撮厤缃�
+ yAxis: {
+ type: 'value',
+ min: props.yAxisMin,
+ max: props.yAxisMax,
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ splitLine: getSplitLineStyle(props.showSplitLine)
+ },
+ // 绯诲垪閰嶇疆
+ series: [
+ // 璐熷悜鏁版嵁绯诲垪
+ createSeriesConfig({
+ name: props.negativeName,
+ data: processedNegativeData,
+ borderRadius: props.negativeBorderRadius,
+ labelPosition: 'bottom',
+ colorIndex: 1,
+ formatter: (params) => String(Math.abs(params.value))
+ }),
+ // 姝e悜鏁版嵁绯诲垪
+ createSeriesConfig({
+ name: props.positiveName,
+ data: props.positiveData,
+ borderRadius: props.positiveBorderRadius,
+ labelPosition: 'top',
+ colorIndex: 0
+ })
+ ]
+ }
+ return options
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/charts/art-h-bar-chart/index.vue b/rsf-design/src/components/core/charts/art-h-bar-chart/index.vue
new file mode 100644
index 0000000..d0820e0
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-h-bar-chart/index.vue
@@ -0,0 +1,162 @@
+<!-- 姘村钩鏌辩姸鍥� -->
+<template>
+ <div
+ ref="chartRef"
+ class="relative w-full"
+ :style="{ height: props.height }"
+ v-loading="props.loading"
+ ></div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ import { getCssVar } from '@/utils/ui'
+ import { graphic } from '@/plugins/echarts'
+ defineOptions({ name: 'ArtHBarChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ data: { required: false, default: () => [0, 0, 0, 0, 0, 0, 0] },
+ xAxisData: { required: false, default: () => [] },
+ barWidth: { required: false, default: '36%' },
+ stack: { required: false, default: false },
+ showAxisLabel: { required: false, default: true },
+ showAxisLine: { required: false, default: true },
+ showSplitLine: { required: false, default: true },
+ showTooltip: { required: false, default: true },
+ showLegend: { required: false, default: false },
+ legendPosition: { required: false, default: 'bottom' }
+ })
+ const isMultipleData = computed(() => {
+ return (
+ Array.isArray(props.data) &&
+ props.data.length > 0 &&
+ typeof props.data[0] === 'object' &&
+ 'name' in props.data[0]
+ )
+ })
+ const getColor = (customColor, index) => {
+ if (customColor) return customColor
+ if (index !== void 0) {
+ return props.colors[index % props.colors.length]
+ }
+ return new graphic.LinearGradient(0, 0, 1, 0, [
+ {
+ offset: 0,
+ color: getCssVar('--el-color-primary')
+ },
+ {
+ offset: 1,
+ color: getCssVar('--el-color-primary-light-4')
+ }
+ ])
+ }
+ const createGradientColor = (color) => {
+ return new graphic.LinearGradient(0, 0, 1, 0, [
+ {
+ offset: 0,
+ color
+ },
+ {
+ offset: 1,
+ color
+ }
+ ])
+ }
+ const getBaseItemStyle = (color) => ({
+ borderRadius: 4,
+ color: typeof color === 'string' ? createGradientColor(color) : color
+ })
+ const createSeriesItem = (config) => {
+ const animationConfig = getAnimationConfig()
+ return {
+ name: config.name,
+ data: config.data,
+ type: 'bar',
+ stack: config.stack,
+ itemStyle: getBaseItemStyle(config.color),
+ barWidth: config.barWidth || props.barWidth,
+ ...animationConfig
+ }
+ }
+ const {
+ chartRef,
+ getAxisLineStyle,
+ getAxisLabelStyle,
+ getAxisTickStyle,
+ getSplitLineStyle,
+ getAnimationConfig,
+ getTooltipStyle,
+ getLegendStyle,
+ getGridWithLegend
+ } = useChartComponent({
+ props,
+ checkEmpty: () => {
+ if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
+ const singleData = props.data
+ return !singleData.length || singleData.every((val) => val === 0)
+ }
+ if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
+ const multiData = props.data
+ return (
+ !multiData.length ||
+ multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
+ )
+ }
+ return true
+ },
+ watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
+ generateOptions: () => {
+ const options = {
+ grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
+ top: 15,
+ right: 0,
+ left: 0
+ }),
+ tooltip: props.showTooltip ? getTooltipStyle() : void 0,
+ xAxis: {
+ type: 'value',
+ axisTick: getAxisTickStyle(),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ splitLine: getSplitLineStyle(props.showSplitLine)
+ },
+ yAxis: {
+ type: 'category',
+ data: props.xAxisData,
+ axisTick: getAxisTickStyle(),
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ axisLine: getAxisLineStyle(props.showAxisLine)
+ }
+ }
+ if (props.showLegend && isMultipleData.value) {
+ options.legend = getLegendStyle(props.legendPosition)
+ }
+ if (isMultipleData.value) {
+ const multiData = props.data
+ options.series = multiData.map((item, index) => {
+ const computedColor = getColor(props.colors[index], index)
+ return createSeriesItem({
+ name: item.name,
+ data: item.data,
+ color: computedColor,
+ barWidth: item.barWidth,
+ stack: props.stack ? item.stack || 'total' : void 0
+ })
+ })
+ } else {
+ const singleData = props.data
+ const computedColor = getColor()
+ options.series = [
+ createSeriesItem({
+ data: singleData,
+ color: computedColor
+ })
+ ]
+ }
+ return options
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/charts/art-k-line-chart/index.vue b/rsf-design/src/components/core/charts/art-k-line-chart/index.vue
new file mode 100644
index 0000000..9cee2a9
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-k-line-chart/index.vue
@@ -0,0 +1,139 @@
+<!-- k绾垮浘琛� -->
+<template>
+ <div
+ ref="chartRef"
+ class="relative w-full"
+ :style="{ height: props.height }"
+ v-loading="props.loading"
+ ></div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ defineOptions({ name: 'ArtKLineChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ data: { required: false, default: () => [] },
+ showDataZoom: { required: false, default: false },
+ dataZoomStart: { required: false, default: 0 },
+ dataZoomEnd: { required: false, default: 100 }
+ })
+ const getActualColors = () => {
+ const defaultUpColor = '#4C87F3'
+ const defaultDownColor = '#8BD8FC'
+ return {
+ upColor: props.colors?.[0] || defaultUpColor,
+ downColor: props.colors?.[1] || defaultDownColor
+ }
+ }
+ const {
+ chartRef,
+ getAxisLineStyle,
+ getAxisLabelStyle,
+ getAxisTickStyle,
+ getSplitLineStyle,
+ getAnimationConfig,
+ getTooltipStyle
+ } = useChartComponent({
+ props,
+ checkEmpty: () => {
+ return (
+ !props.data?.length ||
+ props.data.every(
+ (item) => item.open === 0 && item.close === 0 && item.high === 0 && item.low === 0
+ )
+ )
+ },
+ watchSources: [
+ () => props.data,
+ () => props.colors,
+ () => props.showDataZoom,
+ () => props.dataZoomStart,
+ () => props.dataZoomEnd
+ ],
+ generateOptions: () => {
+ const { upColor, downColor } = getActualColors()
+ return {
+ grid: {
+ top: 20,
+ right: 20,
+ bottom: props.showDataZoom ? 80 : 20,
+ left: 20,
+ containLabel: true
+ },
+ tooltip: getTooltipStyle('axis', {
+ axisPointer: {
+ type: 'cross'
+ },
+ formatter: (params) => {
+ const param = params[0]
+ const data = param.data
+ return `
+ <div style="padding: 5px;">
+ <div><strong>鏃堕棿锛�</strong>${param.name}</div>
+ <div><strong>寮�鐩橈細</strong>${data[0]}</div>
+ <div><strong>鏀剁洏锛�</strong>${data[1]}</div>
+ <div><strong>鏈�浣庯細</strong>${data[2]}</div>
+ <div><strong>鏈�楂橈細</strong>${data[3]}</div>
+ </div>
+ `
+ }
+ }),
+ xAxis: {
+ type: 'category',
+ data: props.data.map((item) => item.time),
+ axisTick: getAxisTickStyle(),
+ axisLine: getAxisLineStyle(true),
+ axisLabel: getAxisLabelStyle(true)
+ },
+ yAxis: {
+ type: 'value',
+ scale: true,
+ axisLabel: getAxisLabelStyle(true),
+ axisLine: getAxisLineStyle(true),
+ splitLine: getSplitLineStyle(true)
+ },
+ series: [
+ {
+ type: 'candlestick',
+ data: props.data.map((item) => [item.open, item.close, item.low, item.high]),
+ itemStyle: {
+ color: upColor,
+ color0: downColor,
+ borderColor: upColor,
+ borderColor0: downColor,
+ borderWidth: 1
+ },
+ emphasis: {
+ itemStyle: {
+ borderWidth: 2,
+ shadowBlur: 10,
+ shadowColor: 'rgba(0, 0, 0, 0.3)'
+ }
+ },
+ ...getAnimationConfig()
+ }
+ ],
+ dataZoom: props.showDataZoom
+ ? [
+ {
+ type: 'inside',
+ start: props.dataZoomStart,
+ end: props.dataZoomEnd
+ },
+ {
+ show: true,
+ type: 'slider',
+ top: '90%',
+ start: props.dataZoomStart,
+ end: props.dataZoomEnd
+ }
+ ]
+ : void 0
+ }
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/charts/art-line-chart/index.vue b/rsf-design/src/components/core/charts/art-line-chart/index.vue
new file mode 100644
index 0000000..838e316
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-line-chart/index.vue
@@ -0,0 +1,288 @@
+<!-- 鎶樼嚎鍥撅紝鏀寔澶氱粍鏁版嵁锛屾敮鎸侀樁姊紡鍔ㄧ敾鏁堟灉 -->
+<template>
+ <div
+ ref="chartRef"
+ class="relative w-[calc(100%+10px)]"
+ :style="{ height: props.height }"
+ v-loading="props.loading"
+ >
+ </div>
+</template>
+
+<script setup>
+ import { graphic } from '@/plugins/echarts'
+ import { getCssVar, hexToRgba } from '@/utils/ui'
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ defineOptions({ name: 'ArtLineChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ data: { required: false, default: () => [0, 0, 0, 0, 0, 0, 0] },
+ xAxisData: { required: false, default: () => [] },
+ lineWidth: { required: false, default: 2.5 },
+ showAreaColor: { required: false, default: false },
+ smooth: { required: false, default: true },
+ symbol: { required: false, default: 'none' },
+ symbolSize: { required: false, default: 6 },
+ animationDelay: { required: false, default: 200 },
+ showAxisLabel: { required: false, default: true },
+ showAxisLine: { required: false, default: true },
+ showSplitLine: { required: false, default: true },
+ showTooltip: { required: false, default: true },
+ showLegend: { required: false, default: false },
+ legendPosition: { required: false, default: 'bottom' }
+ })
+ const isAnimating = ref(false)
+ const animationTimers = ref([])
+ const animatedData = ref([])
+ const clearAnimationTimers = () => {
+ animationTimers.value.forEach((timer) => clearTimeout(timer))
+ animationTimers.value = []
+ }
+ const isMultipleData = computed(() => {
+ return (
+ Array.isArray(props.data) &&
+ props.data.length > 0 &&
+ typeof props.data[0] === 'object' &&
+ 'name' in props.data[0]
+ )
+ })
+ const maxValue = computed(() => {
+ if (isMultipleData.value) {
+ const multiData = props.data
+ return multiData.reduce((max, item) => {
+ if (item.data?.length) {
+ const itemMax = Math.max(...item.data)
+ return Math.max(max, itemMax)
+ }
+ return max
+ }, 0)
+ } else {
+ const singleData = props.data
+ return singleData?.length ? Math.max(...singleData) : 0
+ }
+ })
+ const initAnimationData = () => {
+ if (isMultipleData.value) {
+ const multiData = props.data
+ return multiData.map((item) => ({
+ ...item,
+ data: Array(item.data.length).fill(0)
+ }))
+ }
+ const singleData = props.data
+ return Array(singleData.length).fill(0)
+ }
+ const copyRealData = () => {
+ if (isMultipleData.value) {
+ return props.data.map((item) => ({ ...item, data: [...item.data] }))
+ }
+ return [...props.data]
+ }
+ const primaryColor = computed(() => getCssVar('--el-color-primary'))
+ const getColor = (customColor, index) => {
+ if (customColor) return customColor
+ if (index !== void 0) return props.colors[index % props.colors.length]
+ return primaryColor.value
+ }
+ const generateAreaStyle = (item, color) => {
+ if (!item.areaStyle && !item.showAreaColor && !props.showAreaColor) return void 0
+ const areaConfig = item.areaStyle || {}
+ if (areaConfig.custom) return areaConfig.custom
+ return {
+ color: new graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color: hexToRgba(color, areaConfig.startOpacity || 0.2).rgba
+ },
+ {
+ offset: 1,
+ color: hexToRgba(color, areaConfig.endOpacity || 0.02).rgba
+ }
+ ])
+ }
+ }
+ const generateSingleAreaStyle = () => {
+ if (!props.showAreaColor) return void 0
+ const color = getColor(props.colors[0])
+ return {
+ color: new graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color: hexToRgba(color, 0.2).rgba
+ },
+ {
+ offset: 1,
+ color: hexToRgba(color, 0.02).rgba
+ }
+ ])
+ }
+ }
+ const createSeriesItem = (config) => {
+ return {
+ name: config.name,
+ data: config.data,
+ type: 'line',
+ color: config.color,
+ smooth: config.smooth ?? props.smooth,
+ symbol: config.symbol ?? props.symbol,
+ symbolSize: config.symbolSize ?? props.symbolSize,
+ lineStyle: {
+ width: config.lineWidth ?? props.lineWidth,
+ color: config.color
+ },
+ areaStyle: config.areaStyle,
+ emphasis: {
+ focus: 'series',
+ lineStyle: {
+ width: (config.lineWidth ?? props.lineWidth) + 1
+ }
+ }
+ }
+ }
+ const generateChartOptions = (isInitial = false) => {
+ const options = {
+ animation: true,
+ animationDuration: isInitial ? 0 : 1300,
+ animationDurationUpdate: isInitial ? 0 : 1300,
+ grid: getGridWithLegend(props.showLegend && isMultipleData.value, props.legendPosition, {
+ top: 15,
+ right: 15,
+ left: 0
+ }),
+ tooltip: props.showTooltip ? getTooltipStyle() : void 0,
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: props.xAxisData,
+ axisTick: getAxisTickStyle(),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ axisLabel: getAxisLabelStyle(props.showAxisLabel)
+ },
+ yAxis: {
+ type: 'value',
+ min: 0,
+ max: maxValue.value,
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ splitLine: getSplitLineStyle(props.showSplitLine)
+ }
+ }
+ if (props.showLegend && isMultipleData.value) {
+ options.legend = getLegendStyle(props.legendPosition)
+ }
+ if (isMultipleData.value) {
+ const multiData = animatedData.value
+ options.series = multiData.map((item, index) => {
+ const itemColor = getColor(props.colors[index], index)
+ const areaStyle = generateAreaStyle(item, itemColor)
+ return createSeriesItem({
+ name: item.name,
+ data: item.data,
+ color: itemColor,
+ smooth: item.smooth,
+ symbol: item.symbol,
+ lineWidth: item.lineWidth,
+ areaStyle
+ })
+ })
+ } else {
+ const singleData = animatedData.value
+ const computedColor = getColor(props.colors[0])
+ const areaStyle = generateSingleAreaStyle()
+ options.series = [
+ createSeriesItem({
+ data: singleData,
+ color: computedColor,
+ areaStyle
+ })
+ ]
+ }
+ return options
+ }
+ const updateChartOptions = (options) => {
+ initChart(options)
+ }
+ const initChartWithAnimation = () => {
+ clearAnimationTimers()
+ isAnimating.value = true
+ animatedData.value = initAnimationData()
+ updateChartOptions(generateChartOptions(true))
+ if (isMultipleData.value) {
+ const multiData = props.data
+ const currentAnimatedData = animatedData.value
+ multiData.forEach((item, index) => {
+ const timer = window.setTimeout(
+ () => {
+ currentAnimatedData[index] = { ...item, data: [...item.data] }
+ animatedData.value = [...currentAnimatedData]
+ updateChartOptions(generateChartOptions(false))
+ },
+ index * props.animationDelay + 100
+ )
+ animationTimers.value.push(timer)
+ })
+ const totalDelay = (multiData.length - 1) * props.animationDelay + 1500
+ const finishTimer = window.setTimeout(() => {
+ isAnimating.value = false
+ }, totalDelay)
+ animationTimers.value.push(finishTimer)
+ } else {
+ nextTick(() => {
+ animatedData.value = copyRealData()
+ updateChartOptions(generateChartOptions(false))
+ isAnimating.value = false
+ })
+ }
+ }
+ const checkIsEmpty = () => {
+ if (Array.isArray(props.data) && typeof props.data[0] === 'number') {
+ const singleData = props.data
+ return !singleData.length || singleData.every((val) => val === 0)
+ }
+ if (Array.isArray(props.data) && typeof props.data[0] === 'object') {
+ const multiData = props.data
+ return (
+ !multiData.length ||
+ multiData.every((item) => !item.data?.length || item.data.every((val) => val === 0))
+ )
+ }
+ return true
+ }
+ const {
+ chartRef,
+ initChart,
+ getAxisLineStyle,
+ getAxisLabelStyle,
+ getAxisTickStyle,
+ getSplitLineStyle,
+ getTooltipStyle,
+ getLegendStyle,
+ getGridWithLegend,
+ isEmpty
+ } = useChartComponent({
+ props,
+ checkEmpty: checkIsEmpty,
+ watchSources: [() => props.data, () => props.xAxisData, () => props.colors],
+ onVisible: () => {
+ if (!isEmpty.value) {
+ initChartWithAnimation()
+ }
+ },
+ generateOptions: () => generateChartOptions(false)
+ })
+ const renderChart = () => {
+ if (!isAnimating.value && !isEmpty.value) {
+ initChartWithAnimation()
+ }
+ }
+ watch([() => props.data, () => props.xAxisData, () => props.colors], renderChart, { deep: true })
+ onMounted(() => {
+ renderChart()
+ })
+ onBeforeUnmount(() => {
+ clearAnimationTimers()
+ })
+</script>
diff --git a/rsf-design/src/components/core/charts/art-radar-chart/index.vue b/rsf-design/src/components/core/charts/art-radar-chart/index.vue
new file mode 100644
index 0000000..908e3de
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-radar-chart/index.vue
@@ -0,0 +1,94 @@
+<!-- 闆疯揪鍥� -->
+<template>
+ <div
+ ref="chartRef"
+ class="relative w-full"
+ :style="{ height: props.height }"
+ v-loading="props.loading"
+ ></div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ defineOptions({ name: 'ArtRadarChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ indicator: { required: false, default: () => [] },
+ data: { required: false, default: () => [] },
+ showTooltip: { required: false, default: true },
+ showLegend: { required: false, default: false },
+ legendPosition: { required: false, default: 'bottom' }
+ })
+ const { chartRef, isDark, getAnimationConfig, getTooltipStyle } = useChartComponent({
+ props,
+ checkEmpty: () => {
+ return !props.data?.length || props.data.every((item) => item.value.every((val) => val === 0))
+ },
+ watchSources: [() => props.data, () => props.indicator, () => props.colors],
+ generateOptions: () => {
+ return {
+ tooltip: props.showTooltip ? getTooltipStyle('item') : void 0,
+ radar: {
+ indicator: props.indicator,
+ center: ['50%', '50%'],
+ radius: '70%',
+ axisName: {
+ color: isDark.value ? '#ccc' : '#666',
+ fontSize: 12
+ },
+ splitLine: {
+ lineStyle: {
+ color: isDark.value ? '#444' : '#e6e6e6'
+ }
+ },
+ axisLine: {
+ lineStyle: {
+ color: isDark.value ? '#444' : '#e6e6e6'
+ }
+ },
+ splitArea: {
+ show: true,
+ areaStyle: {
+ color: isDark.value
+ ? ['rgba(255, 255, 255, 0.02)', 'rgba(255, 255, 255, 0.05)']
+ : ['rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.05)']
+ }
+ }
+ },
+ series: [
+ {
+ type: 'radar',
+ data: props.data.map((item, index) => ({
+ name: item.name,
+ value: item.value,
+ symbolSize: 4,
+ lineStyle: {
+ width: 2,
+ color: props.colors[index % props.colors.length]
+ },
+ itemStyle: {
+ color: props.colors[index % props.colors.length]
+ },
+ areaStyle: {
+ color: props.colors[index % props.colors.length],
+ opacity: 0.1
+ },
+ emphasis: {
+ areaStyle: {
+ opacity: 0.25
+ },
+ lineStyle: {
+ width: 3
+ }
+ }
+ })),
+ ...getAnimationConfig(200, 1800)
+ }
+ ]
+ }
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/charts/art-ring-chart/index.vue b/rsf-design/src/components/core/charts/art-ring-chart/index.vue
new file mode 100644
index 0000000..ad79d82
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-ring-chart/index.vue
@@ -0,0 +1,116 @@
+<!-- 鐜舰鍥� -->
+<template>
+ <div
+ ref="chartRef"
+ class="relative w-full"
+ :style="{ height: props.height }"
+ v-loading="props.loading"
+ >
+ </div>
+</template>
+
+<script setup>
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ defineOptions({ name: 'ArtRingChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ data: { required: false, default: () => [] },
+ radius: { required: false, default: () => ['50%', '80%'] },
+ borderRadius: { required: false, default: 10 },
+ centerText: { required: false, default: '' },
+ showLabel: { required: false, default: false },
+ showTooltip: { required: false, default: true },
+ showLegend: { required: false, default: false },
+ legendPosition: { required: false, default: 'right' }
+ })
+ const { chartRef, isDark, getAnimationConfig, getTooltipStyle, getLegendStyle } =
+ useChartComponent({
+ props,
+ checkEmpty: () => {
+ return !props.data?.length || props.data.every((item) => item.value === 0)
+ },
+ watchSources: [() => props.data, () => props.centerText],
+ generateOptions: () => {
+ const getCenterPosition = () => {
+ if (!props.showLegend) return ['50%', '50%']
+ switch (props.legendPosition) {
+ case 'left':
+ return ['60%', '50%']
+ case 'right':
+ return ['40%', '50%']
+ case 'top':
+ return ['50%', '60%']
+ case 'bottom':
+ return ['50%', '40%']
+ default:
+ return ['50%', '50%']
+ }
+ }
+ const option = {
+ tooltip: props.showTooltip
+ ? getTooltipStyle('item', {
+ formatter: '{b}: {c} ({d}%)'
+ })
+ : void 0,
+ legend: props.showLegend ? getLegendStyle(props.legendPosition) : void 0,
+ series: [
+ {
+ name: '鏁版嵁鍗犳瘮',
+ type: 'pie',
+ radius: props.radius,
+ center: getCenterPosition(),
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: props.borderRadius,
+ borderColor: isDark.value ? '#2c2c2c' : '#fff',
+ borderWidth: 0
+ },
+ label: {
+ show: props.showLabel,
+ formatter: '{b}\n{d}%',
+ position: 'outside',
+ color: isDark.value ? '#ccc' : '#999',
+ fontSize: 12
+ },
+ emphasis: {
+ label: {
+ show: false,
+ fontSize: 14,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: props.showLabel,
+ length: 15,
+ length2: 25,
+ smooth: true
+ },
+ data: props.data,
+ color: props.colors,
+ ...getAnimationConfig(),
+ animationType: 'expansion'
+ }
+ ]
+ }
+ if (props.centerText) {
+ const centerPos = getCenterPosition()
+ option.title = {
+ text: props.centerText,
+ left: centerPos[0],
+ top: centerPos[1],
+ textAlign: 'center',
+ textVerticalAlign: 'middle',
+ textStyle: {
+ fontSize: 18,
+ fontWeight: 500,
+ color: isDark.value ? '#999' : '#ADB0BC'
+ }
+ }
+ }
+ return option
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/charts/art-scatter-chart/index.vue b/rsf-design/src/components/core/charts/art-scatter-chart/index.vue
new file mode 100644
index 0000000..b343268
--- /dev/null
+++ b/rsf-design/src/components/core/charts/art-scatter-chart/index.vue
@@ -0,0 +1,101 @@
+<!-- 鏁g偣鍥� -->
+<template>
+ <div
+ ref="chartRef"
+ class="relative w-full"
+ :style="{ height: props.height }"
+ v-loading="props.loading"
+ >
+ </div>
+</template>
+
+<script setup>
+ import { getCssVar } from '@/utils/ui'
+ import { useChartOps, useChartComponent } from '@/hooks/core/useChart'
+ defineOptions({ name: 'ArtScatterChart' })
+ const props = defineProps({
+ height: { required: false, default: useChartOps().chartHeight },
+ loading: { required: false, default: false },
+ isEmpty: { required: false, default: false },
+ colors: { required: false, default: () => useChartOps().colors },
+ data: { required: false, default: () => [{ value: [0, 0] }, { value: [0, 0] }] },
+ symbolSize: { required: false, default: 14 },
+ showAxisLabel: { required: false, default: true },
+ showAxisLine: { required: false, default: true },
+ showSplitLine: { required: false, default: true },
+ showTooltip: { required: false, default: true },
+ showLegend: { required: false, default: false },
+ legendPosition: { required: false, default: 'bottom' }
+ })
+ const {
+ chartRef,
+ isDark,
+ getAxisLineStyle,
+ getAxisLabelStyle,
+ getAxisTickStyle,
+ getSplitLineStyle,
+ getAnimationConfig,
+ getTooltipStyle
+ } = useChartComponent({
+ props,
+ checkEmpty: () => {
+ return !props.data?.length || props.data.every((item) => item.value.every((val) => val === 0))
+ },
+ watchSources: [() => props.data, () => props.colors, () => props.symbolSize],
+ generateOptions: () => {
+ const computedColor = props.colors[0] || getCssVar('--el-color-primary')
+ return {
+ grid: {
+ top: 20,
+ right: 20,
+ bottom: 20,
+ left: 20,
+ containLabel: true
+ },
+ tooltip: props.showTooltip
+ ? getTooltipStyle('item', {
+ formatter: (params) => {
+ const [x, y] = params.value
+ return `X: ${x}<br/>Y: ${y}`
+ }
+ })
+ : void 0,
+ xAxis: {
+ type: 'value',
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ axisTick: getAxisTickStyle(),
+ splitLine: getSplitLineStyle(props.showSplitLine)
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: getAxisLabelStyle(props.showAxisLabel),
+ axisLine: getAxisLineStyle(props.showAxisLine),
+ axisTick: getAxisTickStyle(),
+ splitLine: getSplitLineStyle(props.showSplitLine)
+ },
+ series: [
+ {
+ type: 'scatter',
+ data: props.data,
+ symbolSize: props.symbolSize,
+ itemStyle: {
+ color: computedColor,
+ shadowBlur: 6,
+ shadowColor: isDark.value ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
+ shadowOffsetY: 2
+ },
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 12,
+ shadowColor: isDark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'
+ },
+ scale: true
+ },
+ ...getAnimationConfig()
+ }
+ ]
+ }
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/forms/art-button-more/index.vue b/rsf-design/src/components/core/forms/art-button-more/index.vue
new file mode 100644
index 0000000..b80e770
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-button-more/index.vue
@@ -0,0 +1,41 @@
+<!-- 鏇村鎸夐挳 -->
+<template>
+ <div>
+ <ElDropdown v-if="hasAnyAuthItem">
+ <ArtIconButton icon="ri:more-2-fill" class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm" />
+ <template #dropdown>
+ <ElDropdownMenu>
+ <template v-for="item in list" :key="item.key">
+ <ElDropdownItem
+ v-if="!item.auth || hasAuth(item.auth)"
+ :disabled="item.disabled"
+ @click="handleClick(item)"
+ >
+ <div class="flex-c gap-2" :style="{ color: item.color }">
+ <ArtSvgIcon v-if="item.icon" :icon="item.icon" />
+ <span>{{ item.label }}</span>
+ </div>
+ </ElDropdownItem>
+ </template>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+ </div>
+</template>
+
+<script setup>
+ import { useAuth } from '@/hooks/core/useAuth'
+ defineOptions({ name: 'ArtButtonMore' })
+ const { hasAuth } = useAuth()
+ const props = defineProps({
+ list: { required: true },
+ auth: { required: false }
+ })
+ const hasAnyAuthItem = computed(() => {
+ return props.list.some((item) => !item.auth || hasAuth(item.auth))
+ })
+ const emit = defineEmits(['click'])
+ const handleClick = (item) => {
+ emit('click', item)
+ }
+</script>
diff --git a/rsf-design/src/components/core/forms/art-button-table/index.vue b/rsf-design/src/components/core/forms/art-button-table/index.vue
new file mode 100644
index 0000000..d95861a
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-button-table/index.vue
@@ -0,0 +1,41 @@
+<!-- 琛ㄦ牸鎸夐挳 -->
+<template>
+ <div
+ :class="[
+ 'inline-flex items-center justify-center min-w-8 h-8 px-2.5 mr-2.5 text-sm c-p rounded-md align-middle',
+ buttonClass
+ ]"
+ :style="{ backgroundColor: buttonBgColor, color: iconColor }"
+ @click="handleClick"
+ >
+ <ArtSvgIcon :icon="iconContent" />
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtButtonTable' })
+ const props = defineProps({
+ type: { required: false },
+ icon: { required: false },
+ iconClass: { required: false },
+ iconColor: { required: false },
+ buttonBgColor: { required: false }
+ })
+ const emit = defineEmits(['click'])
+ const defaultButtons = {
+ add: { icon: 'ri:add-fill', class: 'bg-theme/12 text-theme' },
+ edit: { icon: 'ri:pencil-line', class: 'bg-secondary/12 text-secondary' },
+ delete: { icon: 'ri:delete-bin-5-line', class: 'bg-error/12 text-error' },
+ view: { icon: 'ri:eye-line', class: 'bg-info/12 text-info' },
+ more: { icon: 'ri:more-2-fill', class: '' }
+ }
+ const iconContent = computed(() => {
+ return props.icon || (props.type ? defaultButtons[props.type]?.icon : '') || ''
+ })
+ const buttonClass = computed(() => {
+ return props.iconClass || (props.type ? defaultButtons[props.type]?.class : '') || ''
+ })
+ const handleClick = () => {
+ emit('click')
+ }
+</script>
diff --git a/rsf-design/src/components/core/forms/art-drag-verify/index.vue b/rsf-design/src/components/core/forms/art-drag-verify/index.vue
new file mode 100644
index 0000000..db85563
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-drag-verify/index.vue
@@ -0,0 +1,301 @@
+<!-- 鎷栨嫿楠岃瘉缁勪欢 -->
+<template>
+ <div
+ ref="dragVerify"
+ class="drag_verify"
+ :style="dragVerifyStyle"
+ @mousemove="dragMoving"
+ @mouseup="dragFinish"
+ @mouseleave="dragFinish"
+ @touchmove="dragMoving"
+ @touchend="dragFinish"
+ >
+ <!-- 杩涘害鏉� -->
+ <div
+ class="dv_progress_bar"
+ :class="{ goFirst2: isOk }"
+ ref="progressBar"
+ :style="progressBarStyle"
+ >
+ </div>
+
+ <!-- 鎻愮ず鏂囨湰 -->
+ <div class="dv_text" :style="textStyle" ref="messageRef">
+ <slot name="textBefore" v-if="$slots.textBefore"></slot>
+ {{ message }}
+ <slot name="textAfter" v-if="$slots.textAfter"></slot>
+ </div>
+
+ <!-- 婊戝潡澶勭悊鍣� -->
+ <div
+ class="dv_handler dv_handler_bg"
+ :class="{ goFirst: isOk }"
+ @mousedown="dragStart"
+ @touchstart="dragStart"
+ ref="handler"
+ :style="handlerStyle"
+ >
+ <ArtSvgIcon :icon="value ? successIcon : handlerIcon" class="text-g-600"></ArtSvgIcon>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtDragVerify' })
+ const emit = defineEmits(['handlerMove', 'update:value', 'passCallback'])
+ const props = defineProps({
+ value: { required: false, default: false },
+ width: { required: false, default: '100%' },
+ height: { required: false, default: 40 },
+ text: { required: false, default: '鎸変綇婊戝潡鎷栧姩' },
+ successText: { required: false, default: 'success' },
+ background: { required: false, default: '#eee' },
+ progressBarBg: { required: false, default: '#1385FF' },
+ completedBg: { required: false, default: '#57D187' },
+ circle: { required: false, default: false },
+ radius: { required: false, default: 'calc(var(--custom-radius) / 3 + 2px)' },
+ handlerIcon: { required: false, default: 'solar:double-alt-arrow-right-linear' },
+ successIcon: { required: false, default: 'ri:check-fill' },
+ handlerBg: { required: false, default: '#fff' },
+ textSize: { required: false, default: '13px' },
+ textColor: { required: false, default: '#333' }
+ })
+ const state = reactive({
+ isMoving: false,
+ x: 0,
+ isOk: false
+ })
+ const { isOk } = toRefs(state)
+ const dragVerify = ref()
+ const messageRef = ref()
+ const handler = ref()
+ const progressBar = ref()
+ let startX, startY, moveX, moveY
+ const onTouchStart = (e) => {
+ startX = e.targetTouches[0].pageX
+ startY = e.targetTouches[0].pageY
+ }
+ const onTouchMove = (e) => {
+ moveX = e.targetTouches[0].pageX
+ moveY = e.targetTouches[0].pageY
+ if (Math.abs(moveX - startX) > Math.abs(moveY - startY)) {
+ e.preventDefault()
+ }
+ }
+ document.addEventListener('touchstart', onTouchStart)
+ document.addEventListener('touchmove', onTouchMove, { passive: false })
+ const getNumericWidth = () => {
+ if (typeof props.width === 'string') {
+ return dragVerify.value?.offsetWidth || 260
+ }
+ return props.width
+ }
+ const getStyleWidth = () => {
+ if (typeof props.width === 'string') {
+ return props.width
+ }
+ return props.width + 'px'
+ }
+ onMounted(() => {
+ dragVerify.value?.style.setProperty('--textColor', props.textColor)
+ nextTick(() => {
+ const numericWidth = getNumericWidth()
+ dragVerify.value?.style.setProperty('--width', Math.floor(numericWidth / 2) + 'px')
+ dragVerify.value?.style.setProperty('--pwidth', -Math.floor(numericWidth / 2) + 'px')
+ })
+ document.addEventListener('touchstart', onTouchStart)
+ document.addEventListener('touchmove', onTouchMove, { passive: false })
+ })
+ onBeforeUnmount(() => {
+ document.removeEventListener('touchstart', onTouchStart)
+ document.removeEventListener('touchmove', onTouchMove)
+ })
+ const handlerStyle = {
+ left: '0',
+ width: props.height + 'px',
+ height: props.height + 'px',
+ background: props.handlerBg
+ }
+ const dragVerifyStyle = computed(() => ({
+ width: getStyleWidth(),
+ height: props.height + 'px',
+ lineHeight: props.height + 'px',
+ background: props.background,
+ borderRadius: props.circle ? props.height / 2 + 'px' : props.radius
+ }))
+ const progressBarStyle = {
+ background: props.progressBarBg,
+ height: props.height + 'px',
+ borderRadius: props.circle
+ ? props.height / 2 + 'px 0 0 ' + props.height / 2 + 'px'
+ : props.radius
+ }
+ const textStyle = computed(() => ({
+ fontSize: props.textSize
+ }))
+ const message = computed(() => {
+ return props.value ? props.successText : props.text
+ })
+ const dragStart = (e) => {
+ if (!props.value) {
+ state.isMoving = true
+ handler.value.style.transition = 'none'
+ state.x =
+ (e.pageX || e.touches[0].pageX) - parseInt(handler.value.style.left.replace('px', ''), 10)
+ }
+ emit('handlerMove')
+ }
+ const dragMoving = (e) => {
+ if (state.isMoving && !props.value) {
+ const numericWidth = getNumericWidth()
+ let _x = (e.pageX || e.touches[0].pageX) - state.x
+ if (_x > 0 && _x <= numericWidth - props.height) {
+ handler.value.style.left = _x + 'px'
+ progressBar.value.style.width = _x + props.height / 2 + 'px'
+ } else if (_x > numericWidth - props.height) {
+ handler.value.style.left = numericWidth - props.height + 'px'
+ progressBar.value.style.width = numericWidth - props.height / 2 + 'px'
+ passVerify()
+ }
+ }
+ }
+ const dragFinish = (e) => {
+ if (state.isMoving && !props.value) {
+ const numericWidth = getNumericWidth()
+ let _x = (e.pageX || e.changedTouches[0].pageX) - state.x
+ if (_x < numericWidth - props.height) {
+ state.isOk = true
+ handler.value.style.left = '0'
+ handler.value.style.transition = 'all 0.2s'
+ progressBar.value.style.width = '0'
+ state.isOk = false
+ } else {
+ handler.value.style.transition = 'none'
+ handler.value.style.left = numericWidth - props.height + 'px'
+ progressBar.value.style.width = numericWidth - props.height / 2 + 'px'
+ passVerify()
+ }
+ state.isMoving = false
+ }
+ }
+ const passVerify = () => {
+ emit('update:value', true)
+ state.isMoving = false
+ progressBar.value.style.background = props.completedBg
+ messageRef.value.style['-webkit-text-fill-color'] = 'unset'
+ messageRef.value.style.animation = 'slidetounlock2 2s cubic-bezier(0, 0.2, 1, 1) infinite'
+ messageRef.value.style.color = '#fff'
+ emit('passCallback')
+ }
+ const reset = () => {
+ handler.value.style.left = '0'
+ progressBar.value.style.width = '0'
+ progressBar.value.style.background = props.progressBarBg
+ messageRef.value.style['-webkit-text-fill-color'] = 'transparent'
+ messageRef.value.style.animation = 'slidetounlock 2s cubic-bezier(0, 0.2, 1, 1) infinite'
+ messageRef.value.style.color = props.background
+ emit('update:value', false)
+ state.isOk = false
+ state.isMoving = false
+ state.x = 0
+ }
+ defineExpose({
+ reset
+ })
+</script>
+
+<style lang="scss" scoped>
+ .drag_verify {
+ position: relative;
+ box-sizing: border-box;
+ overflow: hidden;
+ text-align: center;
+ border: 1px solid var(--default-border-dashed);
+
+ .dv_handler {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: move;
+
+ i {
+ padding-left: 0;
+ font-size: 14px;
+ color: #999;
+ }
+
+ .el-icon-circle-check {
+ margin-top: 9px;
+ color: #6c6;
+ }
+ }
+
+ .dv_progress_bar {
+ position: absolute;
+ width: 0;
+ height: 34px;
+ }
+
+ .dv_text {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: transparent;
+ user-select: none;
+ background: linear-gradient(
+ to right,
+ var(--textColor) 0%,
+ var(--textColor) 40%,
+ #fff 50%,
+ var(--textColor) 60%,
+ var(--textColor) 100%
+ );
+ -webkit-background-clip: text;
+ background-clip: text;
+ animation: slidetounlock 2s cubic-bezier(0, 0.2, 1, 1) infinite;
+ -webkit-text-fill-color: transparent;
+ text-size-adjust: none;
+
+ * {
+ -webkit-text-fill-color: var(--textColor);
+ }
+ }
+ }
+
+ .goFirst {
+ left: 0 !important;
+ transition: left 0.5s;
+ }
+
+ .goFirst2 {
+ width: 0 !important;
+ transition: width 0.5s;
+ }
+</style>
+
+<style lang="scss">
+ @keyframes slidetounlock {
+ 0% {
+ background-position: var(--pwidth) 0;
+ }
+
+ 100% {
+ background-position: var(--width) 0;
+ }
+ }
+
+ @keyframes slidetounlock2 {
+ 0% {
+ background-position: var(--pwidth) 0;
+ }
+
+ 100% {
+ background-position: var(--pwidth) 0;
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/forms/art-excel-export/index.vue b/rsf-design/src/components/core/forms/art-excel-export/index.vue
new file mode 100644
index 0000000..47de522
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-excel-export/index.vue
@@ -0,0 +1,223 @@
+<!-- 瀵煎嚭 Excel 鏂囦欢 -->
+<template>
+ <ElButton
+ :type="type"
+ :size="size"
+ :loading="isExporting"
+ :disabled="disabled || !hasData"
+ v-ripple
+ @click="handleExport"
+ >
+ <template #loading>
+ <ElIcon class="is-loading">
+ <Loading />
+ </ElIcon>
+ {{ loadingText }}
+ </template>
+ <slot>{{ buttonText }}</slot>
+ </ElButton>
+</template>
+
+<script setup>
+ import * as XLSX from 'xlsx'
+ import FileSaver from 'file-saver'
+ import { ref, computed, nextTick } from 'vue'
+ import { Loading } from '@element-plus/icons-vue'
+ import { useThrottleFn } from '@vueuse/core'
+ defineOptions({ name: 'ArtExcelExport' })
+ const props = defineProps({
+ filename: {
+ required: false,
+ default: () => `export_${/* @__PURE__ */ new Date().toISOString().slice(0, 10)}`
+ },
+ sheetName: { required: false, default: 'Sheet1' },
+ type: { required: false, default: 'primary' },
+ size: { required: false, default: 'default' },
+ disabled: { required: false, default: false },
+ buttonText: { required: false, default: '瀵煎嚭 Excel' },
+ loadingText: { required: false, default: '瀵煎嚭涓�...' },
+ autoIndex: { required: false, default: false },
+ indexColumnTitle: { required: false, default: '搴忓彿' },
+ columns: { required: false, default: () => ({}) },
+ headers: { required: false, default: () => ({}) },
+ maxRows: { required: false, default: 1e5 },
+ showSuccessMessage: { required: false, default: true },
+ showErrorMessage: { required: false, default: true },
+ workbookOptions: { required: false, default: () => ({}) }
+ })
+ const emit = defineEmits(['before-export', 'export-success', 'export-error', 'export-progress'])
+ class ExportError extends Error {
+ constructor(message, code, details) {
+ super(message)
+ this.code = code
+ this.details = details
+ this.name = 'ExportError'
+ }
+ }
+ const isExporting = ref(false)
+ const hasData = computed(() => Array.isArray(props.data) && props.data.length > 0)
+ const validateData = (data) => {
+ if (!Array.isArray(data)) {
+ throw new ExportError('鏁版嵁蹇呴』鏄暟缁勬牸寮�', 'INVALID_DATA_TYPE')
+ }
+ if (data.length === 0) {
+ throw new ExportError('娌℃湁鍙鍑虹殑鏁版嵁', 'NO_DATA')
+ }
+ if (data.length > props.maxRows) {
+ throw new ExportError(`鏁版嵁琛屾暟瓒呰繃闄愬埗锛�${props.maxRows}琛岋級`, 'EXCEED_MAX_ROWS', {
+ currentRows: data.length,
+ maxRows: props.maxRows
+ })
+ }
+ }
+ const formatCellValue = (value, key, row, index) => {
+ const column = props.columns[key]
+ if (column?.formatter) {
+ return column.formatter(value, row, index)
+ }
+ if (value === null || value === void 0) {
+ return ''
+ }
+ if (value instanceof Date) {
+ return value.toLocaleDateString('zh-CN')
+ }
+ if (typeof value === 'boolean') {
+ return value ? '鏄�' : '鍚�'
+ }
+ return String(value)
+ }
+ const processData = (data) => {
+ const processedData = data.map((item, index) => {
+ const processedItem = {}
+ if (props.autoIndex) {
+ processedItem[props.indexColumnTitle] = String(index + 1)
+ }
+ Object.entries(item).forEach(([key, value]) => {
+ let columnTitle = key
+ if (props.columns[key]?.title) {
+ columnTitle = props.columns[key].title
+ } else if (props.headers[key]) {
+ columnTitle = props.headers[key]
+ }
+ processedItem[columnTitle] = formatCellValue(value, key, item, index)
+ })
+ return processedItem
+ })
+ return processedData
+ }
+ const calculateColumnWidths = (data) => {
+ if (data.length === 0) return []
+ const sampleSize = Math.min(data.length, 100)
+ const columns = Object.keys(data[0])
+ return columns.map((column) => {
+ const configWidth = Object.values(props.columns).find((col) => col.title === column)?.width
+ if (configWidth) {
+ return { wch: configWidth }
+ }
+ const maxLength = Math.max(
+ column.length,
+ ...data.slice(0, sampleSize).map((row) => String(row[column] || '').length)
+ )
+ const width = Math.min(Math.max(maxLength + 2, 8), 50)
+ return { wch: width }
+ })
+ }
+ const exportToExcel = async (data, filename, sheetName) => {
+ try {
+ emit('export-progress', 10)
+ const processedData = processData(data)
+ emit('export-progress', 30)
+ const workbook = XLSX.utils.book_new()
+ if (props.workbookOptions) {
+ workbook.Props = {
+ Title: filename,
+ Subject: '鏁版嵁瀵煎嚭',
+ Author: props.workbookOptions.creator || 'Art Design Pro',
+ Manager: props.workbookOptions.lastModifiedBy || '',
+ Company: '绯荤粺瀵煎嚭',
+ Category: '鏁版嵁',
+ Keywords: 'excel,export,data',
+ Comments: '鐢辩郴缁熻嚜鍔ㄧ敓鎴�',
+ CreatedDate: props.workbookOptions.created || /* @__PURE__ */ new Date(),
+ ModifiedDate: props.workbookOptions.modified || /* @__PURE__ */ new Date()
+ }
+ }
+ emit('export-progress', 50)
+ const worksheet = XLSX.utils.json_to_sheet(processedData)
+ worksheet['!cols'] = calculateColumnWidths(processedData)
+ emit('export-progress', 70)
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
+ emit('export-progress', 85)
+ const excelBuffer = XLSX.write(workbook, {
+ bookType: 'xlsx',
+ type: 'array',
+ compression: true
+ })
+ const blob = new Blob([excelBuffer], {
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ })
+ emit('export-progress', 95)
+ const timestamp = /* @__PURE__ */ new Date().toISOString().replace(/[:.]/g, '-')
+ const finalFilename = `${filename}_${timestamp}.xlsx`
+ FileSaver.saveAs(blob, finalFilename)
+ emit('export-progress', 100)
+ await nextTick()
+ return Promise.resolve()
+ } catch (error) {
+ throw new ExportError(`Excel 瀵煎嚭澶辫触: ${error.message}`, 'EXPORT_FAILED', error)
+ }
+ }
+ const handleExport = useThrottleFn(async () => {
+ if (isExporting.value) return
+ isExporting.value = true
+ try {
+ validateData(props.data)
+ emit('before-export', props.data)
+ await exportToExcel(props.data, props.filename, props.sheetName)
+ emit('export-success', props.filename, props.data.length)
+ if (props.showSuccessMessage) {
+ ElMessage.success({
+ message: `鎴愬姛瀵煎嚭 ${props.data.length} 鏉℃暟鎹甡,
+ duration: 3e3
+ })
+ }
+ } catch (error) {
+ const exportError =
+ error instanceof ExportError
+ ? error
+ : new ExportError(`瀵煎嚭澶辫触: ${error.message}`, 'UNKNOWN_ERROR', error)
+ emit('export-error', exportError)
+ if (props.showErrorMessage) {
+ ElMessage.error({
+ message: exportError.message,
+ duration: 5e3
+ })
+ }
+ console.error('Excel 瀵煎嚭閿欒:', exportError)
+ } finally {
+ isExporting.value = false
+ emit('export-progress', 0)
+ }
+ }, 1e3)
+ defineExpose({
+ exportData: handleExport,
+ isExporting: readonly(isExporting),
+ hasData
+ })
+</script>
+
+<style scoped>
+ .is-loading {
+ animation: rotating 2s linear infinite;
+ }
+
+ @keyframes rotating {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/forms/art-excel-import/index.vue b/rsf-design/src/components/core/forms/art-excel-import/index.vue
new file mode 100644
index 0000000..e629a60
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-excel-import/index.vue
@@ -0,0 +1,49 @@
+<!-- 瀵煎叆 Excel 鏂囦欢 -->
+<template>
+ <div class="inline-block">
+ <ElUpload
+ :auto-upload="false"
+ accept=".xlsx, .xls"
+ :show-file-list="false"
+ @change="handleFileChange"
+ >
+ <ElButton type="primary" v-ripple>
+ <slot>瀵煎叆 Excel</slot>
+ </ElButton>
+ </ElUpload>
+ </div>
+</template>
+
+<script setup>
+ import * as XLSX from 'xlsx'
+ defineOptions({ name: 'ArtExcelImport' })
+ async function importExcel(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ try {
+ const data = e.target?.result
+ const workbook = XLSX.read(data, { type: 'array' })
+ const firstSheetName = workbook.SheetNames[0]
+ const worksheet = workbook.Sheets[firstSheetName]
+ const results = XLSX.utils.sheet_to_json(worksheet)
+ resolve(results)
+ } catch (error) {
+ reject(error)
+ }
+ }
+ reader.onerror = (error) => reject(error)
+ reader.readAsArrayBuffer(file)
+ })
+ }
+ const emit = defineEmits(['import-success', 'import-error'])
+ const handleFileChange = async (uploadFile) => {
+ try {
+ if (!uploadFile.raw) return
+ const results = await importExcel(uploadFile.raw)
+ emit('import-success', results)
+ } catch (error) {
+ emit('import-error', error)
+ }
+ }
+</script>
diff --git a/rsf-design/src/components/core/forms/art-form/index.vue b/rsf-design/src/components/core/forms/art-form/index.vue
new file mode 100644
index 0000000..c3bace0
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-form/index.vue
@@ -0,0 +1,367 @@
+<!-- 琛ㄥ崟缁勪欢 -->
+<!-- 鏀寔甯哥敤琛ㄥ崟缁勪欢銆佽嚜瀹氫箟缁勪欢銆佹彃妲姐�佹牎楠屻�侀殣钘忚〃鍗曢」 -->
+<!-- 鍐欐硶鍚� ElementPlus 瀹樻柟鏂囨。缁勪欢锛屾妸灞炴�у啓鍦� props 閲岄潰灏卞彲浠ヤ簡 -->
+<template>
+ <section class="px-4 pb-0 pt-4 md:px-4 md:pt-4">
+ <ElForm
+ ref="formRef"
+ :model="modelValue"
+ :label-position="labelPosition"
+ v-bind="{ ...$attrs }"
+ >
+ <ElRow class="flex flex-wrap" :gutter="gutter">
+ <ElCol
+ v-for="item in visibleFormItems"
+ :key="item.key"
+ :xs="getColSpan(item.span, 'xs')"
+ :sm="getColSpan(item.span, 'sm')"
+ :md="getColSpan(item.span, 'md')"
+ :lg="getColSpan(item.span, 'lg')"
+ :xl="getColSpan(item.span, 'xl')"
+ >
+ <ElFormItem
+ :prop="item.key"
+ :label-width="item.label ? item.labelWidth || labelWidth : undefined"
+ >
+ <template #label v-if="item.label">
+ <component v-if="typeof item.label !== 'string'" :is="item.label" />
+ <span v-else>{{ item.label }}</span>
+ </template>
+ <slot :name="item.key" :item="item" :modelValue="modelValue">
+ <component
+ :is="getComponent(item)"
+ :model-value="getFieldValue(item.key)"
+ @update:model-value="setFieldValue(item.key, $event)"
+ v-bind="getProps(item)"
+ >
+ <!-- 涓嬫媺閫夋嫨 -->
+ <template v-if="item.type === 'select' && getProps(item)?.options">
+ <ElOption
+ v-for="option in getProps(item).options"
+ v-bind="option"
+ :key="option.value"
+ />
+ </template>
+
+ <!-- 澶嶉�夋缁� -->
+ <template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
+ <ElCheckbox
+ v-for="option in getProps(item).options"
+ v-bind="option"
+ :key="option.value"
+ />
+ </template>
+
+ <!-- 鍗曢�夋缁� -->
+ <template v-if="item.type === 'radiogroup' && getProps(item)?.options">
+ <ElRadio
+ v-for="option in getProps(item).options"
+ v-bind="option"
+ :key="option.value"
+ />
+ </template>
+
+ <!-- 鍔ㄦ�佹彃妲芥敮鎸� -->
+ <template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
+ <component :is="slotFn" />
+ </template>
+ </component>
+ </slot>
+ </ElFormItem>
+ </ElCol>
+ <ElCol :xs="24" :sm="24" :md="span" :lg="span" :xl="span" class="max-w-full flex-1">
+ <div
+ class="mb-3 flex-c flex-wrap justify-end md:flex-row md:items-stretch md:gap-2"
+ :style="actionButtonsStyle"
+ >
+ <div class="flex gap-2 md:justify-center">
+ <ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
+ {{ t('table.form.reset') }}
+ </ElButton>
+ <ElButton
+ v-if="showSubmit"
+ type="primary"
+ class="submit-button"
+ @click="handleSubmit"
+ v-ripple
+ :disabled="disabledSubmit"
+ >
+ {{ t('table.form.submit') }}
+ </ElButton>
+ </div>
+ </div>
+ </ElCol>
+ </ElRow>
+ </ElForm>
+ </section>
+</template>
+
+<script setup>
+ import { useWindowSize } from '@vueuse/core'
+ import { useI18n } from 'vue-i18n'
+ import { toRaw } from 'vue'
+ import {
+ ElCascader,
+ ElCheckbox,
+ ElCheckboxGroup,
+ ElDatePicker,
+ ElInput,
+ ElInputTag,
+ ElInputNumber,
+ ElRadioGroup,
+ ElRate,
+ ElSelect,
+ ElSlider,
+ ElSwitch,
+ ElTimePicker,
+ ElTimeSelect,
+ ElTreeSelect
+ } from 'element-plus'
+ import { calculateResponsiveSpan } from '@/utils/form/responsive'
+ defineOptions({ name: 'ArtForm' })
+ const componentMap = {
+ input: ElInput,
+ // 杈撳叆妗�
+ inputtag: ElInputTag,
+ // 鏍囩杈撳叆妗�
+ number: ElInputNumber,
+ // 鏁板瓧杈撳叆妗�
+ select: ElSelect,
+ // 閫夋嫨鍣�
+ switch: ElSwitch,
+ // 寮�鍏�
+ checkbox: ElCheckbox,
+ // 澶嶉�夋
+ checkboxgroup: ElCheckboxGroup,
+ // 澶嶉�夋缁�
+ radiogroup: ElRadioGroup,
+ // 鍗曢�夋缁�
+ date: ElDatePicker,
+ // 鏃ユ湡閫夋嫨鍣�
+ daterange: ElDatePicker,
+ // 鏃ユ湡鑼冨洿閫夋嫨鍣�
+ datetime: ElDatePicker,
+ // 鏃ユ湡鏃堕棿閫夋嫨鍣�
+ datetimerange: ElDatePicker,
+ // 鏃ユ湡鏃堕棿鑼冨洿閫夋嫨鍣�
+ rate: ElRate,
+ // 璇勫垎
+ slider: ElSlider,
+ // 婊戝潡
+ cascader: ElCascader,
+ // 绾ц仈閫夋嫨鍣�
+ timepicker: ElTimePicker,
+ // 鏃堕棿閫夋嫨鍣�
+ timeselect: ElTimeSelect,
+ // 鏃堕棿閫夋嫨
+ treeselect: ElTreeSelect
+ // 鏍戦�夋嫨鍣�
+ }
+ const { width } = useWindowSize()
+ const { t } = useI18n()
+ const isMobile = computed(() => width.value < 500)
+ const formInstance = useTemplateRef('formRef')
+ const props = defineProps({
+ items: { required: false, default: () => [] },
+ span: { required: false, default: 6 },
+ gutter: { required: false, default: 12 },
+ labelPosition: { required: false, default: 'right' },
+ labelWidth: { required: false, default: '70px' },
+ buttonLeftLimit: { required: false, default: 2 },
+ showReset: { required: false, default: true },
+ showSubmit: { required: false, default: true },
+ disabledSubmit: { required: false, default: false },
+ sanitizeOutput: { required: false, default: () => ({}) }
+ })
+ const emit = defineEmits(['reset', 'submit'])
+ const modelValue = defineModel({ default: {} })
+ const initialModelValue = ref({})
+ const cloneModelValue = (value) => {
+ if (!value) return {}
+ const deepClone = (source) => {
+ if (Array.isArray(source)) {
+ return source.map((item) => deepClone(item))
+ }
+ if (source && typeof source === 'object') {
+ const rawSource = toRaw(source)
+ return Object.keys(rawSource).reduce((accumulator, key) => {
+ accumulator[key] = deepClone(rawSource[key])
+ return accumulator
+ }, {})
+ }
+ return source
+ }
+ return deepClone(toRaw(value))
+ }
+ initialModelValue.value = cloneModelValue(modelValue.value)
+ const rootProps = ['label', 'labelWidth', 'key', 'type', 'hidden', 'span', 'slots']
+ const sanitizeOutputOptions = computed(() => ({
+ removeEmptyString: true,
+ removeEmptyArray: true,
+ removeEmptyObject: true,
+ removeEmptyRichText: true,
+ keepZero: true,
+ keepFalse: true,
+ ...props.sanitizeOutput
+ }))
+ const PATH_NUMBER_RE = /^\d+$/
+ const parsePath = (path) => {
+ return path
+ .split('.')
+ .filter(Boolean)
+ .map((segment) => (PATH_NUMBER_RE.test(segment) ? Number(segment) : segment))
+ }
+ const getFieldValue = (path) => {
+ return parsePath(path).reduce((currentValue, segment) => {
+ if (currentValue == null) return void 0
+ return currentValue[segment]
+ }, modelValue.value)
+ }
+ const deleteFieldValue = (path) => {
+ const segments = parsePath(path)
+ if (!segments.length) return
+ const lastSegment = segments.pop()
+ const parent = segments.reduce((currentValue, segment) => {
+ if (currentValue == null) return void 0
+ return currentValue[segment]
+ }, modelValue.value)
+ if (parent != null && lastSegment !== void 0) {
+ delete parent[lastSegment]
+ }
+ }
+ const setFieldValue = (path, value) => {
+ const normalizedValue = value === '' ? void 0 : value
+ const segments = parsePath(path)
+ if (!segments.length) return
+ if (normalizedValue === void 0) {
+ deleteFieldValue(path)
+ return
+ }
+ let currentValue = modelValue.value
+ segments.forEach((segment, index) => {
+ const isLast = index === segments.length - 1
+ if (isLast) {
+ currentValue[segment] = normalizedValue
+ return
+ }
+ const nextSegment = segments[index + 1]
+ const nextContainer = typeof nextSegment === 'number' ? [] : {}
+ if (
+ currentValue[segment] === null ||
+ currentValue[segment] === void 0 ||
+ typeof currentValue[segment] !== 'object'
+ ) {
+ currentValue[segment] = nextContainer
+ }
+ currentValue = currentValue[segment]
+ })
+ }
+ const isRichTextEmpty = (value) => {
+ if (/<(img|video|audio|iframe|embed|object)\b/i.test(value)) {
+ return false
+ }
+ return (
+ value
+ .replace(/ /gi, '')
+ .replace(/<br\s*\/?>/gi, '')
+ .replace(/<[^>]*>/g, '')
+ .trim() === ''
+ )
+ }
+ const sanitizeOutputValue = (value) => {
+ const options = sanitizeOutputOptions.value
+ if (Array.isArray(value)) {
+ const sanitizedArray = value
+ .map((item) => sanitizeOutputValue(item))
+ .filter((item) => item !== void 0)
+ return sanitizedArray.length === 0 && options.removeEmptyArray ? void 0 : sanitizedArray
+ }
+ if (value && typeof value === 'object') {
+ const rawValue = toRaw(value)
+ const sanitizedObject = Object.entries(rawValue).reduce((accumulator, [key, item]) => {
+ const sanitizedItem = sanitizeOutputValue(item)
+ if (sanitizedItem !== void 0) {
+ accumulator[key] = sanitizedItem
+ }
+ return accumulator
+ }, {})
+ return Object.keys(sanitizedObject).length === 0 && options.removeEmptyObject
+ ? void 0
+ : sanitizedObject
+ }
+ if (typeof value === 'string') {
+ if (options.removeEmptyString && value.trim() === '') {
+ return void 0
+ }
+ if (options.removeEmptyRichText && isRichTextEmpty(value)) {
+ return void 0
+ }
+ return value
+ }
+ if (value === 0) {
+ return options.keepZero ? value : void 0
+ }
+ if (value === false) {
+ return options.keepFalse ? value : void 0
+ }
+ return value ?? void 0
+ }
+ const getSanitizedOutput = () => {
+ return sanitizeOutputValue(cloneModelValue(modelValue.value)) || {}
+ }
+ const getProps = (item) => {
+ if (item.props) return item.props
+ const props2 = { ...item }
+ rootProps.forEach((key) => delete props2[key])
+ return props2
+ }
+ const getSlots = (item) => {
+ if (!item.slots) return {}
+ const validSlots = {}
+ Object.entries(item.slots).forEach(([key, slotFn]) => {
+ if (slotFn) {
+ validSlots[key] = slotFn
+ }
+ })
+ return validSlots
+ }
+ const getComponent = (item) => {
+ if (item.render) {
+ return item.render
+ }
+ const { type } = item
+ return componentMap[type] || componentMap['input']
+ }
+ const getColSpan = (itemSpan, breakpoint) => {
+ return calculateResponsiveSpan(itemSpan, span.value, breakpoint)
+ }
+ const visibleFormItems = computed(() => {
+ return props.items.filter((item) => !item.hidden)
+ })
+ const actionButtonsStyle = computed(() => ({
+ 'justify-content': isMobile.value
+ ? 'flex-end'
+ : props.items.filter((item) => !item.hidden).length <= props.buttonLeftLimit
+ ? 'flex-start'
+ : 'flex-end'
+ }))
+ const handleReset = () => {
+ formInstance.value?.resetFields()
+ Object.keys(modelValue.value).forEach((key) => {
+ delete modelValue.value[key]
+ })
+ Object.assign(modelValue.value, cloneModelValue(initialModelValue.value))
+ emit('reset')
+ }
+ const handleSubmit = () => {
+ emit('submit', getSanitizedOutput())
+ }
+ defineExpose({
+ ref: formInstance,
+ validate: (...args) => formInstance.value?.validate(...args),
+ reset: handleReset,
+ // 鍏佽澶栭儴鍦ㄤ笉瑙﹀彂鎻愪氦浜嬩欢鏃朵富鍔ㄨ幏鍙栨竻娲楀悗鐨勮緭鍑恒��
+ getOutput: getSanitizedOutput
+ })
+ const { span, gutter, labelPosition, labelWidth } = toRefs(props)
+</script>
diff --git a/rsf-design/src/components/core/forms/art-search-bar/index.vue b/rsf-design/src/components/core/forms/art-search-bar/index.vue
new file mode 100644
index 0000000..5d5ea8a
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-search-bar/index.vue
@@ -0,0 +1,432 @@
+<!-- 琛ㄦ牸鎼滅储缁勪欢 -->
+<!-- 鏀寔甯哥敤琛ㄥ崟缁勪欢銆佽嚜瀹氫箟缁勪欢銆佹彃妲姐�佹牎楠屻�侀殣钘忚〃鍗曢」 -->
+<!-- 鍐欐硶鍚� ElementPlus 瀹樻柟鏂囨。缁勪欢锛屾妸灞炴�у啓鍦� props 閲岄潰灏卞彲浠ヤ簡 -->
+<template>
+ <section class="art-search-bar art-card-xs" :class="{ 'is-expanded': isExpanded }">
+ <ElForm
+ ref="formRef"
+ :model="modelValue"
+ :label-position="labelPosition"
+ v-bind="{ ...$attrs }"
+ >
+ <ElRow :gutter="gutter">
+ <ElCol
+ v-for="item in visibleFormItems"
+ :key="item.key"
+ :xs="getColSpan(item.span, 'xs')"
+ :sm="getColSpan(item.span, 'sm')"
+ :md="getColSpan(item.span, 'md')"
+ :lg="getColSpan(item.span, 'lg')"
+ :xl="getColSpan(item.span, 'xl')"
+ >
+ <ElFormItem
+ :prop="item.key"
+ :label-width="item.label ? item.labelWidth || labelWidth : undefined"
+ >
+ <template #label v-if="item.label">
+ <component v-if="typeof item.label !== 'string'" :is="item.label" />
+ <span v-else>{{ item.label }}</span>
+ </template>
+ <slot :name="item.key" :item="item" :modelValue="modelValue">
+ <component
+ :is="getComponent(item)"
+ :model-value="getFieldValue(item.key)"
+ @update:model-value="setFieldValue(item.key, $event)"
+ v-bind="getProps(item)"
+ >
+ <!-- 涓嬫媺閫夋嫨 -->
+ <template v-if="item.type === 'select' && getProps(item)?.options">
+ <ElOption
+ v-for="option in getProps(item).options"
+ v-bind="option"
+ :key="option.value"
+ />
+ </template>
+
+ <!-- 澶嶉�夋缁� -->
+ <template v-if="item.type === 'checkboxgroup' && getProps(item)?.options">
+ <ElCheckbox
+ v-for="option in getProps(item).options"
+ v-bind="option"
+ :key="option.value"
+ />
+ </template>
+
+ <!-- 鍗曢�夋缁� -->
+ <template v-if="item.type === 'radiogroup' && getProps(item)?.options">
+ <ElRadio
+ v-for="option in getProps(item).options"
+ v-bind="option"
+ :key="option.value"
+ />
+ </template>
+
+ <!-- 鍔ㄦ�佹彃妲芥敮鎸� -->
+ <template v-for="(slotFn, slotName) in getSlots(item)" :key="slotName" #[slotName]>
+ <component :is="slotFn" />
+ </template>
+ </component>
+ </slot>
+ </ElFormItem>
+ </ElCol>
+ <ElCol :xs="24" :sm="24" :md="span" :lg="span" :xl="span" class="action-column">
+ <div class="action-buttons-wrapper" :style="actionButtonsStyle">
+ <div class="form-buttons">
+ <ElButton v-if="showReset" class="reset-button" @click="handleReset" v-ripple>
+ {{ t('table.searchBar.reset') }}
+ </ElButton>
+ <ElButton
+ v-if="showSearch"
+ type="primary"
+ class="search-button"
+ @click="handleSearch"
+ v-ripple
+ :disabled="disabledSearch"
+ >
+ {{ t('table.searchBar.search') }}
+ </ElButton>
+ </div>
+ <div v-if="shouldShowExpandToggle" class="filter-toggle" @click="toggleExpand">
+ <span>{{ expandToggleText }}</span>
+ <div class="icon-wrapper">
+ <ElIcon>
+ <ArrowUpBold v-if="isExpanded" />
+ <ArrowDownBold v-else />
+ </ElIcon>
+ </div>
+ </div>
+ </div>
+ </ElCol>
+ </ElRow>
+ </ElForm>
+ </section>
+</template>
+
+<script setup>
+ import { ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
+
+ import { useWindowSize } from '@vueuse/core'
+ import { useI18n } from 'vue-i18n'
+ import { toRaw } from 'vue'
+ import {
+ ElCascader,
+ ElCheckbox,
+ ElCheckboxGroup,
+ ElDatePicker,
+ ElInput,
+ ElInputTag,
+ ElInputNumber,
+ ElRadioGroup,
+ ElRate,
+ ElSelect,
+ ElSlider,
+ ElSwitch,
+ ElTimePicker,
+ ElTimeSelect,
+ ElTreeSelect
+ } from 'element-plus'
+ import { calculateResponsiveSpan } from '@/utils/form/responsive'
+ defineOptions({ name: 'ArtSearchBar' })
+ const componentMap = {
+ input: ElInput,
+ // 杈撳叆妗�
+ inputTag: ElInputTag,
+ // 鏍囩杈撳叆妗�
+ number: ElInputNumber,
+ // 鏁板瓧杈撳叆妗�
+ select: ElSelect,
+ // 閫夋嫨鍣�
+ switch: ElSwitch,
+ // 寮�鍏�
+ checkbox: ElCheckbox,
+ // 澶嶉�夋
+ checkboxgroup: ElCheckboxGroup,
+ // 澶嶉�夋缁�
+ radiogroup: ElRadioGroup,
+ // 鍗曢�夋缁�
+ date: ElDatePicker,
+ // 鏃ユ湡閫夋嫨鍣�
+ daterange: ElDatePicker,
+ // 鏃ユ湡鑼冨洿閫夋嫨鍣�
+ datetime: ElDatePicker,
+ // 鏃ユ湡鏃堕棿閫夋嫨鍣�
+ datetimerange: ElDatePicker,
+ // 鏃ユ湡鏃堕棿鑼冨洿閫夋嫨鍣�
+ rate: ElRate,
+ // 璇勫垎
+ slider: ElSlider,
+ // 婊戝潡
+ cascader: ElCascader,
+ // 绾ц仈閫夋嫨鍣�
+ timepicker: ElTimePicker,
+ // 鏃堕棿閫夋嫨鍣�
+ timeselect: ElTimeSelect,
+ // 鏃堕棿閫夋嫨
+ treeselect: ElTreeSelect
+ // 鏍戦�夋嫨鍣�
+ }
+ const { width } = useWindowSize()
+ const { t } = useI18n()
+ const isMobile = computed(() => width.value < 500)
+ const formInstance = useTemplateRef('formRef')
+ const props = defineProps({
+ items: { required: false, default: () => [] },
+ span: { required: false, default: 6 },
+ gutter: { required: false, default: 12 },
+ isExpand: { required: false, default: false },
+ labelPosition: { required: false, default: 'right' },
+ labelWidth: { required: false, default: '70px' },
+ showExpand: { required: false, default: true },
+ defaultExpanded: { required: false, default: false },
+ buttonLeftLimit: { required: false, default: 2 },
+ showReset: { required: false, default: true },
+ showSearch: { required: false, default: true },
+ disabledSearch: { required: false, default: false },
+ sanitizeOutput: { required: false, default: () => ({}) }
+ })
+ const emit = defineEmits(['reset', 'search'])
+ const modelValue = defineModel({ default: {} })
+ const initialModelValue = ref({})
+ const cloneModelValue = (value) => {
+ if (!value) return {}
+ const deepClone = (source) => {
+ if (Array.isArray(source)) {
+ return source.map((item) => deepClone(item))
+ }
+ if (source && typeof source === 'object') {
+ const rawSource = toRaw(source)
+ return Object.keys(rawSource).reduce((accumulator, key) => {
+ accumulator[key] = deepClone(rawSource[key])
+ return accumulator
+ }, {})
+ }
+ return source
+ }
+ return deepClone(toRaw(value))
+ }
+ initialModelValue.value = cloneModelValue(modelValue.value)
+ const isExpanded = ref(props.defaultExpanded)
+ const rootProps = ['label', 'labelWidth', 'key', 'type', 'hidden', 'span', 'slots']
+ const sanitizeOutputOptions = computed(() => ({
+ removeEmptyString: true,
+ removeEmptyArray: true,
+ removeEmptyObject: true,
+ removeEmptyRichText: true,
+ keepZero: true,
+ keepFalse: true,
+ ...props.sanitizeOutput
+ }))
+ const getProps = (item) => {
+ if (item.props) return item.props
+ const props2 = { ...item }
+ rootProps.forEach((key) => delete props2[key])
+ return props2
+ }
+ const getSlots = (item) => {
+ if (!item.slots) return {}
+ const validSlots = {}
+ Object.entries(item.slots).forEach(([key, slotFn]) => {
+ if (slotFn) {
+ validSlots[key] = slotFn
+ }
+ })
+ return validSlots
+ }
+ const getColSpan = (itemSpan, breakpoint) => {
+ return calculateResponsiveSpan(itemSpan, span.value, breakpoint)
+ }
+ const normalizeFieldValue = (value) => {
+ return value === '' ? void 0 : value
+ }
+ const getFieldValue = (key) => modelValue.value[key]
+ const setFieldValue = (key, value) => {
+ const normalizedValue = normalizeFieldValue(value)
+ if (normalizedValue === void 0) {
+ delete modelValue.value[key]
+ return
+ }
+ modelValue.value[key] = normalizedValue
+ }
+ const isRichTextEmpty = (value) => {
+ if (/<(img|video|audio|iframe|embed|object)\b/i.test(value)) {
+ return false
+ }
+ return (
+ value
+ .replace(/ /gi, '')
+ .replace(/<br\s*\/?>/gi, '')
+ .replace(/<[^>]*>/g, '')
+ .trim() === ''
+ )
+ }
+ const sanitizeOutputValue = (value) => {
+ const options = sanitizeOutputOptions.value
+ if (Array.isArray(value)) {
+ const sanitizedArray = value
+ .map((item) => sanitizeOutputValue(item))
+ .filter((item) => item !== void 0)
+ return sanitizedArray.length === 0 && options.removeEmptyArray ? void 0 : sanitizedArray
+ }
+ if (value && typeof value === 'object') {
+ const rawValue = toRaw(value)
+ const sanitizedObject = Object.entries(rawValue).reduce((accumulator, [key, item]) => {
+ const sanitizedItem = sanitizeOutputValue(item)
+ if (sanitizedItem !== void 0) {
+ accumulator[key] = sanitizedItem
+ }
+ return accumulator
+ }, {})
+ return Object.keys(sanitizedObject).length === 0 && options.removeEmptyObject
+ ? void 0
+ : sanitizedObject
+ }
+ if (typeof value === 'string') {
+ if (options.removeEmptyString && value.trim() === '') {
+ return void 0
+ }
+ if (options.removeEmptyRichText && isRichTextEmpty(value)) {
+ return void 0
+ }
+ return value
+ }
+ if (value === 0) {
+ return options.keepZero ? value : void 0
+ }
+ if (value === false) {
+ return options.keepFalse ? value : void 0
+ }
+ return value ?? void 0
+ }
+ const getSanitizedOutput = () => {
+ return sanitizeOutputValue(cloneModelValue(modelValue.value)) || {}
+ }
+ const getComponent = (item) => {
+ if (item.render) {
+ return item.render
+ }
+ const { type } = item
+ return componentMap[type] || componentMap['input']
+ }
+ const visibleFormItems = computed(() => {
+ const filteredItems = props.items.filter((item) => !item.hidden)
+ const shouldShowLess = !props.isExpand && !isExpanded.value
+ if (shouldShowLess) {
+ const maxItemsPerRow = Math.floor(24 / props.span) - 1
+ return filteredItems.slice(0, maxItemsPerRow)
+ }
+ return filteredItems
+ })
+ const shouldShowExpandToggle = computed(() => {
+ const filteredItems = props.items.filter((item) => !item.hidden)
+ return (
+ !props.isExpand && props.showExpand && filteredItems.length > Math.floor(24 / props.span) - 1
+ )
+ })
+ const expandToggleText = computed(() => {
+ return isExpanded.value ? t('table.searchBar.collapse') : t('table.searchBar.expand')
+ })
+ const actionButtonsStyle = computed(() => ({
+ 'justify-content': isMobile.value
+ ? 'flex-end'
+ : props.items.filter((item) => !item.hidden).length <= props.buttonLeftLimit
+ ? 'flex-start'
+ : 'flex-end'
+ }))
+ const toggleExpand = () => {
+ isExpanded.value = !isExpanded.value
+ }
+ const handleReset = () => {
+ formInstance.value?.resetFields()
+ Object.keys(modelValue.value).forEach((key) => {
+ delete modelValue.value[key]
+ })
+ Object.assign(modelValue.value, cloneModelValue(initialModelValue.value))
+ emit('reset')
+ }
+ const handleSearch = () => {
+ emit('search', getSanitizedOutput())
+ }
+ defineExpose({
+ ref: formInstance,
+ validate: (...args) => formInstance.value?.validate(...args),
+ reset: handleReset,
+ // 鍏佽澶栭儴鍦ㄦ墜鍔ㄧ粍瑁呰姹傚墠鐩存帴璇诲彇娓呮礂鍚庣殑鍙傛暟銆�
+ getOutput: getSanitizedOutput
+ })
+ const { span, gutter, labelPosition, labelWidth } = toRefs(props)
+</script>
+
+<style lang="scss" scoped>
+ .art-search-bar {
+ padding: 15px 20px 0;
+
+ .action-column {
+ flex: 1;
+ max-width: 100%;
+
+ .action-buttons-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+ margin-bottom: 12px;
+ }
+
+ .form-buttons {
+ display: flex;
+ gap: 8px;
+ }
+
+ .filter-toggle {
+ display: flex;
+ align-items: center;
+ margin-left: 10px;
+ line-height: 32px;
+ color: var(--theme-color);
+ cursor: pointer;
+ transition: color 0.2s ease;
+
+ &:hover {
+ color: var(--ElColor-primary);
+ }
+
+ span {
+ font-size: 14px;
+ user-select: none;
+ }
+
+ .icon-wrapper {
+ display: flex;
+ align-items: center;
+ margin-left: 4px;
+ font-size: 14px;
+ transition: transform 0.2s ease;
+ }
+ }
+ }
+ }
+
+ // 鍝嶅簲寮忎紭鍖�
+ @media (width <= 768px) {
+ .art-search-bar {
+ padding: 16px 16px 0;
+
+ .action-column {
+ .action-buttons-wrapper {
+ flex-direction: column;
+ gap: 8px;
+ align-items: stretch;
+
+ .form-buttons {
+ justify-content: center;
+ }
+
+ .filter-toggle {
+ justify-content: center;
+ margin-left: 0;
+ }
+ }
+ }
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/forms/art-wang-editor/index.vue b/rsf-design/src/components/core/forms/art-wang-editor/index.vue
new file mode 100644
index 0000000..23219c9
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-wang-editor/index.vue
@@ -0,0 +1,179 @@
+<!-- WangEditor 瀵屾枃鏈紪杈戝櫒 鎻掍欢鍦板潃锛歨ttps://www.wangeditor.com/ -->
+<template>
+ <div class="editor-wrapper">
+ <Toolbar
+ class="editor-toolbar"
+ :editor="editorRef"
+ :mode="mode"
+ :defaultConfig="toolbarConfig"
+ />
+ <Editor
+ :style="{ height: height, overflowY: 'hidden' }"
+ v-model="modelValue"
+ :mode="mode"
+ :defaultConfig="editorConfig"
+ @onCreated="onCreateEditor"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+
+ import '@wangeditor/editor/dist/css/style.css'
+ import { onBeforeUnmount, onMounted, shallowRef, computed } from 'vue'
+ import { useUserStore } from '@/store/modules/user'
+ import EmojiText from '@/utils/ui/emojo'
+ import request from '@/utils/http'
+ defineOptions({ name: 'ArtWangEditor' })
+ const { VITE_API_URL } = import.meta.env
+ const props = defineProps({
+ height: { required: false, default: '500px' },
+ mode: { required: false, default: 'default' },
+ placeholder: { required: false, default: '璇疯緭鍏ュ唴瀹�...' },
+ excludeKeys: { required: false, default: () => ['fontFamily'] },
+ isCustomUpload: { required: false, default: false }
+ })
+ const modelValue = defineModel({ required: true })
+ const editorRef = shallowRef()
+ const userStore = useUserStore()
+ const DEFAULT_UPLOAD_CONFIG = {
+ maxFileSize: 3 * 1024 * 1024,
+ // 3MB
+ maxNumberOfFiles: 10,
+ fieldName: 'file',
+ allowedFileTypes: ['image/*']
+ }
+ const uploadServer = computed(
+ () => props.uploadConfig?.server || `${VITE_API_URL}/api/common/upload/wangeditor`
+ )
+ const mergedUploadConfig = computed(() => ({
+ ...DEFAULT_UPLOAD_CONFIG,
+ ...props.uploadConfig
+ }))
+ const toolbarConfig = computed(() => {
+ const config = {}
+ if (props.toolbarKeys && props.toolbarKeys.length > 0) {
+ config.toolbarKeys = props.toolbarKeys
+ }
+ if (props.insertKeys) {
+ config.insertKeys = props.insertKeys
+ }
+ if (props.excludeKeys && props.excludeKeys.length > 0) {
+ config.excludeKeys = props.excludeKeys
+ }
+ return config
+ })
+ const editorConfig = {
+ placeholder: props.placeholder,
+ MENU_CONF: {
+ uploadImage: {
+ fieldName: mergedUploadConfig.value.fieldName,
+ maxFileSize: mergedUploadConfig.value.maxFileSize,
+ maxNumberOfFiles: mergedUploadConfig.value.maxNumberOfFiles,
+ allowedFileTypes: mergedUploadConfig.value.allowedFileTypes,
+ server: uploadServer.value,
+ headers: {
+ Authorization: userStore.accessToken
+ },
+ onSuccess() {
+ ElMessage.success(`鍥剧墖涓婁紶鎴愬姛 ${EmojiText[200]}`)
+ },
+ onError(file, err, res) {
+ console.error('鍥剧墖涓婁紶澶辫触:', err, res)
+ ElMessage.error(`鍥剧墖涓婁紶澶辫触 ${EmojiText[500]}`)
+ }
+ }
+ }
+ }
+ if (props.uploadConfig?.isCustomUpload && props.uploadConfig?.server && editorConfig.MENU_CONF) {
+ editorConfig.MENU_CONF.uploadImage.customUpload = async (file, insertFn) => {
+ try {
+ const formData = new FormData()
+ formData.append(mergedUploadConfig.value.fieldName, file)
+ const response = await request.post({
+ url: props.uploadConfig?.server,
+ data: formData,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ Authorization: userStore.accessToken
+ }
+ })
+ const { url, alt, href } = response
+ if (!url) {
+ throw new Error('涓婁紶澶辫触锛岃妫�鏌ユ湇鍔$閰嶇疆')
+ }
+ insertFn(url, alt, href)
+ ElMessage.success(`鍥剧墖涓婁紶鎴愬姛 ${EmojiText[200]}`)
+ } catch (error) {
+ console.error('鍥剧墖涓婁紶澶辫触:', error)
+ ElMessage.error(`鍥剧墖涓婁紶澶辫触 ${EmojiText[500]}`)
+ }
+ }
+ }
+ const onCreateEditor = (editor) => {
+ editorRef.value = editor
+ editor.on('fullScreen', () => {
+ console.log('缂栬緫鍣ㄨ繘鍏ュ叏灞忔ā寮�')
+ })
+ applyCustomIcons()
+ }
+ const applyCustomIcons = () => {
+ let retryCount = 0
+ const maxRetries = 10
+ const retryDelay = 100
+ const tryApplyIcons = () => {
+ const editor = editorRef.value
+ if (!editor) {
+ if (retryCount < maxRetries) {
+ retryCount++
+ setTimeout(tryApplyIcons, retryDelay)
+ }
+ return
+ }
+ const editorContainer = editor.getEditableContainer().closest('.editor-wrapper')
+ if (!editorContainer) {
+ if (retryCount < maxRetries) {
+ retryCount++
+ setTimeout(tryApplyIcons, retryDelay)
+ }
+ return
+ }
+ const toolbar = editorContainer.querySelector('.w-e-toolbar')
+ const toolbarButtons = editorContainer.querySelectorAll('.w-e-bar-item button[data-menu-key]')
+ if (toolbar && toolbarButtons.length > 0) {
+ return
+ }
+ if (retryCount < maxRetries) {
+ retryCount++
+ setTimeout(tryApplyIcons, retryDelay)
+ } else {
+ console.warn('宸ュ叿鏍忔覆鏌撹秴鏃讹紝鏃犳硶搴旂敤鑷畾涔夊浘鏍� - 缂栬緫鍣ㄥ疄渚�:', editor.id)
+ }
+ }
+ requestAnimationFrame(tryApplyIcons)
+ }
+ defineExpose({
+ /** 鑾峰彇缂栬緫鍣ㄥ疄渚� */
+ getEditor: () => editorRef.value,
+ /** 璁剧疆缂栬緫鍣ㄥ唴瀹� */
+ setHtml: (html) => editorRef.value?.setHtml(html),
+ /** 鑾峰彇缂栬緫鍣ㄥ唴瀹� */
+ getHtml: () => editorRef.value?.getHtml(),
+ /** 娓呯┖缂栬緫鍣� */
+ clear: () => editorRef.value?.clear(),
+ /** 鑱氱劍缂栬緫鍣� */
+ focus: () => editorRef.value?.focus()
+ })
+ onMounted(() => {})
+ onBeforeUnmount(() => {
+ const editor = editorRef.value
+ if (editor) {
+ editor.destroy()
+ }
+ })
+</script>
+
+<style lang="scss">
+ @use './style';
+</style>
diff --git a/rsf-design/src/components/core/forms/art-wang-editor/style.scss b/rsf-design/src/components/core/forms/art-wang-editor/style.scss
new file mode 100644
index 0000000..7f22e98
--- /dev/null
+++ b/rsf-design/src/components/core/forms/art-wang-editor/style.scss
@@ -0,0 +1,273 @@
+$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;
+ }
+
+ /* 涓嬫媺閫夋嫨妗� 姝f枃鏂囧瓧澶у皬璋冩暣 */
+ .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;
+ }
+ }
+ }
+
+ /* 浠g爜鍧� */
+ .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 {
+ [data-slate-editor] {
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ margin: 0.8em 0 0.4em;
+ font-weight: 700;
+ line-height: 1.35;
+ }
+
+ h1 {
+ font-size: 2em;
+ }
+
+ h2 {
+ font-size: 1.5em;
+ }
+
+ h3 {
+ font-size: 1.25em;
+ }
+
+ h4 {
+ font-size: 1.125em;
+ }
+
+ h5 {
+ font-size: 1em;
+ }
+
+ h6 {
+ font-size: 0.875em;
+ }
+
+ ul,
+ ol {
+ padding-left: 1.5em;
+ margin: 0.8em 0;
+ }
+
+ ul {
+ list-style: disc;
+ }
+
+ ol {
+ list-style: decimal;
+ }
+
+ li {
+ margin: 0.25em 0;
+ }
+
+ ul ul {
+ list-style: circle;
+ }
+
+ ul ul ul {
+ list-style: square;
+ }
+ }
+
+ strong,
+ b {
+ font-weight: 700;
+ }
+
+ 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/rsf-design/src/components/core/layouts/art-breadcrumb/index.vue b/rsf-design/src/components/core/layouts/art-breadcrumb/index.vue
new file mode 100644
index 0000000..5a8c0a2
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-breadcrumb/index.vue
@@ -0,0 +1,99 @@
+<!-- 闈㈠寘灞戝鑸� -->
+<template>
+ <nav class="ml-2.5 max-lg:!hidden" aria-label="breadcrumb">
+ <ul class="flex-c h-full">
+ <li
+ v-for="(item, index) in breadcrumbItems"
+ :key="item.path"
+ class="box-border flex-c h-7 text-sm leading-7"
+ >
+ <div
+ :class="
+ isClickable(item, index)
+ ? 'c-p py-1 rounded tad-200 hover:bg-active-color hover:[&_span]:text-g-600'
+ : ''
+ "
+ @click="handleBreadcrumbClick(item, index)"
+ >
+ <span
+ class="block max-w-46 overflow-hidden text-ellipsis whitespace-nowrap px-1.5 text-sm text-g-600 dark:text-g-800"
+ >{{ formatMenuTitle(item.meta?.title) }}</span
+ >
+ </div>
+ <div
+ v-if="!isLastItem(index) && item.meta?.title"
+ class="mx-1 text-sm not-italic text-g-500"
+ aria-hidden="true"
+ >
+ /
+ </div>
+ </li>
+ </ul>
+ </nav>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { useRouter, useRoute } from 'vue-router'
+ import { formatMenuTitle } from '@/utils/router'
+ defineOptions({ name: 'ArtBreadcrumb' })
+ const route = useRoute()
+ const router = useRouter()
+ const breadcrumbItems = computed(() => {
+ const { matched } = route
+ const matchedLength = matched.length
+ if (!matchedLength || isHomeRoute(matched[0])) {
+ return []
+ }
+ const firstRoute = matched[0]
+ const isFirstLevel = firstRoute.meta?.isFirstLevel
+ const lastIndex = matchedLength - 1
+ const currentRoute = matched[lastIndex]
+ const currentRouteMeta = currentRoute.meta
+ let items = isFirstLevel
+ ? [createBreadcrumbItem(currentRoute)]
+ : matched.map(createBreadcrumbItem)
+ if (items.length > 1 && isWrapperContainer(items[0])) {
+ items = items.slice(1)
+ }
+ if (currentRouteMeta?.isIframe && (items.length === 1 || items.every(isWrapperContainer))) {
+ return [createBreadcrumbItem(currentRoute)]
+ }
+ return items
+ })
+ const isWrapperContainer = (item) => item.path === '/outside' && !!item.meta?.isIframe
+ const createBreadcrumbItem = (route2) => ({
+ path: route2.path,
+ meta: route2.meta
+ })
+ const isHomeRoute = (route2) => route2.name === '/'
+ const isLastItem = (index) => {
+ const itemsLength = breadcrumbItems.value.length
+ return index === itemsLength - 1
+ }
+ const isClickable = (item, index) => item.path !== '/outside' && !isLastItem(index)
+ const findFirstValidChild = (route2) =>
+ route2.children?.find((child) => !child.redirect && !child.meta?.isHide)
+ const buildFullPath = (childPath) => `/${childPath}`.replace('//', '/')
+ async function handleBreadcrumbClick(item, index) {
+ if (isLastItem(index) || item.path === '/outside') {
+ return
+ }
+ try {
+ const routes = router.getRoutes()
+ const targetRoute = routes.find((route2) => route2.path === item.path)
+ if (!targetRoute?.children?.length) {
+ await router.push(item.path)
+ return
+ }
+ const firstValidChild = findFirstValidChild(targetRoute)
+ if (firstValidChild) {
+ await router.push(buildFullPath(firstValidChild.path))
+ } else {
+ await router.push(item.path)
+ }
+ } catch (error) {
+ console.error('瀵艰埅澶辫触:', error)
+ }
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-chat-window/index.vue b/rsf-design/src/components/core/layouts/art-chat-window/index.vue
new file mode 100644
index 0000000..01d78c8
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-chat-window/index.vue
@@ -0,0 +1,228 @@
+<!-- 绯荤粺鑱婂ぉ绐楀彛 -->
+<template>
+ <div>
+ <ElDrawer v-model="isDrawerVisible" :size="isMobile ? '100%' : '480px'" :with-header="false">
+ <div class="mb-5 flex-cb">
+ <div>
+ <span class="text-base font-medium">Art Bot</span>
+ <div class="mt-1.5 flex-c gap-1">
+ <div
+ class="h-2 w-2 rounded-full"
+ :class="isOnline ? 'bg-success/100' : 'bg-danger/100'"
+ ></div>
+ <span class="text-xs text-g-600">{{ isOnline ? '鍦ㄧ嚎' : '绂荤嚎' }}</span>
+ </div>
+ </div>
+ <div>
+ <ElIcon class="c-p" :size="20" @click="closeChat">
+ <Close />
+ </ElIcon>
+ </div>
+ </div>
+ <div class="flex h-[calc(100%-70px)] flex-col">
+ <!-- 鑱婂ぉ娑堟伅鍖哄煙 -->
+ <div
+ class="flex-1 overflow-y-auto border-t-d px-4 py-7.5 [&::-webkit-scrollbar]:!w-1"
+ ref="messageContainer"
+ >
+ <template v-for="(message, index) in messages" :key="index">
+ <div
+ :class="[
+ 'mb-7.5 flex w-full items-start gap-2',
+ message.isMe ? 'flex-row-reverse' : 'flex-row'
+ ]"
+ >
+ <ElAvatar :size="32" :src="message.avatar" class="shrink-0" />
+ <div
+ :class="['flex max-w-[70%] flex-col', message.isMe ? 'items-end' : 'items-start']"
+ >
+ <div
+ :class="[
+ 'mb-1 flex gap-2 text-xs',
+ message.isMe ? 'flex-row-reverse' : 'flex-row'
+ ]"
+ >
+ <span class="font-medium">{{ message.sender }}</span>
+ <span class="text-g-600">{{ message.time }}</span>
+ </div>
+ <div
+ :class="[
+ 'rounded-md px-3.5 py-2.5 text-sm leading-[1.4] text-g-900',
+ message.isMe ? 'message-right bg-theme/15' : 'message-left bg-g-300/50'
+ ]"
+ >{{ message.content }}</div
+ >
+ </div>
+ </div>
+ </template>
+ </div>
+
+ <!-- 鑱婂ぉ杈撳叆鍖哄煙 -->
+ <div class="px-4 pt-4">
+ <ElInput
+ v-model="messageText"
+ type="textarea"
+ :rows="3"
+ placeholder="杈撳叆娑堟伅"
+ resize="none"
+ @keyup.enter.prevent="sendMessage"
+ >
+ <template #append>
+ <div class="flex gap-2 py-2">
+ <ElButton :icon="Paperclip" circle plain />
+ <ElButton :icon="Picture" circle plain />
+ <ElButton type="primary" @click="sendMessage" v-ripple>鍙戦��</ElButton>
+ </div>
+ </template>
+ </ElInput>
+ <div class="mt-3 flex-cb">
+ <div class="flex-c">
+ <ArtSvgIcon icon="ri:image-line" class="mr-5 c-p text-g-600 text-lg" />
+ <ArtSvgIcon icon="ri:emotion-happy-line" class="mr-5 c-p text-g-600 text-lg" />
+ </div>
+ <ElButton type="primary" @click="sendMessage" v-ripple class="min-w-20">鍙戦��</ElButton>
+ </div>
+ </div>
+ </div>
+ </ElDrawer>
+ </div>
+</template>
+
+<script setup>
+ import { Picture, Paperclip, Close } from '@element-plus/icons-vue'
+
+ import { mittBus } from '@/utils/sys'
+ import meAvatar from '@/assets/images/avatar/avatar5.webp'
+ import aiAvatar from '@/assets/images/avatar/avatar10.webp'
+ defineOptions({ name: 'ArtChatWindow' })
+ const MOBILE_BREAKPOINT = 640
+ const SCROLL_DELAY = 100
+ const BOT_NAME = 'Art Bot'
+ const USER_NAME = 'Ricky'
+ const { width } = useWindowSize()
+ const isMobile = computed(() => width.value < MOBILE_BREAKPOINT)
+ const isDrawerVisible = ref(false)
+ const isOnline = ref(true)
+ const messageText = ref('')
+ const messageId = ref(10)
+ const messageContainer = ref(null)
+ const initializeMessages = () => [
+ {
+ id: 1,
+ sender: BOT_NAME,
+ content: '浣犲ソ锛佹垜鏄綘鐨凙I鍔╂墜锛屾湁浠�涔堟垜鍙互甯綘鐨勫悧锛�',
+ time: '10:00',
+ isMe: false,
+ avatar: aiAvatar
+ },
+ {
+ id: 2,
+ sender: USER_NAME,
+ content: '鎴戞兂浜嗚В涓�涓嬬郴缁熺殑浣跨敤鏂规硶銆�',
+ time: '10:01',
+ isMe: true,
+ avatar: meAvatar
+ },
+ {
+ id: 3,
+ sender: BOT_NAME,
+ content: '濂界殑锛屾垜鏉ヤ负鎮ㄤ粙缁嶇郴缁熺殑涓昏鍔熻兘銆傞鍏堬紝鎮ㄥ彲浠ラ�氳繃宸︿晶鑿滃崟璁块棶涓嶅悓鐨勫姛鑳芥ā鍧�...',
+ time: '10:02',
+ isMe: false,
+ avatar: aiAvatar
+ },
+ {
+ id: 4,
+ sender: USER_NAME,
+ content: '鍚捣鏉ュ緢涓嶉敊锛岃兘鍏蜂綋璁茶鏁版嵁鍒嗘瀽閮ㄥ垎鍚楋紵',
+ time: '10:05',
+ isMe: true,
+ avatar: meAvatar
+ },
+ {
+ id: 5,
+ sender: BOT_NAME,
+ content: '褰撶劧鍙互銆傛暟鎹垎鏋愭ā鍧楀彲浠ュ府鍔╂偍瀹炴椂鐩戞帶鍏抽敭鎸囨爣锛屽苟鐢熸垚璇︾粏鐨勬姤琛�...',
+ time: '10:06',
+ isMe: false,
+ avatar: aiAvatar
+ },
+ {
+ id: 6,
+ sender: USER_NAME,
+ content: '澶ソ浜嗭紝閭f垜濡備綍寮�濮嬩娇鐢ㄥ憿锛�',
+ time: '10:08',
+ isMe: true,
+ avatar: meAvatar
+ },
+ {
+ id: 7,
+ sender: BOT_NAME,
+ content: '鎮ㄥ彲浠ュ厛鍒涘缓涓�涓」鐩紝鐒跺悗鍦ㄩ」鐩腑娣诲姞鐩稿叧鐨勬暟鎹簮锛岀郴缁熶細鑷姩杩涜鍒嗘瀽銆�',
+ time: '10:09',
+ isMe: false,
+ avatar: aiAvatar
+ },
+ {
+ id: 8,
+ sender: USER_NAME,
+ content: '鏄庣櫧浜嗭紝璋㈣阿浣犵殑甯姪锛�',
+ time: '10:10',
+ isMe: true,
+ avatar: meAvatar
+ },
+ {
+ id: 9,
+ sender: BOT_NAME,
+ content: '涓嶅姘旓紝鏈変换浣曢棶棰橀殢鏃惰仈绯绘垜銆�',
+ time: '10:11',
+ isMe: false,
+ avatar: aiAvatar
+ }
+ ]
+ const messages = ref(initializeMessages())
+ const formatCurrentTime = () => {
+ return /* @__PURE__ */ new Date().toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ }
+ const scrollToBottom = () => {
+ nextTick(() => {
+ setTimeout(() => {
+ if (messageContainer.value) {
+ messageContainer.value.scrollTop = messageContainer.value.scrollHeight
+ }
+ }, SCROLL_DELAY)
+ })
+ }
+ const sendMessage = () => {
+ const text = messageText.value.trim()
+ if (!text) return
+ const newMessage = {
+ id: messageId.value++,
+ sender: USER_NAME,
+ content: text,
+ time: formatCurrentTime(),
+ isMe: true,
+ avatar: meAvatar
+ }
+ messages.value.push(newMessage)
+ messageText.value = ''
+ scrollToBottom()
+ }
+ const openChat = () => {
+ isDrawerVisible.value = true
+ scrollToBottom()
+ }
+ const closeChat = () => {
+ isDrawerVisible.value = false
+ }
+ onMounted(() => {
+ scrollToBottom()
+ mittBus.on('openChat', openChat)
+ })
+ onUnmounted(() => {
+ mittBus.off('openChat', openChat)
+ })
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-fast-enter/index.vue b/rsf-design/src/components/core/layouts/art-fast-enter/index.vue
new file mode 100644
index 0000000..5272af7
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-fast-enter/index.vue
@@ -0,0 +1,89 @@
+<!-- 椤堕儴蹇�熷叆鍙i潰鏉� -->
+<template>
+ <ElPopover
+ ref="popoverRef"
+ :width="700"
+ :offset="0"
+ :show-arrow="false"
+ trigger="hover"
+ placement="bottom-start"
+ popper-class="fast-enter-popover"
+ :popper-style="{
+ border: '1px solid var(--default-border)',
+ borderRadius: 'calc(var(--custom-radius) / 2 + 4px)'
+ }"
+ >
+ <template #reference>
+ <div class="flex-c gap-2">
+ <slot />
+ </div>
+ </template>
+
+ <div class="grid grid-cols-[2fr_0.8fr]">
+ <div>
+ <div class="grid grid-cols-2 gap-1.5">
+ <!-- 搴旂敤鍒楄〃 -->
+ <div
+ v-for="application in enabledApplications"
+ :key="application.name"
+ class="mr-3 c-p flex-c gap-3 rounded-lg p-2 hover:bg-g-200/70 dark:hover:bg-g-200/90 hover:[&_.app-icon]:!bg-transparent"
+ @click="handleApplicationClick(application)"
+ >
+ <div class="app-icon size-12 flex-cc rounded-lg bg-g-200/80 dark:bg-g-300/30">
+ <ArtSvgIcon
+ class="text-xl"
+ :icon="application.icon"
+ :style="{ color: application.iconColor }"
+ />
+ </div>
+ <div>
+ <h3 class="m-0 text-sm font-medium text-g-800">{{ application.name }}</h3>
+ <p class="mt-1 text-xs text-g-600">{{ application.description }}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="border-l-d pl-6 pt-2">
+ <h3 class="mb-2.5 text-base font-medium text-g-800">蹇�熼摼鎺�</h3>
+ <ul>
+ <li
+ v-for="quickLink in enabledQuickLinks"
+ :key="quickLink.name"
+ class="c-p py-2 hover:[&_span]:text-theme"
+ @click="handleQuickLinkClick(quickLink)"
+ >
+ <span class="text-g-600 no-underline">{{ quickLink.name }}</span>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </ElPopover>
+</template>
+
+<script setup>
+ import { useFastEnter } from '@/hooks/core/useFastEnter'
+ defineOptions({ name: 'ArtFastEnter' })
+ const router = useRouter()
+ const popoverRef = ref()
+ const { enabledApplications, enabledQuickLinks } = useFastEnter()
+ const handleNavigate = (routeName, link) => {
+ const targetPath = routeName || link
+ if (!targetPath) {
+ console.warn('瀵艰埅閰嶇疆鏃犳晥锛氱己灏戣矾鐢卞悕绉版垨閾炬帴')
+ return
+ }
+ if (targetPath.startsWith('http')) {
+ window.open(targetPath, '_blank')
+ } else {
+ router.push({ name: targetPath })
+ }
+ popoverRef.value?.hide()
+ }
+ const handleApplicationClick = (application) => {
+ handleNavigate(application.routeName, application.link)
+ }
+ const handleQuickLinkClick = (quickLink) => {
+ handleNavigate(quickLink.routeName, quickLink.link)
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue b/rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue
new file mode 100644
index 0000000..3ccc3ed
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue
@@ -0,0 +1,427 @@
+<!-- 鐑熻姳鏁堟灉 | 绀艰姳鏁堟灉 -->
+<template>
+ <canvas
+ ref="canvasRef"
+ class="fixed top-0 left-0 z-[9999] w-full h-full pointer-events-none"
+ ></canvas>
+</template>
+
+<script setup>
+ import { useEventListener } from '@vueuse/core'
+ import { mittBus } from '@/utils/sys'
+ import bp from '@/assets/images/ceremony/hb.png'
+ import sd from '@/assets/images/ceremony/sd.png'
+ import yd from '@/assets/images/ceremony/yd.png'
+ defineOptions({ name: 'ArtFireworksEffect' })
+ const CONFIG = {
+ // 鎬ц兘鐩稿叧閰嶇疆
+ POOL_SIZE: 600,
+ // 瀵硅薄姹犲ぇ灏忥紝褰卞搷鍚屾椂瀛樺湪鐨勬渶澶х矑瀛愭暟
+ PARTICLES_PER_BURST: 200,
+ // 姣忔鐖嗙偢鐨勭矑瀛愭暟閲忥紝褰卞搷瑙嗚鏁堟灉瀵嗗害
+ // 绮掑瓙灏哄閰嶇疆
+ SIZES: {
+ RECTANGLE: { WIDTH: 24, HEIGHT: 12 },
+ // 鐭╁舰绮掑瓙灏哄
+ SQUARE: { SIZE: 12 },
+ // 姝f柟褰㈢矑瀛愬昂瀵�
+ CIRCLE: { SIZE: 12 },
+ // 鍦嗗舰绮掑瓙灏哄
+ TRIANGLE: { SIZE: 10 },
+ // 涓夎褰㈢矑瀛愬昂瀵�
+ OVAL: { WIDTH: 24, HEIGHT: 12 },
+ // 妞渾绮掑瓙灏哄
+ IMAGE: { WIDTH: 30, HEIGHT: 30 }
+ // 鍥剧墖绮掑瓙灏哄
+ },
+ // 鏃嬭浆鍔ㄧ敾閰嶇疆
+ ROTATION: {
+ BASE_SPEED: 2,
+ // 鍩虹鏃嬭浆閫熷害
+ RANDOM_SPEED: 3,
+ // 棰濆闅忔満鏃嬭浆閫熷害鑼冨洿
+ DECAY: 0.98
+ // 鏃嬭浆閫熷害琛板噺绯绘暟 (瓒婂皬琛板噺瓒婂揩)
+ },
+ // 鐗╃悊鏁堟灉閰嶇疆
+ PHYSICS: {
+ GRAVITY: 0.525,
+ // 閲嶅姏鍔犻�熷害锛屽奖鍝嶇矑瀛愪笅钀介�熷害
+ VELOCITY_THRESHOLD: 10,
+ // 閫熷害闃堝�硷紝瓒呰繃鏃跺紑濮嬮�忔槑搴﹁“鍑�
+ OPACITY_DECAY: 0.02
+ // 閫忔槑搴﹁“鍑忛�熷害锛屽奖鍝嶇矑瀛愭秷澶卞揩鎱�
+ },
+ // 绮掑瓙棰滆壊閰嶇疆 - 浣跨敤RGBA鏍煎紡鏀寔閫忔槑搴�
+ COLORS: [
+ 'rgba(255, 68, 68, 1)',
+ // 绾㈣壊绯�
+ 'rgba(255, 68, 68, 0.9)',
+ 'rgba(255, 68, 68, 0.8)',
+ 'rgba(255, 116, 188, 1)',
+ // 绮夎壊绯�
+ 'rgba(255, 116, 188, 0.9)',
+ 'rgba(255, 116, 188, 0.8)',
+ 'rgba(68, 68, 255, 0.8)',
+ // 钃濊壊绯�
+ 'rgba(92, 202, 56, 0.7)',
+ // 缁胯壊绯�
+ 'rgba(255, 68, 255, 0.8)',
+ // 绱壊绯�
+ 'rgba(68, 255, 255, 0.7)',
+ // 闈掕壊绯�
+ 'rgba(255, 136, 68, 0.7)',
+ // 姗欒壊绯�
+ 'rgba(68, 136, 255, 1)',
+ // 钃濊壊绯�
+ 'rgba(250, 198, 122, 0.8)'
+ // 閲戣壊绯�
+ ],
+ // 绮掑瓙褰㈢姸閰嶇疆 - 鐭╁舰鍑虹幇姒傜巼鏇撮珮锛岃惀閫犳洿涓板瘜鐨勮瑙夋晥鏋�
+ SHAPES: [
+ 'rectangle',
+ 'rectangle',
+ 'rectangle',
+ 'rectangle',
+ 'rectangle',
+ 'rectangle',
+ 'rectangle',
+ 'circle',
+ 'triangle',
+ 'oval'
+ ]
+ }
+ const canvasRef = ref()
+ const ctx = ref(null)
+ class FireworkSystem {
+ constructor() {
+ this.particlePool = []
+ this.activeParticles = []
+ this.poolIndex = 0
+ this.imageCache = {}
+ this.animationId = 0
+ this.canvasWidth = 0
+ this.canvasHeight = 0
+ this.animate = () => {
+ this.updateParticles()
+ this.render()
+ this.animationId = requestAnimationFrame(this.animate)
+ }
+ this.initializePool()
+ }
+ /**
+ * 鍒濆鍖栧璞℃睜
+ * 棰勫厛鍒涘缓鎸囧畾鏁伴噺鐨勭矑瀛愬璞★紝閬垮厤杩愯鏃堕绻佸垱寤�
+ */
+ initializePool() {
+ for (let i = 0; i < CONFIG.POOL_SIZE; i++) {
+ this.particlePool.push(this.createParticle())
+ }
+ }
+ /**
+ * 鍒涘缓涓�涓柊鐨勭矑瀛愬璞�
+ * 杩斿洖鍒濆鍖栫姸鎬佺殑绮掑瓙
+ */
+ createParticle() {
+ return {
+ x: 0,
+ y: 0,
+ vx: 0,
+ vy: 0,
+ color: '',
+ rotation: 0,
+ rotationSpeed: 0,
+ scale: 1,
+ shape: 'circle',
+ opacity: 1,
+ active: false
+ }
+ }
+ /**
+ * 浠庡璞℃睜鑾峰彇鍙敤绮掑瓙 (鎬ц兘浼樺寲鐗堟湰)
+ * 浣跨敤寰幆绱㈠紩鑰岄潪Array.find()锛屾椂闂村鏉傚害浠嶰(n)闄嶈嚦O(1)
+ * @returns 鍙敤鐨勭矑瀛愬璞℃垨null
+ */
+ getAvailableParticle() {
+ for (let i = 0; i < CONFIG.POOL_SIZE; i++) {
+ const index = (this.poolIndex + i) % CONFIG.POOL_SIZE
+ const particle = this.particlePool[index]
+ if (!particle.active) {
+ this.poolIndex = (index + 1) % CONFIG.POOL_SIZE
+ particle.active = true
+ return particle
+ }
+ }
+ return null
+ }
+ /**
+ * 棰勫姞杞藉崟涓浘鐗囪祫婧�
+ * @param url 鍥剧墖URL
+ * @returns Promise<HTMLImageElement>
+ */
+ async preloadImage(url) {
+ if (this.imageCache[url]) {
+ return this.imageCache[url]
+ }
+ return new Promise((resolve, reject) => {
+ const img = new Image()
+ img.crossOrigin = 'anonymous'
+ img.onload = () => {
+ this.imageCache[url] = img
+ resolve(img)
+ }
+ img.onerror = reject
+ img.src = url
+ })
+ }
+ /**
+ * 棰勫姞杞芥墍鏈夐渶瑕佺殑鍥剧墖璧勬簮
+ * 鍦ㄧ粍浠跺垵濮嬪寲鏃惰皟鐢紝纭繚鍥剧墖ready
+ */
+ async preloadAllImages() {
+ const imageUrls = [bp, sd, yd]
+ try {
+ await Promise.all(imageUrls.map((url) => this.preloadImage(url)))
+ } catch (error) {
+ console.error('Image preloading failed:', error)
+ }
+ }
+ /**
+ * 鍒涘缓鐑熻姳鐖嗙偢鏁堟灉
+ * @param imageUrl 鍙�夌殑鍥剧墖URL锛屽鏋滄彁渚涘垯浣跨敤鍥剧墖绮掑瓙
+ */
+ createFirework(imageUrl) {
+ const startX = Math.random() * this.canvasWidth
+ const startY = this.canvasHeight
+ const availableShapes = imageUrl && this.imageCache[imageUrl] ? ['image'] : CONFIG.SHAPES
+ const particles = []
+ for (let i = 0; i < CONFIG.PARTICLES_PER_BURST; i++) {
+ const particle = this.getAvailableParticle()
+ if (!particle) continue
+ const angle = (Math.PI * i) / (CONFIG.PARTICLES_PER_BURST / 2)
+ const speed = (12 + Math.random() * 6) * 1.5
+ const spread = Math.random() * Math.PI * 2
+ particle.x = startX
+ particle.y = startY
+ particle.vx = Math.cos(angle) * Math.cos(spread) * speed * (Math.random() * 0.5 + 0.5)
+ particle.vy = Math.sin(angle) * speed - 15
+ particle.color = CONFIG.COLORS[Math.floor(Math.random() * CONFIG.COLORS.length)]
+ particle.rotation = Math.random() * 360
+ particle.rotationSpeed =
+ (Math.random() * CONFIG.ROTATION.RANDOM_SPEED + CONFIG.ROTATION.BASE_SPEED) *
+ (Math.random() > 0.5 ? 1 : -1)
+ particle.scale = 0.8 + Math.random() * 0.4
+ particle.shape = availableShapes[Math.floor(Math.random() * availableShapes.length)]
+ particle.opacity = 1
+ particle.imageUrl = imageUrl && this.imageCache[imageUrl] ? imageUrl : void 0
+ particles.push(particle)
+ }
+ this.activeParticles.push(...particles)
+ }
+ /**
+ * 鏇存柊鎵�鏈夌矑瀛愮殑鐗╃悊鐘舵�� (鎬ц兘浼樺寲鐗堟湰)
+ * 鍖呮嫭浣嶇疆銆侀�熷害銆佹棆杞�侀�忔槑搴︾瓑
+ */
+ updateParticles() {
+ const { GRAVITY, VELOCITY_THRESHOLD, OPACITY_DECAY } = CONFIG.PHYSICS
+ const { DECAY } = CONFIG.ROTATION
+ for (let i = this.activeParticles.length - 1; i >= 0; i--) {
+ const particle = this.activeParticles[i]
+ particle.x += particle.vx
+ particle.y += particle.vy
+ particle.vy += GRAVITY
+ particle.rotation += particle.rotationSpeed
+ particle.rotationSpeed *= DECAY
+ if (particle.vy > VELOCITY_THRESHOLD) {
+ particle.opacity -= OPACITY_DECAY
+ if (particle.opacity <= 0) {
+ this.recycleParticle(i)
+ continue
+ }
+ }
+ if (this.isOutOfBounds(particle)) {
+ this.recycleParticle(i)
+ }
+ }
+ }
+ /**
+ * 鍥炴敹绮掑瓙鍒板璞℃睜
+ * @param index 瑕佸洖鏀剁殑绮掑瓙鍦ㄦ椿鍔ㄦ暟缁勪腑鐨勭储寮�
+ */
+ recycleParticle(index) {
+ const particle = this.activeParticles[index]
+ particle.active = false
+ this.activeParticles.splice(index, 1)
+ }
+ /**
+ * 妫�鏌ョ矑瀛愭槸鍚﹁秴鍑哄睆骞曡竟鐣�
+ * @param particle 瑕佹鏌ョ殑绮掑瓙
+ * @returns 鏄惁瓒呭嚭杈圭晫
+ */
+ isOutOfBounds(particle) {
+ const margin = 100
+ return (
+ particle.x < -margin ||
+ particle.x > this.canvasWidth + margin ||
+ particle.y < -margin ||
+ particle.y > this.canvasHeight + margin
+ )
+ }
+ /**
+ * 缁樺埗鍗曚釜绮掑瓙
+ * @param particle 瑕佺粯鍒剁殑绮掑瓙瀵硅薄
+ */
+ drawParticle(particle) {
+ if (!ctx.value) return
+ ctx.value.save()
+ ctx.value.globalAlpha = particle.opacity
+ ctx.value.translate(particle.x, particle.y)
+ ctx.value.rotate((particle.rotation * Math.PI) / 180)
+ ctx.value.scale(particle.scale, particle.scale)
+ this.renderShape(particle)
+ ctx.value.restore()
+ }
+ /**
+ * 鏍规嵁绮掑瓙绫诲瀷娓叉煋瀵瑰簲鐨勫舰鐘�
+ * @param particle 瑕佹覆鏌撶殑绮掑瓙
+ */
+ renderShape(particle) {
+ if (!ctx.value) return
+ const { SIZES } = CONFIG
+ ctx.value.fillStyle = particle.color
+ switch (particle.shape) {
+ case 'rectangle':
+ ctx.value.fillRect(
+ -SIZES.RECTANGLE.WIDTH / 2,
+ -SIZES.RECTANGLE.HEIGHT / 2,
+ SIZES.RECTANGLE.WIDTH,
+ SIZES.RECTANGLE.HEIGHT
+ )
+ break
+ case 'square':
+ ctx.value.fillRect(
+ -SIZES.SQUARE.SIZE / 2,
+ -SIZES.SQUARE.SIZE / 2,
+ SIZES.SQUARE.SIZE,
+ SIZES.SQUARE.SIZE
+ )
+ break
+ case 'circle':
+ ctx.value.beginPath()
+ ctx.value.arc(0, 0, SIZES.CIRCLE.SIZE / 2, 0, Math.PI * 2)
+ ctx.value.fill()
+ break
+ case 'triangle':
+ ctx.value.beginPath()
+ ctx.value.moveTo(0, -SIZES.TRIANGLE.SIZE)
+ ctx.value.lineTo(SIZES.TRIANGLE.SIZE, SIZES.TRIANGLE.SIZE)
+ ctx.value.lineTo(-SIZES.TRIANGLE.SIZE, SIZES.TRIANGLE.SIZE)
+ ctx.value.closePath()
+ ctx.value.fill()
+ break
+ case 'oval':
+ ctx.value.beginPath()
+ ctx.value.ellipse(0, 0, SIZES.OVAL.WIDTH / 2, SIZES.OVAL.HEIGHT / 2, 0, 0, Math.PI * 2)
+ ctx.value.fill()
+ break
+ case 'image':
+ this.renderImage(particle)
+ break
+ }
+ }
+ /**
+ * 娓叉煋鍥剧墖绫诲瀷鐨勭矑瀛�
+ * @param particle 鍖呭惈鍥剧墖URL鐨勭矑瀛愬璞�
+ */
+ renderImage(particle) {
+ if (!ctx.value || !particle.imageUrl) return
+ const img = this.imageCache[particle.imageUrl]
+ if (img?.complete) {
+ const { WIDTH, HEIGHT } = CONFIG.SIZES.IMAGE
+ ctx.value.drawImage(img, -WIDTH / 2, -HEIGHT / 2, WIDTH, HEIGHT)
+ }
+ }
+ /**
+ * 娓叉煋鎵�鏈夋椿鍔ㄧ矑瀛愬埌鐢诲竷
+ * 娓呴櫎鐢诲竷骞堕噸鏂扮粯鍒舵墍鏈夌矑瀛�
+ */
+ render() {
+ if (!ctx.value || !canvasRef.value) return
+ ctx.value.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+ ctx.value.globalCompositeOperation = 'lighter'
+ for (const particle of this.activeParticles) {
+ this.drawParticle(particle)
+ }
+ }
+ /**
+ * 鏇存柊鐢诲竷灏哄缂撳瓨
+ * 鍦ㄧ獥鍙eぇ灏忔敼鍙樻椂璋冪敤
+ * @param width 鏂扮殑鐢诲竷瀹藉害
+ * @param height 鏂扮殑鐢诲竷楂樺害
+ */
+ updateCanvasSize(width, height) {
+ this.canvasWidth = width
+ this.canvasHeight = height
+ }
+ /**
+ * 鍚姩鍔ㄧ敾寰幆
+ */
+ start() {
+ this.animate()
+ }
+ /**
+ * 鍋滄鍔ㄧ敾寰幆
+ * 鍦ㄧ粍浠跺嵏杞芥椂璋冪敤锛岄伩鍏嶅唴瀛樻硠婕�
+ */
+ stop() {
+ if (this.animationId) {
+ cancelAnimationFrame(this.animationId)
+ this.animationId = 0
+ }
+ }
+ /**
+ * 鑾峰彇褰撳墠娲诲姩绮掑瓙鏁伴噺
+ * 鐢ㄤ簬璋冭瘯鍜屾�ц兘鐩戞帶
+ * @returns 娲诲姩绮掑瓙鏁伴噺
+ */
+ getActiveParticleCount() {
+ return this.activeParticles.length
+ }
+ }
+ const fireworkSystem = new FireworkSystem()
+ const handleKeyPress = (event) => {
+ const isFireworkShortcut =
+ (event.ctrlKey && event.shiftKey && event.key.toLowerCase() === 'p') ||
+ (event.metaKey && event.shiftKey && event.key.toLowerCase() === 'p')
+ if (isFireworkShortcut) {
+ event.preventDefault()
+ fireworkSystem.createFirework()
+ }
+ }
+ const resizeCanvas = () => {
+ if (!canvasRef.value) return
+ const { innerWidth, innerHeight } = window
+ canvasRef.value.width = innerWidth
+ canvasRef.value.height = innerHeight
+ fireworkSystem.updateCanvasSize(innerWidth, innerHeight)
+ }
+ const handleFireworkTrigger = (event) => {
+ const imageUrl = event
+ fireworkSystem.createFirework(imageUrl)
+ }
+ onMounted(async () => {
+ if (!canvasRef.value) return
+ ctx.value = canvasRef.value.getContext('2d')
+ if (!ctx.value) return
+ resizeCanvas()
+ await fireworkSystem.preloadAllImages()
+ fireworkSystem.start()
+ useEventListener(window, 'keydown', handleKeyPress)
+ useEventListener(window, 'resize', resizeCanvas)
+ mittBus.on('triggerFireworks', handleFireworkTrigger)
+ })
+ onUnmounted(() => {
+ fireworkSystem.stop()
+ mittBus.off('triggerFireworks', handleFireworkTrigger)
+ })
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-global-component/index.vue b/rsf-design/src/components/core/layouts/art-global-component/index.vue
new file mode 100644
index 0000000..cb3d26e
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-global-component/index.vue
@@ -0,0 +1,14 @@
+<!-- 鍏ㄥ眬缁勪欢 -->
+<template>
+ <component
+ v-for="componentConfig in enabledComponents"
+ :key="componentConfig.key"
+ :is="componentConfig.component"
+ />
+</template>
+
+<script setup>
+ import { getEnabledGlobalComponents } from '@/config/modules/component'
+ defineOptions({ name: 'ArtGlobalComponent' })
+ const enabledComponents = computed(() => getEnabledGlobalComponents())
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-global-search/index.vue b/rsf-design/src/components/core/layouts/art-global-search/index.vue
new file mode 100644
index 0000000..4799a54
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-global-search/index.vue
@@ -0,0 +1,377 @@
+<!-- 鍏ㄥ眬鎼滅储缁勪欢 -->
+<template>
+ <div class="layout-search">
+ <ElDialog
+ v-model="showSearchDialog"
+ width="600"
+ :show-close="false"
+ :lock-scroll="false"
+ modal-class="search-modal"
+ @close="closeSearchDialog"
+ >
+ <ElInput
+ v-model.trim="searchVal"
+ :placeholder="$t('search.placeholder')"
+ @input="search"
+ @blur="searchBlur"
+ ref="searchInput"
+ :prefix-icon="Search"
+ class="h-12"
+ >
+ <template #suffix>
+ <div
+ class="h-4.5 flex-cc rounded border border-g-300 dark:!bg-g-200/50 !bg-box px-1.5 text-g-500"
+ >
+ <ArtSvgIcon icon="fluent:arrow-enter-left-20-filled" />
+ </div>
+ </template>
+ </ElInput>
+ <ElScrollbar class="mt-5" max-height="370px" ref="searchResultScrollbar" always>
+ <div class="result w-full" v-show="searchResult.length">
+ <div
+ class="box !mt-0 c-p text-base leading-none"
+ v-for="(item, index) in searchResult"
+ :key="index"
+ >
+ <div
+ class="mt-2 h-12 flex-cb rounded-custom-sm bg-g-200/80 px-4 text-sm text-g-700"
+ :class="isHighlighted(index) ? 'highlighted !bg-theme/70 !text-white' : ''"
+ @click="searchGoPage(item)"
+ @mouseenter="highlightOnHover(index)"
+ >
+ {{ formatMenuTitle(item.meta.title) }}
+ <ArtSvgIcon v-show="isHighlighted(index)" icon="fluent:arrow-enter-left-20-filled" />
+ </div>
+ </div>
+ </div>
+
+ <div v-show="!searchVal && searchResult.length === 0 && historyResult.length > 0">
+ <p class="text-xs text-g-500">{{ $t('search.historyTitle') }}</p>
+ <div class="mt-1.5 w-full">
+ <div
+ class="box mt-2 h-12 c-p flex-cb rounded-custom-sm bg-g-200/80 px-4 text-sm text-g-800"
+ v-for="(item, index) in historyResult"
+ :key="index"
+ :class="
+ historyHIndex === index
+ ? 'highlighted !bg-theme/70 !text-white [&_.selected-icon]:!text-white'
+ : ''
+ "
+ @click="searchGoPage(item)"
+ @mouseenter="highlightOnHoverHistory(index)"
+ >
+ {{ formatMenuTitle(item.meta.title) }}
+ <div
+ class="size-5 selected-icon select-none rounded-full text-g-500 flex-cc c-p"
+ @click.stop="deleteHistory(index)"
+ >
+ <ArtSvgIcon icon="ri:close-large-fill" class="text-xs" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </ElScrollbar>
+
+ <template #footer>
+ <div class="dialog-footer box-border flex-c border-t-d pt-4.5 pb-1">
+ <div class="flex-cc">
+ <ArtSvgIcon icon="fluent:arrow-enter-left-20-filled" class="keyboard" />
+ <span class="mr-3.5 text-xs text-g-700">{{ $t('search.selectKeydown') }}</span>
+ </div>
+ <div class="flex-c">
+ <ArtSvgIcon icon="ri:arrow-up-wide-fill" class="keyboard" />
+ <ArtSvgIcon icon="ri:arrow-down-wide-fill" class="keyboard" />
+ <span class="mr-3.5 text-xs text-g-700">{{ $t('search.switchKeydown') }}</span>
+ </div>
+ <div class="flex-c">
+ <i class="keyboard !w-8 flex-cc"><p class="text-[10px] font-medium">ESC</p></i>
+ <span class="mr-3.5 text-xs text-g-700">{{ $t('search.exitKeydown') }}</span>
+ </div>
+ </div>
+ </template>
+ </ElDialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from '@element-plus/icons-vue'
+
+ import { useUserStore } from '@/store/modules/user'
+ import { mittBus } from '@/utils/sys'
+ import { useMenuStore } from '@/store/modules/menu'
+ import { formatMenuTitle } from '@/utils/router'
+ import { handleMenuJump } from '@/utils/navigation'
+ defineOptions({ name: 'ArtGlobalSearch' })
+ const userStore = useUserStore()
+ const { menuList } = storeToRefs(useMenuStore())
+ const showSearchDialog = ref(false)
+ const searchVal = ref('')
+ const searchResult = ref([])
+ const historyMaxLength = 10
+ const { searchHistory: historyResult } = storeToRefs(userStore)
+ const searchInput = ref(null)
+ const highlightedIndex = ref(0)
+ const historyHIndex = ref(0)
+ const searchResultScrollbar = ref()
+ const isKeyboardNavigating = ref(false)
+ onMounted(() => {
+ mittBus.on('openSearchDialog', openSearchDialog)
+ document.addEventListener('keydown', handleKeydown)
+ })
+ onUnmounted(() => {
+ document.removeEventListener('keydown', handleKeydown)
+ })
+ const handleKeydown = (event) => {
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
+ const isCommandKey = isMac ? event.metaKey : event.ctrlKey
+ if (isCommandKey && event.key.toLowerCase() === 'k') {
+ event.preventDefault()
+ showSearchDialog.value = true
+ focusInput()
+ }
+ if (showSearchDialog.value) {
+ if (event.key === 'ArrowUp') {
+ event.preventDefault()
+ highlightPrevious()
+ } else if (event.key === 'ArrowDown') {
+ event.preventDefault()
+ highlightNext()
+ } else if (event.key === 'Enter') {
+ event.preventDefault()
+ selectHighlighted()
+ } else if (event.key === 'Escape') {
+ event.preventDefault()
+ showSearchDialog.value = false
+ }
+ }
+ }
+ const focusInput = () => {
+ setTimeout(() => {
+ searchInput.value?.focus()
+ }, 100)
+ }
+ const search = (val) => {
+ if (val) {
+ searchResult.value = flattenAndFilterMenuItems(menuList.value, val)
+ } else {
+ searchResult.value = []
+ }
+ }
+ const flattenAndFilterMenuItems = (items, val) => {
+ const lowerVal = val.toLowerCase()
+ const result = []
+ const flattenAndMatch = (item) => {
+ if (item.meta?.isHide) return
+ const lowerItemTitle = formatMenuTitle(item.meta.title).toLowerCase()
+ if (item.children && item.children.length > 0) {
+ item.children.forEach(flattenAndMatch)
+ return
+ }
+ if (
+ lowerItemTitle.includes(lowerVal) &&
+ ((item.path && item.path.trim()) || item.meta.link || item.meta.isIframe)
+ ) {
+ result.push({ ...item, children: void 0 })
+ }
+ }
+ items.forEach(flattenAndMatch)
+ return result
+ }
+ const highlightPrevious = () => {
+ isKeyboardNavigating.value = true
+ if (searchVal.value) {
+ highlightedIndex.value =
+ (highlightedIndex.value - 1 + searchResult.value.length) % searchResult.value.length
+ scrollToHighlightedItem()
+ } else {
+ historyHIndex.value =
+ (historyHIndex.value - 1 + historyResult.value.length) % historyResult.value.length
+ scrollToHighlightedHistoryItem()
+ }
+ setTimeout(() => {
+ isKeyboardNavigating.value = false
+ }, 100)
+ }
+ const highlightNext = () => {
+ isKeyboardNavigating.value = true
+ if (searchVal.value) {
+ highlightedIndex.value = (highlightedIndex.value + 1) % searchResult.value.length
+ scrollToHighlightedItem()
+ } else {
+ historyHIndex.value = (historyHIndex.value + 1) % historyResult.value.length
+ scrollToHighlightedHistoryItem()
+ }
+ setTimeout(() => {
+ isKeyboardNavigating.value = false
+ }, 100)
+ }
+ const scrollToHighlightedItem = () => {
+ nextTick(() => {
+ if (!searchResultScrollbar.value || !searchResult.value.length) return
+ const scrollWrapper = searchResultScrollbar.value.wrapRef
+ if (!scrollWrapper) return
+ const highlightedElements = scrollWrapper.querySelectorAll('.result .box')
+ if (!highlightedElements[highlightedIndex.value]) return
+ const highlightedElement = highlightedElements[highlightedIndex.value]
+ const itemHeight = highlightedElement.offsetHeight
+ const scrollTop = scrollWrapper.scrollTop
+ const containerHeight = scrollWrapper.clientHeight
+ const itemTop = highlightedElement.offsetTop
+ const itemBottom = itemTop + itemHeight
+ if (itemTop < scrollTop) {
+ searchResultScrollbar.value.setScrollTop(itemTop)
+ } else if (itemBottom > scrollTop + containerHeight) {
+ searchResultScrollbar.value.setScrollTop(itemBottom - containerHeight)
+ }
+ })
+ }
+ const scrollToHighlightedHistoryItem = () => {
+ nextTick(() => {
+ if (!searchResultScrollbar.value || !historyResult.value.length) return
+ const scrollWrapper = searchResultScrollbar.value.wrapRef
+ if (!scrollWrapper) return
+ const historyItems = scrollWrapper.querySelectorAll('.history-result .box')
+ if (!historyItems[historyHIndex.value]) return
+ const highlightedElement = historyItems[historyHIndex.value]
+ const itemHeight = highlightedElement.offsetHeight
+ const scrollTop = scrollWrapper.scrollTop
+ const containerHeight = scrollWrapper.clientHeight
+ const itemTop = highlightedElement.offsetTop
+ const itemBottom = itemTop + itemHeight
+ if (itemTop < scrollTop) {
+ searchResultScrollbar.value.setScrollTop(itemTop)
+ } else if (itemBottom > scrollTop + containerHeight) {
+ searchResultScrollbar.value.setScrollTop(itemBottom - containerHeight)
+ }
+ })
+ }
+ const selectHighlighted = () => {
+ if (searchVal.value && searchResult.value.length) {
+ searchGoPage(searchResult.value[highlightedIndex.value])
+ } else if (!searchVal.value && historyResult.value.length) {
+ searchGoPage(historyResult.value[historyHIndex.value])
+ }
+ }
+ const isHighlighted = (index) => {
+ return highlightedIndex.value === index
+ }
+ const searchBlur = () => {
+ highlightedIndex.value = 0
+ }
+ const searchGoPage = (item) => {
+ showSearchDialog.value = false
+ addHistory(item)
+ handleMenuJump(item)
+ searchVal.value = ''
+ searchResult.value = []
+ }
+ const updateHistory = () => {
+ if (Array.isArray(historyResult.value)) {
+ userStore.setSearchHistory(historyResult.value)
+ }
+ }
+ const addHistory = (item) => {
+ const itemKey = item.path || String(item.meta.link || '')
+ const hasItemIndex = historyResult.value.findIndex(
+ (historyItem) => (historyItem.path || String(historyItem.meta.link || '')) === itemKey
+ )
+ if (hasItemIndex !== -1) {
+ historyResult.value.splice(hasItemIndex, 1)
+ } else if (historyResult.value.length >= historyMaxLength) {
+ historyResult.value.pop()
+ }
+ const cleanedItem = { ...item }
+ delete cleanedItem.children
+ delete cleanedItem.meta.authList
+ historyResult.value.unshift(cleanedItem)
+ updateHistory()
+ }
+ const deleteHistory = (index) => {
+ historyResult.value.splice(index, 1)
+ updateHistory()
+ }
+ const openSearchDialog = () => {
+ showSearchDialog.value = true
+ focusInput()
+ }
+ const closeSearchDialog = () => {
+ searchVal.value = ''
+ searchResult.value = []
+ highlightedIndex.value = 0
+ historyHIndex.value = 0
+ }
+ const highlightOnHover = (index) => {
+ if (!isKeyboardNavigating.value && searchVal.value) {
+ highlightedIndex.value = index
+ }
+ }
+ const highlightOnHoverHistory = (index) => {
+ if (!isKeyboardNavigating.value && !searchVal.value) {
+ historyHIndex.value = index
+ }
+ }
+</script>
+<style lang="scss" scoped>
+ .layout-search {
+ :deep(.search-modal) {
+ background-color: rgb(0 0 0 / 20%);
+ }
+
+ :deep(.el-dialog__body) {
+ padding: 5px 0 0 !important;
+ }
+
+ :deep(.el-dialog__header) {
+ padding: 0;
+ }
+
+ .el-input {
+ :deep(.el-input__wrapper) {
+ background-color: var(--art-gray-200);
+ border: 1px solid var(--default-border-dashed);
+ border-radius: calc(var(--custom-radius) / 2 + 2px) !important;
+ box-shadow: none;
+ }
+
+ :deep(.el-input__inner) {
+ color: var(--art-gray-800) !important;
+ }
+ }
+ }
+
+ .dark .layout-search {
+ .el-input {
+ :deep(.el-input__wrapper) {
+ background-color: #333;
+ border: 1px solid #4c4d50;
+ }
+ }
+
+ :deep(.search-modal) {
+ background-color: rgb(23 23 26 / 60%);
+ backdrop-filter: none;
+ }
+
+ :deep(.el-dialog) {
+ background-color: #252526;
+ }
+ }
+</style>
+
+<style scoped>
+ @reference '@styles/core/tailwind.css';
+
+ .keyboard {
+ @apply mr-2
+ box-border
+ h-5
+ w-5.5
+ rounded
+ border
+ border-g-400
+ px-1
+ text-g-500
+ shadow-[0_2px_0_var(--default-border-dashed)]
+ last-of-type:mr-1.5;
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-header-bar/index.vue b/rsf-design/src/components/core/layouts/art-header-bar/index.vue
new file mode 100644
index 0000000..a7e1a6b
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-header-bar/index.vue
@@ -0,0 +1,416 @@
+<!-- 椤堕儴鏍� -->
+<template>
+ <div
+ class="w-full bg-[var(--default-bg-color)]"
+ :class="[
+ tabStyle === 'tab-card' || tabStyle === 'tab-google' ? 'mb-5 max-sm:mb-3 !bg-box' : ''
+ ]"
+ >
+ <div
+ class="relative box-border flex-b h-15 leading-15 select-none"
+ :class="[
+ tabStyle === 'tab-card' || tabStyle === 'tab-google'
+ ? 'border-b border-[var(--art-card-border)]'
+ : ''
+ ]"
+ >
+ <div class="flex-c flex-1 min-w-0 leading-15" style="display: flex">
+ <!-- 绯荤粺淇℃伅 -->
+ <div class="flex-c c-p" @click="toHome" v-if="isTopMenu">
+ <ArtLogo class="pl-4.5" />
+ <p v-if="width >= 1400" class="my-0 mx-2 ml-2 text-lg">{{ AppConfig.systemInfo.name }}</p>
+ </div>
+
+ <ArtLogo
+ class="!hidden pl-3.5 overflow-hidden align-[-0.15em] fill-current"
+ @click="toHome"
+ />
+
+ <!-- 鑿滃崟鎸夐挳 -->
+ <ArtIconButton
+ v-if="isLeftMenu && shouldShowMenuButton"
+ icon="ri:menu-2-fill"
+ class="ml-3 max-sm:ml-[7px]"
+ @click="visibleMenu"
+ />
+
+ <!-- 鍒锋柊鎸夐挳 -->
+ <ArtIconButton
+ v-if="shouldShowRefreshButton"
+ icon="ri:refresh-line"
+ class="!ml-3 refresh-btn max-sm:!hidden"
+ :style="{ marginLeft: !isLeftMenu ? '10px' : '0' }"
+ @click="reload"
+ />
+
+ <!-- 蹇�熷叆鍙� -->
+ <ArtFastEnter v-if="shouldShowFastEnter && width >= headerBarFastEnterMinWidth">
+ <ArtIconButton icon="ri:function-line" class="ml-3" />
+ </ArtFastEnter>
+
+ <!-- 闈㈠寘灞� -->
+ <ArtBreadcrumb
+ v-if="(shouldShowBreadcrumb && isLeftMenu) || (shouldShowBreadcrumb && isDualMenu)"
+ />
+
+ <!-- 椤堕儴鑿滃崟 -->
+ <ArtHorizontalMenu v-if="isTopMenu" :list="menuList" />
+
+ <!-- 娣峰悎鑿滃崟-椤堕儴 -->
+ <ArtMixedMenu v-if="isTopLeftMenu" :list="menuList" />
+ </div>
+
+ <div class="flex-c gap-2.5">
+ <!-- 鎼滅储 -->
+ <div
+ v-if="shouldShowGlobalSearch"
+ class="flex-cb w-40 h-9 px-2.5 c-p border border-g-400 rounded-custom-sm max-md:!hidden"
+ @click="openSearchDialog"
+ >
+ <div class="flex-c">
+ <ArtSvgIcon icon="ri:search-line" class="text-sm text-g-500" />
+ <span class="ml-1 text-xs font-normal text-g-500">{{ $t('topBar.search.title') }}</span>
+ </div>
+ <div class="flex-c h-5 px-1.5 text-g-500/80 border border-g-400 rounded">
+ <ArtSvgIcon v-if="isWindows" icon="vaadin:ctrl-a" class="text-sm" />
+ <ArtSvgIcon v-else icon="ri:command-fill" class="text-xs" />
+ <span class="ml-0.5 text-xs">k</span>
+ </div>
+ </div>
+
+ <!-- 鍏ㄥ睆鎸夐挳 -->
+ <ArtIconButton
+ v-if="shouldShowFullscreen"
+ :icon="isFullscreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-fill'"
+ :class="[!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn', 'ml-3']"
+ class="max-md:!hidden"
+ @click="toggleFullScreen"
+ />
+
+ <!-- 鍥介檯鍖栨寜閽� -->
+ <ElDropdown
+ @command="changeLanguage"
+ popper-class="langDropDownStyle"
+ v-if="shouldShowLanguage"
+ >
+ <ArtIconButton icon="ri:translate-2" class="language-btn text-[19px]" />
+ <template #dropdown>
+ <ElDropdownMenu>
+ <div v-for="item in languageOptions" :key="item.value" class="lang-btn-item">
+ <ElDropdownItem
+ :command="item.value"
+ :class="{ 'is-selected': locale === item.value }"
+ >
+ <span class="menu-txt">{{ item.label }}</span>
+ <ArtSvgIcon icon="ri:check-fill" v-if="locale === item.value" />
+ </ElDropdownItem>
+ </div>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+
+ <!-- 閫氱煡鎸夐挳 -->
+ <ArtIconButton
+ v-if="shouldShowNotification"
+ icon="ri:notification-2-line"
+ class="notice-button relative"
+ @click="visibleNotice"
+ >
+ <div class="absolute top-2 right-2 size-1.5 !bg-danger rounded-full"></div>
+ </ArtIconButton>
+
+ <!-- 鑱婂ぉ鎸夐挳 -->
+ <ArtIconButton
+ v-if="shouldShowChat"
+ icon="ri:message-3-line"
+ class="chat-button relative"
+ @click="openChat"
+ >
+ <div class="breathing-dot absolute top-2 right-2 size-1.5 !bg-success rounded-full"></div>
+ </ArtIconButton>
+
+ <!-- 璁剧疆鎸夐挳 -->
+ <div v-if="shouldShowSettings">
+ <ElPopover :visible="showSettingGuide" placement="bottom-start" :width="190" :offset="0">
+ <template #reference>
+ <div class="flex-cc">
+ <ArtIconButton icon="ri:settings-line" class="setting-btn" @click="openSetting" />
+ </div>
+ </template>
+ <template #default>
+ <p
+ >{{ $t('topBar.guide.title')
+ }}<span :style="{ color: systemThemeColor }"> {{ $t('topBar.guide.theme') }} </span
+ >銆� <span :style="{ color: systemThemeColor }"> {{ $t('topBar.guide.menu') }} </span
+ >{{ $t('topBar.guide.description') }}
+ </p>
+ </template>
+ </ElPopover>
+ </div>
+
+ <!-- 涓婚鍒囨崲鎸夐挳 -->
+ <ArtIconButton
+ v-if="shouldShowThemeToggle"
+ @click="themeAnimation"
+ :icon="isDark ? 'ri:sun-fill' : 'ri:moon-line'"
+ />
+
+ <!-- 鐢ㄦ埛澶村儚銆佽彍鍗� -->
+ <ArtUserMenu />
+ </div>
+ </div>
+
+ <!-- 鏍囩椤� -->
+ <ArtWorkTab />
+
+ <!-- 閫氱煡 -->
+ <ArtNotification v-model:value="showNotice" ref="notice" />
+ </div>
+</template>
+
+<script setup>
+ import { languageOptions } from '@/locales'
+
+ import { themeAnimation } from '@/utils/ui/animation'
+
+ import { useI18n } from 'vue-i18n'
+ import { useRouter } from 'vue-router'
+ import { useFullscreen, useWindowSize } from '@vueuse/core'
+ import { MenuTypeEnum } from '@/enums/appEnum'
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useUserStore } from '@/store/modules/user'
+ import { useMenuStore } from '@/store/modules/menu'
+ import { mittBus } from '@/utils/sys'
+ import { useCommon } from '@/hooks/core/useCommon'
+ import { useHeaderBar } from '@/hooks/core/useHeaderBar'
+ defineOptions({ name: 'ArtHeaderBar' })
+ const isWindows = navigator.userAgent.includes('Windows')
+ const router = useRouter()
+ const { locale } = useI18n()
+ const { width } = useWindowSize()
+ const settingStore = useSettingStore()
+ const userStore = useUserStore()
+ const menuStore = useMenuStore()
+ const {
+ shouldShowMenuButton,
+ shouldShowRefreshButton,
+ shouldShowFastEnter,
+ shouldShowBreadcrumb,
+ shouldShowGlobalSearch,
+ shouldShowFullscreen,
+ shouldShowNotification,
+ shouldShowChat,
+ shouldShowLanguage,
+ shouldShowSettings,
+ shouldShowThemeToggle,
+ fastEnterMinWidth: headerBarFastEnterMinWidth
+ } = useHeaderBar()
+ const { menuOpen, systemThemeColor, showSettingGuide, menuType, isDark, tabStyle } =
+ storeToRefs(settingStore)
+ const { language } = storeToRefs(userStore)
+ const { menuList } = storeToRefs(menuStore)
+ const showNotice = ref(false)
+ const notice = ref(null)
+ const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
+ const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
+ const isTopMenu = computed(() => menuType.value === MenuTypeEnum.TOP)
+ const isTopLeftMenu = computed(() => menuType.value === MenuTypeEnum.TOP_LEFT)
+ const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
+ onMounted(() => {
+ initLanguage()
+ document.addEventListener('click', bodyCloseNotice)
+ })
+ onUnmounted(() => {
+ document.removeEventListener('click', bodyCloseNotice)
+ })
+ const toggleFullScreen = () => {
+ toggleFullscreen()
+ }
+ const visibleMenu = () => {
+ settingStore.setMenuOpen(!menuOpen.value)
+ }
+ const { homePath } = useCommon()
+ const { refresh } = useCommon()
+ const toHome = () => {
+ router.push(homePath.value)
+ }
+ const reload = (time = 0) => {
+ setTimeout(() => {
+ refresh()
+ }, time)
+ }
+ const initLanguage = () => {
+ locale.value = language.value
+ }
+ const changeLanguage = (lang) => {
+ if (locale.value === lang) return
+ locale.value = lang
+ userStore.setLanguage(lang)
+ reload(50)
+ }
+ const openSetting = () => {
+ mittBus.emit('openSetting')
+ if (showSettingGuide.value) {
+ settingStore.hideSettingGuide()
+ }
+ }
+ const openSearchDialog = () => {
+ mittBus.emit('openSearchDialog')
+ }
+ const bodyCloseNotice = (e) => {
+ if (!showNotice.value) return
+ const target = e.target
+ const isNoticeButton = target.closest('.notice-button')
+ const isNoticePanel = target.closest('.art-notification-panel')
+ if (!isNoticeButton && !isNoticePanel) {
+ showNotice.value = false
+ }
+ }
+ const visibleNotice = () => {
+ showNotice.value = !showNotice.value
+ }
+ const openChat = () => {
+ mittBus.emit('openChat')
+ }
+</script>
+
+<style lang="scss" scoped>
+ /* Custom animations */
+ @keyframes rotate180 {
+ 0% {
+ transform: rotate(0);
+ }
+
+ 100% {
+ transform: rotate(180deg);
+ }
+ }
+
+ @keyframes shake {
+ 0% {
+ transform: rotate(0);
+ }
+
+ 25% {
+ transform: rotate(-5deg);
+ }
+
+ 50% {
+ transform: rotate(5deg);
+ }
+
+ 75% {
+ transform: rotate(-5deg);
+ }
+
+ 100% {
+ transform: rotate(0);
+ }
+ }
+
+ @keyframes expand {
+ 0% {
+ transform: scale(1);
+ }
+
+ 50% {
+ transform: scale(1.1);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+ }
+
+ @keyframes shrink {
+ 0% {
+ transform: scale(1);
+ }
+
+ 50% {
+ transform: scale(0.9);
+ }
+
+ 100% {
+ transform: scale(1);
+ }
+ }
+
+ @keyframes moveUp {
+ 0% {
+ transform: translateY(0);
+ }
+
+ 50% {
+ transform: translateY(-3px);
+ }
+
+ 100% {
+ transform: translateY(0);
+ }
+ }
+
+ @keyframes breathing {
+ 0% {
+ opacity: 0.4;
+ transform: scale(0.9);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: scale(1.1);
+ }
+
+ 100% {
+ opacity: 0.4;
+ transform: scale(0.9);
+ }
+ }
+
+ /* Hover animation classes */
+ .refresh-btn:hover :deep(.art-svg-icon) {
+ animation: rotate180 0.5s;
+ }
+
+ .language-btn:hover :deep(.art-svg-icon) {
+ animation: moveUp 0.4s;
+ }
+
+ .setting-btn:hover :deep(.art-svg-icon) {
+ animation: rotate180 0.5s;
+ }
+
+ .full-screen-btn:hover :deep(.art-svg-icon) {
+ animation: expand 0.6s forwards;
+ }
+
+ .exit-full-screen-btn:hover :deep(.art-svg-icon) {
+ animation: shrink 0.6s forwards;
+ }
+
+ .notice-button:hover :deep(.art-svg-icon) {
+ animation: shake 0.5s ease-in-out;
+ }
+
+ .chat-button:hover :deep(.art-svg-icon) {
+ animation: shake 0.5s ease-in-out;
+ }
+
+ /* Breathing animation for chat dot */
+ .breathing-dot {
+ animation: breathing 1.5s ease-in-out infinite;
+ }
+
+ /* iPad breakpoint adjustments */
+ @media screen and (width <= 768px) {
+ .logo2 {
+ display: block !important;
+ }
+ }
+
+ @media screen and (width <= 640px) {
+ .btn-box {
+ width: 40px;
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue b/rsf-design/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue
new file mode 100644
index 0000000..6efbeec
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue
@@ -0,0 +1,139 @@
+<!-- 鐢ㄦ埛鑿滃崟 -->
+<template>
+ <ElPopover
+ ref="userMenuPopover"
+ placement="bottom-end"
+ :width="240"
+ :hide-after="0"
+ :offset="10"
+ trigger="hover"
+ :show-arrow="false"
+ popper-class="user-menu-popover"
+ popper-style="padding: 5px 16px;"
+ >
+ <template #reference>
+ <img
+ class="size-8.5 mr-5 c-p rounded-full max-sm:w-6.5 max-sm:h-6.5 max-sm:mr-[16px]"
+ src="@imgs/user/avatar.webp"
+ alt="avatar"
+ />
+ </template>
+ <template #default>
+ <div class="pt-3">
+ <div class="flex-c pb-1 px-0">
+ <img
+ class="w-10 h-10 mr-3 ml-0 overflow-hidden rounded-full float-left"
+ src="@imgs/user/avatar.webp"
+ />
+ <div class="w-[calc(100%-60px)] h-full">
+ <span class="block text-sm font-medium text-g-800 truncate">{{
+ userInfo.userName
+ }}</span>
+ <span class="block mt-0.5 text-xs text-g-500 truncate">{{ userInfo.email }}</span>
+ </div>
+ </div>
+ <ul class="py-4 mt-3 border-t border-g-300/80">
+ <li class="btn-item" @click="goPage('/system/user-center')">
+ <ArtSvgIcon icon="ri:user-3-line" />
+ <span>{{ $t('topBar.user.userCenter') }}</span>
+ </li>
+ <li class="btn-item" @click="toDocs()">
+ <ArtSvgIcon icon="ri:book-2-line" />
+ <span>{{ $t('topBar.user.docs') }}</span>
+ </li>
+ <li class="btn-item" @click="toGithub()">
+ <ArtSvgIcon icon="ri:github-line" />
+ <span>{{ $t('topBar.user.github') }}</span>
+ </li>
+ <li class="btn-item" @click="lockScreen()">
+ <ArtSvgIcon icon="ri:lock-line" />
+ <span>{{ $t('topBar.user.lockScreen') }}</span>
+ </li>
+ <div class="w-full h-px my-2 bg-g-300/80"></div>
+ <div class="log-out c-p" @click="loginOut">
+ {{ $t('topBar.user.logout') }}
+ </div>
+ </ul>
+ </div>
+ </template>
+ </ElPopover>
+</template>
+
+<script setup>
+ import { useI18n } from 'vue-i18n'
+ import { useRouter } from 'vue-router'
+ import { ElMessageBox } from 'element-plus'
+ import { useUserStore } from '@/store/modules/user'
+ import { WEB_LINKS } from '@/utils/constants'
+ import { mittBus } from '@/utils/sys'
+ defineOptions({ name: 'ArtUserMenu' })
+ const router = useRouter()
+ const { t } = useI18n()
+ const userStore = useUserStore()
+ const { getUserInfo: userInfo } = storeToRefs(userStore)
+ const userMenuPopover = ref()
+ const goPage = (path) => {
+ router.push(path)
+ }
+ const toDocs = () => {
+ window.open(WEB_LINKS.DOCS)
+ }
+ const toGithub = () => {
+ window.open(WEB_LINKS.GITHUB)
+ }
+ const lockScreen = () => {
+ mittBus.emit('openLockScreen')
+ }
+ const loginOut = () => {
+ closeUserMenu()
+ setTimeout(() => {
+ ElMessageBox.confirm(t('common.logOutTips'), t('common.tips'), {
+ confirmButtonText: t('common.confirm'),
+ cancelButtonText: t('common.cancel'),
+ customClass: 'login-out-dialog'
+ }).then(() => {
+ userStore.logOut()
+ })
+ }, 200)
+ }
+ const closeUserMenu = () => {
+ setTimeout(() => {
+ userMenuPopover.value.hide()
+ }, 100)
+ }
+</script>
+
+<style scoped>
+ @reference '@styles/core/tailwind.css';
+
+ @layer components {
+ .btn-item {
+ @apply flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0;
+
+ span {
+ @apply text-sm;
+ }
+
+ .art-svg-icon {
+ @apply mr-2 text-base;
+ }
+
+ &:hover {
+ background-color: var(--art-gray-200);
+ }
+ }
+ }
+
+ .log-out {
+ @apply py-1.5
+ mt-5
+ text-xs
+ text-center
+ border
+ border-g-400
+ rounded-md
+ transition-all
+ duration-200
+ hover:shadow-xl;
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue b/rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue
new file mode 100644
index 0000000..7dc112c
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue
@@ -0,0 +1,76 @@
+<!-- 姘村钩鑿滃崟 -->
+<template>
+ <div class="flex-1 overflow-hidden">
+ <ElMenu
+ :ellipsis="true"
+ mode="horizontal"
+ :default-active="routerPath"
+ :text-color="isDark ? 'var(--art-gray-800)' : 'var(--art-gray-700)'"
+ :popper-offset="-6"
+ background-color="transparent"
+ :show-timeout="50"
+ :hide-timeout="50"
+ popper-class="horizontal-menu-popper"
+ class="w-full border-none"
+ >
+ <HorizontalSubmenu
+ v-for="item in filteredMenuItems"
+ :key="item.path"
+ :item="item"
+ :isMobile="false"
+ :level="0"
+ />
+ </ElMenu>
+ </div>
+</template>
+
+<script setup>
+ import { useSettingStore } from '@/store/modules/setting'
+ defineOptions({ name: 'ArtHorizontalMenu' })
+ const settingStore = useSettingStore()
+ const { isDark } = storeToRefs(settingStore)
+ const route = useRoute()
+ const props = defineProps({
+ list: { required: false, default: () => [] }
+ })
+ const filteredMenuItems = computed(() => {
+ return filterMenuItems(props.list)
+ })
+ const routerPath = computed(() => String(route.meta.activePath || route.path))
+ const filterMenuItems = (items) => {
+ return items
+ .filter((item) => {
+ if (item.meta.isHide) {
+ return false
+ }
+ if (item.children && item.children.length > 0) {
+ const filteredChildren = filterMenuItems(item.children)
+ return filteredChildren.length > 0
+ }
+ return true
+ })
+ .map((item) => ({
+ ...item,
+ children: item.children ? filterMenuItems(item.children) : void 0
+ }))
+ }
+</script>
+
+<style scoped>
+ /* Remove el-menu bottom border */
+ :deep(.el-menu) {
+ border-bottom: none !important;
+ }
+
+ /* Remove default styles for first-level menu items */
+ :deep(.el-menu-item[tabindex='0']) {
+ background-color: transparent !important;
+ border: none !important;
+ }
+
+ /* Remove bottom border from submenu titles */
+ :deep(.el-menu--horizontal .el-sub-menu__title) {
+ padding: 0 30px 0 10px !important;
+ border: 0 !important;
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue b/rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue
new file mode 100644
index 0000000..2093d01
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue
@@ -0,0 +1,94 @@
+<template>
+ <ElSubMenu v-if="hasChildren" :index="item.path || item.meta.title" class="!p-0">
+ <template #title>
+ <ArtSvgIcon :icon="item.meta.icon" :color="theme?.iconColor" class="mr-1 text-lg" />
+ <span class="text-md">{{ formatMenuTitle(item.meta.title) }}</span>
+ <div v-if="item.meta.showBadge" class="art-badge art-badge-horizontal" />
+ <div v-if="item.meta.showTextBadge" class="art-text-badge">
+ {{ item.meta.showTextBadge }}
+ </div>
+ </template>
+
+ <!-- 閫掑綊璋冪敤鑷韩澶勭悊瀛愯彍鍗� -->
+ <HorizontalSubmenu
+ v-for="child in filteredChildren"
+ :key="child.path"
+ :item="child"
+ :theme="theme"
+ :is-mobile="isMobile"
+ :level="level + 1"
+ @close="closeMenu"
+ />
+ </ElSubMenu>
+
+ <ElMenuItem
+ v-else-if="isNavigableRoute"
+ :index="item.path || item.meta.title"
+ @click="goPage(item)"
+ >
+ <ArtSvgIcon
+ :icon="item.meta.icon"
+ :color="theme?.iconColor"
+ class="mr-1 text-lg"
+ :style="{ color: theme.iconColor }"
+ />
+ <span class="text-md">{{ formatMenuTitle(item.meta.title) }}</span>
+ <div
+ v-if="item.meta.showBadge"
+ class="art-badge"
+ :style="{ right: level === 0 ? '10px' : '20px' }"
+ />
+ <div v-if="item.meta.showTextBadge && level !== 0" class="art-text-badge">
+ {{ item.meta.showTextBadge }}
+ </div>
+ </ElMenuItem>
+</template>
+
+<script setup>
+ import { computed } from 'vue'
+ import { handleMenuJump } from '@/utils/navigation'
+ const props = defineProps({
+ item: {
+ type: Object,
+ required: true
+ },
+ theme: {
+ type: Object,
+ default: () => ({})
+ },
+ isMobile: Boolean,
+ level: {
+ type: Number,
+ default: 0
+ }
+ })
+ const emit = defineEmits(['close'])
+ const filteredChildren = computed(() => {
+ return props.item.children?.filter((child) => !child.meta.isHide) || []
+ })
+ const isNavigableRoute = computed(() => {
+ return !!(
+ !props.item.meta.isHide &&
+ ((props.item.path && props.item.path.trim()) ||
+ props.item.meta.link ||
+ props.item.meta.isIframe === true) &&
+ (props.item.component || props.item.meta.link || props.item.meta.isIframe === true)
+ )
+ })
+ const hasChildren = computed(() => {
+ return filteredChildren.value.length > 0
+ })
+ const goPage = (item) => {
+ closeMenu()
+ handleMenuJump(item)
+ }
+ const closeMenu = () => {
+ emit('close')
+ }
+</script>
+
+<style scoped>
+ :deep(.el-sub-menu__title .el-sub-menu__icon-arrow) {
+ right: 10px !important;
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-menus/art-mixed-menu/index.vue b/rsf-design/src/components/core/layouts/art-menus/art-mixed-menu/index.vue
new file mode 100644
index 0000000..67888c2
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-menus/art-mixed-menu/index.vue
@@ -0,0 +1,195 @@
+<!-- 娣峰悎鑿滃崟 -->
+<template>
+ <div class="relative box-border flex-c w-full overflow-hidden">
+ <!-- 宸︿晶婊氬姩鎸夐挳 -->
+ <div v-show="showLeftArrow" class="button-arrow" @click="scroll('left')">
+ <ElIcon>
+ <ArrowLeft />
+ </ElIcon>
+ </div>
+
+ <!-- 婊氬姩瀹瑰櫒 -->
+ <ElScrollbar
+ ref="scrollbarRef"
+ wrap-class="scrollbar-wrapper"
+ :horizontal="true"
+ @scroll="handleScroll"
+ @wheel="handleWheel"
+ >
+ <div class="box-border flex-c flex-shrink-0 flex-nowrap h-15 whitespace-nowrap">
+ <template v-for="item in processedMenuList" :key="item.meta.title">
+ <div
+ v-if="!item.meta.isHide"
+ class="menu-item relative flex-shrink-0 h-10 px-3 text-sm flex-c c-p hover:text-theme"
+ :class="{
+ 'menu-item-active text-theme': item.isActive
+ }"
+ @click="handleMenuJump(item, true)"
+ >
+ <ArtSvgIcon
+ :icon="item.meta.icon"
+ class="text-lg text-g-700 dark:text-g-800 mr-1"
+ :class="item.isActive && '!text-theme'"
+ />
+ <span
+ class="text-md text-g-700 dark:text-g-800"
+ :class="item.isActive && '!text-theme'"
+ >
+ {{ item.formattedTitle }}
+ </span>
+ <div v-if="item.meta.showBadge" class="art-badge art-badge-mixed" />
+ </div>
+ </template>
+ </div>
+ </ElScrollbar>
+
+ <!-- 鍙充晶婊氬姩鎸夐挳 -->
+ <div v-show="showRightArrow" class="button-arrow right-2" @click="scroll('right')">
+ <ElIcon>
+ <ArrowRight />
+ </ElIcon>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { handleMenuJump } from '@/utils/navigation'
+
+ import { ref, computed, onMounted, nextTick } from 'vue'
+ import { useThrottleFn } from '@vueuse/core'
+ import { formatMenuTitle } from '@/utils/router'
+ defineOptions({ name: 'ArtMixedMenu' })
+ const route = useRoute()
+ const props = defineProps({
+ list: { required: false, default: () => [] }
+ })
+ const scrollbarRef = ref()
+ const showLeftArrow = ref(false)
+ const showRightArrow = ref(false)
+ const SCROLL_CONFIG = {
+ /** 鐐瑰嚮鎸夐挳鏃剁殑婊氬姩璺濈 */
+ BUTTON_SCROLL_DISTANCE: 200,
+ /** 榧犳爣婊氳疆蹇�熸粴鍔ㄦ椂鐨勬闀� */
+ WHEEL_FAST_STEP: 35,
+ /** 榧犳爣婊氳疆鎱㈤�熸粴鍔ㄦ椂鐨勬闀� */
+ WHEEL_SLOW_STEP: 30,
+ /** 鍖哄垎蹇參婊氬姩鐨勯槇鍊� */
+ WHEEL_FAST_THRESHOLD: 100
+ }
+ const currentActivePath = computed(() => {
+ return String(route.meta.activePath || route.path)
+ })
+ const isMenuItemActive = (item) => {
+ const activePath = currentActivePath.value
+ if (item.children?.length) {
+ return item.children.some((child) => {
+ if (child.children?.length) {
+ return isMenuItemActive(child)
+ }
+ return child.path === activePath
+ })
+ }
+ return item.path === activePath
+ }
+ const processedMenuList = computed(() => {
+ return props.list.map((item) => ({
+ ...item,
+ isActive: isMenuItemActive(item),
+ formattedTitle: formatMenuTitle(item.meta.title)
+ }))
+ })
+ const handleScrollCore = () => {
+ if (!scrollbarRef.value?.wrapRef) return
+ const { scrollLeft, scrollWidth, clientWidth } = scrollbarRef.value.wrapRef
+ showLeftArrow.value = scrollLeft > 0
+ showRightArrow.value = scrollLeft + clientWidth < scrollWidth
+ }
+ const handleScroll = useThrottleFn(handleScrollCore, 16)
+ const scroll = (direction) => {
+ if (!scrollbarRef.value?.wrapRef) return
+ const currentScroll = scrollbarRef.value.wrapRef.scrollLeft
+ const targetScroll =
+ direction === 'left'
+ ? currentScroll - SCROLL_CONFIG.BUTTON_SCROLL_DISTANCE
+ : currentScroll + SCROLL_CONFIG.BUTTON_SCROLL_DISTANCE
+ scrollbarRef.value.wrapRef.scrollTo({
+ left: targetScroll,
+ behavior: 'smooth'
+ })
+ }
+ const handleWheel = (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ if (!scrollbarRef.value?.wrapRef) return
+ const { wrapRef } = scrollbarRef.value
+ const { scrollLeft, scrollWidth, clientWidth } = wrapRef
+ const scrollStep =
+ Math.abs(event.deltaY) > SCROLL_CONFIG.WHEEL_FAST_THRESHOLD
+ ? SCROLL_CONFIG.WHEEL_FAST_STEP
+ : SCROLL_CONFIG.WHEEL_SLOW_STEP
+ const scrollDelta = event.deltaY > 0 ? scrollStep : -scrollStep
+ const targetScroll = Math.max(0, Math.min(scrollLeft + scrollDelta, scrollWidth - clientWidth))
+ wrapRef.scrollLeft = targetScroll
+ handleScrollCore()
+ }
+ const initScrollState = () => {
+ nextTick(() => {
+ handleScrollCore()
+ })
+ }
+ onMounted(initScrollState)
+</script>
+
+<style scoped>
+ @reference '@styles/core/tailwind.css';
+
+ .button-arrow {
+ @apply absolute
+ top-1/2
+ z-2
+ flex
+ items-center
+ justify-center
+ size-7.5
+ text-g-600
+ cursor-pointer
+ rounded
+ transition-all
+ duration-300
+ -translate-y-1/2
+ hover:text-g-900
+ hover:bg-g-200;
+ }
+</style>
+
+<style scoped>
+ :deep(.el-scrollbar__bar.is-horizontal) {
+ bottom: 5px;
+ display: none;
+ height: 2px;
+ }
+
+ :deep(.scrollbar-wrapper) {
+ flex: 1;
+ min-width: 0;
+ margin: 0 50px 0 30px;
+ }
+
+ .menu-item-active::after {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: 40px;
+ height: 2px;
+ margin: auto;
+ content: '';
+ background-color: var(--theme-color);
+ }
+
+ @media (width <= 1440px) {
+ :deep(.scrollbar-wrapper) {
+ margin: 0 45px;
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue b/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue
new file mode 100644
index 0000000..9832685
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue
@@ -0,0 +1,283 @@
+<!-- 宸︿晶鑿滃崟 鎴� 鍙屽垪鑿滃崟 -->
+<template>
+ <div
+ class="layout-sidebar"
+ v-if="showLeftMenu || isDualMenu"
+ :class="{ 'no-border': menuList.length === 0 }"
+ >
+ <!-- 鍙屽垪鑿滃崟锛堝乏渚э級 -->
+ <div
+ v-if="isDualMenu"
+ class="dual-menu-left"
+ :style="{ width: dualMenuShowText ? '80px' : '64px', background: getMenuTheme.background }"
+ >
+ <ArtLogo class="logo" @click="navigateToHome" />
+
+ <ElScrollbar style="height: calc(100% - 135px)">
+ <ul>
+ <li v-for="menu in firstLevelMenus" :key="menu.path" @click="handleMenuJump(menu, true)">
+ <ElTooltip
+ class="box-item"
+ effect="dark"
+ :content="$t(menu.meta.title)"
+ placement="right"
+ :offset="15"
+ :hide-after="0"
+ :disabled="dualMenuShowText"
+ >
+ <div
+ :class="{
+ 'is-active': menu.meta.isFirstLevel
+ ? menu.path === route.path
+ : menu.path === firstLevelMenuPath
+ }"
+ :style="{
+ height: dualMenuShowText ? '60px' : '46px'
+ }"
+ >
+ <ArtSvgIcon
+ class="menu-icon text-g-700 dark:text-g-800"
+ :icon="menu.meta.icon"
+ :style="{
+ marginBottom: dualMenuShowText ? '5px' : '0'
+ }"
+ />
+ <span v-if="dualMenuShowText" class="text-md text-g-700">
+ {{ $t(menu.meta.title) }}
+ </span>
+ <div v-if="menu.meta.showBadge" class="art-badge art-badge-dual" />
+ </div>
+ </ElTooltip>
+ </li>
+ </ul>
+ </ElScrollbar>
+
+ <ArtIconButton
+ class="switch-btn size-10"
+ icon="ri:arrow-left-right-fill"
+ @click="toggleDualMenuMode"
+ />
+ </div>
+
+ <!-- 宸︿晶鑿滃崟 || 鍙屽垪鑿滃崟锛堝彸渚э級 -->
+ <div
+ v-show="menuList.length > 0"
+ class="menu-left"
+ :class="`menu-left-${getMenuTheme.theme} menu-left-${!menuOpen ? 'close' : 'open'}`"
+ :style="{ background: getMenuTheme.background }"
+ >
+ <!-- Logo銆佺郴缁熷悕绉� -->
+ <div
+ class="header"
+ @click="navigateToHome"
+ :style="{
+ background: getMenuTheme.background
+ }"
+ >
+ <ArtLogo v-if="!isDualMenu" class="logo" />
+
+ <p
+ :class="{ 'is-dual-menu-name': isDualMenu }"
+ :style="{
+ color: getMenuTheme.systemNameColor,
+ opacity: !menuOpen ? 0 : 1
+ }"
+ >
+ {{ AppConfig.systemInfo.name }}
+ </p>
+ </div>
+ <ElScrollbar :style="scrollbarStyle">
+ <ElMenu
+ :class="'el-menu-' + getMenuTheme.theme"
+ :collapse="!menuOpen"
+ :default-active="routerPath"
+ :text-color="getMenuTheme.textColor"
+ :unique-opened="uniqueOpened"
+ :background-color="getMenuTheme.background"
+ :default-openeds="defaultOpenedMenus"
+ :popper-class="`menu-left-popper menu-left-${getMenuTheme.theme}-popper`"
+ :show-timeout="50"
+ :hide-timeout="50"
+ >
+ <SidebarSubmenu
+ :list="menuList"
+ :isMobile="isMobileMode"
+ :theme="getMenuTheme"
+ @close="handleMenuClose"
+ />
+ </ElMenu>
+ </ElScrollbar>
+
+ <!-- 鍙屽垪鑿滃崟鍙充晶鎶樺彔鎸夐挳 -->
+ <div class="dual-menu-collapse-btn" v-if="isDualMenu" @click="toggleMenuVisibility">
+ <ArtSvgIcon
+ class="text-g-500/70"
+ :icon="menuOpen ? 'ri:arrow-left-wide-fill' : 'ri:arrow-right-wide-fill'"
+ />
+ </div>
+
+ <div
+ class="menu-model"
+ @click="toggleMenuVisibility"
+ :style="{
+ opacity: !menuOpen ? 0 : 1,
+ transform: showMobileModal ? 'scale(1)' : 'scale(0)'
+ }"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import AppConfig from '@/config'
+ import { handleMenuJump } from '@/utils/navigation'
+
+ import SidebarSubmenu from './widget/SidebarSubmenu.vue'
+
+ import { useSettingStore } from '@/store/modules/setting'
+ import { MenuTypeEnum, MenuWidth } from '@/enums/appEnum'
+ import { useMenuStore } from '@/store/modules/menu'
+ import { isIframe } from '@/utils/navigation'
+ import { useCommon } from '@/hooks/core/useCommon'
+ import { useWindowSize, useTimeoutFn } from '@vueuse/core'
+ defineOptions({ name: 'ArtSidebarMenu' })
+ const MOBILE_BREAKPOINT = 800
+ const ANIMATION_DELAY = 350
+ const MENU_CLOSE_WIDTH = MenuWidth.CLOSE
+ const route = useRoute()
+ const router = useRouter()
+ const settingStore = useSettingStore()
+ const { getMenuOpenWidth, menuType, uniqueOpened, dualMenuShowText, menuOpen, getMenuTheme } =
+ storeToRefs(settingStore)
+ const defaultOpenedMenus = ref([])
+ const isMobileMode = ref(false)
+ const showMobileModal = ref(false)
+ const { width } = useWindowSize()
+ const menuopenwidth = computed(() => getMenuOpenWidth.value)
+ const menuclosewidth = computed(() => MENU_CLOSE_WIDTH)
+ const isTopLeftMenu = computed(() => menuType.value === MenuTypeEnum.TOP_LEFT)
+ const showLeftMenu = computed(
+ () => menuType.value === MenuTypeEnum.LEFT || menuType.value === MenuTypeEnum.TOP_LEFT
+ )
+ const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
+ const isMobileScreen = computed(() => width.value < MOBILE_BREAKPOINT)
+ const firstLevelMenuPath = computed(() => route.matched[0]?.path)
+ const routerPath = computed(() => String(route.meta.activePath || route.path))
+ const firstLevelMenus = computed(() => {
+ return useMenuStore().menuList.filter((menu) => !menu.meta.isHide)
+ })
+ const menuList = computed(() => {
+ const menuStore = useMenuStore()
+ const allMenus = menuStore.menuList
+ if (!isTopLeftMenu.value && !isDualMenu.value) {
+ return allMenus
+ }
+ if (isIframe(route.path)) {
+ return findIframeMenuList(route.path, allMenus)
+ }
+ if (route.meta.isFirstLevel) {
+ return []
+ }
+ const currentTopPath = `/${route.path.split('/')[1]}`
+ const currentMenu = allMenus.find((menu) => menu.path === currentTopPath)
+ return currentMenu?.children ?? []
+ })
+ const scrollbarStyle = computed(() => {
+ const isCollapsed = isDualMenu.value && !menuOpen.value
+ return {
+ transform: isCollapsed ? 'translateY(-50px)' : 'translateY(0)',
+ height: isCollapsed ? 'calc(100% + 50px)' : 'calc(100% - 60px)',
+ transition: 'transform 0.3s ease'
+ }
+ })
+ const { start: delayHideMobileModal } = useTimeoutFn(
+ () => {
+ showMobileModal.value = false
+ },
+ ANIMATION_DELAY,
+ { immediate: false }
+ )
+ const findIframeMenuList = (currentPath, menuList2) => {
+ const hasPath = (items) => {
+ for (const item of items) {
+ if (item.path === currentPath) {
+ return true
+ }
+ if (item.children && hasPath(item.children)) {
+ return true
+ }
+ }
+ return false
+ }
+ for (const menu of menuList2) {
+ if (menu.children && hasPath(menu.children)) {
+ return menu.children
+ }
+ }
+ return []
+ }
+ const { homePath } = useCommon()
+ const navigateToHome = () => {
+ router.push(homePath.value)
+ }
+ const toggleMenuVisibility = () => {
+ settingStore.setMenuOpen(!menuOpen.value)
+ if (isMobileScreen.value) {
+ if (!menuOpen.value) {
+ showMobileModal.value = true
+ } else {
+ delayHideMobileModal()
+ }
+ }
+ }
+ const handleMenuClose = () => {
+ if (isMobileScreen.value) {
+ settingStore.setMenuOpen(false)
+ delayHideMobileModal()
+ }
+ }
+ const toggleDualMenuMode = () => {
+ settingStore.setDualMenuShowText(!dualMenuShowText.value)
+ }
+ watch(width, (newWidth) => {
+ if (newWidth < MOBILE_BREAKPOINT) {
+ settingStore.setMenuOpen(false)
+ if (!menuOpen.value) {
+ showMobileModal.value = false
+ }
+ } else {
+ showMobileModal.value = false
+ }
+ })
+ watch(menuOpen, (isMenuOpen) => {
+ if (!isMobileScreen.value) {
+ showMobileModal.value = false
+ } else {
+ if (isMenuOpen) {
+ showMobileModal.value = true
+ } else {
+ delayHideMobileModal()
+ }
+ }
+ })
+</script>
+
+<style lang="scss" scoped>
+ @use './style';
+</style>
+
+<style lang="scss">
+ @use './theme';
+
+ .layout-sidebar {
+ // 灞曞紑鐨勫搴�
+ .el-menu:not(.el-menu--collapse) {
+ width: v-bind(menuopenwidth);
+ }
+
+ // 鎶樺彔鍚庡搴�
+ .el-menu--collapse {
+ width: v-bind(menuclosewidth);
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss b/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss
new file mode 100644
index 0000000..8cccdd9
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss
@@ -0,0 +1,251 @@
+.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;
+ // 闃叉鑿滃崟鍐呯殑婊氬姩褰卞搷鏁翠釜椤甸潰婊氬姩
+ 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;
+ }
+
+ // 鎶樺彔鐘舵�佷笅鐨刪eader鏍峰紡
+ .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/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss b/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss
new file mode 100644
index 0000000..7626c42
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue b/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue
new file mode 100644
index 0000000..07f4dcf
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue
@@ -0,0 +1,120 @@
+<template>
+ <template v-for="(item, index) in filteredMenuItems" :key="getUniqueKey(item, index)">
+ <ElSubMenu v-if="hasChildren(item)" :index="item.path || item.meta.title" :level="level">
+ <template #title>
+ <div class="menu-icon flex-cc">
+ <ArtSvgIcon
+ :icon="item.meta.icon"
+ :color="theme?.iconColor"
+ :style="{ color: theme.iconColor }"
+ />
+ </div>
+ <span class="menu-name">
+ {{ formatMenuTitle(item.meta.title) }}
+ </span>
+ <div v-if="item.meta.showBadge" class="art-badge" style="right: 10px" />
+ </template>
+
+ <SidebarSubmenu
+ :list="item.children"
+ :is-mobile="isMobile"
+ :level="level + 1"
+ :theme="theme"
+ @close="closeMenu"
+ />
+ </ElSubMenu>
+
+ <ElMenuItem
+ v-else
+ :index="isExternalLink(item) ? undefined : item.path || item.meta.title"
+ :level-item="level + 1"
+ @click="goPage(item)"
+ >
+ <div class="menu-icon flex-cc">
+ <ArtSvgIcon
+ :icon="item.meta.icon"
+ :color="theme?.iconColor"
+ :style="{ color: theme.iconColor }"
+ />
+ </div>
+ <div
+ v-show="item.meta.showBadge && level === 0 && !menuOpen"
+ class="art-badge"
+ style="right: 5px"
+ />
+
+ <template #title>
+ <span class="menu-name">
+ {{ formatMenuTitle(item.meta.title) }}
+ </span>
+ <div v-if="item.meta.showBadge" class="art-badge" />
+ <div v-if="item.meta.showTextBadge && (level > 0 || menuOpen)" class="art-text-badge">
+ {{ item.meta.showTextBadge }}
+ </div>
+ </template>
+ </ElMenuItem>
+ </template>
+</template>
+
+<script setup>
+ import { formatMenuTitle } from '@/utils/router'
+
+ import { computed } from 'vue'
+ import { handleMenuJump } from '@/utils/navigation'
+ import { useSettingStore } from '@/store/modules/setting'
+ const props = defineProps({
+ title: { required: false, default: '' },
+ list: { required: false, default: () => [] },
+ theme: { required: false, default: () => ({}) },
+ isMobile: { required: false, default: false },
+ level: { required: false, default: 0 }
+ })
+ const emit = defineEmits(['close'])
+ const settingStore = useSettingStore()
+ const { menuOpen } = storeToRefs(settingStore)
+ const filteredMenuItems = computed(() => filterRoutes(props.list))
+ const goPage = (item) => {
+ closeMenu()
+ handleMenuJump(item)
+ }
+ const closeMenu = () => {
+ emit('close')
+ }
+ const isNavigableRoute = (item) => {
+ return !!(
+ !item.meta.isHide &&
+ ((item.path && item.path.trim()) || item.meta.link || item.meta.isIframe === true) &&
+ (item.component || item.meta.link || item.meta.isIframe === true)
+ )
+ }
+ const filterRoutes = (items) => {
+ return items
+ .filter((item) => {
+ if (item.meta.isHide) {
+ return false
+ }
+ if (item.children && item.children.length > 0) {
+ const filteredChildren = filterRoutes(item.children)
+ return filteredChildren.length > 0 || isNavigableRoute(item)
+ }
+ return isNavigableRoute(item)
+ })
+ .map((item) => ({
+ ...item,
+ children: item.children ? filterRoutes(item.children) : void 0
+ }))
+ }
+ const hasChildren = (item) => {
+ if (!item.children || item.children.length === 0) {
+ return false
+ }
+ const filteredChildren = filterRoutes(item.children)
+ return filteredChildren.length > 0
+ }
+ const isExternalLink = (item) => {
+ return !!(item.meta.link && !item.meta.isIframe)
+ }
+ const getUniqueKey = (item, index) => {
+ return `${item.path || item.meta.title || 'menu'}-${props.level}-${index}`
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-notification/index.vue b/rsf-design/src/components/core/layouts/art-notification/index.vue
new file mode 100644
index 0000000..fdcdec0
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-notification/index.vue
@@ -0,0 +1,357 @@
+<!-- 閫氱煡缁勪欢 -->
+<template>
+ <div
+ class="art-notification-panel art-card-sm !shadow-xl"
+ :style="{
+ transform: show ? 'scaleY(1)' : 'scaleY(0.9)',
+ opacity: show ? 1 : 0
+ }"
+ v-show="visible"
+ @click.stop
+ >
+ <div class="flex-cb px-3.5 mt-3.5">
+ <span class="text-base font-medium text-g-800">{{ $t('notice.title') }}</span>
+ <span class="text-xs text-g-800 px-1.5 py-1 c-p select-none rounded hover:bg-g-200">
+ {{ $t('notice.btnRead') }}
+ </span>
+ </div>
+
+ <ul class="box-border flex items-end w-full h-12.5 px-3.5 border-b-d">
+ <li
+ v-for="(item, index) in barList"
+ :key="index"
+ class="h-12 leading-12 mr-5 overflow-hidden text-[13px] text-g-700 c-p select-none"
+ :class="{ 'bar-active': barActiveIndex === index }"
+ @click="changeBar(index)"
+ >
+ {{ item.name }} ({{ item.num }})
+ </li>
+ </ul>
+
+ <div class="w-full h-[calc(100%-95px)]">
+ <div class="h-[calc(100%-60px)] overflow-y-scroll scrollbar-thin">
+ <!-- 閫氱煡 -->
+ <ul v-show="barActiveIndex === 0">
+ <li
+ v-for="(item, index) in noticeList"
+ :key="index"
+ class="box-border flex-c px-3.5 py-3.5 c-p last:border-b-0 hover:bg-g-200/60"
+ >
+ <div
+ class="size-9 leading-9 text-center rounded-lg flex-cc"
+ :class="[getNoticeStyle(item.type).iconClass]"
+ >
+ <ArtSvgIcon class="text-lg !bg-transparent" :icon="getNoticeStyle(item.type).icon" />
+ </div>
+ <div class="w-[calc(100%-45px)] ml-3.5">
+ <h4 class="text-sm font-normal leading-5.5 text-g-900">{{ item.title }}</h4>
+ <p class="mt-1.5 text-xs text-g-500">{{ item.time }}</p>
+ </div>
+ </li>
+ </ul>
+
+ <!-- 娑堟伅 -->
+ <ul v-show="barActiveIndex === 1">
+ <li
+ v-for="(item, index) in msgList"
+ :key="index"
+ class="box-border flex-c px-3.5 py-3.5 c-p last:border-b-0 hover:bg-g-200/60"
+ >
+ <div class="w-9 h-9">
+ <img :src="item.avatar" class="w-full h-full rounded-lg" />
+ </div>
+ <div class="w-[calc(100%-45px)] ml-3.5">
+ <h4 class="text-xs font-normal leading-5.5">{{ item.title }}</h4>
+ <p class="mt-1.5 text-xs text-g-500">{{ item.time }}</p>
+ </div>
+ </li>
+ </ul>
+
+ <!-- 寰呭姙 -->
+ <ul v-show="barActiveIndex === 2">
+ <li
+ v-for="(item, index) in pendingList"
+ :key="index"
+ class="box-border px-5 py-3.5 last:border-b-0"
+ >
+ <h4>{{ item.title }}</h4>
+ <p class="text-xs text-g-500">{{ item.time }}</p>
+ </li>
+ </ul>
+
+ <!-- 绌虹姸鎬� -->
+ <div
+ v-show="currentTabIsEmpty"
+ class="relative top-25 h-full text-g-500 text-center !bg-transparent"
+ >
+ <ArtSvgIcon icon="system-uicons:inbox" class="text-5xl" />
+ <p class="mt-3.5 text-xs !bg-transparent"
+ >{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p
+ >
+ </div>
+ </div>
+
+ <div class="relative box-border w-full px-3.5">
+ <ElButton class="w-full mt-3" @click="handleViewAll" v-ripple>
+ {{ $t('notice.viewAll') }}
+ </ElButton>
+ </div>
+ </div>
+
+ <div class="h-25"></div>
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ 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'
+ defineOptions({ name: 'ArtNotification' })
+ const { t } = useI18n()
+ const props = defineProps({
+ value: { required: true }
+ })
+ const emit = defineEmits(['update:value'])
+ const show = ref(false)
+ const visible = ref(false)
+ const barActiveIndex = ref(0)
+ const useNotificationData = () => {
+ const noticeList2 = ref([
+ {
+ title: '鏂板鍥介檯鍖�',
+ time: '2024-6-13 0:10',
+ type: 'notice'
+ },
+ {
+ title: '鍐锋湀鍛嗗憜缁欎綘鍙戜簡涓�鏉℃秷鎭�',
+ time: '2024-4-21 8:05',
+ type: 'message'
+ },
+ {
+ title: '灏忚偉鐚叧娉ㄤ簡浣�',
+ time: '2020-3-17 21:12',
+ type: 'collection'
+ },
+ {
+ title: '鏂板浣跨敤鏂囨。',
+ time: '2024-02-14 0:20',
+ type: 'notice'
+ },
+ {
+ title: '灏忚偉鐚粰浣犲彂浜嗕竴灏侀偖浠�',
+ time: '2024-1-20 0:15',
+ type: 'email'
+ },
+ {
+ title: '鑿滃崟mock鏈湴鐪熷疄鏁版嵁',
+ time: '2024-1-17 22:06',
+ type: 'notice'
+ }
+ ])
+ const msgList2 = ref([
+ {
+ title: '姹犱笉鑳� 鍏虫敞浜嗕綘',
+ time: '2021-2-26 23:50',
+ avatar: avatar1
+ },
+ {
+ title: '鍞愪笉鑻� 鍏虫敞浜嗕綘',
+ time: '2021-2-21 8:05',
+ avatar: avatar2
+ },
+ {
+ title: '涓皬楸� 鍏虫敞浜嗕綘',
+ time: '2020-1-17 21:12',
+ avatar: avatar3
+ },
+ {
+ title: '浣曞皬鑽� 鍏虫敞浜嗕綘',
+ time: '2021-01-14 0:20',
+ avatar: avatar4
+ },
+ {
+ title: '瑾惰娣� 鍏虫敞浜嗕綘',
+ time: '2020-12-20 0:15',
+ avatar: avatar5
+ },
+ {
+ title: '鍐锋湀鍛嗗憜 鍏虫敞浜嗕綘',
+ time: '2020-12-17 22:06',
+ avatar: avatar6
+ }
+ ])
+ const pendingList2 = ref([])
+ const barList2 = computed(() => [
+ {
+ name: computed(() => t('notice.bar[0]')),
+ num: noticeList2.value.length
+ },
+ {
+ name: computed(() => t('notice.bar[1]')),
+ num: msgList2.value.length
+ },
+ {
+ name: computed(() => t('notice.bar[2]')),
+ num: pendingList2.value.length
+ }
+ ])
+ return {
+ noticeList: noticeList2,
+ msgList: msgList2,
+ pendingList: pendingList2,
+ barList: barList2
+ }
+ }
+ const useNotificationStyles = () => {
+ const noticeStyleMap = {
+ email: {
+ icon: 'ri:mail-line',
+ iconClass: 'bg-warning/12 text-warning'
+ },
+ message: {
+ icon: 'ri:volume-down-line',
+ iconClass: 'bg-success/12 text-success'
+ },
+ collection: {
+ icon: 'ri:heart-3-line',
+ iconClass: 'bg-danger/12 text-danger'
+ },
+ user: {
+ icon: 'ri:volume-down-line',
+ iconClass: 'bg-info/12 text-info'
+ },
+ notice: {
+ icon: 'ri:notification-3-line',
+ iconClass: 'bg-theme/12 text-theme'
+ }
+ }
+ const getNoticeStyle2 = (type) => {
+ const defaultStyle = {
+ icon: 'ri:arrow-right-circle-line',
+ iconClass: 'bg-theme/12 text-theme'
+ }
+ return noticeStyleMap[type] || defaultStyle
+ }
+ return {
+ getNoticeStyle: getNoticeStyle2
+ }
+ }
+ const useNotificationAnimation = () => {
+ const showNotice2 = (open) => {
+ if (open) {
+ visible.value = true
+ setTimeout(() => {
+ show.value = true
+ }, 5)
+ } else {
+ show.value = false
+ setTimeout(() => {
+ visible.value = false
+ }, 350)
+ }
+ }
+ return {
+ showNotice: showNotice2
+ }
+ }
+ const useTabManagement = (noticeList2, msgList2, pendingList2, businessHandlers) => {
+ const changeBar2 = (index) => {
+ barActiveIndex.value = index
+ }
+ const currentTabIsEmpty2 = computed(() => {
+ const tabDataMap = [noticeList2.value, msgList2.value, pendingList2.value]
+ const currentData = tabDataMap[barActiveIndex.value]
+ return currentData && currentData.length === 0
+ })
+ const handleViewAll2 = () => {
+ const viewAllHandlers = {
+ 0: businessHandlers.handleNoticeAll,
+ 1: businessHandlers.handleMsgAll,
+ 2: businessHandlers.handlePendingAll
+ }
+ const handler = viewAllHandlers[barActiveIndex.value]
+ handler?.()
+ emit('update:value', false)
+ }
+ return {
+ changeBar: changeBar2,
+ currentTabIsEmpty: currentTabIsEmpty2,
+ handleViewAll: handleViewAll2
+ }
+ }
+ const useBusinessLogic = () => {
+ const handleNoticeAll2 = () => {
+ console.log('鏌ョ湅鍏ㄩ儴閫氱煡')
+ }
+ const handleMsgAll2 = () => {
+ console.log('鏌ョ湅鍏ㄩ儴娑堟伅')
+ }
+ const handlePendingAll2 = () => {
+ console.log('鏌ョ湅鍏ㄩ儴寰呭姙')
+ }
+ return {
+ handleNoticeAll: handleNoticeAll2,
+ handleMsgAll: handleMsgAll2,
+ handlePendingAll: handlePendingAll2
+ }
+ }
+ const { noticeList, msgList, pendingList, barList } = useNotificationData()
+ const { getNoticeStyle } = useNotificationStyles()
+ const { showNotice } = useNotificationAnimation()
+ const { handleNoticeAll, handleMsgAll, handlePendingAll } = useBusinessLogic()
+ const { changeBar, currentTabIsEmpty, handleViewAll } = useTabManagement(
+ noticeList,
+ msgList,
+ pendingList,
+ { handleNoticeAll, handleMsgAll, handlePendingAll }
+ )
+ watch(
+ () => props.value,
+ (newValue) => {
+ showNotice(newValue)
+ }
+ )
+</script>
+
+<style scoped>
+ @reference '@styles/core/tailwind.css';
+
+ .art-notification-panel {
+ @apply absolute
+ top-14.5
+ right-5
+ w-90
+ h-125
+ overflow-hidden
+ transition-all
+ duration-300
+ origin-top
+ will-change-[top,left]
+ max-[640px]:top-[65px]
+ max-[640px]:right-0
+ max-[640px]:w-full
+ max-[640px]:h-[80vh];
+ }
+
+ .bar-active {
+ color: var(--theme-color) !important;
+ border-bottom: 2px solid var(--theme-color);
+ }
+
+ .scrollbar-thin::-webkit-scrollbar {
+ width: 5px !important;
+ }
+
+ .dark .scrollbar-thin::-webkit-scrollbar-track {
+ background-color: var(--default-box-color);
+ }
+
+ .dark .scrollbar-thin::-webkit-scrollbar-thumb {
+ background-color: #222 !important;
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-page-content/index.vue b/rsf-design/src/components/core/layouts/art-page-content/index.vue
new file mode 100644
index 0000000..5b7da73
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-page-content/index.vue
@@ -0,0 +1,112 @@
+<!-- 甯冨眬鍐呭 -->
+<template>
+ <div class="layout-content" :class="{ 'overflow-auto': isFullPage }" :style="containerStyle">
+ <div id="app-content-header">
+ <!-- 鑺傛棩婊氬姩 -->
+ <ArtFestivalTextScroll v-if="!isFullPage" />
+
+ <!-- 璺敱淇℃伅璋冭瘯 -->
+ <div
+ v-if="isOpenRouteInfo === 'true'"
+ class="px-2 py-1.5 mb-3 text-sm text-g-500 bg-g-200 border-full-d rounded-md"
+ >
+ router meta锛歿{ route.meta }}
+ </div>
+ </div>
+
+ <RouterView v-if="isRefresh" v-slot="{ Component, route }" :style="contentStyle">
+ <!-- 缂撳瓨璺敱鍔ㄧ敾 -->
+ <Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
+ <KeepAlive :max="10" :exclude="keepAliveExclude">
+ <component
+ class="art-page-view"
+ :is="Component"
+ :key="route.path"
+ v-if="route.meta.keepAlive"
+ />
+ </KeepAlive>
+ </Transition>
+
+ <!-- 闈炵紦瀛樿矾鐢卞姩鐢� -->
+ <Transition :name="showTransitionMask ? '' : actualTransition" mode="out-in" appear>
+ <component
+ class="art-page-view"
+ :is="Component"
+ :key="route.path"
+ v-if="!route.meta.keepAlive"
+ />
+ </Transition>
+ </RouterView>
+
+ <!-- 鍏ㄥ睆椤甸潰鍒囨崲杩囨浮閬僵锛堢敤浜庢彁鍗囬〉闈㈠垏鎹㈣瑙変綋楠岋級 -->
+ <Teleport to="body">
+ <div
+ v-show="showTransitionMask"
+ class="fixed top-0 left-0 z-[2000] w-screen h-screen pointer-events-none bg-box"
+ />
+ </Teleport>
+ </div>
+</template>
+<script setup>
+ import { useRoute } from 'vue-router'
+ import { useAutoLayoutHeight } from '@/hooks/core/useLayoutHeight'
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useWorktabStore } from '@/store/modules/worktab'
+ defineOptions({ name: 'ArtPageContent' })
+ const route = useRoute()
+ const { containerMinHeight } = useAutoLayoutHeight()
+ const { pageTransition, containerWidth, refresh } = storeToRefs(useSettingStore())
+ const { keepAliveExclude } = storeToRefs(useWorktabStore())
+ const isRefresh = shallowRef(true)
+ const isOpenRouteInfo = import.meta.env.VITE_OPEN_ROUTE_INFO
+ const showTransitionMask = ref(false)
+ const isFirstLoad = ref(true)
+ const isFullPage = computed(() => route.matched.some((r) => r.meta?.isFullPage))
+ const prevIsFullPage = ref(isFullPage.value)
+ const actualTransition = computed(() => {
+ if (isFirstLoad.value) return ''
+ if (prevIsFullPage.value && !isFullPage.value) return ''
+ return pageTransition.value
+ })
+ watch(isFullPage, (val, oldVal) => {
+ if (val !== oldVal) {
+ showTransitionMask.value = true
+ setTimeout(() => {
+ showTransitionMask.value = false
+ }, 50)
+ }
+ nextTick(() => {
+ prevIsFullPage.value = val
+ })
+ })
+ const containerStyle = computed(() =>
+ isFullPage.value
+ ? {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ width: '100%',
+ height: '100vh',
+ zIndex: 2500,
+ background: 'var(--default-bg-color)'
+ }
+ : {
+ maxWidth: containerWidth.value
+ }
+ )
+ const contentStyle = computed(() => ({
+ minHeight: containerMinHeight.value
+ }))
+ const reload = () => {
+ isRefresh.value = false
+ nextTick(() => {
+ isRefresh.value = true
+ })
+ }
+ watch(refresh, reload, { flush: 'post' })
+ onMounted(() => {
+ nextTick(() => {
+ isFirstLoad.value = false
+ })
+ })
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-screen-lock/index.vue b/rsf-design/src/components/core/layouts/art-screen-lock/index.vue
new file mode 100644
index 0000000..679c80e
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-screen-lock/index.vue
@@ -0,0 +1,437 @@
+<!-- 閿佸睆 -->
+<template>
+ <div class="layout-lock-screen">
+ <!-- 寮�鍙戣�呭伐鍏疯鍛婅鐩栧眰 -->
+ <div
+ v-if="showDevToolsWarning"
+ class="fixed top-0 left-0 z-[999999] flex-cc w-full h-full text-white bg-gradient-to-br from-[#1e1e1e] to-black animate-fade-in"
+ >
+ <div class="p-5 text-center select-none">
+ <div class="mb-7.5 text-5xl">馃敀</div>
+ <h1 class="m-0 mb-5 text-3xl font-semibold text-danger">绯荤粺宸查攣瀹�</h1>
+ <p class="max-w-125 m-0 text-lg leading-relaxed text-white">
+ 妫�娴嬪埌寮�鍙戣�呭伐鍏峰凡鎵撳紑<br />
+ 涓轰簡绯荤粺瀹夊叏锛岃鍏抽棴寮�鍙戣�呭伐鍏峰悗缁х画浣跨敤
+ </p>
+ <div class="mt-7.5 text-sm text-gray-400">Security Lock Activated</div>
+ </div>
+ </div>
+
+ <!-- 閿佸睆寮圭獥 -->
+ <div v-if="!isLock">
+ <ElDialog v-model="visible" :width="370" :show-close="false" @open="handleDialogOpen">
+ <div class="flex-c flex-col">
+ <img class="w-16 h-16 rounded-full" src="@imgs/user/avatar.webp" alt="鐢ㄦ埛澶村儚" />
+ <div class="mt-7.5 mb-3.5 text-base font-medium">{{ userInfo.userName }}</div>
+ <ElForm
+ ref="formRef"
+ :model="formData"
+ :rules="rules"
+ class="w-[90%]"
+ @submit.prevent="handleLock"
+ >
+ <ElFormItem prop="password">
+ <ElInput
+ v-model="formData.password"
+ type="password"
+ :placeholder="$t('lockScreen.lock.inputPlaceholder')"
+ :show-password="true"
+ autocomplete="new-password"
+ ref="lockInputRef"
+ class="w-full mt-9"
+ @keyup.enter="handleLock"
+ >
+ <template #suffix>
+ <ElIcon class="c-p" @click="handleLock">
+ <Lock />
+ </ElIcon>
+ </template>
+ </ElInput>
+ </ElFormItem>
+ <ElButton type="primary" class="w-full mt-0.5" @click="handleLock" v-ripple>
+ {{ $t('lockScreen.lock.btnText') }}
+ </ElButton>
+ </ElForm>
+ </div>
+ </ElDialog>
+ </div>
+
+ <!-- 瑙i攣鐣岄潰 -->
+ <div v-else class="unlock-content">
+ <div class="flex-c flex-col w-80">
+ <img class="w-16 h-16 mt-5 rounded-full" src="@imgs/user/avatar.webp" alt="鐢ㄦ埛澶村儚" />
+ <div class="mt-3 mb-3.5 text-base font-medium">
+ {{ userInfo.userName }}
+ </div>
+ <ElForm
+ ref="unlockFormRef"
+ :model="unlockForm"
+ :rules="rules"
+ class="w-full !px-2.5"
+ @submit.prevent="handleUnlock"
+ >
+ <ElFormItem prop="password">
+ <ElInput
+ v-model="unlockForm.password"
+ type="password"
+ :placeholder="$t('lockScreen.unlock.inputPlaceholder')"
+ :show-password="true"
+ autocomplete="new-password"
+ ref="unlockInputRef"
+ class="mt-5"
+ >
+ <template #suffix>
+ <ElIcon class="c-p" @click="handleUnlock">
+ <Unlock />
+ </ElIcon>
+ </template>
+ </ElInput>
+ </ElFormItem>
+
+ <ElButton type="primary" class="w-full mt-2" @click="handleUnlock" v-ripple>
+ {{ $t('lockScreen.unlock.btnText') }}
+ </ElButton>
+ <div class="w-full text-center">
+ <ElButton
+ text
+ class="mt-2.5 !text-g-600 hover:!text-theme hover:!bg-transparent"
+ @click="toLogin"
+ >
+ {{ $t('lockScreen.unlock.backBtnText') }}
+ </ElButton>
+ </div>
+ </ElForm>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { Lock, Unlock } from '@element-plus/icons-vue'
+
+ import { useI18n } from 'vue-i18n'
+ import CryptoJS from 'crypto-js'
+ import { useUserStore } from '@/store/modules/user'
+ import { mittBus } from '@/utils/sys'
+ const { t } = useI18n()
+ const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY
+ const userStore = useUserStore()
+ const { info: userInfo, lockPassword, isLock } = storeToRefs(userStore)
+ const visible = ref(false)
+ const lockInputRef = ref(null)
+ const unlockInputRef = ref(null)
+ const showDevToolsWarning = ref(false)
+ const formRef = ref()
+ const unlockFormRef = ref()
+ const formData = reactive({
+ password: ''
+ })
+ const unlockForm = reactive({
+ password: ''
+ })
+ const rules = computed(() => ({
+ password: [
+ {
+ required: true,
+ message: t('lockScreen.lock.inputPlaceholder'),
+ trigger: 'blur'
+ }
+ ]
+ }))
+ const isMobile = () => {
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
+ navigator.userAgent
+ )
+ }
+ const disableDevTools = () => {
+ const handleContextMenu = (e) => {
+ if (isLock.value) {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ }
+ document.addEventListener('contextmenu', handleContextMenu, true)
+ const handleKeyDown = (e) => {
+ if (!isLock.value) return
+ if (e.key === 'F12') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.shiftKey) {
+ const key = e.key.toLowerCase()
+ if (['i', 'j', 'c', 'k'].includes(key)) {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ }
+ if (e.ctrlKey && e.key.toLowerCase() === 'u') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.key.toLowerCase() === 's') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.key.toLowerCase() === 'a') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.key.toLowerCase() === 'p') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.key.toLowerCase() === 'f') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.altKey && e.key === 'Tab') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.key === 'Tab') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.key.toLowerCase() === 'w') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if ((e.ctrlKey && e.key.toLowerCase() === 'r') || e.key === 'F5') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'r') {
+ e.preventDefault()
+ e.stopPropagation()
+ return false
+ }
+ }
+ document.addEventListener('keydown', handleKeyDown, true)
+ const handleSelectStart = (e) => {
+ if (isLock.value) {
+ e.preventDefault()
+ return false
+ }
+ }
+ document.addEventListener('selectstart', handleSelectStart, true)
+ const handleDragStart = (e) => {
+ if (isLock.value) {
+ e.preventDefault()
+ return false
+ }
+ }
+ document.addEventListener('dragstart', handleDragStart, true)
+ let devtools = { open: false }
+ const threshold = 160
+ let devToolsInterval = null
+ const checkDevTools = () => {
+ if (!isLock.value || isMobile()) return
+ const isDevToolsOpen =
+ window.outerHeight - window.innerHeight > threshold ||
+ window.outerWidth - window.innerWidth > threshold
+ if (isDevToolsOpen && !devtools.open) {
+ devtools.open = true
+ showDevToolsWarning.value = true
+ } else if (!isDevToolsOpen && devtools.open) {
+ devtools.open = false
+ showDevToolsWarning.value = false
+ }
+ }
+ if (!isMobile()) {
+ devToolsInterval = setInterval(checkDevTools, 500)
+ }
+ return () => {
+ document.removeEventListener('contextmenu', handleContextMenu, true)
+ document.removeEventListener('keydown', handleKeyDown, true)
+ document.removeEventListener('selectstart', handleSelectStart, true)
+ document.removeEventListener('dragstart', handleDragStart, true)
+ if (devToolsInterval) {
+ clearInterval(devToolsInterval)
+ }
+ }
+ }
+ const verifyPassword = (inputPassword, storedPassword) => {
+ try {
+ const decryptedPassword = CryptoJS.AES.decrypt(storedPassword, ENCRYPT_KEY).toString(
+ CryptoJS.enc.Utf8
+ )
+ return inputPassword === decryptedPassword
+ } catch (error) {
+ console.error('瀵嗙爜瑙e瘑澶辫触:', error)
+ return false
+ }
+ }
+ const handleKeydown = (event) => {
+ if (event.altKey && event.key.toLowerCase() === '卢') {
+ event.preventDefault()
+ visible.value = true
+ }
+ }
+ const handleDialogOpen = () => {
+ setTimeout(() => {
+ lockInputRef.value?.input?.focus()
+ }, 100)
+ }
+ const handleLock = async () => {
+ if (!formRef.value) return
+ await formRef.value.validate((valid, fields) => {
+ if (valid) {
+ const encryptedPassword = CryptoJS.AES.encrypt(formData.password, ENCRYPT_KEY).toString()
+ userStore.setLockStatus(true)
+ userStore.setLockPassword(encryptedPassword)
+ visible.value = false
+ formData.password = ''
+ } else {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', fields)
+ }
+ })
+ }
+ const handleUnlock = async () => {
+ if (!unlockFormRef.value) return
+ await unlockFormRef.value.validate((valid, fields) => {
+ if (valid) {
+ const isValid = verifyPassword(unlockForm.password, lockPassword.value)
+ if (isValid) {
+ try {
+ userStore.setLockStatus(false)
+ userStore.setLockPassword('')
+ unlockForm.password = ''
+ visible.value = false
+ showDevToolsWarning.value = false
+ } catch (error) {
+ console.error('鏇存柊store澶辫触:', error)
+ }
+ } else {
+ const inputElement = unlockInputRef.value?.$el
+ if (inputElement) {
+ inputElement.classList.add('shake-animation')
+ setTimeout(() => {
+ inputElement.classList.remove('shake-animation')
+ }, 300)
+ }
+ ElMessage.error(t('lockScreen.pwdError'))
+ unlockForm.password = ''
+ }
+ } else {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', fields)
+ }
+ })
+ }
+ const toLogin = () => {
+ userStore.logOut()
+ }
+ const openLockScreen = () => {
+ visible.value = true
+ }
+ watch(isLock, (newValue) => {
+ if (newValue) {
+ document.body.style.overflow = 'hidden'
+ setTimeout(() => {
+ unlockInputRef.value?.input?.focus()
+ }, 100)
+ } else {
+ document.body.style.overflow = 'auto'
+ showDevToolsWarning.value = false
+ }
+ })
+ let cleanupDevTools = null
+ onMounted(() => {
+ mittBus.on('openLockScreen', openLockScreen)
+ document.addEventListener('keydown', handleKeydown)
+ if (isLock.value) {
+ visible.value = true
+ setTimeout(() => {
+ unlockInputRef.value?.input?.focus()
+ }, 100)
+ }
+ cleanupDevTools = disableDevTools()
+ })
+ onUnmounted(() => {
+ document.removeEventListener('keydown', handleKeydown)
+ document.body.style.overflow = 'auto'
+ if (cleanupDevTools) {
+ cleanupDevTools()
+ cleanupDevTools = null
+ }
+ })
+</script>
+
+<style lang="scss" scoped>
+ .layout-lock-screen :deep(.el-dialog) {
+ border-radius: 10px;
+ }
+
+ .unlock-content {
+ position: fixed;
+ inset: 0;
+ z-index: 2500;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ background-color: #fff;
+ background-image: url('@imgs/lock/bg_light.webp');
+ background-size: cover;
+ transition: transform 0.3s ease-in-out;
+ }
+
+ .dark {
+ .unlock-content {
+ background-image: url('@imgs/lock/bg_dark.webp');
+ }
+ }
+
+ @keyframes fade-in {
+ from {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+
+ .animate-fade-in {
+ animation: fade-in 0.3s ease-in-out;
+ }
+
+ @keyframes shake {
+ 0%,
+ 100% {
+ transform: translateX(0);
+ }
+
+ 10%,
+ 30%,
+ 50%,
+ 70%,
+ 90% {
+ transform: translateX(-10px);
+ }
+
+ 20%,
+ 40%,
+ 60%,
+ 80% {
+ transform: translateX(10px);
+ }
+ }
+
+ .shake-animation {
+ animation: shake 0.5s ease-in-out;
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.js b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.js
new file mode 100644
index 0000000..c772273
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.js
@@ -0,0 +1,231 @@
+import { computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { ContainerWidthEnum } from '@/enums/appEnum'
+import AppConfig from '@/config'
+import { headerBarConfig } from '@/config/modules/headerBar'
+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'
+ },
+ {
+ value: 'shadow-mode',
+ label: t('setting.box.list[1]'),
+ type: 'shadow-mode'
+ }
+ ])
+ 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',
+ handler: 'workTab',
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'uniqueOpened',
+ label: t('setting.basics.list.accordion'),
+ type: 'switch',
+ handler: 'uniqueOpened',
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'showMenuButton',
+ label: t('setting.basics.list.collapseSidebar'),
+ type: 'switch',
+ handler: 'menuButton',
+ headerBarKey: 'menuButton'
+ },
+ {
+ key: 'showFastEnter',
+ label: t('setting.basics.list.fastEnter'),
+ type: 'switch',
+ handler: 'fastEnter',
+ headerBarKey: 'fastEnter'
+ },
+ {
+ key: 'showRefreshButton',
+ label: t('setting.basics.list.reloadPage'),
+ type: 'switch',
+ handler: 'refreshButton',
+ headerBarKey: 'refreshButton'
+ },
+ {
+ key: 'showCrumbs',
+ label: t('setting.basics.list.breadcrumb'),
+ type: 'switch',
+ handler: 'crumbs',
+ mobileHide: true,
+ headerBarKey: 'breadcrumb'
+ },
+ {
+ key: 'showLanguage',
+ label: t('setting.basics.list.language'),
+ type: 'switch',
+ handler: 'language',
+ headerBarKey: 'language'
+ },
+ {
+ key: 'showNprogress',
+ label: t('setting.basics.list.progressBar'),
+ type: 'switch',
+ handler: 'nprogress',
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'colorWeak',
+ label: t('setting.basics.list.weakMode'),
+ type: 'switch',
+ handler: 'colorWeak',
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'watermarkVisible',
+ label: t('setting.basics.list.watermark'),
+ type: 'switch',
+ handler: 'watermark',
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'menuOpenWidth',
+ label: t('setting.basics.list.menuWidth'),
+ type: 'input-number',
+ handler: 'menuOpenWidth',
+ min: 180,
+ max: 320,
+ step: 10,
+ style: { width: '120px' },
+ controlsPosition: 'right',
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'tabStyle',
+ label: t('setting.basics.list.tabStyle'),
+ type: 'select',
+ handler: 'tabStyle',
+ options: tabStyleOptions.value,
+ style: { width: '120px' },
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'pageTransition',
+ label: t('setting.basics.list.pageTransition'),
+ type: 'select',
+ handler: 'pageTransition',
+ options: pageTransitionOptions.value,
+ style: { width: '120px' },
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ },
+ {
+ key: 'customRadius',
+ label: t('setting.basics.list.borderRadius'),
+ type: 'select',
+ handler: 'customRadius',
+ options: customRadiusOptions,
+ style: { width: '120px' },
+ headerBarKey: null
+ // 涓嶄緷璧杊eaderBar閰嶇疆
+ }
+ ]
+ return allSettings
+ .filter((setting) => {
+ if (setting.headerBarKey === null) {
+ return true
+ }
+ const headerBarFeature = headerBarConfig[setting.headerBarKey]
+ return headerBarFeature?.enabled !== false
+ })
+ .map((setting) => {
+ const nextSetting = { ...setting }
+ delete nextSetting.headerBarKey
+ return nextSetting
+ })
+ })
+ return {
+ // 閫夐」閰嶇疆
+ tabStyleOptions,
+ pageTransitionOptions,
+ customRadiusOptions,
+ containerWidthOptions,
+ boxStyleOptions,
+ configOptions,
+ // 璁剧疆椤归厤缃�
+ basicSettingsConfig
+ }
+}
+export { useSettingsConfig }
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.js b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.js
new file mode 100644
index 0000000..58f4907
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.js
@@ -0,0 +1,121 @@
+import { useSettingStore } from '@/store/modules/setting'
+import { storeToRefs } from 'pinia'
+function useSettingsHandlers() {
+ const settingStore = useSettingStore()
+ const domOperations = {
+ // 璁剧疆HTML绫诲悕
+ setHtmlClass: (className, add) => {
+ const el = document.getElementsByTagName('html')[0]
+ if (add) {
+ el.classList.add(className)
+ } else {
+ el.classList.remove(className)
+ }
+ },
+ // 璁剧疆鏍瑰厓绱犲睘鎬�
+ setRootAttribute: (attribute, value) => {
+ const el = document.documentElement
+ el.setAttribute(attribute, value)
+ },
+ // 璁剧疆body绫诲悕
+ setBodyClass: (className, add) => {
+ const el = document.getElementsByTagName('body')[0]
+ if (add) {
+ el.classList.add(className)
+ } else {
+ el.classList.remove(className)
+ }
+ }
+ }
+ const createToggleHandler = (storeMethod, callback) => {
+ return () => {
+ storeMethod()
+ callback?.()
+ }
+ }
+ const createValueHandler = (storeMethod, callback) => {
+ return (value) => {
+ if (value !== void 0 && 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) => settingStore.setMenuOpenWidth(width)),
+ // 鏍囩椤甸鏍�
+ tabStyle: createValueHandler((style) => settingStore.setTabStyle(style)),
+ // 椤甸潰鍒囨崲鍔ㄧ敾
+ pageTransition: createValueHandler((transition) => settingStore.setPageTransition(transition)),
+ // 鍦嗚澶у皬
+ customRadius: createValueHandler((radius) => settingStore.setCustomRadius(radius))
+ }
+ const boxStyleHandlers = {
+ // 璁剧疆鐩掑瓙妯″紡
+ setBoxMode: (type) => {
+ 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) => {
+ settingStore.setElementTheme(theme)
+ settingStore.reload()
+ }
+ }
+ const containerHandlers = {
+ // 璁剧疆瀹瑰櫒瀹藉害
+ setWidth: (type) => {
+ settingStore.setContainerWidth(type)
+ settingStore.reload()
+ }
+ }
+ return {
+ domOperations,
+ basicHandlers,
+ boxStyleHandlers,
+ colorHandlers,
+ containerHandlers,
+ createToggleHandler,
+ createValueHandler
+ }
+}
+export { useSettingsHandlers }
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js
new file mode 100644
index 0000000..5f9a752
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js
@@ -0,0 +1,170 @@
+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 { StorageConfig } from '@/utils'
+import { useTheme } from '@/hooks/core/useTheme'
+import { useCeremony } from '@/hooks/core/useCeremony'
+import { useSettingsState } from './useSettingsState'
+import { useSettingsHandlers } from './useSettingsHandlers'
+function useSettingsPanel() {
+ const settingStore = useSettingStore()
+ const { systemThemeType, systemThemeMode, menuType } = storeToRefs(settingStore)
+ const { openFestival, cleanup } = useCeremony()
+ const { setSystemTheme, setSystemAutoTheme } = useTheme()
+ const { initColorWeak } = useSettingsState()
+ const { domOperations } = useSettingsHandlers()
+ const showDrawer = ref(false)
+ const breakpoints = useBreakpoints({ tablet: 1e3 })
+ const isMobile = breakpoints.smaller('tablet')
+ const getStoredDesktopMenuType = () => {
+ const storedMenuType = localStorage.getItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY)
+ return Object.values(MenuTypeEnum).includes(storedMenuType) ? storedMenuType : void 0
+ }
+ const setStoredDesktopMenuType = (type) => {
+ localStorage.setItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY, type)
+ }
+ const clearStoredDesktopMenuType = () => {
+ localStorage.removeItem(StorageConfig.RESPONSIVE_MENU_TYPE_KEY)
+ }
+ const storedDesktopMenuType = getStoredDesktopMenuType()
+ const beforeMenuType = ref(storedDesktopMenuType)
+ const hasChangedMenu = ref(Boolean(storedDesktopMenuType))
+ const systemThemeColor = computed(() => settingStore.systemThemeColor)
+ 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 = () => {
+ const stopWatch = watch(
+ isMobile,
+ (mobile) => {
+ if (mobile) {
+ if (!hasChangedMenu.value) {
+ beforeMenuType.value = menuType.value
+ if (menuType.value !== MenuTypeEnum.LEFT) {
+ setStoredDesktopMenuType(menuType.value)
+ useSettingsState().switchMenuLayouts(MenuTypeEnum.LEFT)
+ hasChangedMenu.value = true
+ }
+ }
+ settingStore.setMenuOpen(false)
+ } else {
+ if (hasChangedMenu.value && beforeMenuType.value) {
+ if (menuType.value === MenuTypeEnum.LEFT) {
+ useSettingsState().switchMenuLayouts(beforeMenuType.value)
+ }
+ clearStoredDesktopMenuType()
+ hasChangedMenu.value = false
+ }
+ settingStore.setMenuOpen(true)
+ }
+ },
+ { immediate: true }
+ )
+ return { stopWatch }
+ }
+ const useDrawerControl = () => {
+ let themeChangeTimer = null
+ const handleOpen = () => {
+ if (themeChangeTimer) {
+ clearTimeout(themeChangeTimer)
+ }
+ themeChangeTimer = setTimeout(() => {
+ domOperations.setBodyClass('theme-change', true)
+ themeChangeTimer = null
+ }, 500)
+ }
+ const handleClose = () => {
+ if (themeChangeTimer) {
+ clearTimeout(themeChangeTimer)
+ themeChangeTimer = null
+ }
+ domOperations.setBodyClass('theme-change', false)
+ }
+ const openSetting = () => {
+ showDrawer.value = true
+ }
+ const closeDrawer = () => {
+ showDrawer.value = false
+ }
+ return {
+ handleOpen,
+ handleClose,
+ openSetting,
+ closeDrawer
+ }
+ }
+ const usePropsWatcher = (props) => {
+ watch(
+ () => props.open,
+ (val) => {
+ if (val !== void 0) {
+ showDrawer.value = val
+ }
+ }
+ )
+ }
+ const useSettingsInitializer = () => {
+ const themeHandlers = useThemeHandlers()
+ const { openSetting } = useDrawerControl()
+ const { stopWatch } = useResponsiveLayout()
+ let themeCleanup = 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
+ }
+}
+export { useSettingsPanel }
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsState.js b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsState.js
new file mode 100644
index 0000000..ee95813
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsState.js
@@ -0,0 +1,29 @@
+import { useSettingStore } from '@/store/modules/setting'
+import { MenuThemeEnum, MenuTypeEnum } from '@/enums/appEnum'
+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) => {
+ 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
+ }
+}
+export { useSettingsState }
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/index.vue b/rsf-design/src/components/core/layouts/art-settings-panel/index.vue
new file mode 100644
index 0000000..3797677
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/index.vue
@@ -0,0 +1,58 @@
+<!-- 璁剧疆闈㈡澘 -->
+<template>
+ <div class="layout-settings">
+ <SettingDrawer v-model="showDrawer" @open="handleOpen" @close="handleClose">
+ <!-- 澶撮儴鍏抽棴鎸夐挳 -->
+ <SettingHeader @close="closeDrawer" />
+ <!-- 涓婚椋庢牸 -->
+ <ThemeSettings />
+ <!-- 鑿滃崟甯冨眬 -->
+ <MenuLayoutSettings />
+ <!-- 鑿滃崟椋庢牸 -->
+ <MenuStyleSettings />
+ <!-- 绯荤粺涓婚鑹� -->
+ <ColorSettings />
+ <!-- 鐩掑瓙鏍峰紡 -->
+ <BoxStyleSettings />
+ <!-- 瀹瑰櫒瀹藉害 -->
+ <ContainerSettings />
+ <!-- 鍩虹閰嶇疆 -->
+ <BasicSettings />
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <SettingActions />
+ </SettingDrawer>
+ </div>
+</template>
+
+<script setup>
+ import SettingHeader from './widget/SettingHeader.vue'
+
+ import MenuLayoutSettings from './widget/MenuLayoutSettings.vue'
+
+ import ColorSettings from './widget/ColorSettings.vue'
+
+ import ContainerSettings from './widget/ContainerSettings.vue'
+
+ import SettingActions from './widget/SettingActions.vue'
+
+ import { useSettingsPanel } from './composables/useSettingsPanel'
+ defineOptions({ name: 'ArtSettingsPanel' })
+ const props = defineProps({
+ open: { required: false }
+ })
+ const settingsPanel = useSettingsPanel()
+ const { showDrawer } = settingsPanel
+ const { handleOpen, handleClose, closeDrawer } = settingsPanel.useDrawerControl()
+ const { initializeSettings, cleanupSettings } = settingsPanel.useSettingsInitializer()
+ settingsPanel.usePropsWatcher(props)
+ onMounted(() => {
+ initializeSettings()
+ })
+ onUnmounted(() => {
+ cleanupSettings()
+ })
+</script>
+
+<style lang="scss">
+ @use './style';
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/style.scss b/rsf-design/src/components/core/layouts/art-settings-panel/style.scss
new file mode 100644
index 0000000..e863074
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue
new file mode 100644
index 0000000..941c0b9
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue
@@ -0,0 +1,68 @@
+<template>
+ <div>
+ <SectionTitle :title="$t('setting.basics.title')" class="mt-10" />
+ <SettingItem
+ v-for="config in basicSettingsConfig"
+ :key="config.key"
+ :config="config"
+ :model-value="getSettingValue(config.key)"
+ @change="handleSettingChange(config.handler, $event)"
+ />
+ </div>
+</template>
+
+<script setup>
+ import SectionTitle from './SectionTitle.vue'
+
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useSettingsConfig } from '../composables/useSettingsConfig'
+ import { useSettingsHandlers } from '../composables/useSettingsHandlers'
+ import { storeToRefs } from 'pinia'
+ const settingStore = useSettingStore()
+ const { basicSettingsConfig } = useSettingsConfig()
+ const { basicHandlers } = useSettingsHandlers()
+ const {
+ uniqueOpened,
+ showMenuButton,
+ showFastEnter,
+ showRefreshButton,
+ showCrumbs,
+ showWorkTab,
+ showLanguage,
+ showNprogress,
+ colorWeak,
+ watermarkVisible,
+ menuOpenWidth,
+ tabStyle,
+ pageTransition,
+ customRadius
+ } = storeToRefs(settingStore)
+ const settingValueMap = {
+ uniqueOpened,
+ showMenuButton,
+ showFastEnter,
+ showRefreshButton,
+ showCrumbs,
+ showWorkTab,
+ showLanguage,
+ showNprogress,
+ colorWeak,
+ watermarkVisible,
+ menuOpenWidth,
+ tabStyle,
+ pageTransition,
+ customRadius
+ }
+ const getSettingValue = (key) => {
+ const settingRef = settingValueMap[key]
+ return settingRef?.value ?? null
+ }
+ const handleSettingChange = (handlerName, value) => {
+ const handler = basicHandlers[handlerName]
+ if (typeof handler === 'function') {
+ handler(value)
+ } else {
+ console.warn(`Handler "${handlerName}" not found in basicHandlers`)
+ }
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue
new file mode 100644
index 0000000..ceba9ee
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue
@@ -0,0 +1,36 @@
+<template>
+ <div>
+ <SectionTitle :title="$t('setting.box.title')" class="mt-10" />
+ <div class="box-border flex-cb p-1 mt-5 rounded-lg bg-g-200">
+ <div
+ v-for="option in boxStyleOptions"
+ :key="option.value"
+ class="w-[calc(50%-3px)] h-8.5 leading-8.5 text-sm text-center c-p select-none rounded-md transition-all duration-200"
+ :class="
+ isActive(option.type)
+ ? 'text-g-800 bg-[var(--default-box-color)] dark:!text-white dark:bg-g-300'
+ : 'hover:text-g-800 hover:bg-black/[0.04] dark:hover:bg-black/20'
+ "
+ @click="boxStyleHandlers.setBoxMode(option.type)"
+ >
+ {{ option.label }}
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import SectionTitle from './SectionTitle.vue'
+
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useSettingsConfig } from '../composables/useSettingsConfig'
+ import { useSettingsHandlers } from '../composables/useSettingsHandlers'
+ import { storeToRefs } from 'pinia'
+ const settingStore = useSettingStore()
+ const { boxBorderMode } = storeToRefs(settingStore)
+ const { boxStyleOptions } = useSettingsConfig()
+ const { boxStyleHandlers } = useSettingsHandlers()
+ const isActive = (type) => {
+ return type === 'border-mode' ? boxBorderMode.value : !boxBorderMode.value
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue
new file mode 100644
index 0000000..451f86b
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue
@@ -0,0 +1,35 @@
+<template>
+ <div>
+ <SectionTitle :title="$t('setting.color.title')" class="mt-10" />
+ <div class="-mr-4">
+ <div class="flex flex-wrap">
+ <div
+ v-for="color in configOptions.mainColors"
+ :key="color"
+ class="flex items-center justify-center size-[23px] mr-4 mb-2.5 cursor-pointer rounded-full transition-all duration-200 hover:opacity-85"
+ :style="{ background: `${color} !important` }"
+ @click="colorHandlers.selectColor(color)"
+ >
+ <ArtSvgIcon
+ icon="ri:check-fill"
+ class="text-base !text-white"
+ v-show="color === systemThemeColor"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import SectionTitle from './SectionTitle.vue'
+
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useSettingsConfig } from '../composables/useSettingsConfig'
+ import { useSettingsHandlers } from '../composables/useSettingsHandlers'
+ import { storeToRefs } from 'pinia'
+ const settingStore = useSettingStore()
+ const { systemThemeColor } = storeToRefs(settingStore)
+ const { configOptions } = useSettingsConfig()
+ const { colorHandlers } = useSettingsHandlers()
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue
new file mode 100644
index 0000000..b9d11e0
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue
@@ -0,0 +1,33 @@
+<template>
+ <div>
+ <SectionTitle :title="$t('setting.container.title')" class="mt-12.5" />
+ <div class="flex">
+ <div
+ v-for="option in containerWidthOptions"
+ :key="option.value"
+ class="flex-cc flex-1 h-16 mt-5 mr-3.5 mb-3.5 cursor-pointer !border-2 rounded-lg !text-g-800 last:mr-0"
+ :class="{
+ 'border-theme [&_i]:!text-theme': containerWidth === option.value,
+ 'border-full-d': containerWidth !== option.value
+ }"
+ @click="containerHandlers.setWidth(option.value)"
+ >
+ <ArtSvgIcon :icon="option.icon" class="mr-2 text-lg" />
+ <span class="text-sm">{{ option.label }}</span>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import SectionTitle from './SectionTitle.vue'
+
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useSettingsConfig } from '../composables/useSettingsConfig'
+ import { useSettingsHandlers } from '../composables/useSettingsHandlers'
+ import { storeToRefs } from 'pinia'
+ const settingStore = useSettingStore()
+ const { containerWidth } = storeToRefs(settingStore)
+ const { containerWidthOptions } = useSettingsConfig()
+ const { containerHandlers } = useSettingsHandlers()
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue
new file mode 100644
index 0000000..bbca03c
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue
@@ -0,0 +1,31 @@
+<template>
+ <div v-if="width > 1000">
+ <SectionTitle :title="$t('setting.menuType.title')" />
+ <div class="setting-box-wrap">
+ <div
+ class="setting-item"
+ v-for="(item, index) in configOptions.menuLayoutList"
+ :key="item.value"
+ @click="switchMenuLayouts(item.value)"
+ >
+ <div class="box" :class="{ 'is-active': item.value === menuType, 'mt-16': index > 2 }">
+ <img :src="item.img" />
+ </div>
+ <p class="name">{{ $t(`setting.menuType.list[${index}]`) }}</p>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import SectionTitle from './SectionTitle.vue'
+
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useSettingsConfig } from '../composables/useSettingsConfig'
+ import { useSettingsState } from '../composables/useSettingsState'
+ const { width } = useWindowSize()
+ const settingStore = useSettingStore()
+ const { menuType } = storeToRefs(settingStore)
+ const { configOptions } = useSettingsConfig()
+ const { switchMenuLayouts } = useSettingsState()
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue
new file mode 100644
index 0000000..19bb502
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue
@@ -0,0 +1,39 @@
+<template>
+ <SectionTitle :title="$t('setting.menu.title')" />
+ <div class="setting-box-wrap">
+ <div
+ class="setting-item"
+ v-for="item in menuThemeList"
+ :key="item.theme"
+ @click="switchMenuStyles(item.theme)"
+ >
+ <div
+ class="box"
+ :class="{ 'is-active': item.theme === menuThemeType }"
+ :style="{
+ cursor: disabled ? 'no-drop' : 'pointer'
+ }"
+ >
+ <img :src="item.img" />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import AppConfig from '@/config'
+ import { MenuTypeEnum } from '@/enums/appEnum'
+ import { useSettingStore } from '@/store/modules/setting'
+ const menuThemeList = AppConfig.themeList
+ const settingStore = useSettingStore()
+ const { menuThemeType, menuType, isDark } = storeToRefs(settingStore)
+ const isTopMenu = computed(() => menuType.value === MenuTypeEnum.TOP)
+ const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
+ const disabled = computed(() => isTopMenu.value || isDualMenu.value || isDark.value)
+ const switchMenuStyles = (theme) => {
+ if (isDualMenu.value || isTopMenu.value || isDark.value) {
+ return
+ }
+ settingStore.switchMenuStyles(theme)
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue
new file mode 100644
index 0000000..ab47911
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue
@@ -0,0 +1,15 @@
+<template>
+ <p
+ class="relative mt-7.5 mb-5.5 text-sm text-center text-g-800 before:absolute before:top-[10px] before:left-0 before:w-[50px] before:m-auto before:content-[''] before:border-b before:border-[var(--art-gray-300)] after:absolute after:top-[10px] after:right-0 after:w-[50px] after:m-auto after:content-[''] after:border-b after:border-g-300"
+ :style="style"
+ >
+ {{ title }}
+ </p>
+</template>
+
+<script setup>
+ defineProps({
+ title: { required: true },
+ style: { required: false }
+ })
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue
new file mode 100644
index 0000000..c11991f
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue
@@ -0,0 +1,172 @@
+<!-- 璁剧疆鎿嶄綔鎸夐挳 -->
+<template>
+ <div
+ class="mt-10 flex gap-8 border-t border-[var(--default-border)] bg-[var(--art-bg-color)] pt-5"
+ >
+ <ElButton type="primary" class="flex-1 !h-8" @click="handleCopyConfig">
+ {{ $t('setting.actions.copyConfig') }}
+ </ElButton>
+ <ElButton type="danger" plain class="flex-1 !h-8" @click="handleResetConfig">
+ {{ $t('setting.actions.resetConfig') }}
+ </ElButton>
+ </div>
+</template>
+
+<script setup>
+ import { nextTick } from 'vue'
+ import { useSettingStore } from '@/store/modules/setting'
+ import { SETTING_DEFAULT_CONFIG } from '@/config/setting'
+ import { useClipboard } from '@vueuse/core'
+ import { useI18n } from 'vue-i18n'
+ import { MenuThemeEnum } from '@/enums/appEnum'
+ import { useTheme } from '@/hooks/core/useTheme'
+ defineOptions({ name: 'SettingActions' })
+ const { t } = useI18n()
+ const settingStore = useSettingStore()
+ const { copy, copied } = useClipboard()
+ const { switchThemeStyles } = useTheme()
+ const ENUM_MAPS = {
+ menuType: {
+ left: 'MenuTypeEnum.LEFT',
+ top: 'MenuTypeEnum.TOP',
+ 'top-left': 'MenuTypeEnum.TOP_LEFT',
+ 'dual-menu': 'MenuTypeEnum.DUAL_MENU'
+ },
+ systemTheme: {
+ auto: 'SystemThemeEnum.AUTO',
+ light: 'SystemThemeEnum.LIGHT',
+ dark: 'SystemThemeEnum.DARK'
+ },
+ menuTheme: {
+ design: 'MenuThemeEnum.DESIGN',
+ light: 'MenuThemeEnum.LIGHT',
+ dark: 'MenuThemeEnum.DARK'
+ },
+ containerWidth: {
+ '100%': 'ContainerWidthEnum.FULL',
+ '1200px': 'ContainerWidthEnum.BOXED'
+ }
+ }
+ const CONFIG_ITEMS = [
+ { comment: '鑿滃崟绫诲瀷', key: 'menuType', enumMap: ENUM_MAPS.menuType },
+ { comment: '鑿滃崟灞曞紑瀹藉害', key: 'menuOpenWidth' },
+ { comment: '鑿滃崟鏄惁灞曞紑', key: 'menuOpen' },
+ { comment: '鍙岃彍鍗曟槸鍚︽樉绀烘枃鏈�', key: 'dualMenuShowText' },
+ { comment: '绯荤粺涓婚绫诲瀷', key: 'systemThemeType', enumMap: ENUM_MAPS.systemTheme },
+ { comment: '绯荤粺涓婚妯″紡', key: 'systemThemeMode', enumMap: ENUM_MAPS.systemTheme },
+ { comment: '鑿滃崟椋庢牸', key: 'menuThemeType', enumMap: ENUM_MAPS.menuTheme },
+ { comment: '绯荤粺涓婚棰滆壊', key: 'systemThemeColor' },
+ { comment: '鏄惁鏄剧ず鑿滃崟鎸夐挳', key: 'showMenuButton' },
+ { comment: '鏄惁鏄剧ず蹇�熷叆鍙�', key: 'showFastEnter' },
+ { comment: '鏄惁鏄剧ず鍒锋柊鎸夐挳', key: 'showRefreshButton' },
+ { comment: '鏄惁鏄剧ず闈㈠寘灞�', key: 'showCrumbs' },
+ { comment: '鏄惁鏄剧ず宸ヤ綔鍙版爣绛�', key: 'showWorkTab' },
+ { comment: '鏄惁鏄剧ず璇█鍒囨崲', key: 'showLanguage' },
+ { comment: '鏄惁鏄剧ず杩涘害鏉�', key: 'showNprogress' },
+ { comment: '鏄惁鏄剧ず璁剧疆寮曞', key: 'showSettingGuide' },
+ { comment: '鏄惁鏄剧ず鑺傛棩鏂囨湰', key: 'showFestivalText' },
+ { comment: '鏄惁鏄剧ず姘村嵃', key: 'watermarkVisible' },
+ { comment: '鏄惁鑷姩鍏抽棴', key: 'autoClose' },
+ { comment: '鏄惁鍞竴灞曞紑', key: 'uniqueOpened' },
+ { comment: '鏄惁鑹插急妯″紡', key: 'colorWeak' },
+ { comment: '鏄惁鍒锋柊', key: 'refresh' },
+ { comment: '鏄惁鍔犺浇鑺傛棩鐑熻姳', key: 'holidayFireworksLoaded' },
+ { comment: '杈规妯″紡', key: 'boxBorderMode' },
+ { comment: '椤甸潰杩囨浮鏁堟灉', key: 'pageTransition' },
+ { comment: '鏍囩椤垫牱寮�', key: 'tabStyle' },
+ { comment: '鑷畾涔夊渾瑙�', key: 'customRadius' },
+ { comment: '瀹瑰櫒瀹藉害', key: 'containerWidth', enumMap: ENUM_MAPS.containerWidth },
+ { comment: '鑺傛棩鏃ユ湡', key: 'festivalDate', forceValue: '' }
+ ]
+ const valueToCode = (value, enumMap) => {
+ if (value === null) return 'null'
+ if (value === void 0) return 'undefined'
+ if (enumMap && typeof value === 'string' && enumMap[value]) {
+ return enumMap[value]
+ }
+ if (typeof value === 'string') return `'${value}'`
+ if (typeof value === 'boolean' || typeof value === 'number') return String(value)
+ return JSON.stringify(value)
+ }
+ const generateConfigCode = () => {
+ const lines = ['export const SETTING_DEFAULT_CONFIG = {']
+ CONFIG_ITEMS.forEach((item) => {
+ lines.push(` /** ${item.comment} */`)
+ const value = item.forceValue !== void 0 ? item.forceValue : settingStore[item.key]
+ lines.push(` ${String(item.key)}: ${valueToCode(value, item.enumMap)},`)
+ })
+ lines.push('}')
+ return lines.join('\n')
+ }
+ const handleCopyConfig = async () => {
+ try {
+ const configText = generateConfigCode()
+ await copy(configText)
+ if (copied.value) {
+ ElMessage.success({
+ message: t('setting.actions.copySuccess'),
+ duration: 3e3
+ })
+ }
+ } catch (error) {
+ console.error('澶嶅埗閰嶇疆澶辫触:', error)
+ ElMessage.error(t('setting.actions.copyFailed'))
+ }
+ }
+ const toggleIfDifferent = (currentValue, defaultValue, toggleFn) => {
+ if (currentValue !== defaultValue) {
+ toggleFn()
+ }
+ }
+ const handleResetConfig = async () => {
+ try {
+ const config = SETTING_DEFAULT_CONFIG
+ settingStore.switchMenuLayouts(config.menuType)
+ settingStore.setMenuOpenWidth(config.menuOpenWidth)
+ settingStore.setMenuOpen(config.menuOpen)
+ settingStore.setDualMenuShowText(config.dualMenuShowText)
+ switchThemeStyles(config.systemThemeMode)
+ await nextTick()
+ const menuTheme = settingStore.isDark ? MenuThemeEnum.DARK : config.menuThemeType
+ settingStore.switchMenuStyles(menuTheme)
+ settingStore.setElementTheme(config.systemThemeColor)
+ toggleIfDifferent(settingStore.showMenuButton, config.showMenuButton, () =>
+ settingStore.setButton()
+ )
+ toggleIfDifferent(settingStore.showFastEnter, config.showFastEnter, () =>
+ settingStore.setFastEnter()
+ )
+ toggleIfDifferent(settingStore.showRefreshButton, config.showRefreshButton, () =>
+ settingStore.setShowRefreshButton()
+ )
+ toggleIfDifferent(settingStore.showCrumbs, config.showCrumbs, () => settingStore.setCrumbs())
+ toggleIfDifferent(settingStore.showLanguage, config.showLanguage, () =>
+ settingStore.setLanguage()
+ )
+ toggleIfDifferent(settingStore.showNprogress, config.showNprogress, () =>
+ settingStore.setNprogress()
+ )
+ settingStore.setWorkTab(config.showWorkTab)
+ settingStore.setShowFestivalText(config.showFestivalText)
+ settingStore.setWatermarkVisible(config.watermarkVisible)
+ toggleIfDifferent(settingStore.autoClose, config.autoClose, () => settingStore.setAutoClose())
+ toggleIfDifferent(settingStore.uniqueOpened, config.uniqueOpened, () =>
+ settingStore.setUniqueOpened()
+ )
+ toggleIfDifferent(settingStore.colorWeak, config.colorWeak, () => settingStore.setColorWeak())
+ toggleIfDifferent(settingStore.boxBorderMode, config.boxBorderMode, () =>
+ settingStore.setBorderMode()
+ )
+ settingStore.setPageTransition(config.pageTransition)
+ settingStore.setTabStyle(config.tabStyle)
+ settingStore.setCustomRadius(config.customRadius)
+ settingStore.setContainerWidth(config.containerWidth)
+ settingStore.setFestivalDate(config.festivalDate)
+ settingStore.setholidayFireworksLoaded(config.holidayFireworksLoaded)
+ location.reload()
+ } catch (error) {
+ console.error('閲嶇疆閰嶇疆澶辫触:', error)
+ ElMessage.error(t('setting.actions.resetFailed'))
+ }
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue
new file mode 100644
index 0000000..cb90736
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue
@@ -0,0 +1,39 @@
+<template>
+ <div class="setting-drawer">
+ <ElDrawer
+ size="300px"
+ v-model="visible"
+ :lock-scroll="true"
+ :with-header="false"
+ :before-close="handleClose"
+ :destroy-on-close="false"
+ modal-class="setting-modal"
+ @open="handleOpen"
+ @close="handleDrawerClose"
+ >
+ <div class="drawer-con">
+ <slot />
+ </div>
+ </ElDrawer>
+ </div>
+</template>
+
+<script setup>
+ const props = defineProps({
+ modelValue: { required: true }
+ })
+ const emit = defineEmits(['update:modelValue', 'open', 'close'])
+ const visible = computed({
+ get: () => props.modelValue,
+ set: (value) => emit('update:modelValue', value)
+ })
+ const handleOpen = () => {
+ emit('open')
+ }
+ const handleDrawerClose = () => {
+ emit('close')
+ }
+ const handleClose = () => {
+ visible.value = false
+ }
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue
new file mode 100644
index 0000000..8c97c5b
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue
@@ -0,0 +1,16 @@
+<template>
+ <div>
+ <div class="flex justify-end">
+ <div
+ @click="$emit('close')"
+ class="flex-cc c-p size-7.5 !transition-all duration-200 rounded hover:bg-g-300/80"
+ >
+ <ArtSvgIcon icon="ri:close-fill" class="block text-xl text-g-600" />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ defineEmits(['close'])
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue
new file mode 100644
index 0000000..19728f5
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue
@@ -0,0 +1,70 @@
+<template>
+ <div class="flex-cb mb-4 last:mb-2" :class="{ 'mobile-hide': config.mobileHide }">
+ <span class="text-sm">{{ config.label }}</span>
+
+ <!-- 寮�鍏崇被鍨� -->
+ <ElSwitch v-if="config.type === 'switch'" :model-value="modelValue" @change="handleChange" />
+
+ <!-- 鏁板瓧杈撳叆绫诲瀷 -->
+ <ElInputNumber
+ v-else-if="config.type === 'input-number'"
+ :model-value="modelValue"
+ :min="config.min"
+ :max="config.max"
+ :step="config.step"
+ :style="config.style"
+ :controls-position="config.controlsPosition"
+ @change="handleChange"
+ />
+
+ <!-- 閫夋嫨鍣ㄧ被鍨� -->
+ <ElSelect
+ v-else-if="config.type === 'select'"
+ :model-value="modelValue"
+ :style="config.style"
+ @change="handleChange"
+ >
+ <ElOption
+ v-for="option in normalizedOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </ElSelect>
+ </div>
+</template>
+
+<script setup>
+ const props = defineProps({
+ config: { required: true },
+ modelValue: { required: true }
+ })
+ const emit = defineEmits(['change'])
+ const normalizedOptions = computed(() => {
+ if (!props.config.options) return []
+ try {
+ if (typeof props.config.options === 'object' && 'value' in props.config.options) {
+ return props.config.options.value || []
+ }
+ return Array.isArray(props.config.options) ? props.config.options : []
+ } catch (error) {
+ console.warn('Error processing options for config:', props.config.key, error)
+ return []
+ }
+ })
+ const handleChange = (value) => {
+ try {
+ emit('change', value)
+ } catch (error) {
+ console.error('Error handling change for config:', props.config.key, error)
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ @media screen and (width <= 768px) {
+ .mobile-hide {
+ display: none !important;
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue b/rsf-design/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue
new file mode 100644
index 0000000..3a20027
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue
@@ -0,0 +1,28 @@
+<template>
+ <SectionTitle :title="$t('setting.theme.title')" />
+ <div class="setting-box-wrap">
+ <div
+ class="setting-item"
+ v-for="(item, index) in configOptions.themeList"
+ :key="item.theme"
+ @click="switchThemeStyles(item.theme)"
+ >
+ <div class="box" :class="{ 'is-active': item.theme === systemThemeMode }">
+ <img :src="item.img" />
+ </div>
+ <p class="name">{{ $t(`setting.theme.list[${index}]`) }}</p>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import SectionTitle from './SectionTitle.vue'
+
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useSettingsConfig } from '../composables/useSettingsConfig'
+ import { useTheme } from '@/hooks/core/useTheme'
+ const settingStore = useSettingStore()
+ const { systemThemeMode } = storeToRefs(settingStore)
+ const { configOptions } = useSettingsConfig()
+ const { switchThemeStyles } = useTheme()
+</script>
diff --git a/rsf-design/src/components/core/layouts/art-work-tab/index.vue b/rsf-design/src/components/core/layouts/art-work-tab/index.vue
new file mode 100644
index 0000000..8930b3e
--- /dev/null
+++ b/rsf-design/src/components/core/layouts/art-work-tab/index.vue
@@ -0,0 +1,494 @@
+<!-- 鏍囩椤� -->
+<template>
+ <div
+ v-if="showWorkTab"
+ class="box-border flex-b w-full px-5 mb-3 select-none max-sm:px-[15px]"
+ :class="[
+ tabStyle === 'tab-card' ? 'py-1 border-b border-[var(--art-card-border)]' : '',
+ tabStyle === 'tab-google' ? 'pt-1 pb-0 border-b border-[var(--art-card-border)]' : ''
+ ]"
+ >
+ <div class="w-full overflow-hidden" ref="scrollRef">
+ <ul
+ class="float-left whitespace-nowrap !bg-transparent flex"
+ :class="[tabStyle === 'tab-google' ? 'pl-1' : '']"
+ ref="tabsRef"
+ :style="{
+ transform: `translateX(${scrollState.translateX}px)`,
+ transition: `${scrollState.transition}`
+ }"
+ >
+ <li
+ class="art-card-xs inline-flex flex-cc h-8 mr-1.5 text-xs c-p hover:text-theme group"
+ :class="[
+ item.path === activeTab ? 'activ-tab !text-theme' : 'text-g-600 dark:text-g-800',
+ tabStyle === 'tab-google' ? 'google-tab relative !h-8 !leading-8 !border-none' : ''
+ ]"
+ :style="{
+ padding: item.fixedTab ? '0 10px' : '0 8px 0 12px',
+ borderRadius:
+ tabStyle === 'tab-google'
+ ? 'calc(var(--custom-radius) / 2.5 + 4px) !important'
+ : 'calc(var(--custom-radius) / 2.5 + 2px) !important'
+ }"
+ v-for="(item, index) in list"
+ :key="item.path"
+ :ref="item.path"
+ :id="`scroll-li-${index}`"
+ @click="clickTab(item)"
+ @contextmenu.prevent="(e) => showMenu(e, item.path)"
+ >
+ <ArtSvgIcon
+ v-show="item.icon"
+ :icon="item.icon"
+ class="text-base mr-1 group-hover:text-theme"
+ :class="item.path === activeTab ? 'text-theme' : 'text-g-600'"
+ />
+ {{ item.customTitle || formatMenuTitle(item.title) }}
+ <span
+ v-if="list.length > 1 && !item.fixedTab"
+ class="inline-flex flex-cc relative ml-0.5 p-1 rounded-full tad-200 hover:bg-g-200"
+ @click.stop="closeWorktab('current', item.path)"
+ >
+ <ArtSvgIcon icon="ri:close-large-fill" class="text-[10px] text-g-600" />
+ </span>
+ <div
+ v-if="tabStyle === 'tab-google'"
+ class="line absolute top-0 bottom-0 left-0 w-px h-4 my-auto bg-g-400 transition-opacity duration-150"
+ />
+ </li>
+ </ul>
+ </div>
+
+ <div class="flex">
+ <div
+ class="flex-cc art-card-xs relative top-0 size-8 leading-8 text-center c-p tad-200 hover:!bg-hover-color"
+ :style="{
+ borderRadius: 'calc(var(--custom-radius) / 2.5 + 0px)',
+ marginTop: tabStyle === 'tab-google' ? '-2px' : ''
+ }"
+ @click="(e) => showMenu(e, activeTab)"
+ >
+ <ArtSvgIcon icon="iconamoon:arrow-down-2-thin" class="text-2xl text-g-700" />
+ </div>
+ </div>
+
+ <ArtMenuRight
+ ref="menuRef"
+ :menu-items="menuItems"
+ :menu-width="140"
+ :border-radius="10"
+ @select="handleSelect"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { formatMenuTitle } from '@/utils/router'
+
+ import { computed, onMounted, ref, watch, nextTick, onUnmounted } from 'vue'
+ import { useRoute, useRouter } from 'vue-router'
+ import { useI18n } from 'vue-i18n'
+ import { storeToRefs } from 'pinia'
+ import { useWorktabStore } from '@/store/modules/worktab'
+ import { useUserStore } from '@/store/modules/user'
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useCommon } from '@/hooks/core/useCommon'
+ defineOptions({ name: 'ArtWorkTab' })
+ const { t } = useI18n()
+ const store = useWorktabStore()
+ const userStore = useUserStore()
+ const route = useRoute()
+ const router = useRouter()
+ const { currentRoute } = router
+ const settingStore = useSettingStore()
+ const { tabStyle, showWorkTab } = storeToRefs(settingStore)
+ const scrollRef = ref(null)
+ const tabsRef = ref(null)
+ const menuRef = ref()
+ const scrollState = ref({
+ translateX: 0,
+ transition: ''
+ })
+ const touchState = ref({
+ startX: 0,
+ currentX: 0
+ })
+ const clickedPath = ref('')
+ const list = computed(() => store.opened)
+ const activeTab = computed(() => currentRoute.value.path)
+ const activeTabIndex = computed(() => list.value.findIndex((tab) => tab.path === activeTab.value))
+ const useContextMenu = () => {
+ const getClickedTabInfo = () => {
+ const clickedIndex = list.value.findIndex((tab) => tab.path === clickedPath.value)
+ const currentTab = list.value[clickedIndex]
+ return {
+ clickedIndex,
+ currentTab,
+ isLastTab: clickedIndex === list.value.length - 1,
+ isOneTab: list.value.length === 1,
+ isCurrentTab: clickedPath.value === activeTab.value
+ }
+ }
+ const checkTabsFixedStatus = (clickedIndex) => {
+ const leftTabs = list.value.slice(0, clickedIndex)
+ const rightTabs = list.value.slice(clickedIndex + 1)
+ const otherTabs = list.value.filter((_, index) => index !== clickedIndex)
+ return {
+ areAllLeftTabsFixed: leftTabs.length > 0 && leftTabs.every((tab) => tab.fixedTab),
+ areAllRightTabsFixed: rightTabs.length > 0 && rightTabs.every((tab) => tab.fixedTab),
+ areAllOtherTabsFixed: otherTabs.length > 0 && otherTabs.every((tab) => tab.fixedTab),
+ areAllTabsFixed: list.value.every((tab) => tab.fixedTab)
+ }
+ }
+ const menuItems2 = computed(() => {
+ const { clickedIndex, currentTab, isLastTab, isOneTab, isCurrentTab } = getClickedTabInfo()
+ const fixedStatus = checkTabsFixedStatus(clickedIndex)
+ return [
+ {
+ key: 'refresh',
+ label: t('worktab.btn.refresh'),
+ icon: 'ri:refresh-line',
+ disabled: !isCurrentTab
+ },
+ {
+ key: 'fixed',
+ label: currentTab?.fixedTab ? t('worktab.btn.unfixed') : t('worktab.btn.fixed'),
+ icon: 'ri:pushpin-2-line',
+ disabled: false,
+ showLine: true
+ },
+ {
+ key: 'left',
+ label: t('worktab.btn.closeLeft'),
+ icon: 'ri:arrow-left-s-line',
+ disabled: clickedIndex === 0 || fixedStatus.areAllLeftTabsFixed
+ },
+ {
+ key: 'right',
+ label: t('worktab.btn.closeRight'),
+ icon: 'ri:arrow-right-s-line',
+ disabled: isLastTab || fixedStatus.areAllRightTabsFixed
+ },
+ {
+ key: 'other',
+ label: t('worktab.btn.closeOther'),
+ icon: 'ri:close-fill',
+ disabled: isOneTab || fixedStatus.areAllOtherTabsFixed
+ },
+ {
+ key: 'all',
+ label: t('worktab.btn.closeAll'),
+ icon: 'ri:close-circle-line',
+ disabled: isOneTab || fixedStatus.areAllTabsFixed
+ }
+ ]
+ })
+ return { menuItems: menuItems2 }
+ }
+ const useScrolling = () => {
+ const setTransition2 = () => {
+ scrollState.value.transition = 'transform 0.5s cubic-bezier(0.15, 0, 0.15, 1)'
+ setTimeout(() => {
+ scrollState.value.transition = ''
+ }, 250)
+ }
+ const getCurrentTabElement = () => {
+ return document.getElementById(`scroll-li-${activeTabIndex.value}`)
+ }
+ const calculateScrollPosition = () => {
+ if (!scrollRef.value || !tabsRef.value) return
+ const scrollWidth = scrollRef.value.offsetWidth
+ const ulWidth = tabsRef.value.offsetWidth
+ const curTabEl = getCurrentTabElement()
+ if (!curTabEl) return
+ const { offsetLeft, clientWidth } = curTabEl
+ const curTabRight = offsetLeft + clientWidth
+ const targetLeft = scrollWidth - curTabRight
+ return {
+ scrollWidth,
+ ulWidth,
+ offsetLeft,
+ clientWidth,
+ curTabRight,
+ targetLeft
+ }
+ }
+ const autoPositionTab2 = () => {
+ const positions = calculateScrollPosition()
+ if (!positions) return
+ const { scrollWidth, ulWidth, offsetLeft, curTabRight, targetLeft } = positions
+ if (
+ (offsetLeft > Math.abs(scrollState.value.translateX) && curTabRight <= scrollWidth) ||
+ (scrollState.value.translateX < targetLeft && targetLeft < 0)
+ ) {
+ return
+ }
+ requestAnimationFrame(() => {
+ if (curTabRight > scrollWidth) {
+ scrollState.value.translateX = Math.max(targetLeft - 6, scrollWidth - ulWidth)
+ } else if (offsetLeft < Math.abs(scrollState.value.translateX)) {
+ scrollState.value.translateX = -offsetLeft
+ }
+ })
+ }
+ const adjustPositionAfterClose2 = () => {
+ const positions = calculateScrollPosition()
+ if (!positions) return
+ const { scrollWidth, ulWidth, offsetLeft, clientWidth } = positions
+ const curTabLeft = offsetLeft + clientWidth
+ requestAnimationFrame(() => {
+ scrollState.value.translateX = curTabLeft > scrollWidth ? scrollWidth - ulWidth : 0
+ })
+ }
+ return {
+ setTransition: setTransition2,
+ autoPositionTab: autoPositionTab2,
+ adjustPositionAfterClose: adjustPositionAfterClose2
+ }
+ }
+ const useEventHandlers = () => {
+ const { setTransition: setTransition2, adjustPositionAfterClose: adjustPositionAfterClose2 } =
+ useScrolling()
+ const handleWheelScroll = (event) => {
+ if (!scrollRef.value || !tabsRef.value) return
+ event.preventDefault()
+ if (tabsRef.value.offsetWidth <= scrollRef.value.offsetWidth) return
+ const xMax = 0
+ const xMin = scrollRef.value.offsetWidth - tabsRef.value.offsetWidth
+ const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY
+ scrollState.value.translateX = Math.min(
+ Math.max(scrollState.value.translateX - delta, xMin),
+ xMax
+ )
+ }
+ const handleTouchStart = (event) => {
+ touchState.value.startX = event.touches[0].clientX
+ }
+ const handleTouchMove = (event) => {
+ if (!scrollRef.value || !tabsRef.value) return
+ touchState.value.currentX = event.touches[0].clientX
+ const deltaX = touchState.value.currentX - touchState.value.startX
+ const xMin = scrollRef.value.offsetWidth - tabsRef.value.offsetWidth
+ scrollState.value.translateX = Math.min(
+ Math.max(scrollState.value.translateX + deltaX, xMin),
+ 0
+ )
+ touchState.value.startX = touchState.value.currentX
+ }
+ const handleTouchEnd = () => {
+ setTransition2()
+ }
+ const setupEventListeners2 = () => {
+ if (tabsRef.value) {
+ tabsRef.value.addEventListener('wheel', handleWheelScroll, { passive: false })
+ tabsRef.value.addEventListener('touchstart', handleTouchStart, { passive: true })
+ tabsRef.value.addEventListener('touchmove', handleTouchMove, { passive: true })
+ tabsRef.value.addEventListener('touchend', handleTouchEnd, { passive: true })
+ }
+ }
+ const cleanupEventListeners2 = () => {
+ if (tabsRef.value) {
+ tabsRef.value.removeEventListener('wheel', handleWheelScroll)
+ tabsRef.value.removeEventListener('touchstart', handleTouchStart)
+ tabsRef.value.removeEventListener('touchmove', handleTouchMove)
+ tabsRef.value.removeEventListener('touchend', handleTouchEnd)
+ }
+ }
+ return {
+ setupEventListeners: setupEventListeners2,
+ cleanupEventListeners: cleanupEventListeners2,
+ adjustPositionAfterClose: adjustPositionAfterClose2
+ }
+ }
+ const useTabOperations = (adjustPositionAfterClose2) => {
+ const clickTab2 = (item) => {
+ router.push({
+ path: item.path,
+ query: item.query
+ })
+ }
+ const closeWorktab2 = (type, tabPath) => {
+ const path = typeof tabPath === 'string' ? tabPath : route.path
+ const closeActions = {
+ current: () => store.removeTab(path),
+ left: () => store.removeLeft(path),
+ right: () => store.removeRight(path),
+ other: () => store.removeOthers(path),
+ all: () => store.removeAll()
+ }
+ closeActions[type]?.()
+ setTimeout(() => {
+ adjustPositionAfterClose2()
+ }, 100)
+ }
+ const showMenu2 = (e, path) => {
+ clickedPath.value = path || ''
+ menuRef.value?.show(e)
+ e.preventDefault()
+ e.stopPropagation()
+ }
+ const handleSelect2 = (item) => {
+ const { key } = item
+ if (key === 'refresh') {
+ useCommon().refresh()
+ return
+ }
+ if (key === 'fixed') {
+ useWorktabStore().toggleFixedTab(clickedPath.value)
+ return
+ }
+ const activeIndex = list.value.findIndex((tab) => tab.path === activeTab.value)
+ const clickedIndex = list.value.findIndex((tab) => tab.path === clickedPath.value)
+ const navigationRules = {
+ left: activeIndex < clickedIndex,
+ right: activeIndex > clickedIndex,
+ other: true
+ }
+ const shouldNavigate = navigationRules[key]
+ if (shouldNavigate) {
+ router.push(clickedPath.value)
+ }
+ closeWorktab2(key, clickedPath.value)
+ }
+ return {
+ clickTab: clickTab2,
+ closeWorktab: closeWorktab2,
+ showMenu: showMenu2,
+ handleSelect: handleSelect2
+ }
+ }
+ const { menuItems } = useContextMenu()
+ const { setTransition, autoPositionTab } = useScrolling()
+ const { setupEventListeners, cleanupEventListeners, adjustPositionAfterClose } =
+ useEventHandlers()
+ const { clickTab, closeWorktab, showMenu, handleSelect } =
+ useTabOperations(adjustPositionAfterClose)
+ onMounted(() => {
+ setupEventListeners()
+ autoPositionTab()
+ })
+ onUnmounted(() => {
+ cleanupEventListeners()
+ })
+ watch(
+ () => currentRoute.value,
+ () => {
+ setTransition()
+ autoPositionTab()
+ }
+ )
+ watch(
+ () => userStore.language,
+ () => {
+ scrollState.value.translateX = 0
+ nextTick(() => {
+ autoPositionTab()
+ })
+ }
+ )
+</script>
+
+<style scoped>
+ .google-tab.activ-tab {
+ color: var(--theme-color) !important;
+ background-color: var(--el-color-primary-light-9) !important;
+ border-bottom: 0 !important;
+ border-bottom-right-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+ }
+
+ .google-tab.activ-tab::before,
+ .google-tab.activ-tab::after {
+ position: absolute;
+ bottom: 0;
+ width: 20px;
+ height: 20px;
+ content: '';
+ border-radius: 50%;
+ box-shadow: 0 0 0 30px var(--el-color-primary-light-9);
+ }
+
+ .google-tab.activ-tab::before {
+ left: -20px;
+ clip-path: inset(50% -10px 0 50%);
+ }
+
+ .google-tab.activ-tab::after {
+ right: -20px;
+ clip-path: inset(50% 50% 0 -10px);
+ }
+
+ .dark .google-tab.activ-tab {
+ color: var(--art-gray-800) !important;
+ background-color: var(--art-hover-color) !important;
+ }
+
+ .dark .google-tab.activ-tab::before,
+ .dark .google-tab.activ-tab::after {
+ box-shadow: 0 0 0 30px var(--art-hover-color);
+ }
+
+ .google-tab:not(.activ-tab):hover {
+ box-sizing: border-box;
+ color: var(--art-gray-600) !important;
+ background-color: var(--art-gray-200) !important;
+ border-bottom: 1px solid var(--default-box-color) !important;
+ border-radius: calc(var(--custom-radius) / 2.5 + 4px) !important;
+ }
+
+ .dark .google-tab:not(.activ-tab):hover {
+ background-color: var(--art-hover-color) !important;
+ }
+
+ .google-tab:hover .line,
+ .google-tab.activ-tab .line,
+ .google-tab:first-child .line {
+ opacity: 0;
+ }
+
+ .google-tab:hover + .google-tab .line,
+ .google-tab.activ-tab + .google-tab .line {
+ opacity: 0;
+ }
+
+ .google-tab::before,
+ .google-tab::after {
+ position: absolute;
+ bottom: 0;
+ width: 20px;
+ height: 20px;
+ content: '';
+ border-radius: 50%;
+ box-shadow: 0 0 0 30px transparent;
+ }
+
+ .google-tab::before {
+ left: -20px;
+ clip-path: inset(50% -10px 0 50%);
+ }
+
+ .google-tab::after {
+ right: -20px;
+ clip-path: inset(50% 50% 0 -10px);
+ }
+
+ .google-tab i:hover {
+ color: var(--art-gray-700);
+ background: var(--art-gray-300);
+ }
+
+ @media only screen and (width <= 768px) {
+ .box-border.flex.justify-between {
+ padding-right: 0.625rem;
+ padding-left: 0.625rem;
+ }
+ }
+
+ @media only screen and (width <= 640px) {
+ .box-border.flex.justify-between {
+ padding-right: 0.9375rem;
+ padding-left: 0.9375rem;
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/media/art-cutter-img/index.vue b/rsf-design/src/components/core/media/art-cutter-img/index.vue
new file mode 100644
index 0000000..f7f613d
--- /dev/null
+++ b/rsf-design/src/components/core/media/art-cutter-img/index.vue
@@ -0,0 +1,244 @@
+<!-- 鍥剧墖瑁佸壀缁勪欢 github: https://github.com/acccccccb/vue-img-cutter/tree/master -->
+<template>
+ <div class="cutter-container">
+ <div class="cutter-component">
+ <div class="title">{{ title }}</div>
+ <ImgCutter
+ ref="imgCutterModal"
+ @cutDown="cutDownImg"
+ @onPrintImg="cutterPrintImg"
+ @onImageLoadComplete="handleImageLoadComplete"
+ @onImageLoadError="handleImageLoadError"
+ @onClearAll="handleClearAll"
+ v-bind="cutterProps"
+ class="img-cutter"
+ >
+ <template #choose>
+ <ElButton type="primary" plain v-ripple>閫夋嫨鍥剧墖</ElButton>
+ </template>
+ <template #cancel>
+ <ElButton type="danger" plain v-ripple>娓呴櫎</ElButton>
+ </template>
+ <template #confirm>
+ <!-- <ElButton type="primary" style="margin-left: 10px">纭畾</ElButton> -->
+ <div></div>
+ </template>
+ </ImgCutter>
+ </div>
+
+ <div v-if="showPreview" class="preview-container">
+ <div class="title">{{ previewTitle }}</div>
+ <div
+ class="preview-box"
+ :style="{
+ width: `${cutterProps.cutWidth}px`,
+ height: `${cutterProps.cutHeight}px`
+ }"
+ >
+ <img class="preview-img" :src="temImgPath" alt="棰勮鍥�" v-if="temImgPath" />
+ </div>
+ <ElButton class="download-btn" @click="downloadImg" :disabled="!temImgPath" v-ripple
+ >涓嬭浇鍥剧墖</ElButton
+ >
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import ImgCutter from 'vue-img-cutter'
+
+ defineOptions({ name: 'ArtCutterImg' })
+ const props = defineProps({
+ isModal: { required: false, default: false },
+ tool: { required: false, default: true },
+ toolBgc: { required: false, default: '#fff' },
+ title: { required: false, default: '' },
+ previewTitle: { required: false, default: '' },
+ showPreview: { required: false, default: true },
+ boxWidth: { required: false, default: 700 },
+ boxHeight: { required: false, default: 458 },
+ cutWidth: { required: false, default: 470 },
+ cutHeight: { required: false, default: 270 },
+ sizeChange: { required: false, default: true },
+ moveAble: { required: false, default: true },
+ imgMove: { required: false, default: true },
+ scaleAble: { required: false, default: true },
+ originalGraph: { required: false, default: true },
+ crossOrigin: { required: false, default: true },
+ fileType: { required: false, default: 'png' },
+ quality: { required: false, default: 0.9 },
+ watermarkText: { required: false, default: '' },
+ watermarkFontSize: { required: false, default: 20 },
+ watermarkColor: { required: false, default: '#ffffff' },
+ saveCutPosition: { required: false, default: true },
+ previewMode: { required: false, default: true }
+ })
+ const emit = defineEmits(['update:imgUrl', 'error', 'imageLoadComplete', 'imageLoadError'])
+ const temImgPath = ref('')
+ const imgCutterModal = ref()
+ const cutterProps = computed(() => ({
+ ...props,
+ WatermarkText: props.watermarkText,
+ WatermarkFontSize: props.watermarkFontSize,
+ WatermarkColor: props.watermarkColor
+ }))
+ function preloadImage(url) {
+ return new Promise((resolve, reject) => {
+ const img = new Image()
+ img.crossOrigin = 'anonymous'
+ img.onload = () => resolve()
+ img.onerror = reject
+ img.src = url
+ })
+ }
+ async function initImgCutter() {
+ if (props.imgUrl) {
+ try {
+ await preloadImage(props.imgUrl)
+ imgCutterModal.value?.handleOpen({
+ name: '灏侀潰鍥剧墖',
+ src: props.imgUrl
+ })
+ } catch (error) {
+ emit('error', error)
+ console.error('鍥剧墖鍔犺浇澶辫触:', error)
+ }
+ }
+ }
+ onMounted(() => {
+ if (props.imgUrl) {
+ temImgPath.value = props.imgUrl
+ initImgCutter()
+ }
+ })
+ watch(
+ () => props.imgUrl,
+ (newVal) => {
+ if (newVal) {
+ temImgPath.value = newVal
+ initImgCutter()
+ }
+ }
+ )
+ function cutterPrintImg(result) {
+ temImgPath.value = result.dataURL
+ }
+ function cutDownImg(result) {
+ emit('update:imgUrl', result.dataURL)
+ }
+ function handleImageLoadComplete(result) {
+ emit('imageLoadComplete', result)
+ }
+ function handleImageLoadError(error) {
+ emit('error', error)
+ emit('imageLoadError', error)
+ }
+ function handleClearAll() {
+ temImgPath.value = ''
+ }
+ function downloadImg() {
+ console.log('涓嬭浇鍥剧墖')
+ const a = document.createElement('a')
+ a.href = temImgPath.value
+ a.download = 'image.png'
+ a.click()
+ }
+</script>
+
+<style lang="scss" scoped>
+ .cutter-container {
+ display: flex;
+ flex-flow: row wrap;
+
+ .title {
+ padding-bottom: 10px;
+ font-size: 18px;
+ font-weight: 500;
+ }
+
+ .cutter-component {
+ margin-right: 30px;
+ }
+
+ .preview-container {
+ .preview-box {
+ background-color: var(--art-active-color) !important;
+
+ .preview-img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+ }
+
+ .download-btn {
+ display: block;
+ margin: 20px auto;
+ }
+ }
+
+ :deep(.toolBoxControl) {
+ z-index: 100;
+ }
+
+ :deep(.dockMain) {
+ right: 0;
+ bottom: -40px;
+ left: 0;
+ z-index: 10;
+ padding: 0;
+ background-color: transparent !important;
+ opacity: 1;
+ }
+
+ :deep(.copyright) {
+ display: none !important;
+ }
+
+ :deep(.i-dialog-footer) {
+ margin-top: 60px !important;
+ }
+
+ :deep(.dockBtn) {
+ height: 26px;
+ padding: 0 10px;
+ font-size: 12px;
+ line-height: 26px;
+ color: var(--el-color-primary) !important;
+ background-color: var(--el-color-primary-light-9) !important;
+ border: 1px solid var(--el-color-primary-light-4) !important;
+ }
+
+ :deep(.dockBtnScrollBar) {
+ margin: 0 10px 0 6px;
+ background-color: var(--el-color-primary-light-1);
+ }
+
+ :deep(.scrollBarControl) {
+ border-color: var(--el-color-primary);
+ }
+
+ :deep(.closeIcon) {
+ line-height: 15px !important;
+ }
+ }
+
+ .dark {
+ .cutter-container {
+ :deep(.toolBox) {
+ border: transparent;
+ }
+
+ :deep(.dialogMain) {
+ background-color: transparent !important;
+ }
+
+ :deep(.i-dialog-footer) {
+ .btn {
+ background-color: var(--el-color-primary) !important;
+ border: transparent;
+ }
+ }
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/media/art-video-player/index.vue b/rsf-design/src/components/core/media/art-video-player/index.vue
new file mode 100644
index 0000000..879db23
--- /dev/null
+++ b/rsf-design/src/components/core/media/art-video-player/index.vue
@@ -0,0 +1,67 @@
+<!-- 瑙嗛鎾斁鍣ㄧ粍浠讹細https://h5player.bytedance.com/-->
+<template>
+ <div :id="playerId" />
+</template>
+
+<script setup>
+ import Player from 'xgplayer'
+ import 'xgplayer/dist/index.min.css'
+ defineOptions({ name: 'ArtVideoPlayer' })
+ const props = defineProps({
+ playerId: { required: false, default: '' },
+ videoUrl: { required: false, default: '' },
+ posterUrl: { required: false, default: '' },
+ autoplay: { required: false, default: false },
+ volume: { required: false, default: 1 },
+ loop: { required: false, default: false },
+ muted: { required: false, default: false }
+ })
+ const playerInstance = ref(null)
+ const defaultStyle = {
+ progressColor: 'rgba(255, 255, 255, 0.3)',
+ playedColor: '#00AEED',
+ cachedColor: 'rgba(255, 255, 255, 0.6)',
+ sliderBtnStyle: {
+ width: '10px',
+ height: '10px',
+ backgroundColor: '#00AEED'
+ },
+ volumeColor: '#00AEED'
+ }
+ onMounted(() => {
+ playerInstance.value = new Player({
+ id: props.playerId,
+ lang: 'zh',
+ // 璁剧疆鐣岄潰璇█涓轰腑鏂�
+ volume: props.volume,
+ autoplay: props.autoplay,
+ screenShot: true,
+ // 鍚敤鎴浘鍔熻兘
+ url: props.videoUrl,
+ poster: props.posterUrl,
+ fluid: true,
+ // 鍚敤娴佸紡甯冨眬锛岃嚜閫傚簲瀹瑰櫒澶у皬
+ playbackRate: props.playbackRates,
+ loop: props.loop,
+ muted: props.muted,
+ commonStyle: {
+ ...defaultStyle,
+ ...props.commonStyle
+ }
+ })
+ playerInstance.value.on('play', () => {
+ console.log('Video is playing')
+ })
+ playerInstance.value.on('pause', () => {
+ console.log('Video is paused')
+ })
+ playerInstance.value.on('error', (error) => {
+ console.error('Error occurred:', error)
+ })
+ })
+ onBeforeUnmount(() => {
+ if (playerInstance.value) {
+ playerInstance.value.destroy()
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/others/art-menu-right/index.vue b/rsf-design/src/components/core/others/art-menu-right/index.vue
new file mode 100644
index 0000000..b7d1047
--- /dev/null
+++ b/rsf-design/src/components/core/others/art-menu-right/index.vue
@@ -0,0 +1,303 @@
+<!-- 鍙抽敭鑿滃崟 -->
+<template>
+ <div class="menu-right">
+ <Transition name="context-menu" @before-enter="onBeforeEnter" @after-leave="onAfterLeave">
+ <div
+ v-show="visible"
+ :style="menuStyle"
+ class="context-menu art-card-xs !shadow-xl min-w-[var(--menu-width)] w-[var(--menu-width)]"
+ >
+ <ul class="menu-list m-0 list-none" :style="menuListStyle">
+ <template v-for="item in menuItems" :key="item.key">
+ <!-- 鏅�氳彍鍗曢」 -->
+ <li
+ v-if="!item.children"
+ class="menu-item relative flex-c c-p select-none rounded text-xs transition-colors duration-150 hover:bg-g-200"
+ :class="{ 'is-disabled': item.disabled, 'has-line': item.showLine }"
+ :style="menuItemStyle"
+ @click="handleMenuClick(item)"
+ >
+ <ArtSvgIcon
+ v-if="item.icon"
+ class="mr-2 shrink-0 text-base text-g-800"
+ :icon="item.icon"
+ />
+ <span
+ class="menu-label flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-g-800"
+ >{{ item.label }}</span
+ >
+ </li>
+
+ <!-- 瀛愯彍鍗� -->
+ <li
+ v-else
+ class="menu-item submenu relative flex-c c-p select-none rounded text-xs transition-colors duration-150 hover:bg-g-200"
+ :style="menuItemStyle"
+ >
+ <div class="submenu-title flex-c w-full">
+ <ArtSvgIcon
+ v-if="item.icon"
+ class="mr-2 shrink-0 text-base text-g-800"
+ :icon="item.icon"
+ />
+ <span
+ class="menu-label flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-g-800"
+ >{{ item.label }}</span
+ >
+ <ArtSvgIcon
+ icon="ri:arrow-right-s-line"
+ class="ubmenu-arrow ml-auto mr-0 text-base text-g-500 transition-transform duration-150"
+ />
+ </div>
+ <ul
+ class="submenu-list art-card-xs absolute left-full top-0 z-[2001] hidden w-max min-w-max list-none !shadow-xl"
+ :style="submenuListStyle"
+ >
+ <li
+ v-for="child in item.children"
+ :key="child.key"
+ class="menu-item relative mx-1.5 flex-c c-p select-none rounded text-xs transition-colors duration-150 hover:bg-g-200"
+ :class="{ 'is-disabled': child.disabled, 'has-line': child.showLine }"
+ :style="menuItemStyle"
+ @click="handleMenuClick(child)"
+ >
+ <ArtSvgIcon
+ v-if="child.icon"
+ class="r-2 shrink-0 text-base text-g-800 mr-1"
+ :icon="child.icon"
+ />
+ <span
+ class="menu-label flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-g-800"
+ >{{ child.label }}</span
+ >
+ </li>
+ </ul>
+ </li>
+ </template>
+ </ul>
+ </div>
+ </Transition>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtMenuRight' })
+ const props = defineProps({
+ menuItems: { required: false, default: () => [] },
+ menuWidth: { required: false, default: 120 },
+ submenuWidth: { required: false, default: 150 },
+ itemHeight: { required: false, default: 32 },
+ boundaryDistance: { required: false, default: 10 },
+ menuPadding: { required: false, default: 5 },
+ itemPaddingX: { required: false, default: 6 },
+ borderRadius: { required: false, default: 6 },
+ animationDuration: { required: false, default: 100 }
+ })
+ const emit = defineEmits(['select', 'show', 'hide'])
+ const visible = ref(false)
+ const position = ref({ x: 0, y: 0 })
+ let showTimer = null
+ let eventListenersAdded = false
+ const menuStyle = computed(() => ({
+ position: 'fixed',
+ left: `${position.value.x}px`,
+ top: `${position.value.y}px`,
+ zIndex: 2e3,
+ width: `${props.menuWidth}px`
+ }))
+ const menuListStyle = computed(() => ({
+ padding: `${props.menuPadding}px`
+ }))
+ const menuItemStyle = computed(() => ({
+ height: `${props.itemHeight}px`,
+ padding: `0 ${props.itemPaddingX}px`,
+ borderRadius: '4px'
+ }))
+ const submenuListStyle = computed(() => ({
+ minWidth: `${props.submenuWidth}px`,
+ padding: `${props.menuPadding}px 0`,
+ borderRadius: `${props.borderRadius}px`
+ }))
+ const calculateMenuHeight = () => {
+ let totalHeight = props.menuPadding * 2
+ props.menuItems.forEach((item) => {
+ totalHeight += props.itemHeight
+ if (item.showLine) {
+ totalHeight += 10
+ }
+ })
+ return totalHeight
+ }
+ const calculatePosition = (e) => {
+ const screenWidth = window.innerWidth
+ const screenHeight = window.innerHeight
+ const menuHeight = calculateMenuHeight()
+ let x = e.clientX
+ let y = e.clientY
+ if (x + props.menuWidth > screenWidth - props.boundaryDistance) {
+ x = Math.max(props.boundaryDistance, x - props.menuWidth)
+ }
+ if (y + menuHeight > screenHeight - props.boundaryDistance) {
+ y = Math.max(props.boundaryDistance, screenHeight - menuHeight - props.boundaryDistance)
+ }
+ x = Math.max(
+ props.boundaryDistance,
+ Math.min(x, screenWidth - props.menuWidth - props.boundaryDistance)
+ )
+ y = Math.max(
+ props.boundaryDistance,
+ Math.min(y, screenHeight - menuHeight - props.boundaryDistance)
+ )
+ return { x, y }
+ }
+ const addEventListeners = () => {
+ if (eventListenersAdded) return
+ document.addEventListener('click', handleDocumentClick)
+ document.addEventListener('contextmenu', handleDocumentContextmenu)
+ document.addEventListener('keydown', handleKeydown)
+ eventListenersAdded = true
+ }
+ const removeEventListeners = () => {
+ if (!eventListenersAdded) return
+ document.removeEventListener('click', handleDocumentClick)
+ document.removeEventListener('contextmenu', handleDocumentContextmenu)
+ document.removeEventListener('keydown', handleKeydown)
+ eventListenersAdded = false
+ }
+ const handleDocumentClick = (e) => {
+ const target = e.target
+ const menuElement = document.querySelector('.context-menu')
+ if (menuElement && menuElement.contains(target)) {
+ return
+ }
+ hide()
+ }
+ const handleDocumentContextmenu = () => {
+ hide()
+ }
+ const handleKeydown = (e) => {
+ if (e.key === 'Escape') {
+ hide()
+ }
+ }
+ const show = (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ if (showTimer) {
+ window.clearTimeout(showTimer)
+ showTimer = null
+ }
+ position.value = calculatePosition(e)
+ visible.value = true
+ emit('show')
+ showTimer = window.setTimeout(() => {
+ if (visible.value) {
+ addEventListeners()
+ }
+ showTimer = null
+ }, 50)
+ }
+ const hide = () => {
+ if (!visible.value) return
+ visible.value = false
+ emit('hide')
+ if (showTimer) {
+ window.clearTimeout(showTimer)
+ showTimer = null
+ }
+ removeEventListeners()
+ }
+ const handleMenuClick = (item) => {
+ if (item.disabled) return
+ emit('select', item)
+ hide()
+ }
+ const onBeforeEnter = (el) => {
+ const element = el
+ element.style.transformOrigin = 'top left'
+ }
+ const onAfterLeave = () => {
+ removeEventListeners()
+ if (showTimer) {
+ window.clearTimeout(showTimer)
+ showTimer = null
+ }
+ }
+ onUnmounted(() => {
+ removeEventListeners()
+ if (showTimer) {
+ window.clearTimeout(showTimer)
+ showTimer = null
+ }
+ })
+ defineExpose({
+ show,
+ hide,
+ visible: computed(() => visible.value)
+ })
+</script>
+
+<style scoped>
+ .menu-right {
+ --menu-width: v-bind('props.menuWidth + "px"');
+ --border-radius: v-bind('props.borderRadius + "px"');
+ }
+
+ .menu-item.has-line {
+ margin-bottom: 10px;
+ }
+
+ .menu-item.has-line::after {
+ position: absolute;
+ right: 0;
+ bottom: -5px;
+ left: 0;
+ height: 1px;
+ content: '';
+ background-color: var(--art-gray-300);
+ }
+
+ .menu-item.is-disabled {
+ color: var(--el-text-color-disabled);
+ cursor: not-allowed;
+ }
+
+ .menu-item.is-disabled:hover {
+ background-color: transparent !important;
+ }
+
+ .menu-item.is-disabled i:not(.submenu-arrow),
+ .menu-item.is-disabled :deep(.art-svg-icon) {
+ color: var(--el-text-color-disabled) !important;
+ }
+
+ .menu-item.is-disabled .menu-label {
+ color: var(--el-text-color-disabled) !important;
+ }
+
+ .menu-item.submenu:hover .submenu-list {
+ display: block;
+ }
+
+ .menu-item.submenu:hover .submenu-title .submenu-arrow {
+ transform: rotate(90deg);
+ }
+
+ /* 鍔ㄧ敾鏍峰紡 */
+ .context-menu-enter-active,
+ .context-menu-leave-active {
+ transition: all v-bind('props.animationDuration + "ms"') ease-out;
+ }
+
+ .context-menu-enter-from,
+ .context-menu-leave-to {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+
+ .context-menu-enter-to,
+ .context-menu-leave-from {
+ opacity: 1;
+ transform: scale(1);
+ }
+</style>
diff --git a/rsf-design/src/components/core/others/art-watermark/index.vue b/rsf-design/src/components/core/others/art-watermark/index.vue
new file mode 100644
index 0000000..31a6d19
--- /dev/null
+++ b/rsf-design/src/components/core/others/art-watermark/index.vue
@@ -0,0 +1,38 @@
+<!-- 姘村嵃缁勪欢 -->
+<template>
+ <div
+ v-if="watermarkVisible"
+ class="fixed left-0 top-0 h-screen w-screen pointer-events-none"
+ :style="{ zIndex: zIndex }"
+ >
+ <ElWatermark
+ :content="content"
+ :font="{ fontSize: fontSize, color: fontColor }"
+ :rotate="rotate"
+ :gap="[gapX, gapY]"
+ :offset="[offsetX, offsetY]"
+ >
+ <div style="height: 100vh"></div>
+ </ElWatermark>
+ </div>
+</template>
+
+<script setup>
+ import AppConfig from '@/config'
+ import { useSettingStore } from '@/store/modules/setting'
+ defineOptions({ name: 'ArtWatermark' })
+ const settingStore = useSettingStore()
+ const { watermarkVisible } = storeToRefs(settingStore)
+ defineProps({
+ content: { required: false, default: AppConfig.systemInfo.name },
+ visible: { required: false, default: false },
+ fontSize: { required: false, default: 16 },
+ fontColor: { required: false, default: 'rgba(128, 128, 128, 0.2)' },
+ rotate: { required: false, default: -22 },
+ gapX: { required: false, default: 100 },
+ gapY: { required: false, default: 100 },
+ offsetX: { required: false, default: 50 },
+ offsetY: { required: false, default: 50 },
+ zIndex: { required: false, default: 3100 }
+ })
+</script>
diff --git a/rsf-design/src/components/core/tables/art-table-header/index.vue b/rsf-design/src/components/core/tables/art-table-header/index.vue
new file mode 100644
index 0000000..ee1ce5b
--- /dev/null
+++ b/rsf-design/src/components/core/tables/art-table-header/index.vue
@@ -0,0 +1,247 @@
+<!-- 琛ㄦ牸澶撮儴锛屽寘鍚〃鏍煎ぇ灏忋�佸埛鏂般�佸叏灞忋�佸垪璁剧疆銆佸叾浠栬缃� -->
+<template>
+ <div class="flex-cb max-md:!block" id="art-table-header">
+ <div class="flex-wrap">
+ <slot name="left"></slot>
+ </div>
+
+ <div class="flex-c md:justify-end max-md:mt-3 max-sm:!hidden">
+ <div
+ v-if="showSearchBar != null"
+ class="button"
+ @click="search"
+ :class="showSearchBar ? 'active !bg-theme hover:!bg-theme/80' : ''"
+ >
+ <ArtSvgIcon icon="ri:search-line" :class="showSearchBar ? 'text-white' : 'text-g-700'" />
+ </div>
+ <div
+ v-if="shouldShow('refresh')"
+ class="button"
+ @click="refresh"
+ :class="{ loading: loading && isManualRefresh }"
+ >
+ <ArtSvgIcon
+ icon="ri:refresh-line"
+ :class="loading && isManualRefresh ? 'animate-spin text-g-600' : ''"
+ />
+ </div>
+
+ <ElDropdown v-if="shouldShow('size')" @command="handleTableSizeChange">
+ <div class="button">
+ <ArtSvgIcon icon="ri:arrow-up-down-fill" />
+ </div>
+ <template #dropdown>
+ <ElDropdownMenu>
+ <div
+ v-for="item in tableSizeOptions"
+ :key="item.value"
+ class="table-size-btn-item [&_.el-dropdown-menu__item]:!mb-[3px] last:[&_.el-dropdown-menu__item]:!mb-0"
+ >
+ <ElDropdownItem
+ :key="item.value"
+ :command="item.value"
+ :class="tableSize === item.value ? '!bg-g-300/55' : ''"
+ >
+ {{ item.label }}
+ </ElDropdownItem>
+ </div>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+
+ <div v-if="shouldShow('fullscreen')" class="button" @click="toggleFullScreen">
+ <ArtSvgIcon :icon="isFullScreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-line'" />
+ </div>
+
+ <!-- 鍒楄缃� -->
+ <ElPopover v-if="shouldShow('columns')" placement="bottom" trigger="click">
+ <template #reference>
+ <div class="button">
+ <ArtSvgIcon icon="ri:align-right" />
+ </div>
+ </template>
+ <div>
+ <ElScrollbar max-height="380px">
+ <VueDraggable
+ v-model="columns"
+ :disabled="false"
+ filter=".fixed-column"
+ :prevent-on-filter="false"
+ @move="checkColumnMove"
+ >
+ <div
+ v-for="item in columns"
+ :key="item.prop || item.type"
+ class="column-option flex-c"
+ :class="{ 'fixed-column': item.fixed }"
+ >
+ <div
+ class="drag-icon mr-2 h-4.5 flex-cc text-g-500"
+ :class="item.fixed ? 'cursor-default text-g-300' : 'cursor-move'"
+ >
+ <ArtSvgIcon
+ :icon="item.fixed ? 'ri:unpin-line' : 'ri:drag-move-2-fill'"
+ class="text-base"
+ />
+ </div>
+ <ElCheckbox
+ :model-value="getColumnVisibility(item)"
+ @update:model-value="(val) => updateColumnVisibility(item, val)"
+ :disabled="item.disabled"
+ class="flex-1 min-w-0 [&_.el-checkbox__label]:overflow-hidden [&_.el-checkbox__label]:text-ellipsis [&_.el-checkbox__label]:whitespace-nowrap"
+ >{{
+ item.label || (item.type === 'selection' ? t('table.selection') : '')
+ }}</ElCheckbox
+ >
+ </div>
+ </VueDraggable>
+ </ElScrollbar>
+ </div>
+ </ElPopover>
+ <!-- 鍏朵粬璁剧疆 -->
+ <ElPopover v-if="shouldShow('settings')" placement="bottom" trigger="click">
+ <template #reference>
+ <div class="button">
+ <ArtSvgIcon icon="ri:settings-line" />
+ </div>
+ </template>
+ <div>
+ <ElCheckbox v-if="showZebra" v-model="isZebra" :value="true">{{
+ t('table.zebra')
+ }}</ElCheckbox>
+ <ElCheckbox v-if="showBorder" v-model="isBorder" :value="true">{{
+ t('table.border')
+ }}</ElCheckbox>
+ <ElCheckbox v-if="showHeaderBackground" v-model="isHeaderBackground" :value="true">{{
+ t('table.headerBackground')
+ }}</ElCheckbox>
+ </div>
+ </ElPopover>
+ <slot name="right"></slot>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { VueDraggable } from 'vue-draggable-plus'
+
+ import { computed, ref, onMounted, onUnmounted } from 'vue'
+ import { storeToRefs } from 'pinia'
+ import { TableSizeEnum } from '@/enums/formEnum'
+ import { useTableStore } from '@/store/modules/table'
+ import { useI18n } from 'vue-i18n'
+ defineOptions({ name: 'ArtTableHeader' })
+ const { t } = useI18n()
+ const props = defineProps({
+ showZebra: { required: false, default: true },
+ showBorder: { required: false, default: true },
+ showHeaderBackground: { required: false, default: true },
+ fullClass: { required: false, default: 'art-page-view' },
+ layout: { required: false, default: 'search,refresh,size,fullscreen,columns,settings' },
+ loading: { required: false, default: false },
+ showSearchBar: { required: false, default: void 0 }
+ })
+ const columns = defineModel('columns', {
+ required: false,
+ default: () => []
+ })
+ const emit = defineEmits(['refresh', 'search', 'update:showSearchBar'])
+ const getColumnVisibility = (col) => {
+ if (col.visible !== void 0) {
+ return col.visible
+ }
+ return col.checked ?? true
+ }
+ const updateColumnVisibility = (col, value) => {
+ const boolValue = !!value
+ col.checked = boolValue
+ col.visible = boolValue
+ }
+ const tableSizeOptions = [
+ { value: TableSizeEnum.SMALL, label: t('table.sizeOptions.small') },
+ { value: TableSizeEnum.DEFAULT, label: t('table.sizeOptions.default') },
+ { value: TableSizeEnum.LARGE, label: t('table.sizeOptions.large') }
+ ]
+ const tableStore = useTableStore()
+ const { tableSize, isZebra, isBorder, isHeaderBackground } = storeToRefs(tableStore)
+ const layoutItems = computed(() => {
+ return props.layout.split(',').map((item) => item.trim())
+ })
+ const shouldShow = (componentName) => {
+ return layoutItems.value.includes(componentName)
+ }
+ const checkColumnMove = (event) => {
+ const toElement = event.related
+ if (toElement && toElement.classList.contains('fixed-column')) {
+ return false
+ }
+ return true
+ }
+ const search = () => {
+ emit('update:showSearchBar', !props.showSearchBar)
+ emit('search')
+ }
+ const refresh = () => {
+ isManualRefresh.value = true
+ emit('refresh')
+ }
+ const handleTableSizeChange = (command) => {
+ useTableStore().setTableSize(command)
+ }
+ const isManualRefresh = ref(false)
+ const isFullScreen = ref(false)
+ const originalOverflow = ref('')
+ const toggleFullScreen = () => {
+ const el = document.querySelector(`.${props.fullClass}`)
+ if (!el) return
+ isFullScreen.value = !isFullScreen.value
+ if (isFullScreen.value) {
+ originalOverflow.value = document.body.style.overflow
+ document.body.style.overflow = 'hidden'
+ el.classList.add('el-full-screen')
+ tableStore.setIsFullScreen(true)
+ } else {
+ document.body.style.overflow = originalOverflow.value
+ el.classList.remove('el-full-screen')
+ tableStore.setIsFullScreen(false)
+ }
+ }
+ const handleEscapeKey = (e) => {
+ if (e.key === 'Escape' && isFullScreen.value) {
+ toggleFullScreen()
+ }
+ }
+ onMounted(() => {
+ document.addEventListener('keydown', handleEscapeKey)
+ })
+ onUnmounted(() => {
+ document.removeEventListener('keydown', handleEscapeKey)
+ if (isFullScreen.value) {
+ document.body.style.overflow = originalOverflow.value
+ const el = document.querySelector(`.${props.fullClass}`)
+ if (el) {
+ el.classList.remove('el-full-screen')
+ }
+ }
+ })
+</script>
+
+<style scoped>
+ @reference '@styles/core/tailwind.css';
+
+ .button {
+ @apply ml-2
+ size-8
+ flex
+ items-center
+ justify-center
+ cursor-pointer
+ rounded-md
+ bg-g-300/55
+ dark:bg-g-300/40
+ text-g-700
+ hover:bg-g-300
+ md:ml-0
+ md:mr-2.5;
+ }
+</style>
diff --git a/rsf-design/src/components/core/tables/art-table/index.vue b/rsf-design/src/components/core/tables/art-table/index.vue
new file mode 100644
index 0000000..3fd2c73
--- /dev/null
+++ b/rsf-design/src/components/core/tables/art-table/index.vue
@@ -0,0 +1,257 @@
+<!-- 琛ㄦ牸缁勪欢 -->
+<!-- 鏀寔锛歟l-table 鍏ㄩ儴灞炴�с�佷簨浠躲�佹彃妲斤紝鍚屽畼鏂规枃妗e啓娉� -->
+<!-- 鎵╁睍鍔熻兘锛氬垎椤电粍浠躲�佹覆鏌撹嚜瀹氫箟鍒椼�乴oading銆佽〃鏍煎叏灞�杈规銆佹枒椹汗銆佽〃鏍煎昂瀵搞�佽〃澶磋儗鏅厤缃� -->
+<!-- 鑾峰彇 ref锛氶粯璁ゆ毚闇蹭簡 elTableRef 澶栭儴閫氳繃 ref.value.elTableRef 鍙互璋冪敤 el-table 鏂规硶 -->
+<template>
+ <div class="art-table" :class="{ 'is-empty': isEmpty }" :style="containerHeight">
+ <ElTable ref="elTableRef" v-loading="!!loading" v-bind="mergedTableProps">
+ <template v-for="col in columns" :key="col.prop || col.type">
+ <!-- 娓叉煋鍏ㄥ眬搴忓彿鍒� -->
+ <ElTableColumn v-if="col.type === 'globalIndex'" v-bind="{ ...col }">
+ <template #default="{ $index }">
+ <span>{{ getGlobalIndex($index) }}</span>
+ </template>
+ </ElTableColumn>
+
+ <!-- 娓叉煋灞曞紑琛� -->
+ <ElTableColumn v-else-if="col.type === 'expand'" v-bind="cleanColumnProps(col)">
+ <template #default="{ row }">
+ <component :is="col.formatter ? col.formatter(row) : null" />
+ </template>
+ </ElTableColumn>
+
+ <!-- 娓叉煋鏅�氬垪 -->
+ <ElTableColumn v-else v-bind="cleanColumnProps(col)">
+ <template v-if="col.useHeaderSlot && col.prop" #header="headerScope">
+ <slot
+ :name="col.headerSlotName || `${col.prop}-header`"
+ v-bind="{ ...headerScope, prop: col.prop, label: col.label }"
+ >
+ {{ col.label }}
+ </slot>
+ </template>
+ <template v-if="col.useSlot && col.prop" #default="slotScope">
+ <slot
+ v-if="shouldRenderSlotScope(slotScope)"
+ :name="col.slotName || col.prop"
+ v-bind="{
+ ...slotScope,
+ prop: col.prop,
+ value: col.prop ? slotScope.row[col.prop] : undefined
+ }"
+ />
+ </template>
+ </ElTableColumn>
+ </template>
+
+ <template v-if="$slots.default" #default><slot /></template>
+
+ <template #empty>
+ <div v-if="loading"></div>
+ <ElEmpty v-else :description="emptyText" :image-size="120" />
+ </template>
+ </ElTable>
+
+ <div
+ class="pagination custom-pagination"
+ v-if="showPagination"
+ :class="mergedPaginationOptions?.align"
+ ref="paginationRef"
+ >
+ <ElPagination
+ v-bind="mergedPaginationOptions"
+ :total="pagination?.total"
+ :disabled="loading"
+ :page-size="pagination?.size"
+ :current-page="pagination?.current"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, computed, nextTick, watchEffect, getCurrentInstance, useAttrs } from 'vue'
+ import { storeToRefs } from 'pinia'
+ import { useTableStore } from '@/store/modules/table'
+ import { useCommon } from '@/hooks/core/useCommon'
+ import { useTableHeight } from '@/hooks/core/useTableHeight'
+ import { useResizeObserver, useWindowSize } from '@vueuse/core'
+ defineOptions({ name: 'ArtTable' })
+ const { width } = useWindowSize()
+ const elTableRef = ref(null)
+ const paginationRef = ref()
+ const tableHeaderRef = ref()
+ const tableStore = useTableStore()
+ const { isBorder, isZebra, tableSize, isFullScreen, isHeaderBackground } = storeToRefs(tableStore)
+ const props = defineProps({
+ loading: { required: false, default: false },
+ columns: { required: false, default: () => [] },
+ pagination: { required: false },
+ paginationOptions: { required: false },
+ fit: { required: false, default: true },
+ showHeader: { required: false, default: true },
+ stripe: { required: false, default: void 0 },
+ border: { required: false, default: void 0 },
+ size: { required: false, default: void 0 },
+ emptyHeight: { required: false, default: '100%' },
+ emptyText: { required: false, default: '鏆傛棤鏁版嵁' },
+ showTableHeader: { required: false, default: true }
+ })
+ const instance = getCurrentInstance()
+ const attrs = useAttrs()
+ const LAYOUT = {
+ MOBILE: 'prev, pager, next, sizes, jumper, total',
+ IPAD: 'prev, pager, next, jumper, total',
+ DESKTOP: 'total, prev, pager, next, sizes, jumper'
+ }
+ const layout = computed(() => {
+ if (width.value < 768) {
+ return LAYOUT.MOBILE
+ } else if (width.value < 1024) {
+ return LAYOUT.IPAD
+ } else {
+ return LAYOUT.DESKTOP
+ }
+ })
+ const DEFAULT_PAGINATION_OPTIONS = {
+ pageSizes: [10, 20, 30, 50, 100],
+ align: 'center',
+ background: true,
+ layout: layout.value,
+ hideOnSinglePage: false,
+ size: 'default',
+ pagerCount: width.value > 1200 ? 7 : 5
+ }
+ const mergedPaginationOptions = computed(() => ({
+ ...DEFAULT_PAGINATION_OPTIONS,
+ ...props.paginationOptions
+ }))
+ const border = computed(() => props.border ?? isBorder.value)
+ const stripe = computed(() => props.stripe ?? isZebra.value)
+ const size = computed(() => props.size ?? tableSize.value)
+ const isEmpty = computed(() => props.data?.length === 0)
+ const paginationHeight = ref(0)
+ const tableHeaderHeight = ref(0)
+ useResizeObserver(paginationRef, (entries) => {
+ const entry = entries[0]
+ if (entry) {
+ requestAnimationFrame(() => {
+ paginationHeight.value = entry.contentRect.height
+ })
+ }
+ })
+ useResizeObserver(tableHeaderRef, (entries) => {
+ const entry = entries[0]
+ if (entry) {
+ requestAnimationFrame(() => {
+ tableHeaderHeight.value = entry.contentRect.height
+ })
+ }
+ })
+ const PAGINATION_SPACING = computed(() => (props.showTableHeader ? 6 : 15))
+ const { containerHeight } = useTableHeight({
+ showTableHeader: computed(() => props.showTableHeader),
+ paginationHeight,
+ tableHeaderHeight,
+ paginationSpacing: PAGINATION_SPACING
+ })
+ const height = computed(() => {
+ if (isFullScreen.value) return '100%'
+ if (isEmpty.value && !props.loading) return props.emptyHeight
+ if (props.height) return props.height
+ return '100%'
+ })
+ const headerCellStyle = computed(() => ({
+ background: isHeaderBackground.value
+ ? 'var(--el-fill-color-lighter)'
+ : 'var(--default-box-color)',
+ ...(props.headerCellStyle || {})
+ // 鍚堝苟鐢ㄦ埛浼犲叆鐨勬牱寮�
+ }))
+ const hasExplicitTableProp = (propName) => {
+ const rawProps = instance?.vnode.props || {}
+ const kebabName = propName.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
+ return propName in rawProps || kebabName in rawProps
+ }
+ const mergedTableProps = computed(() => ({
+ ...attrs,
+ ...props,
+ height: height.value,
+ stripe: stripe.value,
+ border: border.value,
+ size: size.value,
+ headerCellStyle: headerCellStyle.value,
+ // Element Plus 榛樿鍊间负 true锛屾湭鏄惧紡浼犲叆鏃朵笉搴旇 ArtTable 瑕嗙洊鎴� false銆�
+ selectOnIndeterminate: hasExplicitTableProp('selectOnIndeterminate')
+ ? props.selectOnIndeterminate
+ : void 0
+ }))
+ const showPagination = computed(() => props.pagination && !isEmpty.value)
+ const shouldRenderSlotScope = (slotScope) => {
+ return slotScope.$index === void 0 || slotScope.$index >= 0
+ }
+ const cleanColumnProps = (col) => {
+ const columnProps = { ...col }
+ delete columnProps.useHeaderSlot
+ delete columnProps.headerSlotName
+ delete columnProps.useSlot
+ delete columnProps.slotName
+ return columnProps
+ }
+ const handleSizeChange = (val) => {
+ emit('pagination:size-change', val)
+ }
+ const handleCurrentChange = (val) => {
+ emit('pagination:current-change', val)
+ scrollToTop()
+ }
+ const { scrollToTop: scrollPageToTop } = useCommon()
+ const scrollToTop = () => {
+ nextTick(() => {
+ elTableRef.value?.setScrollTop(0)
+ scrollPageToTop()
+ })
+ }
+ const getGlobalIndex = (index) => {
+ if (!props.pagination) return index + 1
+ const { current, size: size2 } = props.pagination
+ return (current - 1) * size2 + index + 1
+ }
+ const emit = defineEmits(['pagination:size-change', 'pagination:current-change'])
+ const findTableHeader = () => {
+ if (!props.showTableHeader) {
+ tableHeaderRef.value = void 0
+ return
+ }
+ const tableHeader = document.getElementById('art-table-header')
+ if (tableHeader) {
+ tableHeaderRef.value = tableHeader
+ } else {
+ tableHeaderRef.value = void 0
+ }
+ }
+ watchEffect(
+ () => {
+ void props.data?.length
+ const shouldShow = props.showTableHeader
+ if (shouldShow) {
+ nextTick(() => {
+ findTableHeader()
+ })
+ } else {
+ tableHeaderRef.value = void 0
+ }
+ },
+ { flush: 'post' }
+ )
+ defineExpose({
+ scrollToTop,
+ elTableRef
+ })
+</script>
+
+<style lang="scss" scoped>
+ @use './style';
+</style>
diff --git a/rsf-design/src/components/core/tables/art-table/style.scss b/rsf-design/src/components/core/tables/art-table/style.scss
new file mode 100644
index 0000000..67459e8
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/components/core/text-effect/art-count-to/index.vue b/rsf-design/src/components/core/text-effect/art-count-to/index.vue
new file mode 100644
index 0000000..e1b4924
--- /dev/null
+++ b/rsf-design/src/components/core/text-effect/art-count-to/index.vue
@@ -0,0 +1,209 @@
+<!-- 鏁板瓧婊氬姩 -->
+<template>
+ <span
+ class="text-g-900 tabular-nums"
+ :class="isRunning ? 'transition-opacity duration-300 ease-in-out' : ''"
+ >
+ {{ formattedValue }}
+ </span>
+</template>
+
+<script setup>
+ import { computed, watch, nextTick, onUnmounted, shallowRef } from 'vue'
+ import { useTransition, TransitionPresets } from '@vueuse/core'
+ const EPSILON = Number.EPSILON
+ const MIN_DURATION = 100
+ const MAX_DURATION = 6e4
+ const MAX_DECIMALS = 10
+ const DEFAULT_EASING = 'easeOutExpo'
+ const DEFAULT_DURATION = 2e3
+ const props = defineProps({
+ target: { required: false, default: 0 },
+ duration: { required: false, default: 2e3 },
+ autoStart: { required: false, default: true },
+ decimals: { required: false, default: 0 },
+ decimal: { required: false, default: '.' },
+ separator: { required: false, default: '' },
+ prefix: { required: false, default: '' },
+ suffix: { required: false, default: '' },
+ easing: { required: false, default: 'easeOutExpo' },
+ disabled: { required: false, default: false }
+ })
+ const emit = defineEmits(['started', 'finished', 'paused', 'reset'])
+ const validateNumber = (value, name, defaultValue) => {
+ if (!Number.isFinite(value)) {
+ console.warn(`[CountTo] Invalid ${name} value:`, value)
+ return defaultValue
+ }
+ return value
+ }
+ const clamp = (value, min, max) => {
+ return Math.max(min, Math.min(value, max))
+ }
+ const formatNumber = (value, decimals, decimal, separator) => {
+ let result = decimals > 0 ? value.toFixed(decimals) : Math.floor(value).toString()
+ if (decimal !== '.' && result.includes('.')) {
+ result = result.replace('.', decimal)
+ }
+ if (separator) {
+ const parts = result.split(decimal)
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, separator)
+ result = parts.join(decimal)
+ }
+ return result
+ }
+ const safeTarget = computed(() => validateNumber(props.target, 'target', 0))
+ const safeDuration = computed(() =>
+ clamp(validateNumber(props.duration, 'duration', DEFAULT_DURATION), MIN_DURATION, MAX_DURATION)
+ )
+ const safeDecimals = computed(() =>
+ clamp(validateNumber(props.decimals, 'decimals', 0), 0, MAX_DECIMALS)
+ )
+ const safeEasing = computed(() => {
+ const easing = props.easing
+ if (!(easing in TransitionPresets)) {
+ console.warn('[CountTo] Invalid easing value:', easing)
+ return DEFAULT_EASING
+ }
+ return easing
+ })
+ const currentValue = shallowRef(0)
+ const targetValue = shallowRef(safeTarget.value)
+ const isRunning = shallowRef(false)
+ const isPaused = shallowRef(false)
+ const pausedValue = shallowRef(0)
+ const transitionValue = useTransition(currentValue, {
+ duration: safeDuration,
+ transition: computed(() => TransitionPresets[safeEasing.value]),
+ onStarted: () => {
+ isRunning.value = true
+ isPaused.value = false
+ emit('started', targetValue.value)
+ },
+ onFinished: () => {
+ isRunning.value = false
+ isPaused.value = false
+ emit('finished', targetValue.value)
+ }
+ })
+ const formattedValue = computed(() => {
+ const value = isPaused.value ? pausedValue.value : transitionValue.value
+ if (!Number.isFinite(value)) {
+ return `${props.prefix}0${props.suffix}`
+ }
+ const formattedNumber = formatNumber(value, safeDecimals.value, props.decimal, props.separator)
+ return `${props.prefix}${formattedNumber}${props.suffix}`
+ })
+ const shouldSkipAnimation = (target) => {
+ const current = isPaused.value ? pausedValue.value : transitionValue.value
+ return Math.abs(current - target) < EPSILON
+ }
+ const resetPauseState = () => {
+ isPaused.value = false
+ pausedValue.value = 0
+ }
+ const start = (target) => {
+ if (props.disabled) {
+ console.warn('[CountTo] Animation is disabled')
+ return
+ }
+ const finalTarget = target !== void 0 ? target : targetValue.value
+ if (!Number.isFinite(finalTarget)) {
+ console.warn('[CountTo] Invalid target value for start:', finalTarget)
+ return
+ }
+ targetValue.value = finalTarget
+ if (shouldSkipAnimation(finalTarget)) {
+ return
+ }
+ if (isPaused.value) {
+ currentValue.value = pausedValue.value
+ resetPauseState()
+ }
+ nextTick(() => {
+ currentValue.value = finalTarget
+ })
+ }
+ const pause = () => {
+ if (!isRunning.value || isPaused.value) {
+ return
+ }
+ isPaused.value = true
+ pausedValue.value = transitionValue.value
+ currentValue.value = pausedValue.value
+ emit('paused', pausedValue.value)
+ }
+ const reset = (newTarget = 0) => {
+ const target = validateNumber(newTarget, 'reset target', 0)
+ currentValue.value = target
+ targetValue.value = target
+ resetPauseState()
+ emit('reset')
+ }
+ const setTarget = (target) => {
+ if (!Number.isFinite(target)) {
+ console.warn('[CountTo] Invalid target value for setTarget:', target)
+ return
+ }
+ targetValue.value = target
+ if ((isRunning.value || props.autoStart) && !props.disabled) {
+ start(target)
+ }
+ }
+ const stop = () => {
+ if (isRunning.value || isPaused.value) {
+ currentValue.value = 0
+ resetPauseState()
+ emit('paused', 0)
+ }
+ }
+ watch(
+ safeTarget,
+ (newTarget) => {
+ if (props.autoStart && !props.disabled) {
+ start(newTarget)
+ } else {
+ targetValue.value = newTarget
+ }
+ },
+ { immediate: props.autoStart && !props.disabled }
+ )
+ watch(
+ () => props.disabled,
+ (disabled) => {
+ if (disabled && isRunning.value) {
+ stop()
+ }
+ }
+ )
+ onUnmounted(() => {
+ if (isRunning.value) {
+ stop()
+ }
+ })
+ defineExpose({
+ start,
+ pause,
+ reset,
+ stop,
+ setTarget,
+ get isRunning() {
+ return isRunning.value
+ },
+ get isPaused() {
+ return isPaused.value
+ },
+ get currentValue() {
+ return isPaused.value ? pausedValue.value : transitionValue.value
+ },
+ get targetValue() {
+ return targetValue.value
+ },
+ get progress() {
+ const current = isPaused.value ? pausedValue.value : transitionValue.value
+ const target = targetValue.value
+ if (target === 0) return current === 0 ? 1 : 0
+ return Math.abs(current / target)
+ }
+ })
+</script>
diff --git a/rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue b/rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue
new file mode 100644
index 0000000..0904cc8
--- /dev/null
+++ b/rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue
@@ -0,0 +1,29 @@
+<!-- 鑺傛棩鏂囨湰婊氬姩 -->
+<template>
+ <div
+ class="overflow-hidden transition-[height] duration-600 ease-in-out"
+ :style="{
+ height: showFestivalText ? '48px' : '0'
+ }"
+ >
+ <ArtTextScroll
+ v-if="showFestivalText && currentFestivalData?.scrollText !== ''"
+ :text="currentFestivalData?.scrollText || ''"
+ style="margin-bottom: 12px"
+ showClose
+ @close="handleClose"
+ />
+ </div>
+</template>
+
+<script setup>
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useCeremony } from '@/hooks/core/useCeremony'
+ defineOptions({ name: 'ArtFestivalTextScroll' })
+ const settingStore = useSettingStore()
+ const { showFestivalText } = storeToRefs(settingStore)
+ const { currentFestivalData } = useCeremony()
+ const handleClose = () => {
+ settingStore.setShowFestivalText(false)
+ }
+</script>
diff --git a/rsf-design/src/components/core/text-effect/art-text-scroll/index.vue b/rsf-design/src/components/core/text-effect/art-text-scroll/index.vue
new file mode 100644
index 0000000..5e6f6f1
--- /dev/null
+++ b/rsf-design/src/components/core/text-effect/art-text-scroll/index.vue
@@ -0,0 +1,197 @@
+<!-- 鏂囧瓧婊氬姩 -->
+<template>
+ <div
+ ref="containerRef"
+ class="relative overflow-hidden rounded-custom-sm border flex-c box-border text-sm"
+ :class="themeClasses"
+ :style="containerStyle"
+ >
+ <div class="flex-cc absolute left-0 h-full w-9 z-10" :style="{ backgroundColor: bgColor }">
+ <ArtSvgIcon icon="ri:volume-down-line" class="text-lg" />
+ </div>
+
+ <div
+ ref="contentRef"
+ class="whitespace-nowrap inline-block transition-opacity duration-600 [&_a]:text-danger [&_a:hover]:underline [&_a:hover]:text-danger/80 px-9"
+ :class="[contentClass, { 'opacity-0': !isReady, 'opacity-100': isReady }]"
+ :style="contentStyle"
+ @click="handleContentClick"
+ >
+ <!-- 鍘熷鍐呭 -->
+ <span ref="textRef" class="inline-block">
+ <slot>
+ <span v-html="text"></span>
+ </slot>
+ </span>
+ <!-- 鍏嬮殕鍐呭鐢ㄤ簬鏃犵紳寰幆 -->
+ <span v-if="shouldClone" class="inline-block" :style="cloneSpacing">
+ <slot>
+ <span v-html="text"></span>
+ </slot>
+ </span>
+ </div>
+
+ <div
+ v-if="showClose"
+ class="flex-cc absolute right-0 h-full w-9 c-p"
+ :style="{ backgroundColor: bgColor }"
+ @click="handleClose"
+ >
+ <ArtSvgIcon icon="ri:close-fill" class="text-lg" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import {
+ useElementSize,
+ useRafFn,
+ useElementHover,
+ useDebounceFn,
+ useTimeoutFn
+ } from '@vueuse/core'
+ import { useSettingStore } from '@/store/modules/setting'
+ const props = defineProps({
+ text: { required: false, default: '' },
+ direction: { required: false, default: 'left' },
+ speed: { required: false, default: 80 },
+ width: { required: false, default: '100%' },
+ height: { required: false, default: '36px' },
+ pauseOnHover: { required: false, default: true },
+ type: { required: false, default: 'theme' },
+ showClose: { required: false, default: false },
+ alwaysScroll: { required: false, default: true }
+ })
+ const emit = defineEmits(['close'])
+ const handleClose = () => {
+ emit('close')
+ }
+ const settingStore = useSettingStore()
+ const { isDark } = storeToRefs(settingStore)
+ const containerRef = ref()
+ const contentRef = ref()
+ const textRef = ref()
+ const isReady = ref(false)
+ const currentPosition = ref(0)
+ const textSize = ref(0)
+ const containerSize = ref(0)
+ const shouldClone = ref(false)
+ const isHorizontal = computed(() => props.direction === 'left' || props.direction === 'right')
+ const isReverse = computed(() => props.direction === 'right' || props.direction === 'down')
+ const { width: containerWidth, height: containerHeight } = useElementSize(containerRef)
+ const isHovered = useElementHover(containerRef)
+ const isPaused = computed(() => {
+ if (!props.alwaysScroll && textSize.value <= containerSize.value) {
+ return true
+ }
+ return props.pauseOnHover && isHovered.value
+ })
+ const themeClasses = computed(() => {
+ const themeMap = {
+ theme: 'text-theme/90 !border-theme/50',
+ primary: 'text-primary/90 !border-primary/50',
+ secondary: 'text-secondary/90 !border-secondary/50',
+ error: 'text-error/90 !border-error/50',
+ info: 'text-info/90 !border-info/50',
+ success: 'text-success/90 !border-success/50',
+ warning: 'text-warning/90 !border-warning/50',
+ danger: 'text-danger/90 !border-danger/50'
+ }
+ return themeMap[props.type] || themeMap.theme
+ })
+ const bgColor = computed(
+ () =>
+ `color-mix(in oklch, var(--color-${props.type}) ${isDark.value ? '25' : '10'}%, var(--art-color))`
+ )
+ const containerStyle = computed(() => ({
+ width: props.width,
+ height: props.height,
+ backgroundColor: bgColor.value
+ }))
+ const contentClass = computed(() => {
+ if (!isHorizontal.value) {
+ return 'flex flex-col'
+ }
+ return ''
+ })
+ const contentStyle = computed(() => {
+ const transform = isHorizontal.value
+ ? `translateX(${currentPosition.value}px)`
+ : `translateY(${currentPosition.value}px)`
+ return {
+ transform,
+ willChange: 'transform'
+ }
+ })
+ const cloneSpacing = computed(() => {
+ const spacing = '2em'
+ return isHorizontal.value ? { marginLeft: spacing } : { marginTop: spacing }
+ })
+ const measureSizes = () => {
+ if (!containerRef.value || !textRef.value) return
+ const text = textRef.value
+ if (isHorizontal.value) {
+ containerSize.value = containerWidth.value
+ textSize.value = text.offsetWidth
+ } else {
+ containerSize.value = containerHeight.value
+ textSize.value = text.offsetHeight
+ }
+ const isOverflow = textSize.value > containerSize.value
+ shouldClone.value = isOverflow
+ currentPosition.value = (containerSize.value - textSize.value) / 2
+ if (!isReady.value) {
+ isReady.value = true
+ }
+ }
+ const debouncedMeasure = useDebounceFn(measureSizes, 150)
+ let lastTimestamp = 0
+ const { pause, resume } = useRafFn(
+ ({ timestamp }) => {
+ if (!lastTimestamp) lastTimestamp = timestamp
+ if (!isPaused.value) {
+ const delta = (timestamp - lastTimestamp) / 1e3
+ const distance = props.speed * delta
+ const spacing = textSize.value * 0.1
+ currentPosition.value += isReverse.value ? distance : -distance
+ if (isReverse.value) {
+ if (currentPosition.value > containerSize.value) {
+ currentPosition.value = -(textSize.value + spacing)
+ }
+ } else {
+ if (currentPosition.value < -(textSize.value + spacing)) {
+ currentPosition.value = containerSize.value
+ }
+ }
+ }
+ lastTimestamp = timestamp
+ },
+ { immediate: false }
+ )
+ const handleContentClick = (e) => {
+ const target = e.target
+ if (target.tagName === 'A') {
+ e.stopPropagation()
+ }
+ }
+ watch([containerWidth, containerHeight], () => {
+ debouncedMeasure()
+ })
+ watch(
+ () => [props.direction, props.speed, props.text],
+ () => {
+ measureSizes()
+ lastTimestamp = 0
+ }
+ )
+ const { start: startMeasure } = useTimeoutFn(() => {
+ measureSizes()
+ resume()
+ }, 100)
+ onMounted(() => {
+ startMeasure()
+ })
+ onBeforeUnmount(() => {
+ pause()
+ })
+</script>
diff --git a/rsf-design/src/components/core/theme/theme-svg/index.vue b/rsf-design/src/components/core/theme/theme-svg/index.vue
new file mode 100644
index 0000000..2472276
--- /dev/null
+++ b/rsf-design/src/components/core/theme/theme-svg/index.vue
@@ -0,0 +1,81 @@
+<!-- 涓�涓 SVG 鍥剧墖璺熼殢涓婚鐨勭粍浠讹紝鍙鐗瑰畾 svg 鍥剧墖鐢熸晥锛屼笉寤鸿寮�鍙戣�呬娇鐢� -->
+<!-- 鍥剧墖鍦板潃 https://iconpark.oceanengine.com/illustrations/13 -->
+<template>
+ <div class="theme-svg" :style="sizeStyle">
+ <div v-if="src" class="svg-container" v-html="svgContent"></div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, computed, watchEffect } from 'vue'
+ const props = defineProps({
+ size: { required: false, default: 500 },
+ themeColor: { required: false, default: 'var(--el-color-primary)' },
+ src: { required: false }
+ })
+ const svgContent = ref('')
+ const sizeStyle = computed(() => {
+ const sizeValue = typeof props.size === 'number' ? `${props.size}px` : props.size
+ return {
+ width: sizeValue,
+ height: sizeValue
+ }
+ })
+ const COLOR_MAPPINGS = {
+ '#C7DEFF': 'var(--el-color-primary-light-6)',
+ '#071F4D': 'var(--el-color-primary-dark-2)',
+ '#00E4E5': 'var(--el-color-primary-light-1)',
+ '#006EFF': 'var(--el-color-primary)',
+ '#fff': 'var(--default-box-color)',
+ '#ffffff': 'var(--default-box-color)',
+ '#DEEBFC': 'var(--el-color-primary-light-7)'
+ }
+ const applyThemeToSvg = (content) => {
+ return Object.entries(COLOR_MAPPINGS).reduce(
+ (processedContent, [originalColor, themeColor]) => {
+ const fillRegex = new RegExp(`fill="${originalColor}"`, 'gi')
+ const strokeRegex = new RegExp(`stroke="${originalColor}"`, 'gi')
+ return processedContent
+ .replace(fillRegex, `fill="${themeColor}"`)
+ .replace(strokeRegex, `stroke="${themeColor}"`)
+ },
+ content
+ )
+ }
+ const loadSvgContent = async () => {
+ if (!props.src) {
+ svgContent.value = ''
+ return
+ }
+ try {
+ const response = await fetch(props.src)
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+ const content = await response.text()
+ svgContent.value = applyThemeToSvg(content)
+ } catch (error) {
+ console.error('Failed to load SVG:', error)
+ svgContent.value = ''
+ }
+ }
+ watchEffect(() => {
+ loadSvgContent()
+ })
+</script>
+
+<style lang="scss" scoped>
+ .theme-svg {
+ display: inline-block;
+
+ .svg-container {
+ width: 100%;
+ height: 100%;
+
+ :deep(svg) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/views/exception/ArtException.vue b/rsf-design/src/components/core/views/exception/ArtException.vue
new file mode 100644
index 0000000..cc23fa8
--- /dev/null
+++ b/rsf-design/src/components/core/views/exception/ArtException.vue
@@ -0,0 +1,35 @@
+<template>
+ <div class="page-content !border-0 !bg-transparent min-h-screen flex-cc">
+ <div class="flex-cc max-md:!block max-md:text-center">
+ <ThemeSvg :src="data.imgUrl" size="100%" class="!w-100" />
+ <div class="ml-15 w-75 max-md:mx-auto max-md:mt-10 max-md:w-full max-md:text-center">
+ <p class="text-xl leading-7 text-g-600 max-md:text-lg">{{ data.desc }}</p>
+ <ElButton type="primary" size="large" @click="backHome" v-ripple class="mt-5">{{
+ data.btnText
+ }}</ElButton>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { useCommon } from '@/hooks/core/useCommon'
+ import { useUserStore } from '@/store/modules/user'
+ const router = useRouter()
+ const userStore = useUserStore()
+ defineProps({
+ data: { required: true }
+ })
+ const { homePath } = useCommon()
+ const backHome = () => {
+ const targetHomePath = homePath.value || '/'
+ if (!userStore.isLogin) {
+ router.push({
+ name: 'Login',
+ query: { redirect: targetHomePath }
+ })
+ return
+ }
+ router.push(targetHomePath)
+ }
+</script>
diff --git a/rsf-design/src/components/core/views/login/AuthTopBar.vue b/rsf-design/src/components/core/views/login/AuthTopBar.vue
new file mode 100644
index 0000000..9e086e0
--- /dev/null
+++ b/rsf-design/src/components/core/views/login/AuthTopBar.vue
@@ -0,0 +1,143 @@
+<!-- 鎺堟潈椤靛彸涓婅缁勪欢 -->
+<template>
+ <div
+ class="absolute w-full flex-cb top-4.5 z-10 flex-c !justify-end max-[1180px]:!justify-between"
+ >
+ <div class="flex-cc !hidden max-[1180px]:!flex ml-2 max-sm:ml-6">
+ <ArtLogo class="icon" size="46" />
+ <h1 class="text-xl ont-mediumf ml-2">{{ AppConfig.systemInfo.name }}</h1>
+ </div>
+
+ <div class="flex-cc gap-1.5 mr-2 max-sm:mr-5">
+ <div class="color-picker-expandable relative flex-c max-sm:!hidden">
+ <div
+ class="color-dots absolute right-0 rounded-full flex-c gap-2 rounded-5 px-2.5 py-2 pr-9 pl-2.5 opacity-0"
+ >
+ <div
+ v-for="(color, index) in mainColors"
+ :key="color"
+ class="color-dot relative size-5 c-p flex-cc rounded-full opacity-0"
+ :class="{ active: color === systemThemeColor }"
+ :style="{ background: color, '--index': index }"
+ @click="changeThemeColor(color)"
+ >
+ <ArtSvgIcon v-if="color === systemThemeColor" icon="ri:check-fill" class="text-white" />
+ </div>
+ </div>
+ <div class="btn palette-btn relative z-[2] h-8 w-8 c-p flex-cc tad-300">
+ <ArtSvgIcon
+ icon="ri:palette-line"
+ class="text-xl text-g-800 transition-colors duration-300"
+ />
+ </div>
+ </div>
+ <ElDropdown
+ v-if="shouldShowLanguage"
+ @command="changeLanguage"
+ popper-class="langDropDownStyle"
+ >
+ <div class="btn language-btn h-8 w-8 c-p flex-cc tad-300">
+ <ArtSvgIcon
+ icon="ri:translate-2"
+ class="text-[19px] text-g-800 transition-colors duration-300"
+ />
+ </div>
+ <template #dropdown>
+ <ElDropdownMenu>
+ <div v-for="lang in languageOptions" :key="lang.value" class="lang-btn-item">
+ <ElDropdownItem
+ :command="lang.value"
+ :class="{ 'is-selected': locale === lang.value }"
+ >
+ <span class="menu-txt">{{ lang.label }}</span>
+ <ArtSvgIcon icon="ri:check-fill" class="text-base" v-if="locale === lang.value" />
+ </ElDropdownItem>
+ </div>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+ <div
+ v-if="shouldShowThemeToggle"
+ class="btn theme-btn h-8 w-8 c-p flex-cc tad-300"
+ @click="themeAnimation"
+ >
+ <ArtSvgIcon
+ :icon="isDark ? 'ri:sun-fill' : 'ri:moon-line'"
+ class="text-xl text-g-800 transition-colors duration-300"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { useI18n } from 'vue-i18n'
+ import { useSettingStore } from '@/store/modules/setting'
+ import { useUserStore } from '@/store/modules/user'
+ import { useHeaderBar } from '@/hooks/core/useHeaderBar'
+ import { themeAnimation } from '@/utils/ui/animation'
+ import { languageOptions } from '@/locales'
+ import AppConfig from '@/config'
+ defineOptions({ name: 'AuthTopBar' })
+ const settingStore = useSettingStore()
+ const userStore = useUserStore()
+ const { isDark, systemThemeColor } = storeToRefs(settingStore)
+ const { shouldShowThemeToggle, shouldShowLanguage } = useHeaderBar()
+ const { locale } = useI18n()
+ const mainColors = AppConfig.systemMainColor
+ const color = systemThemeColor
+ const changeLanguage = (lang) => {
+ if (locale.value === lang) return
+ locale.value = lang
+ userStore.setLanguage(lang)
+ }
+ const changeThemeColor = (color2) => {
+ if (systemThemeColor.value === color2) return
+ settingStore.setElementTheme(color2)
+ settingStore.reload()
+ }
+</script>
+
+<style scoped>
+ .color-dots {
+ pointer-events: none;
+ backdrop-filter: blur(10px);
+ box-shadow: 0 2px 12px var(--art-gray-300);
+ transition:
+ opacity 0.3s ease,
+ transform 0.3s ease;
+ transform: translateX(10px);
+ }
+
+ .color-dot {
+ box-shadow: 0 2px 4px rgb(0 0 0 / 15%);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ transition-delay: calc(var(--index) * 0.05s);
+ transform: translateX(20px) scale(0.8);
+ }
+
+ .color-dot:hover {
+ box-shadow: 0 4px 8px rgb(0 0 0 / 20%);
+ transform: translateX(0) scale(1.1);
+ }
+
+ .color-picker-expandable:hover .color-dots {
+ pointer-events: auto;
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ .color-picker-expandable:hover .color-dot {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ }
+
+ .dark .color-dots {
+ background-color: var(--art-gray-200);
+ box-shadow: none;
+ }
+
+ .color-picker-expandable:hover .palette-btn :deep(.art-svg-icon) {
+ color: v-bind(color);
+ }
+</style>
diff --git a/rsf-design/src/components/core/views/login/LoginLeftView.vue b/rsf-design/src/components/core/views/login/LoginLeftView.vue
new file mode 100644
index 0000000..287bb52
--- /dev/null
+++ b/rsf-design/src/components/core/views/login/LoginLeftView.vue
@@ -0,0 +1,600 @@
+<!-- 鐧诲綍銆佹敞鍐屻�佸繕璁板瘑鐮佸乏渚ц儗鏅� -->
+<template>
+ <div class="login-left-view">
+ <div class="logo">
+ <ArtLogo class="icon" size="46" />
+ <h1 class="title">{{ AppConfig.systemInfo.name }}</h1>
+ </div>
+
+ <div class="left-img">
+ <ThemeSvg :src="loginIcon" size="100%" />
+ </div>
+
+ <div class="text-wrap">
+ <h1> {{ $t('login.leftView.title') }} </h1>
+ <p> {{ $t('login.leftView.subTitle') }} </p>
+ </div>
+
+ <!-- 鍑犱綍瑁呴グ鍏冪礌 -->
+ <div class="geometric-decorations">
+ <!-- 鍩虹鍑犱綍褰㈢姸 -->
+ <div class="geo-element circle-outline animate-fade-in-up" style="animation-delay: 0s"></div>
+ <div
+ class="geo-element square-rotated animate-fade-in-left"
+ style="animation-delay: 0s"
+ ></div>
+ <div class="geo-element circle-small animate-fade-in-up" style="animation-delay: 0.3s"></div>
+
+ <div
+ class="geo-element square-bottom-right animate-fade-in-right"
+ style="animation-delay: 0s"
+ ></div>
+
+ <!-- 鑳屾櫙娉℃场 -->
+ <div class="geo-element bg-bubble animate-scale-in" style="animation-delay: 0.5"></div>
+
+ <!-- 澶槼/鏈堜寒 -->
+ <div
+ class="geo-element circle-top-right animate-fade-in-down"
+ style="animation-delay: 0.5"
+ @click="themeAnimation"
+ ></div>
+
+ <!-- 瑁呴グ鐐� -->
+ <div class="geo-element dot dot-top-left animate-bounce-in" style="animation-delay: 0s"></div>
+ <div
+ class="geo-element dot dot-top-right animate-bounce-in"
+ style="animation-delay: 0s"
+ ></div>
+ <div
+ class="geo-element dot dot-center-right animate-bounce-in"
+ style="animation-delay: 0s"
+ ></div>
+
+ <!-- 鍙犲姞鏂瑰潡缁� -->
+ <div class="squares-group">
+ <i
+ class="geo-element square square-blue animate-fade-in-left-rotated-blue"
+ style="animation-delay: 0.2s"
+ ></i>
+ <i
+ class="geo-element square square-pink animate-fade-in-left-rotated-pink"
+ style="animation-delay: 0.4s"
+ ></i>
+ <i
+ class="geo-element square square-purple animate-fade-in-left-no-rotation"
+ style="animation-delay: 0.6s"
+ ></i>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import AppConfig from '@/config'
+ import loginIcon from '@imgs/svg/login_icon.svg'
+ import { themeAnimation } from '@/utils/ui/animation'
+ defineProps({
+ hideContent: { required: false }
+ })
+</script>
+
+<style lang="scss" scoped>
+ // 棰滆壊鍙橀噺瀹氫箟
+ $primary-light-7: var(--el-color-primary-light-7);
+ $primary-light-8: var(--el-color-primary-light-8);
+ $primary-light-9: var(--el-color-primary-light-9);
+ $primary-base: var(--el-color-primary);
+ $main-bg: var(--default-box-color);
+
+ // 娣峰悎棰滆壊鍑芥暟
+ $bg-mix-light-9: color-mix(in srgb, $primary-light-9 100%, $main-bg);
+ $bg-mix-light-8: color-mix(in srgb, $primary-light-8 80%, $main-bg);
+ $bg-mix-light-7: color-mix(in srgb, $primary-light-7 80%, $main-bg);
+
+ .login-left-view {
+ position: relative;
+ box-sizing: border-box;
+ width: 65vw;
+ height: 100%;
+ padding: 15px;
+ overflow: hidden;
+ background-color: $bg-mix-light-9;
+
+ .logo {
+ position: relative;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+
+ .title {
+ margin-left: 10px;
+ font-size: 20px;
+ font-weight: 400;
+ }
+ }
+
+ .left-img {
+ position: absolute;
+ inset: 0 0 10.5%;
+ z-index: 10;
+ width: 40%;
+ margin: auto;
+ animation: slideInLeft 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
+ }
+
+ .text-wrap {
+ position: absolute;
+ bottom: 80px;
+ width: 100%;
+ text-align: center;
+ animation: slideInLeft 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
+
+ h1 {
+ font-size: 24px;
+ font-weight: 400;
+ color: var(--art-gray-900) !important;
+ }
+
+ p {
+ margin-top: 10px;
+ font-size: 14px;
+ color: var(--art-gray-600) !important;
+ }
+ }
+
+ .geometric-decorations {
+ .geo-element {
+ position: absolute;
+ opacity: 0;
+ animation-fill-mode: forwards;
+ animation-duration: 0.8s;
+ animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ }
+
+ // 鍔ㄧ敾 mixin
+ @mixin fadeAnimation($direction: '', $rotation: 0deg) {
+ from {
+ opacity: 0;
+
+ @if $direction == 'up' {
+ transform: translateY(30px) rotate($rotation);
+ } @else if $direction == 'down' {
+ transform: translateY(-30px) rotate($rotation);
+ } @else if $direction == 'left' {
+ transform: translateX(-30px) rotate($rotation);
+ } @else if $direction == 'right' {
+ transform: translateX(30px) rotate($rotation);
+ }
+ }
+
+ to {
+ opacity: 1;
+
+ @if $direction == 'up' or $direction == 'down' {
+ transform: translateY(0) rotate($rotation);
+ } @else {
+ transform: translateX(0) rotate($rotation);
+ }
+ }
+ }
+
+ // 鍔ㄧ敾瀹氫箟
+ @keyframes fadeInUp {
+ @include fadeAnimation('up');
+ }
+
+ @keyframes fadeInDown {
+ @include fadeAnimation('down');
+ }
+
+ @keyframes fadeInLeft {
+ @include fadeAnimation('left');
+ }
+
+ @keyframes fadeInLeftRotated {
+ @include fadeAnimation('left', -25deg);
+ }
+
+ @keyframes fadeInRight {
+ @include fadeAnimation('right');
+ }
+
+ @keyframes fadeInRightRotated {
+ @include fadeAnimation('right', 45deg);
+ }
+
+ @keyframes fadeInLeftRotatedBlue {
+ @include fadeAnimation('left', -10deg);
+ }
+
+ @keyframes fadeInLeftRotatedPink {
+ @include fadeAnimation('left', 10deg);
+ }
+
+ @keyframes fadeInLeftNoRotation {
+ @include fadeAnimation('left');
+ }
+
+ @keyframes scaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+
+ @keyframes bounceIn {
+ 0% {
+ opacity: 0;
+ transform: scale(0.3);
+ }
+
+ 50% {
+ opacity: 1;
+ transform: scale(1.05);
+ }
+
+ 70% {
+ transform: scale(0.9);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+
+ @keyframes lineGrow {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+ }
+
+ @keyframes slideInLeft {
+ from {
+ opacity: 0;
+ transform: translateX(-30px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ // 鍔ㄧ敾绫�
+ .animate-fade-in-up {
+ animation-name: fadeInUp;
+ }
+
+ .animate-fade-in-down {
+ animation-name: fadeInDown;
+ }
+
+ .animate-fade-in-left {
+ animation-name: fadeInLeft;
+ }
+
+ .animate-fade-in-right {
+ animation-name: fadeInRight;
+ }
+
+ .animate-scale-in {
+ animation-name: scaleIn;
+ animation-duration: 1.2s;
+ }
+
+ .animate-bounce-in {
+ animation-name: bounceIn;
+ animation-duration: 0.6s;
+ }
+
+ .animate-fade-in-left-rotated-blue {
+ animation-name: fadeInLeftRotatedBlue;
+ }
+
+ .animate-fade-in-left-rotated-pink {
+ animation-name: fadeInLeftRotatedPink;
+ }
+
+ .animate-fade-in-left-no-rotation {
+ animation-name: fadeInLeftNoRotation;
+ }
+
+ // 鍩虹鍑犱綍褰㈢姸
+ .circle-outline {
+ top: 10%;
+ left: 25%;
+ width: 42px;
+ height: 42px;
+ border: 2px solid $primary-light-8;
+ border-radius: 50%;
+ }
+
+ .square-rotated {
+ top: 50%;
+ left: 16%;
+ width: 60px;
+ height: 60px;
+ background-color: $bg-mix-light-8;
+
+ &.animate-fade-in-left {
+ animation-name: fadeInLeftRotated;
+ }
+ }
+
+ .circle-small {
+ bottom: 26%;
+ left: 30%;
+ width: 18px;
+ height: 18px;
+ background-color: $primary-light-8;
+ border-radius: 50%;
+ }
+
+ // 澶槼/鏈堜寒鏁堟灉
+ .circle-top-right {
+ top: 3%;
+ right: 3%;
+ z-index: 100;
+ width: 50px;
+ height: 50px;
+ cursor: pointer;
+ background: $bg-mix-light-7;
+ border-radius: 50%;
+ transition: all 0.3s;
+
+ &::after {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 100%;
+ height: 100%;
+ content: '';
+ background: linear-gradient(to right, #fcbb04, #fffc00);
+ border-radius: 50%;
+ opacity: 0;
+ transition: all 0.5s;
+ transform: translate(-50%, -50%);
+ }
+
+ &:hover {
+ box-shadow: 0 0 36px #fffc00;
+
+ &::after {
+ opacity: 1;
+ }
+ }
+ }
+
+ .square-bottom-right {
+ right: 10%;
+ bottom: 10%;
+ width: 50px;
+ height: 50px;
+ background-color: $primary-light-8;
+
+ &.animate-fade-in-right {
+ animation-name: fadeInRightRotated;
+ }
+ }
+
+ // 鑳屾櫙娉℃场
+ .bg-bubble {
+ top: -120px;
+ right: -120px;
+ width: 360px;
+ height: 360px;
+ background-color: $bg-mix-light-8;
+ border-radius: 50%;
+ }
+
+ // 瑁呴グ鐐�
+ .dot {
+ width: 14px;
+ height: 14px;
+ background-color: $primary-light-7;
+ border-radius: 50%;
+
+ &.dot-top-left {
+ top: 140px;
+ left: 100px;
+ }
+
+ &.dot-top-right {
+ top: 140px;
+ right: 120px;
+ }
+
+ &.dot-center-right {
+ top: 46%;
+ right: 22%;
+ background-color: $primary-light-8;
+ }
+ }
+
+ // 鍙犲姞鏂瑰潡缁�
+ .squares-group {
+ position: absolute;
+ bottom: 18px;
+ left: 20px;
+ width: 140px;
+ height: 140px;
+ pointer-events: none;
+
+ .square {
+ position: absolute;
+ display: block;
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgb(64 87 167 / 12%);
+
+ &.square-blue {
+ top: 12px;
+ left: 30px;
+ z-index: 2;
+ width: 50px;
+ height: 50px;
+ background-color: rgb(from $primary-base r g b / 30%);
+ }
+
+ &.square-pink {
+ top: 30px;
+ left: 48px;
+ z-index: 1;
+ width: 70px;
+ height: 70px;
+ background-color: rgb(from $primary-base r g b / 15%);
+ }
+
+ &.square-purple {
+ top: 66px;
+ left: 86px;
+ z-index: 3;
+ width: 32px;
+ height: 32px;
+ background-color: rgb(from $primary-base r g b / 45%);
+ }
+ }
+
+ // 瑁呴グ绾挎潯
+ &::after {
+ position: absolute;
+ top: 86px;
+ left: 72px;
+ width: 80px;
+ height: 1px;
+ content: '';
+ background: linear-gradient(90deg, var(--el-color-primary-light-6), transparent);
+ opacity: 0;
+ transform: rotate(50deg);
+ animation: lineGrow 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
+ animation-delay: 1.2s;
+ }
+ }
+ }
+
+ @media only screen and (width <= 1600px) {
+ width: 60vw;
+
+ .text-wrap {
+ bottom: 40px;
+ }
+ }
+
+ @media only screen and (width <= 1180px) {
+ width: auto;
+ height: auto;
+ padding: 0;
+ // 闅愯棌鑳屾櫙鍜屽叾浠栧唴瀹癸紝鍙繚鐣� logo
+ background: transparent;
+
+ .left-img,
+ .text-wrap,
+ .geometric-decorations {
+ display: none;
+ }
+
+ .logo {
+ display: none;
+ }
+ }
+ }
+
+ // 鏆楄壊涓婚
+ .dark .login-left-view {
+ background-color: color-mix(in srgb, $primary-light-9 60%, #070707);
+
+ @media only screen and (width <= 1180px) {
+ background: transparent;
+ }
+
+ .geometric-decorations {
+ // 鏈堜寒鏁堟灉
+ .circle-top-right {
+ background-color: $bg-mix-light-8;
+ box-shadow: 0 0 25px #333 inset;
+ transition: all 0.3s ease-in-out 0.1s;
+ rotate: -48deg;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 15px;
+ width: 50px;
+ height: 50px;
+ content: '';
+ background-color: $bg-mix-light-9;
+ border-radius: 50%;
+ transition: all 0.3s ease-in-out;
+ }
+
+ &:hover {
+ background-color: transparent;
+ box-shadow: 0 40px 25px #ddd inset;
+
+ &::before {
+ left: 18px;
+ }
+
+ &::after {
+ opacity: 0;
+ }
+ }
+ }
+
+ .bg-bubble {
+ background-color: $bg-mix-light-9;
+ }
+
+ // 鍏朵粬鍏冪礌棰滆壊璋冩暣
+ .square-rotated {
+ background-color: $bg-mix-light-9;
+ }
+
+ .circle-small,
+ .dot {
+ background-color: $primary-light-8;
+ }
+
+ .square-bottom-right {
+ background-color: $primary-light-9;
+ }
+
+ .dot.dot-top-right {
+ background-color: $primary-light-8;
+ }
+ }
+
+ // 鏂瑰潡缁勬殫鑹茶皟鏁�
+ .squares-group {
+ .square {
+ box-shadow: none;
+
+ &.square-blue {
+ background-color: rgb(from $primary-base r g b / 18%);
+ }
+
+ &.square-pink {
+ background-color: rgb(from $primary-base r g b / 10%);
+ }
+
+ &.square-purple {
+ background-color: rgb(from $primary-base r g b / 20%);
+ }
+ }
+
+ &::after {
+ background: linear-gradient(90deg, $primary-light-8, transparent);
+ }
+ }
+ }
+</style>
diff --git a/rsf-design/src/components/core/views/result/ArtResultPage.vue b/rsf-design/src/components/core/views/result/ArtResultPage.vue
new file mode 100644
index 0000000..6e6fed2
--- /dev/null
+++ b/rsf-design/src/components/core/views/result/ArtResultPage.vue
@@ -0,0 +1,31 @@
+<template>
+ <div class="page-content box-border !px-20 py-3.5 text-center max-md:!px-5" :class="type">
+ <ArtSvgIcon
+ class="icon size-22 p-2 mt-16 block rounded-full !text-white"
+ :icon="iconCode"
+ :class="type === 'success' ? 'bg-[#19BE6B]' : 'bg-[#ED4014]'"
+ />
+ <h1 class="title mt-8 text-3xl font-medium !text-g-900 max-md:mt-2.5 max-md:text-2xl">{{
+ title
+ }}</h1>
+ <p class="msg mt-5 text-base text-g-600">{{ message }}</p>
+ <div
+ class="res mt-7.5 rounded bg-g-200/80 dark:bg-g-300/40 px-7.5 py-5.5 text-left max-md:px-7.5 max-md:py-2.5 [&_p]:flex [&_p]:items-center [&_p]:py-2 [&_p]:text-sm [&_p]:text-[#808695] [&_p_i]:mr-1.5"
+ >
+ <slot name="content"></slot>
+ </div>
+ <div class="btn-group mt-12.5">
+ <slot name="buttons"></slot>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtResultPage' })
+ defineProps({
+ type: { required: false, default: 'success' },
+ title: { required: false, default: '' },
+ message: { required: false, default: '' },
+ iconCode: { required: false, default: '' }
+ })
+</script>
diff --git a/rsf-design/src/components/core/widget/art-icon-button/index.vue b/rsf-design/src/components/core/widget/art-icon-button/index.vue
new file mode 100644
index 0000000..372bb47
--- /dev/null
+++ b/rsf-design/src/components/core/widget/art-icon-button/index.vue
@@ -0,0 +1,18 @@
+<!-- 鎸夐挳缁勪欢 -->
+<template>
+ <div
+ class="size-8.5 inline-flex flex-cc c-p text-g-600 dark:text-g-800 text-xl rounded tad-300 hover:bg-hover-color"
+ :class="{ 'rounded-full': circle }"
+ >
+ <ArtSvgIcon :icon="icon"></ArtSvgIcon>
+ <slot></slot>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ArtIconButton' })
+ defineProps({
+ icon: { required: true },
+ circle: { required: false }
+ })
+</script>
diff --git a/rsf-design/src/config/assets/images.js b/rsf-design/src/config/assets/images.js
new file mode 100644
index 0000000..878f480
--- /dev/null
+++ b/rsf-design/src/config/assets/images.js
@@ -0,0 +1,42 @@
+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'
+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
+ }
+}
+export { configImages }
diff --git a/rsf-design/src/config/index.js b/rsf-design/src/config/index.js
new file mode 100644
index 0000000..5880ec6
--- /dev/null
+++ b/rsf-design/src/config/index.js
@@ -0,0 +1,95 @@
+import { MenuThemeEnum, MenuTypeEnum, SystemThemeEnum } from '@/enums/appEnum'
+import { configImages } from './assets/images'
+import fastEnterConfig from './modules/fastEnter'
+import { headerBarConfig } from './modules/headerBar'
+const appConfig = {
+ // 绯荤粺淇℃伅
+ systemInfo: {
+ name: 'Art Design Pro'
+ // 绯荤粺鍚嶇О
+ },
+ // 绯荤粺涓婚
+ 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'],
+ // 蹇�熷叆鍙i厤缃�
+ fastEnter: fastEnterConfig,
+ // 椤堕儴鏍忓姛鑳介厤缃�
+ headerBar: headerBarConfig
+}
+
+export default Object.freeze(appConfig)
diff --git a/rsf-design/src/config/modules/component.js b/rsf-design/src/config/modules/component.js
new file mode 100644
index 0000000..dcb9fdc
--- /dev/null
+++ b/rsf-design/src/config/modules/component.js
@@ -0,0 +1,58 @@
+import { defineAsyncComponent } from 'vue'
+const globalComponentsConfig = [
+ {
+ 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
+ }
+]
+const getEnabledGlobalComponents = () => {
+ return globalComponentsConfig.filter((config) => config.enabled !== false)
+}
+const getGlobalComponentByKey = (key) => {
+ return globalComponentsConfig.find((config) => config.key === key)
+}
+export { getEnabledGlobalComponents, getGlobalComponentByKey, globalComponentsConfig }
diff --git a/rsf-design/src/config/modules/fastEnter.js b/rsf-design/src/config/modules/fastEnter.js
new file mode 100644
index 0000000..0e85625
--- /dev/null
+++ b/rsf-design/src/config/modules/fastEnter.js
@@ -0,0 +1,71 @@
+import { WEB_LINKS } from '@/utils/constants'
+
+const 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/rsf-design/src/config/modules/festival.js b/rsf-design/src/config/modules/festival.js
new file mode 100644
index 0000000..6519a98
--- /dev/null
+++ b/rsf-design/src/config/modules/festival.js
@@ -0,0 +1,21 @@
+const festivalConfigList = [
+ // 璺ㄦ棩鏈熺ず渚�
+ // {
+ // name: 'v3.0 Sass 鍗囩骇鑷� TailwindCSS',
+ // date: '2025-11-03',
+ // endDate: '2025-11-09',
+ // image: '',
+ // count: 3,
+ // scrollText:
+ // '馃殌 绯荤粺 v3.0 娴嬭瘯闃舵姝e紡寮�鍚紒娴嬭瘯鍛ㄦ湡涓� 11 鏈� 3 鏃� - 11 鏈� 16 鏃ワ紝閫氳繃 TailwindCSS 閲嶆瀯鏍峰紡浣撶郴銆佺粺涓� Iconify 鍥炬爣鏂规锛屽甫鏉ユ洿楂樻晥鐜颁唬鐨勫紑鍙戜綋楠岋紝姝e紡鍙戝竷鏁鏈熷緟锝�'
+ // }
+ // 鍗曟棩绀轰緥锛氬湥璇炶妭
+ // {
+ // name: '鍦h癁鑺�',
+ // date: '2024-12-25',
+ // image: sd,
+ // count: 3 // 鍙�夛紝涓嶈缃垯浣跨敤榛樿鍊� 3 娆�
+ // scrollText: 'Merry Christmas锛丄rt Design Pro 绁濇偍鍦h癁蹇箰锛屾効鑺傛棩鐨勬涔愪笌绁濈濡傞洩鑺辫埇绾疯嚦娌撴潵锛�',
+ // }
+]
+export { festivalConfigList }
diff --git a/rsf-design/src/config/modules/headerBar.js b/rsf-design/src/config/modules/headerBar.js
new file mode 100644
index 0000000..cedefc9
--- /dev/null
+++ b/rsf-design/src/config/modules/headerBar.js
@@ -0,0 +1,49 @@
+const headerBarConfig = {
+ menuButton: {
+ enabled: true,
+ description: '鎺у埗宸︿晶鑿滃崟鐨勫睍寮�/鏀惰捣鎸夐挳'
+ },
+ refreshButton: {
+ enabled: true,
+ description: '椤甸潰鍒锋柊鎸夐挳'
+ },
+ fastEnter: {
+ enabled: true,
+ description: '蹇�熷叆鍙e姛鑳斤紝鎻愪緵甯哥敤搴旂敤鍜岄摼鎺ョ殑蹇�熻闂�'
+ },
+ 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
+export { headerBarConfig }
diff --git a/rsf-design/src/config/setting.js b/rsf-design/src/config/setting.js
new file mode 100644
index 0000000..a15ffab
--- /dev/null
+++ b/rsf-design/src/config/setting.js
@@ -0,0 +1,74 @@
+import AppConfig from '@/config'
+import { SystemThemeEnum, MenuThemeEnum, MenuTypeEnum, ContainerWidthEnum } from '@/enums/appEnum'
+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: ''
+}
+function getSettingDefaults() {
+ return { ...SETTING_DEFAULT_CONFIG }
+}
+function resetToDefaults(currentSettings) {
+ const defaults = getSettingDefaults()
+ Object.keys(defaults).forEach((key) => {
+ if (key in currentSettings) {
+ currentSettings[key] = defaults[key]
+ }
+ })
+}
+export { SETTING_DEFAULT_CONFIG, getSettingDefaults, resetToDefaults }
diff --git a/rsf-design/src/directives/business/highlight.js b/rsf-design/src/directives/business/highlight.js
new file mode 100644
index 0000000..27cf279
--- /dev/null
+++ b/rsf-design/src/directives/business/highlight.js
@@ -0,0 +1,148 @@
+import hljs from 'highlight.js'
+function highlightCode(block) {
+ hljs.highlightElement(block)
+}
+function insertLineNumbers(block) {
+ const lines = block.innerHTML.split('\n')
+ const numberedLines = lines
+ .map((line, index) => {
+ return `<span class="line-number">${index + 1}</span> ${line}`
+ })
+ .join('\n')
+ block.innerHTML = numberedLines
+}
+function addCopyButton(block) {
+ const copyButton = document.createElement('i')
+ copyButton.className = 'copy-button'
+ copyButton.innerHTML =
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 0 1 3 21l.003-14c0-.552.45-1 1.006-1zM5.002 8L5 20h10V8zM9 6h8v10h2V4H9z"/></svg>'
+ 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
+ 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
+ }
+ preElement.appendChild(copyButton)
+ }
+}
+function isBlockProcessed(block) {
+ return (
+ block.hasAttribute('data-highlighted') ||
+ !!block.querySelector('.line-number') ||
+ !!block.parentElement?.querySelector('.copy-button')
+ )
+}
+function markBlockAsProcessed(block) {
+ block.setAttribute('data-highlighted', 'true')
+}
+function processBlock(block) {
+ if (isBlockProcessed(block)) {
+ return
+ }
+ try {
+ highlightCode(block)
+ insertLineNumbers(block)
+ addCopyButton(block)
+ markBlockAsProcessed(block)
+ } catch (error) {
+ console.warn('澶勭悊浠g爜鍧楁椂鍑洪敊:', error)
+ }
+}
+function processAllCodeBlocks(el) {
+ const blocks = Array.from(el.querySelectorAll('pre code'))
+ const unprocessedBlocks = blocks.filter((block) => !isBlockProcessed(block))
+ if (unprocessedBlocks.length === 0) {
+ return
+ }
+ if (unprocessedBlocks.length <= 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(processBatch)
+ }
+ }
+ processBatch()
+ }
+}
+function retryProcessing(el, maxRetries = 3, delay = 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 = {
+ mounted(el) {
+ processAllCodeBlocks(el)
+ setTimeout(() => {
+ retryProcessing(el)
+ }, 100)
+ 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
+ if (element.tagName === 'PRE' || element.querySelector('pre code')) {
+ hasNewCodeBlocks = true
+ }
+ }
+ })
+ }
+ })
+ if (hasNewCodeBlocks) {
+ setTimeout(() => {
+ processAllCodeBlocks(el)
+ }, 50)
+ }
+ })
+ observer.observe(el, {
+ childList: true,
+ subtree: true
+ })
+ el._highlightObserver = observer
+ },
+ updated(el) {
+ setTimeout(() => {
+ processAllCodeBlocks(el)
+ }, 50)
+ },
+ unmounted(el) {
+ const observer = el._highlightObserver
+ if (observer) {
+ observer.disconnect()
+ delete el._highlightObserver
+ }
+ }
+}
+function setupHighlightDirective(app) {
+ app.directive('highlight', highlightDirective)
+}
+export { setupHighlightDirective }
diff --git a/rsf-design/src/directives/business/ripple.js b/rsf-design/src/directives/business/ripple.js
new file mode 100644
index 0000000..3f444c2
--- /dev/null
+++ b/rsf-design/src/directives/business/ripple.js
@@ -0,0 +1,47 @@
+const vRipple = {
+ mounted(el, binding) {
+ const options = binding.value || {}
+ el.style.position = 'relative'
+ el.style.overflow = 'hidden'
+ el.addEventListener('mousedown', (e) => {
+ 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'
+ 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'
+ el.appendChild(ripple)
+ requestAnimationFrame(() => {
+ ripple.style.transform = 'scale(2)'
+ ripple.style.opacity = '0'
+ })
+ setTimeout(() => {
+ ripple.remove()
+ }, animationDuration + 500)
+ })
+ }
+}
+function setupRippleDirective(app) {
+ app.directive('ripple', vRipple)
+}
+export { setupRippleDirective, vRipple }
diff --git a/rsf-design/src/directives/core/auth.js b/rsf-design/src/directives/core/auth.js
new file mode 100644
index 0000000..266328b
--- /dev/null
+++ b/rsf-design/src/directives/core/auth.js
@@ -0,0 +1,21 @@
+import { router } from '@/router'
+function checkAuthPermission(el, binding) {
+ const authList = router.currentRoute.value.meta.authList || []
+ const hasPermission = authList.some((item) => item.authMark === binding.value)
+ if (!hasPermission) {
+ removeElement(el)
+ }
+}
+function removeElement(el) {
+ if (el.parentNode) {
+ el.parentNode.removeChild(el)
+ }
+}
+const authDirective = {
+ mounted: checkAuthPermission,
+ updated: checkAuthPermission
+}
+function setupAuthDirective(app) {
+ app.directive('auth', authDirective)
+}
+export { setupAuthDirective }
diff --git a/rsf-design/src/directives/core/roles.js b/rsf-design/src/directives/core/roles.js
new file mode 100644
index 0000000..2fb4155
--- /dev/null
+++ b/rsf-design/src/directives/core/roles.js
@@ -0,0 +1,27 @@
+import { useUserStore } from '@/store/modules/user'
+function checkRolePermission(el, binding) {
+ 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) => userRoles.includes(role))
+ if (!hasPermission) {
+ removeElement(el)
+ }
+}
+function removeElement(el) {
+ if (el.parentNode) {
+ el.parentNode.removeChild(el)
+ }
+}
+const rolesDirective = {
+ mounted: checkRolePermission,
+ updated: checkRolePermission
+}
+function setupRolesDirective(app) {
+ app.directive('roles', rolesDirective)
+}
+export { setupRolesDirective }
diff --git a/rsf-design/src/directives/index.js b/rsf-design/src/directives/index.js
new file mode 100644
index 0000000..6552920
--- /dev/null
+++ b/rsf-design/src/directives/index.js
@@ -0,0 +1,11 @@
+import { setupAuthDirective } from './core/auth'
+import { setupHighlightDirective } from './business/highlight'
+import { setupRippleDirective } from './business/ripple'
+import { setupRolesDirective } from './core/roles'
+function setupGlobDirectives(app) {
+ setupAuthDirective(app)
+ setupRolesDirective(app)
+ setupHighlightDirective(app)
+ setupRippleDirective(app)
+}
+export { setupGlobDirectives }
diff --git a/rsf-design/src/enums/appEnum.js b/rsf-design/src/enums/appEnum.js
new file mode 100644
index 0000000..62699a6
--- /dev/null
+++ b/rsf-design/src/enums/appEnum.js
@@ -0,0 +1,34 @@
+const MenuTypeEnum = Object.freeze({
+ LEFT: 'left',
+ TOP: 'top',
+ TOP_LEFT: 'top-left',
+ DUAL_MENU: 'dual-menu'
+})
+
+const SystemThemeEnum = Object.freeze({
+ DARK: 'dark',
+ LIGHT: 'light',
+ AUTO: 'auto'
+})
+
+const MenuThemeEnum = Object.freeze({
+ DARK: 'dark',
+ LIGHT: 'light',
+ DESIGN: 'design'
+})
+
+const MenuWidth = Object.freeze({
+ CLOSE: '64px'
+})
+
+const LanguageEnum = Object.freeze({
+ ZH: 'zh',
+ EN: 'en'
+})
+
+const ContainerWidthEnum = Object.freeze({
+ FULL: '100%',
+ BOXED: '1200px'
+})
+
+export { ContainerWidthEnum, LanguageEnum, MenuThemeEnum, MenuTypeEnum, MenuWidth, SystemThemeEnum }
diff --git a/rsf-design/src/enums/formEnum.js b/rsf-design/src/enums/formEnum.js
new file mode 100644
index 0000000..991b98b
--- /dev/null
+++ b/rsf-design/src/enums/formEnum.js
@@ -0,0 +1,12 @@
+const PageModeEnum = Object.freeze({
+ Add: 0,
+ Edit: 1
+})
+
+const TableSizeEnum = Object.freeze({
+ DEFAULT: 'default',
+ SMALL: 'small',
+ LARGE: 'large'
+})
+
+export { PageModeEnum, TableSizeEnum }
diff --git a/rsf-design/src/hooks/core/useAppMode.js b/rsf-design/src/hooks/core/useAppMode.js
new file mode 100644
index 0000000..1cef5ad
--- /dev/null
+++ b/rsf-design/src/hooks/core/useAppMode.js
@@ -0,0 +1,13 @@
+import { computed } from 'vue'
+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
+ }
+}
+export { useAppMode }
diff --git a/rsf-design/src/hooks/core/useAuth.js b/rsf-design/src/hooks/core/useAuth.js
new file mode 100644
index 0000000..5a4949c
--- /dev/null
+++ b/rsf-design/src/hooks/core/useAuth.js
@@ -0,0 +1,22 @@
+import { useRoute } from 'vue-router'
+import { storeToRefs } from 'pinia'
+import { useUserStore } from '@/store/modules/user'
+import { useAppMode } from '@/hooks/core/useAppMode'
+const userStore = useUserStore()
+const useAuth = () => {
+ const route = useRoute()
+ const { isFrontendMode } = useAppMode()
+ const { info } = storeToRefs(userStore)
+ const frontendAuthList = info.value?.buttons ?? []
+ const backendAuthList = Array.isArray(route.meta.authList) ? route.meta.authList : []
+ const hasAuth = (auth) => {
+ if (isFrontendMode.value) {
+ return frontendAuthList.includes(auth)
+ }
+ return backendAuthList.some((item) => item?.authMark === auth)
+ }
+ return {
+ hasAuth
+ }
+}
+export { useAuth }
diff --git a/rsf-design/src/hooks/core/useCeremony.js b/rsf-design/src/hooks/core/useCeremony.js
new file mode 100644
index 0000000..fbcf000
--- /dev/null
+++ b/rsf-design/src/hooks/core/useCeremony.js
@@ -0,0 +1,83 @@
+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: 1e3,
+ /** 鏂囨湰鏄剧ず寤惰繜锛堟绉掞級 */
+ TEXT_DELAY: 2e3,
+ /** 榛樿鐑熻姳鎾斁娆℃暟 */
+ DEFAULT_FIREWORKS_COUNT: 3
+}
+function useCeremony() {
+ const settingStore = useSettingStore()
+ const { holidayFireworksLoaded, isShowFireworks } = storeToRefs(settingStore)
+ let fireworksInterval = null
+ const isDateInRange = (currentDate, festivalDate, festivalEndDate) => {
+ 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(/* @__PURE__ */ new Date(), 'YYYY-MM-DD').value
+ return festivalConfigList.find((item) => isDateInRange(currentDate, item.date, item.endDate))
+ })
+ 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
+ }
+}
+export { useCeremony }
diff --git a/rsf-design/src/hooks/core/useChart.js b/rsf-design/src/hooks/core/useChart.js
new file mode 100644
index 0000000..8710c7a
--- /dev/null
+++ b/rsf-design/src/hooks/core/useChart.js
@@ -0,0 +1,534 @@
+import { echarts } from '@/plugins/echarts'
+import { storeToRefs } from 'pinia'
+import { useSettingStore } from '@/store/modules/setting'
+import { getCssVar } from '@/utils/ui'
+const useChartOps = () => ({
+ /** */
+ 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]
+const MENU_RESIZE_DELAYS = [50, 100, 200]
+const RESIZE_DEBOUNCE_DELAY = 100
+function useChart(options = {}) {
+ const { initOptions, initDelay = 0, threshold = 0.1, autoTheme = true } = options
+ const settingStore = useSettingStore()
+ const { isDark, menuOpen, menuType } = storeToRefs(settingStore)
+ const chartRef = ref()
+ let chart = null
+ let intersectionObserver = null
+ let pendingOptions = null
+ let resizeTimeoutId = null
+ let resizeFrameId = null
+ let isDestroyed = false
+ let emptyStateDiv = null
+ const clearTimers = () => {
+ if (resizeTimeoutId) {
+ clearTimeout(resizeTimeoutId)
+ resizeTimeoutId = null
+ }
+ if (resizeFrameId) {
+ cancelAnimationFrame(resizeFrameId)
+ resizeFrameId = null
+ }
+ }
+ const requestAnimationResize = () => {
+ if (resizeFrameId) {
+ cancelAnimationFrame(resizeFrameId)
+ }
+ resizeFrameId = requestAnimationFrame(() => {
+ handleResize()
+ resizeFrameId = null
+ })
+ }
+ const debouncedResize = () => {
+ if (resizeTimeoutId) {
+ clearTimeout(resizeTimeoutId)
+ }
+ resizeTimeoutId = window.setTimeout(() => {
+ requestAnimationResize()
+ resizeTimeoutId = null
+ }, RESIZE_DEBOUNCE_DELAY)
+ }
+ const multiDelayResize = (delays) => {
+ nextTick(requestAnimationResize)
+ delays.forEach((delay) => {
+ setTimeout(requestAnimationResize, delay)
+ })
+ }
+ let menuOpenStopHandle = null
+ let menuTypeStopHandle = 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 = null
+ const setupThemeWatcher = () => {
+ if (autoTheme) {
+ themeStopHandle = watch(isDark, () => {
+ emptyStateManager.updateStyle()
+ if (chart && !isDestroyed) {
+ requestAnimationFrame(() => {
+ if (chart && !isDestroyed) {
+ const currentOptions = chart.getOption()
+ if (currentOptions) {
+ updateChart(currentOptions)
+ }
+ }
+ })
+ }
+ })
+ }
+ }
+ const cleanupThemeWatcher = () => {
+ themeStopHandle?.()
+ themeStopHandle = null
+ }
+ const createLineStyle = (color, width = 1, type) => ({
+ color,
+ width,
+ ...(type && { type })
+ })
+ const styleCache = {
+ axisLine: null,
+ splitLine: null,
+ axisLabel: null,
+ lastDarkValue: isDark.value
+ }
+ const clearStyleCache = () => {
+ styleCache.axisLine = null
+ styleCache.splitLine = null
+ styleCache.axisLabel = null
+ styleCache.lastDarkValue = isDark.value
+ }
+ const getAxisLineStyle = (show = 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 = 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 = 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 = 50, animationDuration = 1500) => ({
+ animationDelay: (idx) => idx * animationDelay + 200,
+ animationDuration: (idx) => animationDuration - idx * 50,
+ animationEasing: 'quarticOut'
+ })
+ const getTooltipStyle = (trigger = 'axis', customOptions = {}) => ({
+ 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', customOptions = {}) => {
+ 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
+ }
+ }
+ const getGridWithLegend = (showLegend, legendPosition = 'bottom', baseGrid = {}) => {
+ const defaultGrid = {
+ top: 15,
+ right: 15,
+ bottom: 8,
+ left: 0,
+ containLabel: true,
+ ...baseGrid
+ }
+ if (!showLegend) {
+ return defaultGrid
+ }
+ 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
+ }
+ }
+ const createIntersectionObserver = () => {
+ if (intersectionObserver || !chartRef.value) return
+ intersectionObserver = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting && pendingOptions && !isDestroyed) {
+ requestAnimationFrame(() => {
+ if (!isDestroyed && pendingOptions) {
+ try {
+ if (!chart) {
+ chart = echarts.init(entry.target)
+ }
+ 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)
+ }
+ const cleanupIntersectionObserver = () => {
+ if (intersectionObserver) {
+ intersectionObserver.disconnect()
+ intersectionObserver = null
+ }
+ }
+ const isContainerVisible = (element) => {
+ const rect = element.getBoundingClientRect()
+ return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight && rect.bottom > 0
+ }
+ const performChartInit = (options2) => {
+ if (!chart && chartRef.value && !isDestroyed) {
+ chart = echarts.init(chartRef.value)
+ setupMenuWatchers()
+ setupThemeWatcher()
+ }
+ if (chart && !isDestroyed) {
+ chart.setOption(options2)
+ 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 = `<span>鏆傛棤鏁版嵁</span>`
+ 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 = (options2 = {}, isEmpty = false) => {
+ if (!chartRef.value || isDestroyed) return
+ const mergedOptions = { ...initOptions, ...options2 }
+ try {
+ if (isEmpty) {
+ if (chart) {
+ chart.clear()
+ }
+ emptyStateManager.create()
+ return
+ } else {
+ 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 = (options2) => {
+ if (isDestroyed) return
+ try {
+ if (!chart) {
+ initChart(options2)
+ return
+ }
+ chart.setOption(options2)
+ } 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
+ }
+}
+function useChartComponent(options) {
+ 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) {
+ if (chart.getChartInstance()) {
+ chart.getChartInstance()?.clear()
+ }
+ emptyStateManager.create()
+ } else {
+ emptyStateManager.remove()
+ initChart(generateOptions())
+ }
+ })
+ }
+ const handleChartVisible = () => {
+ if (onVisible) {
+ onVisible()
+ } else {
+ updateChart()
+ }
+ }
+ const stopHandles = []
+ 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()
+ emptyStateManager.remove()
+ })
+ }
+ setupWatchers()
+ setupLifecycle()
+ return {
+ ...chart,
+ isEmpty,
+ updateChart,
+ handleChartVisible
+ }
+}
+export { useChart, useChartComponent, useChartOps }
diff --git a/rsf-design/src/hooks/core/useCommon.js b/rsf-design/src/hooks/core/useCommon.js
new file mode 100644
index 0000000..c5558de
--- /dev/null
+++ b/rsf-design/src/hooks/core/useCommon.js
@@ -0,0 +1,43 @@
+import { computed } from 'vue'
+import { useMenuStore } from '@/store/modules/menu'
+import { useSettingStore } from '@/store/modules/setting'
+function useCommon() {
+ const menuStore = useMenuStore()
+ const settingStore = useSettingStore()
+ const homePath = computed(() => menuStore.getHomePath())
+ const refresh = () => {
+ settingStore.reload()
+ }
+ const scrollToTop = () => {
+ const scrollContainer = document.getElementById('app-main')
+ if (scrollContainer) {
+ scrollContainer.scrollTop = 0
+ }
+ }
+ const smoothScrollToTop = () => {
+ const scrollContainer = document.getElementById('app-main')
+ if (scrollContainer) {
+ scrollContainer.scrollTo({
+ top: 0,
+ behavior: 'smooth'
+ })
+ }
+ }
+ const scrollTo = (top, smooth = false) => {
+ const scrollContainer = document.getElementById('app-main')
+ if (scrollContainer) {
+ scrollContainer.scrollTo({
+ top,
+ behavior: smooth ? 'smooth' : 'auto'
+ })
+ }
+ }
+ return {
+ homePath,
+ refresh,
+ scrollTo,
+ scrollToTop,
+ smoothScrollToTop
+ }
+}
+export { useCommon }
diff --git a/rsf-design/src/hooks/core/useFastEnter.js b/rsf-design/src/hooks/core/useFastEnter.js
new file mode 100644
index 0000000..d2a0158
--- /dev/null
+++ b/rsf-design/src/hooks/core/useFastEnter.js
@@ -0,0 +1,27 @@
+import { computed } from 'vue'
+import appConfig from '@/config'
+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
+ }
+}
+export { useFastEnter }
diff --git a/rsf-design/src/hooks/core/useHeaderBar.js b/rsf-design/src/hooks/core/useHeaderBar.js
new file mode 100644
index 0000000..f4e7939
--- /dev/null
+++ b/rsf-design/src/hooks/core/useHeaderBar.js
@@ -0,0 +1,123 @@
+import { computed } from 'vue'
+import { storeToRefs } from 'pinia'
+import { useSettingStore } from '@/store/modules/setting'
+import { headerBarConfig } from '@/config/modules/headerBar'
+function useHeaderBar() {
+ const settingStore = useSettingStore()
+ const headerBarConfigRef = computed(() => headerBarConfig)
+ const { showMenuButton, showFastEnter, showRefreshButton, showCrumbs, showLanguage } =
+ storeToRefs(settingStore)
+ const isFeatureEnabled = (feature) => {
+ return headerBarConfigRef.value[feature]?.enabled ?? false
+ }
+ const getFeatureConfig = (feature) => {
+ 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?.minWidth || 1200
+ })
+ const isFeatureActive = (feature) => {
+ return isFeatureEnabled(feature)
+ }
+ const getFeatureInfo = (feature) => {
+ return getFeatureConfig(feature)
+ }
+ const getEnabledFeatures = () => {
+ return Object.keys(headerBarConfigRef.value).filter(
+ (key) => headerBarConfigRef.value[key]?.enabled
+ )
+ }
+ const getDisabledFeatures = () => {
+ return Object.keys(headerBarConfigRef.value).filter(
+ (key) => !headerBarConfigRef.value[key]?.enabled
+ )
+ }
+ const getActiveFeatures = () => {
+ return getEnabledFeatures()
+ }
+ const getInactiveFeatures = () => {
+ return getDisabledFeatures()
+ }
+ return {
+ // 閰嶇疆
+ headerBarConfig: headerBarConfigRef,
+ // 鏄剧ず鐘舵�佽绠楀睘鎬�
+ shouldShowMenuButton,
+ // 鏄惁鏄剧ず鑿滃崟鎸夐挳
+ shouldShowRefreshButton,
+ // 鏄惁鏄剧ず鍒锋柊鎸夐挳
+ shouldShowFastEnter,
+ // 鏄惁鏄剧ず蹇�熷叆鍙�
+ shouldShowBreadcrumb,
+ // 鏄惁鏄剧ず闈㈠寘灞�
+ shouldShowGlobalSearch,
+ // 鏄惁鏄剧ず鍏ㄥ眬鎼滅储
+ shouldShowFullscreen,
+ // 鏄惁鏄剧ず鍏ㄥ睆鎸夐挳
+ shouldShowNotification,
+ // 鏄惁鏄剧ず閫氱煡涓績
+ shouldShowChat,
+ // 鏄惁鏄剧ず鑱婂ぉ鍔熻兘
+ shouldShowLanguage,
+ // 鏄惁鏄剧ず璇█鍒囨崲
+ shouldShowSettings,
+ // 鏄惁鏄剧ず璁剧疆闈㈡澘
+ shouldShowThemeToggle,
+ // 鏄惁鏄剧ず涓婚鍒囨崲
+ // 閰嶇疆鐩稿叧
+ fastEnterMinWidth,
+ // 蹇�熷叆鍙f渶灏忓搴�
+ // 鏂规硶
+ isFeatureEnabled,
+ // 妫�鏌ュ姛鑳芥槸鍚﹀惎鐢�
+ isFeatureActive,
+ // 妫�鏌ュ姛鑳芥槸鍚﹀惎鐢紙鍒悕锛�
+ getFeatureConfig,
+ // 鑾峰彇鍔熻兘閰嶇疆
+ getFeatureInfo,
+ // 鑾峰彇鍔熻兘閰嶇疆锛堝埆鍚嶏級
+ getEnabledFeatures,
+ // 鑾峰彇鎵�鏈夊惎鐢ㄧ殑鍔熻兘
+ getDisabledFeatures,
+ // 鑾峰彇鎵�鏈夌鐢ㄧ殑鍔熻兘
+ getActiveFeatures,
+ // 鑾峰彇鎵�鏈夊惎鐢ㄧ殑鍔熻兘锛堝埆鍚嶏級
+ getInactiveFeatures
+ // 鑾峰彇鎵�鏈夌鐢ㄧ殑鍔熻兘锛堝埆鍚嶏級
+ }
+}
+export { useHeaderBar }
diff --git a/rsf-design/src/hooks/core/useLayoutHeight.js b/rsf-design/src/hooks/core/useLayoutHeight.js
new file mode 100644
index 0000000..16da15b
--- /dev/null
+++ b/rsf-design/src/hooks/core/useLayoutHeight.js
@@ -0,0 +1,85 @@
+import { ref, computed, watch, onMounted } from 'vue'
+import { useElementSize } from '@vueuse/core'
+function useLayoutHeight(options = {}) {
+ const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
+ const headerRef = ref()
+ const contentHeaderRef = ref()
+ 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
+ }
+}
+function useAutoLayoutHeight(headerIds = ['app-header', 'app-content-header'], options = {}) {
+ const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
+ const headerRef = ref()
+ const contentHeaderRef = ref()
+ 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 }
+ )
+ }
+ onMounted(() => {
+ if (typeof document !== 'undefined') {
+ 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
+ }
+}
+export { useAutoLayoutHeight, useLayoutHeight }
diff --git a/rsf-design/src/hooks/core/useTable.js b/rsf-design/src/hooks/core/useTable.js
new file mode 100644
index 0000000..953a980
--- /dev/null
+++ b/rsf-design/src/hooks/core/useTable.js
@@ -0,0 +1,457 @@
+import { ref, reactive, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue'
+import { useWindowSize } from '@vueuse/core'
+import { useTableColumns } from './useTableColumns'
+import { TableCache, CacheInvalidationStrategy } from '../../utils/table/tableCache'
+import {
+ defaultResponseAdapter,
+ extractTableData,
+ updatePaginationFromResponse,
+ createSmartDebounce,
+ createErrorHandler
+} from '../../utils/table/tableUtils'
+import { tableConfig } from '../../utils/table/tableConfig'
+function useTable(config) {
+ return useTableImpl(config)
+}
+function useTableImpl(config) {
+ const {
+ core: {
+ apiFn,
+ apiParams = {},
+ excludeParams = [],
+ immediate = true,
+ columnsFactory,
+ paginationKey
+ },
+ transform: { dataTransformer, responseAdapter = defaultResponseAdapter } = {},
+ performance: {
+ enableCache = false,
+ cacheTime = 5 * 60 * 1e3,
+ 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, ...args) => {
+ if (enableLog) {
+ console.log(`[useTable] ${message}`, ...args)
+ }
+ },
+ warn: (message, ...args) => {
+ if (enableLog) {
+ console.warn(`[useTable] ${message}`, ...args)
+ }
+ },
+ error: (message, ...args) => {
+ if (enableLog) {
+ console.error(`[useTable] ${message}`, ...args)
+ }
+ }
+ }
+ const cache = enableCache ? new TableCache(cacheTime, maxCacheSize, enableLog) : null
+ const loadingState = ref('idle')
+ const loading = computed(() => loadingState.value === 'loading')
+ const error = ref(null)
+ const data = ref([])
+ let abortController = null
+ let cacheCleanupTimer = null
+ const searchParams = reactive(
+ Object.assign(
+ {
+ [pageKey]: 1,
+ [sizeKey]: 10
+ },
+ apiParams || {}
+ )
+ )
+ const pagination = reactive({
+ current: searchParams[pageKey] || 1,
+ size: searchParams[sizeKey] || 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, context) => {
+ 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, useCache = enableCache) => {
+ if (abortController) {
+ abortController.abort()
+ }
+ const currentController = new AbortController()
+ abortController = currentController
+ loadingState.value = 'loading'
+ error.value = null
+ try {
+ let requestParams = Object.assign(
+ {},
+ searchParams,
+ {
+ [pageKey]: pagination.current,
+ [sizeKey]: pagination.size
+ },
+ params || {}
+ )
+ if (excludeParams.length > 0) {
+ const filteredParams = { ...requestParams }
+ excludeParams.forEach((key) => {
+ delete filteredParams[key]
+ })
+ requestParams = filteredParams
+ }
+ if (useCache && cache) {
+ const cachedItem = cache.get(requestParams)
+ if (cachedItem) {
+ data.value = cachedItem.data
+ updatePaginationFromResponse(pagination, cachedItem.response)
+ const paramsRecord2 = searchParams
+ if (paramsRecord2[pageKey] !== pagination.current) {
+ paramsRecord2[pageKey] = pagination.current
+ }
+ if (paramsRecord2[sizeKey] !== pagination.size) {
+ paramsRecord2[sizeKey] = pagination.size
+ }
+ loadingState.value = 'success'
+ 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
+ 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(`鏁版嵁宸茬紦瀛榒)
+ }
+ loadingState.value = 'success'
+ if (onSuccess) {
+ onSuccess(tableData, standardResponse)
+ }
+ return standardResponse
+ } catch (err) {
+ if (err instanceof Error && err.message === '璇锋眰宸插彇娑�') {
+ loadingState.value = 'idle'
+ return { records: [], total: 0, current: 1, size: 10 }
+ }
+ loadingState.value = 'error'
+ data.value = []
+ const tableError = handleError(err, '鑾峰彇琛ㄦ牸鏁版嵁澶辫触')
+ throw tableError
+ } finally {
+ if (abortController === currentController) {
+ abortController = null
+ }
+ }
+ }
+ const getData = async (params) => {
+ try {
+ return await fetchData(params)
+ } catch {
+ return Promise.resolve()
+ }
+ }
+ const getDataByPage = async (params) => {
+ pagination.current = 1
+ searchParams[pageKey] = 1
+ clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '鎼滅储鏁版嵁')
+ try {
+ return await fetchData(params, false)
+ } catch {
+ return Promise.resolve()
+ }
+ }
+ const debouncedGetDataByPage = createSmartDebounce(getDataByPage, debounceTime)
+ const resetSearchParams = async () => {
+ debouncedGetDataByPage.cancel()
+ const paramsRecord = searchParams
+ const defaultPagination = {
+ [pageKey]: 1,
+ [sizeKey]: paramsRecord[sizeKey] || 10
+ }
+ Object.keys(searchParams).forEach((key) => {
+ delete paramsRecord[key]
+ })
+ Object.assign(searchParams, apiParams || {}, defaultPagination)
+ pagination.current = 1
+ pagination.size = defaultPagination[sizeKey]
+ error.value = null
+ clearCache(CacheInvalidationStrategy.CLEAR_ALL, '閲嶇疆鎼滅储')
+ await getData()
+ if (resetFormCallback) {
+ await nextTick()
+ resetFormCallback()
+ }
+ }
+ const replaceSearchParams = (params) => {
+ const paramsRecord = searchParams
+ const currentSize = pagination.size || (paramsRecord[sizeKey] ?? 10)
+ Object.keys(searchParams).forEach((key) => {
+ if (key !== pageKey && key !== sizeKey) {
+ delete paramsRecord[key]
+ }
+ })
+ Object.assign(
+ searchParams,
+ {
+ [pageKey]: 1,
+ [sizeKey]: currentSize
+ },
+ params || {}
+ )
+ pagination.current = 1
+ pagination.size = currentSize
+ }
+ let isCurrentChanging = false
+ const handleSizeChange = async (newSize) => {
+ if (newSize <= 0) return
+ debouncedGetDataByPage.cancel()
+ const paramsRecord = searchParams
+ pagination.size = newSize
+ pagination.current = 1
+ paramsRecord[sizeKey] = newSize
+ paramsRecord[pageKey] = 1
+ clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '鍒嗛〉澶у皬鍙樺寲')
+ await getData()
+ }
+ const handleCurrentChange = async (newCurrent) => {
+ if (newCurrent <= 0) return
+ if (isCurrentChanging) {
+ return
+ }
+ if (pagination.current === newCurrent) {
+ logger.log('鍒嗛〉椤电爜鏈彉鍖栵紝璺宠繃璇锋眰')
+ return
+ }
+ try {
+ isCurrentChanging = true
+ const paramsRecord = searchParams
+ pagination.current = newCurrent
+ if (paramsRecord[pageKey] !== newCurrent) {
+ paramsRecord[pageKey] = newCurrent
+ }
+ await getData()
+ } finally {
+ isCurrentChanging = false
+ }
+ }
+ const refreshCreate = async () => {
+ debouncedGetDataByPage.cancel()
+ pagination.current = 1
+ searchParams[pageKey] = 1
+ clearCache(CacheInvalidationStrategy.CLEAR_PAGINATION, '鏂板鏁版嵁')
+ await getData()
+ }
+ const refreshUpdate = async () => {
+ clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '缂栬緫鏁版嵁')
+ await getData()
+ }
+ const refreshRemove = async () => {
+ const { current } = pagination
+ clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '鍒犻櫎鏁版嵁')
+ await getData()
+ if (data.value.length === 0 && current > 1) {
+ pagination.current = current - 1
+ searchParams[pageKey] = current - 1
+ await getData()
+ }
+ }
+ const refreshData = async () => {
+ debouncedGetDataByPage.cancel()
+ clearCache(CacheInvalidationStrategy.CLEAR_ALL, '鎵嬪姩鍒锋柊')
+ await getData()
+ }
+ const refreshSoft = async () => {
+ clearCache(CacheInvalidationStrategy.CLEAR_CURRENT, '杞埛鏂�')
+ await getData()
+ }
+ const cancelRequest = () => {
+ if (abortController) {
+ abortController.abort()
+ }
+ debouncedGetDataByPage.cancel()
+ }
+ const clearData = () => {
+ data.value = []
+ error.value = null
+ clearCache(CacheInvalidationStrategy.CLEAR_ALL, '娓呯┖鏁版嵁')
+ }
+ const clearExpiredCache = () => {
+ 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,
+ /** 鏇挎崲鎼滅储鍙傛暟锛堥�傜敤浜庤〃鍗曟煡璇紝閬垮厤鏃у瓧娈垫畫鐣欙級 */
+ replaceSearchParams,
+ /** 閲嶇疆鎼滅储鍙傛暟 */
+ 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
+ })
+ }
+}
+import { CacheInvalidationStrategy as CacheInvalidationStrategy2 } from '../../utils/table/tableCache'
+export { CacheInvalidationStrategy2 as CacheInvalidationStrategy, useTable }
diff --git a/rsf-design/src/hooks/core/useTableColumns.js b/rsf-design/src/hooks/core/useTableColumns.js
new file mode 100644
index 0000000..b9bc4bb
--- /dev/null
+++ b/rsf-design/src/hooks/core/useTableColumns.js
@@ -0,0 +1,164 @@
+import { ref, computed, watch } from 'vue'
+import { $t } from '@/locales'
+const SPECIAL_COLUMNS = {
+ selection: { prop: '__selection__', label: $t('table.column.selection') },
+ expand: { prop: '__expand__', label: $t('table.column.expand') },
+ index: { prop: '__index__', label: $t('table.column.index') }
+}
+const getColumnKey = (col) => SPECIAL_COLUMNS[col.type]?.prop ?? col.prop
+const getColumnVisibility = (col) => {
+ if (col.visible !== void 0) {
+ return col.visible
+ }
+ return col.checked ?? true
+}
+const getColumnChecks = (columns) =>
+ 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 }
+ })
+function useTableColumns(columnsFactory) {
+ const dynamicColumns = ref(columnsFactory())
+ const columnChecks = ref(getColumnChecks(dynamicColumns.value))
+ 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 }
+ )
+ 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)
+ })
+ const setDynamicColumns = (updater) => {
+ const copy = [...dynamicColumns.value]
+ const result = updater(copy)
+ dynamicColumns.value = Array.isArray(result) ? result : copy
+ }
+ return {
+ columns,
+ columnChecks,
+ /**
+ * 鏂板鍒楋紙鏀寔鍗曚釜鎴栨壒閲忥級
+ */
+ addColumn: (column, index) =>
+ 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) =>
+ setDynamicColumns((cols) => {
+ const propsToRemove = Array.isArray(prop) ? prop : [prop]
+ return cols.filter((c) => !propsToRemove.includes(getColumnKey(c)))
+ }),
+ /**
+ * 鏇存柊鍒楋紙鏀寔鍗曚釜鎴栨壒閲忥級
+ */
+ updateColumn: (prop, updates) => {
+ 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
+ })
+ })
+ } else if (updates) {
+ setDynamicColumns((cols) =>
+ cols.map((c) => (getColumnKey(c) === prop ? { ...c, ...updates } : c))
+ )
+ }
+ },
+ /**
+ * 鍒囨崲鍒楁樉绀虹姸鎬侊紙鏀寔鍗曚釜鎴栨壒閲忥級
+ */
+ toggleColumn: (prop, visible) => {
+ 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
+ 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, toIndex) =>
+ 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) => dynamicColumns.value.find((c) => getColumnKey(c) === prop),
+ /**
+ * 鑾峰彇鎵�鏈夊垪閰嶇疆
+ */
+ getAllColumns: () => [...dynamicColumns.value]
+ }
+}
+export { getColumnChecks, getColumnKey, getColumnVisibility, useTableColumns }
diff --git a/rsf-design/src/hooks/core/useTableHeight.js b/rsf-design/src/hooks/core/useTableHeight.js
new file mode 100644
index 0000000..dcb7be0
--- /dev/null
+++ b/rsf-design/src/hooks/core/useTableHeight.js
@@ -0,0 +1,54 @@
+import { computed } from 'vue'
+class TableHeightCalculator {
+ constructor(options) {
+ this.options = options
+ }
+ /**
+ * 璁$畻瀹瑰櫒楂樺害
+ */
+ calculate() {
+ const offset = this.calculateOffset()
+ return {
+ height: offset === 0 ? '100%' : `calc(100% - ${offset}px)`
+ }
+ }
+ /**
+ * 璁$畻鍋忕Щ閲�
+ */
+ calculateOffset() {
+ if (!this.options.showTableHeader.value) {
+ return this.calculatePaginationOffset()
+ }
+ const headerHeight = this.getHeaderHeight()
+ const paginationOffset = this.calculatePaginationOffset()
+ return headerHeight + paginationOffset + TableHeightCalculator.TABLE_HEADER_SPACING
+ }
+ /**
+ * 鑾峰彇琛ㄦ牸澶撮儴楂樺害
+ */
+ getHeaderHeight() {
+ return this.options.tableHeaderHeight.value || TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT
+ }
+ /**
+ * 璁$畻鍒嗛〉鍣ㄥ亸绉婚噺
+ */
+ calculatePaginationOffset() {
+ const { paginationHeight, paginationSpacing } = this.options
+ return paginationHeight.value === 0 ? 0 : paginationHeight.value + paginationSpacing.value
+ }
+}
+
+TableHeightCalculator.DEFAULT_TABLE_HEADER_HEIGHT = 44
+TableHeightCalculator.TABLE_HEADER_SPACING = 12
+
+function useTableHeight(options) {
+ const containerHeight = computed(() => {
+ const calculator = new TableHeightCalculator(options)
+ return calculator.calculate()
+ })
+ return {
+ /** 瀹瑰櫒楂樺害鏍峰紡瀵硅薄 */
+ containerHeight
+ }
+}
+export { useTableHeight }
diff --git a/rsf-design/src/hooks/core/useTheme.js b/rsf-design/src/hooks/core/useTheme.js
new file mode 100644
index 0000000..1220571
--- /dev/null
+++ b/rsf-design/src/hooks/core/useTheme.js
@@ -0,0 +1,95 @@
+import { useSettingStore } from '@/store/modules/setting'
+import { SystemThemeEnum } from '@/enums/appEnum'
+import AppConfig from '@/config'
+import { getDarkColor, getLightColor, setElementThemeColor } from '@/utils/ui'
+import { usePreferredDark } from '@vueuse/core'
+import { watch } from 'vue'
+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, themeMode) => {
+ disableTransitions()
+ const el = document.getElementsByTagName('html')[0]
+ const isDark = theme === SystemThemeEnum.DARK
+ if (!themeMode) {
+ themeMode = theme
+ }
+ const currentTheme = AppConfig.systemThemeStyles[theme]
+ 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)}`
+ )
+ }
+ settingStore.setGlopTheme(theme, themeMode)
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ enableTransitions()
+ })
+ })
+ }
+ const prefersDark = usePreferredDark()
+ const setSystemAutoTheme = () => {
+ const theme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT
+ setSystemTheme(theme, SystemThemeEnum.AUTO)
+ }
+ const switchThemeStyles = (theme) => {
+ if (theme === SystemThemeEnum.AUTO) {
+ setSystemAutoTheme()
+ } else {
+ setSystemTheme(theme)
+ }
+ }
+ return {
+ setSystemTheme,
+ setSystemAutoTheme,
+ switchThemeStyles,
+ prefersDark
+ }
+}
+function initializeTheme() {
+ const settingStore = useSettingStore()
+ const prefersDark = usePreferredDark()
+ const applyThemeByMode = () => {
+ const el = document.getElementsByTagName('html')[0]
+ let actualTheme = settingStore.systemThemeType
+ if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) {
+ actualTheme = prefersDark.value ? SystemThemeEnum.DARK : SystemThemeEnum.LIGHT
+ settingStore.systemThemeType = actualTheme
+ }
+ const currentTheme = AppConfig.systemThemeStyles[actualTheme]
+ if (currentTheme) {
+ el.setAttribute('class', currentTheme.className)
+ }
+ setElementThemeColor(settingStore.systemThemeColor)
+ document.documentElement.style.setProperty('--custom-radius', `${settingStore.customRadius}rem`)
+ }
+ applyThemeByMode()
+ if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) {
+ watch(
+ prefersDark,
+ () => {
+ if (settingStore.systemThemeMode === SystemThemeEnum.AUTO) {
+ applyThemeByMode()
+ }
+ },
+ { immediate: false }
+ )
+ }
+}
+export { initializeTheme, useTheme }
diff --git a/rsf-design/src/hooks/index.js b/rsf-design/src/hooks/index.js
new file mode 100644
index 0000000..1aa58d3
--- /dev/null
+++ b/rsf-design/src/hooks/index.js
@@ -0,0 +1,27 @@
+import { useCommon } from './core/useCommon'
+import { useAppMode } from './core/useAppMode'
+import { useAuth } from './core/useAuth'
+import { useTable } from './core/useTable'
+import { useTableColumns } from './core/useTableColumns'
+import { useTheme } from './core/useTheme'
+import { useCeremony } from './core/useCeremony'
+import { useFastEnter } from './core/useFastEnter'
+import { useHeaderBar } from './core/useHeaderBar'
+import { useChart, useChartComponent, useChartOps } from './core/useChart'
+import { useLayoutHeight, useAutoLayoutHeight } from './core/useLayoutHeight'
+export {
+ useAppMode,
+ useAuth,
+ useAutoLayoutHeight,
+ useCeremony,
+ useChart,
+ useChartComponent,
+ useChartOps,
+ useCommon,
+ useFastEnter,
+ useHeaderBar,
+ useLayoutHeight,
+ useTable,
+ useTableColumns,
+ useTheme
+}
diff --git a/rsf-design/src/locales/index.js b/rsf-design/src/locales/index.js
new file mode 100644
index 0000000..5f46dce
--- /dev/null
+++ b/rsf-design/src/locales/index.js
@@ -0,0 +1,54 @@
+import { createI18n } 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'
+const storageKeyManager = new StorageKeyManager()
+const messages = {
+ [LanguageEnum.EN]: enMessages,
+ [LanguageEnum.ZH]: zhMessages
+}
+const languageOptions = [
+ { value: LanguageEnum.ZH, label: '绠�浣撲腑鏂�' },
+ { value: LanguageEnum.EN, label: 'English' }
+]
+const getDefaultLanguage = () => {
+ 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
+}
+const i18nOptions = {
+ locale: getDefaultLanguage(),
+ legacy: false,
+ globalInjection: true,
+ fallbackLocale: LanguageEnum.ZH,
+ messages
+}
+const i18n = createI18n(i18nOptions)
+const $t = i18n.global.t
+
+export default i18n
+export { $t, languageOptions }
diff --git a/rsf-design/src/locales/langs/en.json b/rsf-design/src/locales/langs/en.json
new file mode 100644
index 0000000..4dbade2
--- /dev/null
+++ b/rsf-design/src/locales/langs/en.json
@@ -0,0 +1,296 @@
+{
+ "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",
+ "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"
+ }
+ },
+ "common": {
+ "tips": "Prompt",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "logOutTips": "Do you want to log out?"
+ },
+ "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"
+ },
+ "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": {
+ "username": "Please enter your account",
+ "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"
+ },
+ "register": {
+ "title": "Create account",
+ "subTitle": "Welcome to join us, please fill in the following information to complete the registration",
+ "placeholder": {
+ "username": "Please enter your account",
+ "password": "Please enter your password",
+ "confirmPassword": "Please enter your password again"
+ },
+ "rule": {
+ "confirmPasswordRequired": "Please enter your password again",
+ "passwordMismatch": "The two passwords are inconsistent!",
+ "usernameLength": "The length is 3 to 20 characters",
+ "passwordLength": "The password length cannot be less than 6 digits",
+ "agreementRequired": "Please agree to the privacy policy"
+ },
+ "agreeText": "I agree",
+ "privacyPolicy": "Privacy policy",
+ "submitBtnText": "Register",
+ "hasAccount": "Already have an account?",
+ "toLogin": "To login"
+ },
+ "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"
+ },
+ "menus": {
+ "login": {
+ "title": "Login"
+ },
+ "register": {
+ "title": "Register"
+ },
+ "forgetPassword": {
+ "title": "Forget 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",
+ "userCenter": "User Center",
+ "menu": "Menu Manage"
+ }
+ },
+ "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"
+ }
+}
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
new file mode 100644
index 0000000..2e9bdcc
--- /dev/null
+++ b/rsf-design/src/locales/langs/zh.json
@@ -0,0 +1,296 @@
+{
+ "httpMsg": {
+ "unauthorized": "鏈巿鏉冭闂紝璇烽噸鏂扮櫥褰�",
+ "forbidden": "绂佹璁块棶璇ヨ祫婧�",
+ "notFound": "璇锋眰鐨勮祫婧愪笉瀛樺湪",
+ "methodNotAllowed": "璇锋眰鏂规硶涓嶅厑璁�",
+ "requestTimeout": "璇锋眰瓒呮椂锛岃绋嶅悗閲嶈瘯",
+ "internalServerError": "鏈嶅姟鍣ㄥ唴閮ㄩ敊璇紝璇风◢鍚庨噸璇�",
+ "badGateway": "缃戝叧閿欒锛岃绋嶅悗閲嶈瘯",
+ "serviceUnavailable": "鏈嶅姟鏆傛椂涓嶅彲鐢紝璇风◢鍚庨噸璇�",
+ "gatewayTimeout": "缃戝叧瓒呮椂锛岃绋嶅悗閲嶈瘯",
+ "requestCancelled": "璇锋眰宸插彇娑�",
+ "networkError": "缃戠粶杩炴帴寮傚父锛岃妫�鏌ョ綉缁滆繛鎺�",
+ "requestFailed": "璇锋眰澶辫触",
+ "requestConfigError": "璇锋眰閰嶇疆閿欒"
+ },
+ "topBar": {
+ "search": {
+ "title": "鎼滅储"
+ },
+ "user": {
+ "userCenter": "涓汉涓績",
+ "docs": "浣跨敤鏂囨。",
+ "github": "Github",
+ "lockScreen": "閿佸畾灞忓箷",
+ "logout": "閫�鍑虹櫥褰�"
+ },
+ "guide": {
+ "title": "鐐瑰嚮杩欓噷鏌ョ湅",
+ "theme": "涓婚椋庢牸",
+ "menu": "寮�鍚《鏍忚彍鍗�",
+ "description": "绛夋洿澶氶厤缃�"
+ }
+ },
+ "common": {
+ "tips": "鎻愮ず",
+ "cancel": "鍙栨秷",
+ "confirm": "纭畾",
+ "logOutTips": "鎮ㄦ槸鍚﹁閫�鍑虹櫥褰�?"
+ },
+ "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": [
+ "閫氱煡",
+ "娑堟伅",
+ "浠e姙"
+ ],
+ "text": [
+ "鏆傛棤"
+ ],
+ "viewAll": "鏌ョ湅鍏ㄩ儴"
+ },
+ "worktab": {
+ "btn": {
+ "refresh": "鍒锋柊",
+ "fixed": "鍥哄畾",
+ "unfixed": "鍙栨秷鍥哄畾",
+ "closeLeft": "鍏抽棴宸︿晶",
+ "closeRight": "鍏抽棴鍙充晶",
+ "closeOther": "鍏抽棴鍏朵粬",
+ "closeAll": "鍏抽棴鍏ㄩ儴"
+ }
+ },
+ "login": {
+ "leftView": {
+ "title": "涓�娆惧吋鍏疯璁$編瀛︿笌楂樻晥寮�鍙戠殑鍚庡彴绯荤粺",
+ "subTitle": "缇庤瀹炵敤鐨勭晫闈紝缁忚繃瑙嗚浼樺寲锛岀‘淇濆崜瓒婄殑鐢ㄦ埛浣撻獙"
+ },
+ "title": "娆㈣繋鍥炴潵",
+ "subTitle": "杈撳叆鎮ㄧ殑璐﹀彿鍜屽瘑鐮佺櫥褰�",
+ "roles": {
+ "super": "瓒呯骇绠$悊鍛�",
+ "admin": "绠$悊鍛�",
+ "user": "鏅�氱敤鎴�"
+ },
+ "placeholder": {
+ "username": "璇疯緭鍏ヨ处鍙�",
+ "password": "璇疯緭鍏ュ瘑鐮�",
+ "slider": "璇锋嫋鍔ㄦ粦鍧楀畬鎴愰獙璇�"
+ },
+ "sliderText": "鎸変綇婊戝潡鎷栧姩",
+ "sliderSuccessText": "楠岃瘉鎴愬姛",
+ "rememberPwd": "璁颁綇瀵嗙爜",
+ "forgetPwd": "蹇樿瀵嗙爜",
+ "btnText": "鐧诲綍",
+ "noAccount": "杩樻病鏈夎处鍙凤紵",
+ "register": "娉ㄥ唽",
+ "success": {
+ "title": "鐧诲綍鎴愬姛",
+ "message": "娆㈣繋鍥炴潵"
+ }
+ },
+ "forgetPassword": {
+ "title": "蹇樿瀵嗙爜锛�",
+ "subTitle": "杈撳叆鎮ㄧ殑鐢靛瓙閭欢鏉ラ噸缃偍鐨勫瘑鐮�",
+ "placeholder": "璇疯緭鍏ユ偍鐨勭數瀛愰偖浠�",
+ "submitBtnText": "鎻愪氦",
+ "backBtnText": "杩斿洖"
+ },
+ "register": {
+ "title": "鍒涘缓璐﹀彿",
+ "subTitle": "娆㈣繋鍔犲叆鎴戜滑锛岃濉啓浠ヤ笅淇℃伅瀹屾垚娉ㄥ唽",
+ "placeholder": {
+ "username": "璇疯緭鍏ヨ处鍙�",
+ "password": "璇疯緭鍏ュ瘑鐮�",
+ "confirmPassword": "璇峰啀娆¤緭鍏ュ瘑鐮�"
+ },
+ "rule": {
+ "confirmPasswordRequired": "璇峰啀娆¤緭鍏ュ瘑鐮�",
+ "passwordMismatch": "涓ゆ杈撳叆瀵嗙爜涓嶄竴鑷�!",
+ "usernameLength": "闀垮害鍦� 3 鍒� 20 涓瓧绗�",
+ "passwordLength": "瀵嗙爜闀垮害涓嶈兘灏忎簬6浣�",
+ "agreementRequired": "璇峰悓鎰忛殣绉佸崗璁�"
+ },
+ "agreeText": "鎴戝悓鎰�",
+ "privacyPolicy": "銆婇殣绉佹斂绛栥��",
+ "submitBtnText": "娉ㄥ唽",
+ "hasAccount": "宸叉湁璐﹀彿锛�",
+ "toLogin": "鍘荤櫥褰�"
+ },
+ "lockScreen": {
+ "pwdError": "瀵嗙爜閿欒",
+ "lock": {
+ "inputPlaceholder": "璇疯緭鍏ラ攣灞忓瘑鐮�",
+ "btnText": "閿佸畾"
+ },
+ "unlock": {
+ "inputPlaceholder": "璇疯緭鍏ヨВ閿佸瘑鐮�",
+ "btnText": "瑙i攣",
+ "backBtnText": "杩斿洖鐧诲綍"
+ }
+ },
+ "greeting": {
+ "dawn": "鍑屾櫒浜嗭紒",
+ "morning": "涓婂崍濂斤紒",
+ "afternoon": "涓嬪崍濂斤紒",
+ "evening": "鏅氫笂濂斤紒"
+ },
+ "exceptionPage": {
+ "403": "鎶辨瓑锛屾偍鏃犳潈璁块棶璇ラ〉闈�",
+ "404": "鎶辨瓑锛屾偍璁块棶鐨勯〉闈笉瀛樺湪",
+ "500": "鎶辨瓑锛屾湇鍔″櫒鍑洪敊浜�",
+ "gohome": "杩斿洖棣栭〉"
+ },
+ "menus": {
+ "login": {
+ "title": "鐧诲綍"
+ },
+ "register": {
+ "title": "娉ㄥ唽"
+ },
+ "forgetPassword": {
+ "title": "蹇樿瀵嗙爜"
+ },
+ "outside": {
+ "title": "鍐呭祵椤甸潰"
+ },
+ "dashboard": {
+ "title": "浠〃鐩�",
+ "console": "宸ヤ綔鍙�"
+ },
+ "result": {
+ "title": "缁撴灉椤甸潰",
+ "success": "鎴愬姛椤�",
+ "fail": "澶辫触椤�"
+ },
+ "exception": {
+ "title": "寮傚父椤甸潰",
+ "forbidden": "403",
+ "notFound": "404",
+ "serverError": "500"
+ },
+ "system": {
+ "title": "绯荤粺绠$悊",
+ "user": "鐢ㄦ埛绠$悊",
+ "role": "瑙掕壊绠$悊",
+ "userCenter": "涓汉涓績",
+ "menu": "鑿滃崟绠$悊"
+ }
+ },
+ "table": {
+ "form": {
+ "reset": "閲嶇疆",
+ "submit": "鎻愪氦"
+ },
+ "searchBar": {
+ "reset": "閲嶇疆",
+ "search": "鏌ヨ",
+ "expand": "灞曞紑",
+ "collapse": "鏀惰捣",
+ "searchInputPlaceholder": "璇疯緭鍏�",
+ "searchSelectPlaceholder": "璇烽�夋嫨"
+ },
+ "selection": "閫夋嫨",
+ "sizeOptions": {
+ "small": "绱у噾",
+ "default": "榛樿",
+ "large": "瀹芥澗"
+ },
+ "column": {
+ "selection": "鍕鹃��",
+ "expand": "灞曞紑",
+ "index": "搴忓彿"
+ },
+ "zebra": "鏂戦┈绾�",
+ "border": "杈规",
+ "headerBackground": "琛ㄥご鑳屾櫙"
+ }
+}
diff --git a/rsf-design/src/main.js b/rsf-design/src/main.js
new file mode 100644
index 0000000..65788a6
--- /dev/null
+++ b/rsf-design/src/main.js
@@ -0,0 +1,20 @@
+import App from './App.vue'
+import { createApp } from 'vue'
+import { initStore } from './store'
+import { initRouter } from './router'
+import language from './locales'
+import '@styles/core/tailwind.css'
+import '@styles/index.scss'
+import '@utils/sys/console.js'
+import { setupGlobDirectives } from './directives'
+import { registerLocalIconCollections } from './plugins/iconify'
+import { setupErrorHandle } from './utils/sys/error-handle'
+document.addEventListener('touchstart', function () {}, { passive: false })
+registerLocalIconCollections()
+const app = createApp(App)
+initStore(app)
+initRouter(app)
+setupGlobDirectives(app)
+setupErrorHandle(app)
+app.use(language)
+app.mount('#app')
diff --git a/rsf-design/src/mock/temp/formData.js b/rsf-design/src/mock/temp/formData.js
new file mode 100644
index 0000000..13fcd17
--- /dev/null
+++ b/rsf-design/src/mock/temp/formData.js
@@ -0,0 +1,250 @@
+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'
+const ACCOUNT_TABLE_DATA = [
+ {
+ 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
+ }
+]
+const ROLE_LIST_DATA = [
+ {
+ 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
+ }
+]
+export { ACCOUNT_TABLE_DATA, ROLE_LIST_DATA }
diff --git a/rsf-design/src/mock/upgrade/changeLog.js b/rsf-design/src/mock/upgrade/changeLog.js
new file mode 100644
index 0000000..a35980d
--- /dev/null
+++ b/rsf-design/src/mock/upgrade/changeLog.js
@@ -0,0 +1,3 @@
+import { ref } from 'vue'
+
+export const upgradeLogList = ref([])
diff --git a/rsf-design/src/plugins/echarts.js b/rsf-design/src/plugins/echarts.js
new file mode 100644
index 0000000..20cf76c
--- /dev/null
+++ b/rsf-design/src/plugins/echarts.js
@@ -0,0 +1,50 @@
+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
+])
+const graphic = echarts.graphic
+export { echarts, graphic }
diff --git a/rsf-design/src/plugins/iconify.collections.js b/rsf-design/src/plugins/iconify.collections.js
new file mode 100644
index 0000000..8c6428e
--- /dev/null
+++ b/rsf-design/src/plugins/iconify.collections.js
@@ -0,0 +1,552 @@
+// Auto-generated by scripts/build-local-iconify-collections.mjs
+// Do not edit manually.
+
+export const LOCAL_ICON_COLLECTIONS = Object.freeze({
+ 'icon-park-outline': {
+ height: 48,
+ icons: {
+ 'auto-width': {
+ body: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M11.988 32L4 24.006L12 16m24.012 0L44 23.994L36 32M4 24h40"/>'
+ }
+ },
+ prefix: 'icon-park-outline',
+ width: 48
+ },
+ 'line-md': {
+ height: 24,
+ icons: {
+ 'coffee-half-empty-filled-loop': {
+ body: '<defs><mask id="SVG5AkzhcyZ"><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M12 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M16 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4"><animate attributeName="d" dur="3s" repeatCount="indefinite" values="M8 0c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M12 0c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M16 0c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4;M8 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M12 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M16 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4"/></path><path d="M4 7h16v0h-16v12h16v-32h-16Z"><animate fill="freeze" attributeName="d" begin="1s" dur="0.6s" to="M4 2h16v5h-16v12h16v-24h-16Z"/></path></mask></defs><path fill="currentColor" fill-opacity="0" d="M17 14v4c0 1.66 -1.34 3 -3 3h-6c-1.66 0 -3 -1.34 -3 -3v-4Z"><animate fill="freeze" attributeName="fill-opacity" begin="1.6s" dur="0.4s" to="1"/></path><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="48" d="M17 9v9c0 1.66 -1.34 3 -3 3h-6c-1.66 0 -3 -1.34 -3 -3v-9Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="48;0"/></path><path stroke-dasharray="16" stroke-dashoffset="16" d="M17 9h3c0.55 0 1 0.45 1 1v3c0 0.55 -0.45 1 -1 1h-3"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.3s" to="0"/></path></g><path fill="currentColor" d="M0 0h24v24H0z" mask="url(#SVG5AkzhcyZ)"/>'
+ },
+ 'github-twotone': {
+ body: '<path fill="currentColor" fill-opacity="0" d="M15 4.5c-0.39 -0.1 -1.33 -0.5 -3 -0.5c-1.67 0 -2.61 0.4 -3 0.5c-0.53 -0.43 -1.94 -1.5 -3.5 -1.5c-0.34 1 -0.29 2.22 0 3c-0.75 1 -1 2 -1 3.5c0 2.19 0.48 3.58 1.5 4.5c1.02 0.92 2.11 1.37 3.5 1.5c-0.65 0.54 -0.5 1.87 -0.5 2.5v4h6v-4c0 -0.63 0.15 -1.96 -0.5 -2.5c1.39 -0.13 2.48 -0.58 3.5 -1.5c1.02 -0.92 1.5 -2.31 1.5 -4.5c0 -1.5 -0.25 -2.5 -1 -3.5c0.29 -0.78 0.34 -2 0 -3c-1.56 0 -2.97 1.07 -3.5 1.5Z"><animate fill="freeze" attributeName="fill-opacity" begin="0.9s" dur="0.15s" to=".3"/></path><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="32" d="M12 4c1.67 0 2.61 0.4 3 0.5c0.53 -0.43 1.94 -1.5 3.5 -1.5c0.34 1 0.29 2.22 0 3c0.75 1 1 2 1 3.5c0 2.19 -0.48 3.58 -1.5 4.5c-1.02 0.92 -2.11 1.37 -3.5 1.5c0.65 0.54 0.5 1.87 0.5 2.5c0 0.73 0 3 0 3M12 4c-1.67 0 -2.61 0.4 -3 0.5c-0.53 -0.43 -1.94 -1.5 -3.5 -1.5c-0.34 1 -0.29 2.22 0 3c-0.75 1 -1 2 -1 3.5c0 2.19 0.48 3.58 1.5 4.5c1.02 0.92 2.11 1.37 3.5 1.5c-0.65 0.54 -0.5 1.87 -0.5 2.5c0 0.73 0 3 0 3"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="32;0"/></path><path stroke-dasharray="10" stroke-dashoffset="10" d="M9 19c-1.41 0 -2.84 -0.56 -3.69 -1.19c-0.84 -0.63 -1.09 -1.66 -2.31 -2.31"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.7s" dur="0.2s" to="0"/></path></g>'
+ },
+ 'phone-call-twotone-loop': {
+ body: '<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path fill="currentColor" fill-opacity="0" stroke-dasharray="62" d="M8 3c0.5 0 2.5 4.5 2.5 5c0 1 -1.5 2 -2 3c-0.5 1 0.5 2 1.5 3c0.39 0.39 2 2 3 1.5c1 -0.5 2 -2 3 -2c0.5 0 5 2 5 2.5c0 2 -1.5 3.5 -3 4c-1.5 0.5 -2.5 0.5 -4.5 0c-2 -0.5 -3.5 -1 -6 -3.5c-2.5 -2.5 -3 -4 -3.5 -6c-0.5 -2 -0.5 -3 0 -4.5c0.5 -1.5 2 -3 4 -3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="62;0"/><animateTransform attributeName="transform" dur="2.7s" keyTimes="0;0.035;0.07;0.105;0.14;0.175;0.21;0.245;0.28;1" repeatCount="indefinite" type="rotate" values="0 12 12;15 12 12;0 12 12;-12 12 12;0 12 12;12 12 12;0 12 12;-15 12 12;0 12 12;0 12 12"/><animate fill="freeze" attributeName="fill-opacity" begin="1.3s" dur="0.15s" to=".3"/></path><g fill="none"><path stroke-dasharray="6" stroke-dashoffset="6" d="M15.76 8.28c-0.5 -0.51 -1.1 -0.93 -1.76 -1.24M15.76 8.28c0.49 0.49 0.9 1.08 1.2 1.72"><animate attributeName="stroke-dashoffset" begin="0.7s" dur="2.7s" keyTimes="0;0.15;0.3;1" repeatCount="indefinite" values="6;0;6;6"/></path><path stroke-dasharray="8" stroke-dashoffset="8" d="M18.67 5.35c-1 -1 -2.26 -1.73 -3.67 -2.1M18.67 5.35c0.99 1 1.72 2.25 2.08 3.65"><animate attributeName="stroke-dashoffset" begin="1s" dur="2.7s" keyTimes="0;0.15;0.3;1" repeatCount="indefinite" values="8;0;8;8"/></path></g></g>'
+ },
+ 'reddit-loop': {
+ body: '<defs><mask id="SVGC2McScuF"><g fill="#fff"><path fill-opacity="0" stroke="#fff" stroke-dasharray="46" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9.42c4.42 0 8 2.37 8 5.29c0 2.92 -3.58 5.29 -8 5.29c-4.42 0 -8 -2.37 -8 -5.29c0 -2.92 3.58 -5.29 8 -5.29Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="46;0"/><animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.4s" to="1"/></path><path d="M3.94 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24ZM20.06 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24Z" opacity="0"><set fill="freeze" attributeName="opacity" begin="1s" to="1"/><animate fill="freeze" attributeName="d" begin="1s" dur="0.2s" values="M7.24 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24ZM16.76 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24Z;M3.94 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24ZM20.06 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24Z"/></path><circle cx="18.45" cy="4.23" r="1.61" opacity="0"><animate attributeName="cx" begin="2s" dur="6s" keyTimes="0;0.5;1" repeatCount="indefinite" values="18.45;5.75;18.45"/><set fill="freeze" attributeName="opacity" begin="2.2s" to="1"/></circle></g><circle cx="8.45" cy="13.59" r="1.61" opacity="0"><animate fill="freeze" attributeName="opacity" begin="1.2s" dur="0.4s" to="1"/></circle><circle cx="15.55" cy="13.59" r="1.61" opacity="0"><animate fill="freeze" attributeName="opacity" begin="1.6s" dur="0.4s" to="1"/></circle><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width=".8"><path stroke="#fff" stroke-dasharray="14" stroke-dashoffset="14" d="M12 8.75l1.18 -5.64l5.03 1.07"><animate attributeName="d" begin="2s" dur="6s" keyTimes="0;0.25;0.5;0.75;1" repeatCount="indefinite" values="M12 8.75l1.18 -5.64l5.03 1.07;M12 8.75l0 -6.75l0 2.18;M12 8.75l-1.18 -5.64l-5.03 1.07;M12 8.75l0 -6.75l0 2.18;M12 8.75l1.18 -5.64l5.03 1.07"/><animate fill="freeze" attributeName="stroke-dashoffset" begin="2s" dur="0.2s" to="0"/></path><path stroke="#000" stroke-dasharray="10" stroke-dashoffset="10" d="M8.47 17.52c0 0 0.94 1.06 3.53 1.06c2.58 0 3.53 -1.06 3.53 -1.06"><animate fill="freeze" attributeName="stroke-dashoffset" begin="2s" dur="0.2s" to="0"/></path></g></mask></defs><path fill="currentColor" d="M0 0h24v24H0z" mask="url(#SVGC2McScuF)"/>'
+ },
+ 'sun-rising-filled-loop': {
+ body: '<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path fill="currentColor" d="M12 6c3.31 0 6 2.69 6 6c0 3.31 -2.69 6 -6 6c-3.31 0 -6 -2.69 -6 -6c0 -3.31 2.69 -6 6 -6Z"><animate fill="freeze" attributeName="d" dur="0.6s" values="M12 26c3.31 0 6 2.69 6 6c0 3.31 -2.69 6 -6 6c-3.31 0 -6 -2.69 -6 -6c0 -3.31 2.69 -6 6 -6Z;M12 6c3.31 0 6 2.69 6 6c0 3.31 -2.69 6 -6 6c-3.31 0 -6 -2.69 -6 -6c0 -3.31 2.69 -6 6 -6Z"/></path><g fill="none"><path d="M12 21v1M21 12h1M12 3v-1M3 12h-1" opacity="0"><animateTransform attributeName="transform" dur="30s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/><set fill="freeze" attributeName="opacity" begin="0.7s" to="1"/><animate fill="freeze" attributeName="d" begin="0.7s" dur="0.2s" values="M12 19v1M19 12h1M12 5v-1M5 12h-1;M12 21v1M21 12h1M12 3v-1M3 12h-1"/></path><path d="M18.5 18.5l0.5 0.5M18.5 5.5l0.5 -0.5M5.5 5.5l-0.5 -0.5M5.5 18.5l-0.5 0.5" opacity="0"><animateTransform attributeName="transform" dur="30s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/><set fill="freeze" attributeName="opacity" begin="0.9s" to="1"/><animate fill="freeze" attributeName="d" begin="0.9s" dur="0.2s" values="M17 17l0.5 0.5M17 7l0.5 -0.5M7 7l-0.5 -0.5M7 17l-0.5 0.5;M18.5 18.5l0.5 0.5M18.5 5.5l0.5 -0.5M5.5 5.5l-0.5 -0.5M5.5 18.5l-0.5 0.5"/></path></g></g>'
+ },
+ 'switch-off': {
+ body: '<path fill="none" stroke="currentColor" stroke-dasharray="54" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7h5c2.76 0 5 2.24 5 5c0 2.76 -2.24 5 -5 5h-10c-2.76 0 -5 -2.24 -5 -5c0 -2.76 2.24 -5 5 -5Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="54;0"/></path><circle cx="7" cy="12" r="3" fill="currentColor" opacity="0"><animate fill="freeze" attributeName="opacity" begin="0.6s" dur="0.2s" to="1"/></circle>'
+ },
+ 'volume-high-filled': {
+ body: '<g fill="currentColor"><path fill-opacity="0" stroke="currentColor" stroke-dasharray="34" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 10h3.5l3.5 -3.5v10.5l-3.5 -3.5h-3.5Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="34;0"/><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" to="1"/></path><path d="M14 12c0 0 0 0 0 0c0 0 0 0 0 0Z"><animate fill="freeze" attributeName="d" begin="0.4s" dur="0.2s" to="M14 16c1.5 -0.71 2.5 -2.24 2.5 -4c0 -1.77 -1 -3.26 -2.5 -4Z"/></path><path d="M14 12c0 0 0 0 0 0c0 0 0 0 0 0v0c0 0 0 0 0 0c0 0 0 0 0 0Z"><animate fill="freeze" attributeName="d" begin="0.4s" dur="0.4s" to="M14 3.23c4 0.91 7 4.49 7 8.77c0 4.28 -3 7.86 -7 8.77v-2.07c2.89 -0.86 5 -3.53 5 -6.7c0 -3.17 -2.11 -5.85 -5 -6.71Z"/></path></g>'
+ },
+ telegram: {
+ body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="18" d="M21 5l-2.5 15M21 5l-12 8.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="18;0"/></path><path stroke-dasharray="24" d="M21 5l-19 7.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="24;0"/></path><path stroke-dasharray="14" stroke-dashoffset="14" d="M18.5 20l-9.5 -6.5"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.4s" dur="0.3s" to="0"/></path><path stroke-dasharray="10" stroke-dashoffset="10" d="M2 12.5l7 1"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.4s" dur="0.3s" to="0"/></path><path stroke-dasharray="8" stroke-dashoffset="8" d="M12 16l-3 3M9 13.5l0 5.5"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.7s" dur="0.3s" to="0"/></path></g>'
+ }
+ },
+ prefix: 'line-md',
+ width: 24
+ },
+ 'svg-spinners': {
+ height: 24,
+ icons: {
+ '3-dots-bounce': {
+ body: '<circle cx="4" cy="12" r="3" fill="currentColor"><animate id="SVGKiXXedfO" attributeName="cy" begin="0;SVGgLulOGrw.end+0.25s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="12" cy="12" r="3" fill="currentColor"><animate attributeName="cy" begin="SVGKiXXedfO.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="20" cy="12" r="3" fill="currentColor"><animate id="SVGgLulOGrw" attributeName="cy" begin="SVGKiXXedfO.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle>'
+ },
+ '3-dots-fade': {
+ body: '<circle cx="4" cy="12" r="3" fill="currentColor"><animate id="SVG7x14Dcom" fill="freeze" attributeName="opacity" begin="0;SVGqSjG0dUp.end-0.25s" dur="0.75s" values="1;.2"/></circle><circle cx="12" cy="12" r="3" fill="currentColor" opacity=".4"><animate fill="freeze" attributeName="opacity" begin="SVG7x14Dcom.begin+0.15s" dur="0.75s" values="1;.2"/></circle><circle cx="20" cy="12" r="3" fill="currentColor" opacity=".3"><animate id="SVGqSjG0dUp" fill="freeze" attributeName="opacity" begin="SVG7x14Dcom.begin+0.3s" dur="0.75s" values="1;.2"/></circle>'
+ },
+ '3-dots-move': {
+ body: '<circle cx="4" cy="12" r="0" fill="currentColor"><animate fill="freeze" attributeName="r" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/><animate fill="freeze" attributeName="cx" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/><animate fill="freeze" attributeName="cx" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/><animate id="SVG3V8yEdYE" fill="freeze" attributeName="r" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVGUppsBdVN" fill="freeze" attributeName="cx" begin="SVG3V8yEdYE.end" dur="0.001s" values="20;4"/></circle><circle cx="4" cy="12" r="3" fill="currentColor"><animate fill="freeze" attributeName="cx" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/><animate fill="freeze" attributeName="cx" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/><animate id="SVG4PgJdbds" fill="freeze" attributeName="r" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVG6wCQhd9Q" fill="freeze" attributeName="cx" begin="SVG4PgJdbds.end" dur="0.001s" values="20;4"/><animate fill="freeze" attributeName="r" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/></circle><circle cx="12" cy="12" r="3" fill="currentColor"><animate fill="freeze" attributeName="cx" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/><animate id="SVG38aCdcdI" fill="freeze" attributeName="r" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVG3PwDNd6F" fill="freeze" attributeName="cx" begin="SVG38aCdcdI.end" dur="0.001s" values="20;4"/><animate fill="freeze" attributeName="r" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/><animate fill="freeze" attributeName="cx" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/></circle><circle cx="20" cy="12" r="3" fill="currentColor"><animate id="SVGwaWzveSq" fill="freeze" attributeName="r" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVGqCgsydxJ" fill="freeze" attributeName="cx" begin="SVGwaWzveSq.end" dur="0.001s" values="20;4"/><animate fill="freeze" attributeName="r" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/><animate fill="freeze" attributeName="cx" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/><animate fill="freeze" attributeName="cx" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/></circle>'
+ },
+ '3-dots-rotate': {
+ body: '<circle cx="12" cy="12" r="3" fill="currentColor"/><g><circle cx="4" cy="12" r="3" fill="currentColor"/><circle cx="20" cy="12" r="3" fill="currentColor"/><animateTransform attributeName="transform" calcMode="spline" dur="1s" keySplines=".36,.6,.31,1;.36,.6,.31,1" repeatCount="indefinite" type="rotate" values="0 12 12;180 12 12;360 12 12"/></g>'
+ },
+ 'blocks-shuffle-2': {
+ body: '<rect width="10" height="10" x="1" y="1" fill="currentColor" rx="1"><animate id="SVG7JagGz2Y" fill="freeze" attributeName="x" begin="0;SVGgDT19bUV.end" dur="0.2s" values="1;13"/><animate id="SVGpS1BddYk" fill="freeze" attributeName="y" begin="SVGc7yq8dne.end" dur="0.2s" values="1;13"/><animate id="SVGboa7EdFl" fill="freeze" attributeName="x" begin="SVG0ZX9C6Fa.end" dur="0.2s" values="13;1"/><animate id="SVG6rrusL2C" fill="freeze" attributeName="y" begin="SVGTOnnO5Dr.end" dur="0.2s" values="13;1"/></rect><rect width="10" height="10" x="1" y="13" fill="currentColor" rx="1"><animate id="SVGc7yq8dne" fill="freeze" attributeName="y" begin="SVG7JagGz2Y.end" dur="0.2s" values="13;1"/><animate id="SVG0ZX9C6Fa" fill="freeze" attributeName="x" begin="SVGpS1BddYk.end" dur="0.2s" values="1;13"/><animate id="SVGTOnnO5Dr" fill="freeze" attributeName="y" begin="SVGboa7EdFl.end" dur="0.2s" values="1;13"/><animate id="SVGgDT19bUV" fill="freeze" attributeName="x" begin="SVG6rrusL2C.end" dur="0.2s" values="13;1"/></rect>'
+ },
+ 'blocks-wave': {
+ body: '<rect width="7.33" height="7.33" x="1" y="1" fill="currentColor"><animate id="SVGzjrPLenI" attributeName="x" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="y" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="width" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="8.33" y="1" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="1;4;1"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="1" y="8.33" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="1;4;1"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="15.66" y="1" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="8.33" y="8.33" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="1" y="15.66" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="15.66" y="8.33" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="8.33" y="15.66" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="15.66" y="15.66" fill="currentColor"><animate id="SVGXAURnSRI" attributeName="x" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="7.33;1.33;7.33"/></rect>'
+ },
+ clock: {
+ body: '<path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"/><rect width="2" height="7" x="11" y="6" fill="currentColor" rx="1"><animateTransform attributeName="transform" dur="9s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></rect><rect width="2" height="9" x="11" y="11" fill="currentColor" rx="1"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></rect>'
+ },
+ tadpole: {
+ body: '<path fill="currentColor" d="M12,23a9.63,9.63,0,0,1-8-9.5,9.51,9.51,0,0,1,6.79-9.1A1.66,1.66,0,0,0,12,2.81h0a1.67,1.67,0,0,0-1.94-1.64A11,11,0,0,0,12,23Z"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path>'
+ }
+ },
+ prefix: 'svg-spinners',
+ width: 24
+ },
+ 'system-uicons': {
+ height: 21,
+ icons: {
+ inbox: {
+ body: '<g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M6.16 4.5h8.68a1 1 0 0 1 .92.606L18.5 11.5v4a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2v-4l2.74-6.394a1 1 0 0 1 .92-.606"/><path d="M2.5 11.5h4a1 1 0 0 1 1 1v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h4"/></g>'
+ }
+ },
+ prefix: 'system-uicons',
+ width: 21
+ },
+ fluent: {
+ height: 20,
+ icons: {
+ 'arrow-enter-left-20-filled': {
+ body: '<path fill="currentColor" d="m4.641 12.5l2.873 2.704a.75.75 0 0 1-1.028 1.092l-4.25-4a.75.75 0 0 1 0-1.092l4.25-4a.75.75 0 1 1 1.028 1.092L4.641 11H14.75a1.75 1.75 0 0 0 1.75-1.75v-4.5a.75.75 0 0 1 1.5 0v4.5a3.25 3.25 0 0 1-3.25 3.25z"/>'
+ }
+ },
+ prefix: 'fluent',
+ width: 20
+ },
+ iconamoon: {
+ height: 24,
+ icons: {
+ 'arrow-down-2-thin': {
+ body: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="m7 10l5 5l5-5"/>'
+ }
+ },
+ prefix: 'iconamoon',
+ width: 24
+ },
+ ix: {
+ height: 512,
+ icons: {
+ width: {
+ body: '<path fill="currentColor" fill-rule="evenodd" d="M85.333 128H42.667v256h42.666V256l79.085 79.085l30.17-30.17l-27.582-27.582h178.017l-27.583 27.584l30.17 30.17l79.057-79.057V384h42.666V128h-42.666v127.973l-79.057-79.056l-30.17 30.17l27.58 27.58H167.006l27.582-27.582l-30.17-30.17l-79.085 79.084z" clip-rule="evenodd"/>'
+ }
+ },
+ prefix: 'ix',
+ width: 512
+ },
+ ri: {
+ height: 24,
+ icons: {
+ 'account-box-2-line': {
+ body: '<path fill="currentColor" d="M4.995 3A1.995 1.995 0 0 0 3 4.995v14.01C3 20.107 3.893 21 4.995 21h14.01A1.995 1.995 0 0 0 21 19.005V4.995A1.995 1.995 0 0 0 19.005 3zM5 19V5h14v14zm7-11a1 1 0 1 1 0 2a1 1 0 0 1 0-2m0 4a3 3 0 1 0 0-6a3 3 0 0 0 0 6m0 3a2 2 0 0 0-2 2H8a4 4 0 0 1 8 0h-2a2 2 0 0 0-2-2"/>'
+ },
+ 'account-circle-line': {
+ body: '<path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m.16 14a6.98 6.98 0 0 0-5.147 2.256A7.97 7.97 0 0 0 12 20a7.97 7.97 0 0 0 5.167-1.892A6.98 6.98 0 0 0 12.16 16M12 4a8 8 0 0 0-6.384 12.821A8.98 8.98 0 0 1 12.16 14a8.97 8.97 0 0 1 6.362 2.634A8 8 0 0 0 12 4m0 1a4 4 0 1 1 0 8a4 4 0 0 1 0-8m0 2a2 2 0 1 0 0 4a2 2 0 0 0 0-4"/>'
+ },
+ 'add-fill': {
+ body: '<path fill="currentColor" d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/>'
+ },
+ 'align-item-bottom-line': {
+ body: '<path fill="currentColor" d="M9 5v10H6V5zM5 3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm10 6v6h3V9zm-2-1a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1zm8 11H3v2h18z"/>'
+ },
+ 'align-justify': {
+ body: '<path fill="currentColor" d="M3 4h18v2H3zm0 15h18v2H3zm0-5h18v2H3zm0-5h18v2H3z"/>'
+ },
+ 'align-right': {
+ body: '<path fill="currentColor" d="M3 4h18v2H3zm4 15h14v2H7zm-4-5h18v2H3zm4-5h14v2H7z"/>'
+ },
+ 'anthropic-line': {
+ body: '<path fill="currentColor" d="M14.122 5h2.146L22.1 20h-2.146zM7.66 5h2.681l5.77 15h-2.144l-1.538-4H5.572l-1.539 4H1.891zm4 9L9 7.086L6.341 14z"/>'
+ },
+ 'apple-line': {
+ body: '<path fill="currentColor" d="M15.778 8.208c-.473-.037-.98.076-1.758.373c.065-.025-.742.29-.969.37c-.502.175-.915.271-1.378.271c-.458 0-.88-.092-1.365-.255a11 11 0 0 1-.505-.186l-.449-.177c-.648-.254-1.012-.35-1.315-.342c-1.153.014-2.243.68-2.877 1.782c-1.292 2.243-.576 6.299 1.313 9.031c1.005 1.444 1.556 1.96 1.777 1.953c.222-.01.386-.057.784-.225l.166-.071c1.006-.429 1.71-.618 2.771-.618c1.021 0 1.703.186 2.669.602l.168.072c.397.17.54.208.792.202c.357-.005.798-.417 1.777-1.854c.268-.391.505-.803.71-1.22a7 7 0 0 1-.391-.347c-1.29-1.228-2.087-2.884-2.109-4.93A6.63 6.63 0 0 1 17 8.458a4.1 4.1 0 0 0-1.221-.25m.155-1.994c.708.048 2.736.264 4.056 2.196c-.108.06-2.424 1.404-2.4 4.212c.036 3.36 2.94 4.476 2.976 4.488c-.024.084-.468 1.596-1.536 3.156c-.924 1.356-1.884 2.7-3.396 2.724c-1.488.036-1.968-.876-3.66-.876c-1.704 0-2.232.852-3.636.912c-1.464.048-2.568-1.464-3.504-2.808c-1.908-2.76-3.36-7.776-1.404-11.172c.972-1.692 2.7-2.76 4.584-2.784c1.428-.036 2.784.96 3.66.96c.864 0 2.412-1.152 4.26-1.008m-1.14-1.824c-.78.936-2.052 1.668-3.288 1.572c-.168-1.272.456-2.604 1.176-3.432c.804-.936 2.148-1.632 3.264-1.68c.144 1.296-.372 2.604-1.152 3.54"/>'
+ },
+ 'apps-2-add-line': {
+ body: '<path fill="currentColor" d="M2.5 7a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0m0 10a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0m10 0a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0m-3-10a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0m0 10a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0m10 0a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0M16 11V8h-3V6h3V3h2v3h3v2h-3v3z"/>'
+ },
+ 'apps-2-line': {
+ body: '<path fill="currentColor" d="M7 11.5a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m0 10a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m10-10a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m0 10a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9M7 9.5a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5m0 10a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5m10-10a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5m0 10a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5"/>'
+ },
+ 'arrow-down-wide-fill': {
+ body: '<path fill="currentColor" d="m12 15.632l8.968-4.748l-.936-1.768L12 13.368L3.968 9.116l-.936 1.768z"/>'
+ },
+ 'arrow-left-right-fill': {
+ body: '<path fill="currentColor" d="M16 16v-4l5 5l-5 5v-4H4v-2zM8 2v3.999L20 6v2H8v4L3 7z"/>'
+ },
+ 'arrow-left-s-line': {
+ body: '<path fill="currentColor" d="m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z"/>'
+ },
+ 'arrow-right-circle-line': {
+ body: '<path fill="currentColor" d="M12 11V8l4 4l-4 4v-3H8v-2zm0-9c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12S6.48 2 12 2m0 18c4.42 0 8-3.58 8-8s-3.58-8-8-8s-8 3.58-8 8s3.58 8 8 8"/>'
+ },
+ 'arrow-right-s-line': {
+ body: '<path fill="currentColor" d="m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z"/>'
+ },
+ 'arrow-right-up-line': {
+ body: '<path fill="currentColor" d="m16.004 9.414l-8.607 8.607l-1.414-1.414L14.59 8H7.003V6h11v11h-2z"/>'
+ },
+ 'arrow-up-circle-line': {
+ body: '<path fill="currentColor" d="M12 2c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12S6.48 2 12 2m0 18c4.42 0 8-3.58 8-8s-3.58-8-8-8s-8 3.58-8 8s3.58 8 8 8m1-8v4h-2v-4H8l4-4l4 4z"/>'
+ },
+ 'arrow-up-down-fill': {
+ body: '<path fill="currentColor" d="M12 8H8.001L8 20H6V8H2l5-5zm10 8l-5 5l-5-5h4V4h2v12z"/>'
+ },
+ 'arrow-up-line': {
+ body: '<path fill="currentColor" d="M13 7.828V20h-2V7.828l-5.364 5.364l-1.414-1.414L12 4l7.778 7.778l-1.414 1.414z"/>'
+ },
+ 'arrow-up-wide-fill': {
+ body: '<path fill="currentColor" d="m12 8.369l8.968 4.747l-.936 1.768L12 10.632l-8.032 4.252l-.936-1.768z"/>'
+ },
+ 'arrow-up-wide-line': {
+ body: '<path fill="currentColor" d="m12 8.369l8.968 4.747l-.936 1.768L12 10.632l-8.032 4.252l-.936-1.768z"/>'
+ },
+ 'article-line': {
+ body: '<path fill="currentColor" d="M20 22H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1m-1-2V4H5v16zM7 6h4v4H7zm0 6h10v2H7zm0 4h10v2H7zm6-9h4v2h-4z"/>'
+ },
+ 'bar-chart-2-line': {
+ body: '<path fill="currentColor" d="M2 13h6v8H2zm14-5h6v13h-6zM9 3h6v18H9zM4 15v4h2v-4zm7-10v14h2V5zm7 5v9h2v-9z"/>'
+ },
+ 'bar-chart-box-ai-line': {
+ body: '<path fill="currentColor" d="m20.713 8.128l-.246.566a.506.506 0 0 1-.934 0l-.246-.566a4.36 4.36 0 0 0-2.22-2.25l-.759-.339a.53.53 0 0 1 0-.963l.717-.319a4.37 4.37 0 0 0 2.251-2.326l.253-.611a.506.506 0 0 1 .942 0l.253.61a4.37 4.37 0 0 0 2.25 2.327l.718.32a.53.53 0 0 1 0 .962l-.76.338a4.36 4.36 0 0 0-2.219 2.251M2 4a1 1 0 0 1 1-1h11v2H4v14h16v-8h2v9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm5 9h2v4H7zm4-6h2v10h-2zm4 3h2v7h-2z"/>'
+ },
+ 'bar-chart-box-line': {
+ body: '<path fill="currentColor" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m1 2v14h16V5zm3 8h2v4H7zm4-6h2v10h-2zm4 3h2v7h-2z"/>'
+ },
+ 'bar-chart-grouped-line': {
+ body: '<path fill="currentColor" d="M2 12h2v9H2zm3 2h2v7H5zm11-6h2v13h-2zm3 2h2v11h-2zM9 2h2v19H9zm3 2h2v17h-2z"/>'
+ },
+ 'bilibili-line': {
+ body: '<path fill="currentColor" d="M7.172 2.757L10.414 6h3.171l3.243-3.242a1 1 0 1 1 1.415 1.415L16.414 6H18.5A3.5 3.5 0 0 1 22 9.5v8a3.5 3.5 0 0 1-3.5 3.5h-13A3.5 3.5 0 0 1 2 17.5v-8A3.5 3.5 0 0 1 5.5 6h2.085L5.757 4.171a1 1 0 0 1 1.415-1.415M18.5 8h-13a1.5 1.5 0 0 0-1.493 1.356L4 9.5v8a1.5 1.5 0 0 0 1.356 1.493L5.5 19h13a1.5 1.5 0 0 0 1.493-1.355L20 17.5v-8A1.5 1.5 0 0 0 18.5 8M8 11a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1"/>'
+ },
+ 'bill-line': {
+ body: '<path fill="currentColor" d="M20 22H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1m-1-2V4H5v16zM8 9h8v2H8zm0 4h8v2H8z"/>'
+ },
+ 'book-2-line': {
+ body: '<path fill="currentColor" d="M21 18H6a1 1 0 1 0 0 2h15v2H6a3 3 0 0 1-3-3V4a2 2 0 0 1 2-2h16zM5 16.05q.243-.05.5-.05H19V4H5zM16 9H8V7h8z"/>'
+ },
+ 'box-1-line': {
+ body: '<path fill="currentColor" d="m12 1l9.5 5.5v11L12 23l-9.5-5.5v-11zM5.494 7.078L13 11.423v8.687l6.5-3.763V7.653L12 3.311zM4.5 8.813v7.534L11 20.11v-7.533z"/>'
+ },
+ 'bus-2-line': {
+ body: '<path fill="currentColor" d="M17 20H7v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9H2V8h1V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3h1v4h-1v9a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1zM5 5v6h14V5zm14 8H5v5h14zM7.5 17a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m9 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>'
+ },
+ 'calendar-2-line': {
+ body: '<path fill="currentColor" d="M9 1v2h6V1h2v2h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h4V1zm11 10H4v8h16zM8 13v2H6v-2zm5 0v2h-2v-2zm5 0v2h-2v-2zM7 5H4v4h16V5h-3v2h-2V5H9v2H7z"/>'
+ },
+ 'camera-4-line': {
+ body: '<path fill="currentColor" d="M14.434 3a2 2 0 0 1 1.714.97l.773 1.287a.5.5 0 0 0 .429.243H19a3 3 0 0 1 3 3V18a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V8.5a3 3 0 0 1 3-3h1.65a.5.5 0 0 0 .43-.243l.772-1.286A2 2 0 0 1 9.566 3zm-5.64 3.286A2.5 2.5 0 0 1 6.65 7.5H5a1 1 0 0 0-1 1V18a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V8.5a1 1 0 0 0-1-1h-1.65a2.5 2.5 0 0 1-2.145-1.214L14.434 5H9.566zM12 8.5a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9m0 2a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5"/>'
+ },
+ 'capsule-line': {
+ body: '<path fill="currentColor" d="M19.779 4.222a6 6 0 0 1 0 8.485l-7.071 7.071a6 6 0 0 1-8.486-8.485l7.071-7.071a6 6 0 0 1 8.486 0m-5.657 11.313L8.466 9.878l-2.83 2.83a4 4 0 0 0 5.657 5.656zm4.242-9.899a4 4 0 0 0-5.657 0L9.88 8.464l5.657 5.657l2.827-2.828a4 4 0 0 0 0-5.657"/>'
+ },
+ 'check-fill': {
+ body: '<path fill="currentColor" d="m10 15.17l9.192-9.191l1.414 1.414L10 17.999l-6.364-6.364l1.414-1.414z"/>'
+ },
+ 'checkbox-circle-line': {
+ body: '<path fill="currentColor" d="M4 12a8 8 0 1 1 16 0a8 8 0 0 1-16 0m8-10C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2m5.457 7.457l-1.414-1.414L11 13.086l-2.793-2.793l-1.414 1.414L11 15.914z"/>'
+ },
+ 'clipboard-line': {
+ body: '<path fill="currentColor" d="M7 4V2h10v2h3.007c.548 0 .993.445.993.993v16.014a.994.994 0 0 1-.993.993H3.993A.993.993 0 0 1 3 21.007V4.993C3 4.445 3.445 4 3.993 4zm0 2H5v14h14V6h-2v2H7zm2-2v2h6V4z"/>'
+ },
+ 'close-circle-line': {
+ body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0-9.414l2.828-2.829l1.415 1.415L13.414 12l2.829 2.828l-1.415 1.415L12 13.414l-2.828 2.829l-1.415-1.415L10.586 12L7.757 9.172l1.415-1.415z"/>'
+ },
+ 'close-fill': {
+ body: '<path fill="currentColor" d="m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z"/>'
+ },
+ 'close-large-fill': {
+ body: '<path fill="currentColor" d="M10.586 12L2.793 4.207l1.414-1.414L12 10.586l7.793-7.793l1.414 1.414L13.414 12l7.793 7.793l-1.414 1.414L12 13.414l-7.793 7.793l-1.414-1.414z"/>'
+ },
+ 'command-fill': {
+ body: '<path fill="currentColor" d="M10 8h4V6.5a3.5 3.5 0 1 1 3.5 3.5H16v4h1.5a3.5 3.5 0 1 1-3.5 3.5V16h-4v1.5A3.5 3.5 0 1 1 6.5 14H8v-4H6.5A3.5 3.5 0 1 1 10 6.5zM8 8V6.5A1.5 1.5 0 1 0 6.5 8zm0 8H6.5A1.5 1.5 0 1 0 8 17.5zm8-8h1.5A1.5 1.5 0 1 0 16 6.5zm0 8v1.5a1.5 1.5 0 1 0 1.5-1.5zm-6-6v4h4v-4z"/>'
+ },
+ 'contacts-line': {
+ body: '<path fill="currentColor" d="M19 7h5v2h-5zm-2 5h7v2h-7zm3 5h4v2h-4zM2 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4"/>'
+ },
+ 'copilot-line': {
+ body: '<path fill="currentColor" d="M5.4 7.8c0-2.088 1.178-3 3.172-3c1.196 0 2.129.264 2.129 1.6c0 1.814-.575 3.75-2.7 3.75c-1.229 0-1.798-.176-2.09-.424c-.247-.21-.51-.67-.51-1.926m3.172-5C5.497 2.8 3.4 4.626 3.4 7.8c0 .999.137 1.89.53 2.605l-.183.364a6.3 6.3 0 0 0-1.425 1.107c-1.061 1.126-.973 2.389-.973 3.824c0 2.267 2.512 3.62 4.315 4.373c2.133.89 4.677 1.427 6.336 1.427c1.658 0 4.202-.537 6.335-1.427c1.803-.753 4.315-2.106 4.315-4.373c0-1.435.088-2.698-.973-3.824a6.3 6.3 0 0 0-1.425-1.107l-.182-.364c.392-.716.53-1.606.53-2.605c0-3.174-2.097-5-5.172-5c-1.24 0-2.618.259-3.428 1.283C11.19 3.059 9.813 2.8 8.57 2.8M8 12.15c1.692 0 3.224-.815 4-2.334c.775 1.519 2.307 2.334 4 2.334c.894 0 1.769-.074 2.517-.38c.511.596 1.17.911 1.705 1.478c.639.678.428 1.585.428 2.452c0 1.272-2.166 2.143-3.086 2.527c-1.942.81-4.223 1.273-5.565 1.273c-1.341 0-3.623-.463-5.565-1.273c-.919-.384-3.085-1.255-3.085-2.527c0-.867-.21-1.774.428-2.452c.56-.594 1.341-.75 1.705-1.478c.748.306 1.623.38 2.518.38m5.3-5.75c0-1.336.932-1.6 2.128-1.6c1.994 0 3.172.912 3.172 3c0 1.257-.264 1.715-.511 1.926c-.292.248-.861.424-2.09.424c-2.125 0-2.7-1.936-2.7-3.75m-4.638 8.084a1.001 1.001 0 1 1 2.002 0v1.997a1.001 1.001 0 1 1-2.002 0zm6.675 0a1.001 1.001 0 1 0-2.003 0v1.997a1.001 1.001 0 1 0 2.003 0z"/>'
+ },
+ 'customer-service-2-line': {
+ body: '<path fill="currentColor" d="M19.938 8H21a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-1.062A8 8 0 0 1 12 23v-2a6 6 0 0 0 6-6V9A6 6 0 0 0 6 9v7H3a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1.062a8.001 8.001 0 0 1 15.876 0M3 10v4h1v-4zm17 0v4h1v-4zM7.76 15.785l1.06-1.696A5.97 5.97 0 0 0 12 15a5.97 5.97 0 0 0 3.18-.911l1.06 1.696A7.96 7.96 0 0 1 12 17a7.96 7.96 0 0 1-4.24-1.215"/>'
+ },
+ 'delete-bin-4-line': {
+ body: '<path fill="currentColor" d="M20 7v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7H2V5h20v2zM6 7v13h12V7zm1-5h10v2H7zm4 8h2v7h-2z"/>'
+ },
+ 'delete-bin-5-line': {
+ body: '<path fill="currentColor" d="M4 8h16v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm2 2v10h12V10zm3 2h2v6H9zm4 0h2v6h-2zM7 5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v2h5v2H2V5zm2-1v1h6V4z"/>'
+ },
+ 'delete-bin-line': {
+ body: '<path fill="currentColor" d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"/>'
+ },
+ 'download-2-line': {
+ body: '<path fill="currentColor" d="M13 10h5l-6 6l-6-6h5V3h2zm-9 9h16v-7h2v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-8h2z"/>'
+ },
+ 'drag-move-fill': {
+ body: '<path fill="currentColor" d="m12 22l-4-4h8zm0-20l4 4H8zm0 12a2 2 0 1 1 0-4a2 2 0 0 1 0 4M2 12l4-4v8zm20 0l-4 4V8z"/>'
+ },
+ 'dribbble-fill': {
+ body: '<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2m6.605 4.61a8.5 8.5 0 0 1 1.93 5.314c-.281-.054-3.101-.629-5.943-.271c-.065-.141-.12-.293-.184-.445a25 25 0 0 0-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362M12 3.475c2.17 0 4.154.814 5.662 2.148c-.152.216-1.443 1.941-4.48 3.08c-1.399-2.57-2.95-4.675-3.189-5A8.7 8.7 0 0 1 12 3.475m-3.633.803a54 54 0 0 1 3.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.58 8.58 0 0 1 4.729-5.975M3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215c.25.477.477.965.694 1.453c-.109.033-.228.065-.336.098c-4.404 1.42-6.747 5.303-6.942 5.629a8.52 8.52 0 0 1-2.19-5.705M12 20.547a8.48 8.48 0 0 1-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337c.022-.01.033-.01.054-.022a35.3 35.3 0 0 1 1.823 6.475a8.4 8.4 0 0 1-3.341.684m4.761-1.465c-.086-.52-.542-3.015-1.66-6.084c2.68-.423 5.023.271 5.315.369a8.47 8.47 0 0 1-3.655 5.715"/>'
+ },
+ 'edge-line': {
+ body: '<path fill="currentColor" d="M8.008 14.001A5 5 0 0 0 8 14.25C8 16.632 9.753 19 13 19c2.373 0 4.528-.655 6-1.553v3.35C17.211 21.564 15.112 22 13 22c-5.502 0-8-3.47-8-7.75c0-3.231 2.041-6 4.943-7.164C8.54 8.663 8 10.341 8 10.996L18 11c0-3.406-2.548-6-6-6c-5 0-8.001 3.988-9 5.999C3.29 6.237 7.01 2 12 2c5.2 0 9 4.03 9 9v3H8z"/>'
+ },
+ 'edit-2-line': {
+ body: '<path fill="currentColor" d="M5 18.89h1.414l9.314-9.314l-1.414-1.414L5 17.476zm16 2H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L9.243 18.89H21zM15.728 6.748l1.414 1.414l1.414-1.414l-1.414-1.414z"/>'
+ },
+ 'emotion-happy-line': {
+ body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-5-7h2a3 3 0 1 0 6 0h2a5 5 0 0 1-10 0m1-2a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m8 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>'
+ },
+ 'error-warning-line': {
+ body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-1-5h2v2h-2zm0-8h2v6h-2z"/>'
+ },
+ 'export-line': {
+ body: '<path fill="currentColor" d="M22 4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1zM4 15h3.416a5.001 5.001 0 0 0 9.168 0H20v4H4zM4 5h16v8h-5a3 3 0 1 1-6 0H4zm12 6h-3v3h-2v-3H8l4-4.5z"/>'
+ },
+ 'eye-line': {
+ body: '<path fill="currentColor" d="M12 3c5.392 0 9.878 3.88 10.819 9c-.94 5.12-5.427 9-10.819 9s-9.878-3.88-10.818-9C2.122 6.88 6.608 3 12 3m0 16a9.005 9.005 0 0 0 8.778-7a9.005 9.005 0 0 0-17.555 0A9.005 9.005 0 0 0 12 19m0-2.5a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m0-2a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5"/>'
+ },
+ 'file-copy-line': {
+ body: '<path fill="currentColor" d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 0 1 3 21l.003-14c0-.552.45-1 1.006-1zM5.002 8L5 20h10V8zM9 6h8v10h2V4H9z"/>'
+ },
+ 'file-excel-2-line': {
+ body: '<path fill="currentColor" d="m2.859 2.877l12.57-1.795a.5.5 0 0 1 .571.494v20.848a.5.5 0 0 1-.57.494L2.858 21.123a1 1 0 0 1-.859-.99V3.867a1 1 0 0 1 .859-.99M4 4.735v14.53l10 1.429V3.306zM17 19h3V5h-3V3h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4zm-6.8-7l2.8 4h-2.4L9 13.714L7.4 16H5l2.8-4L5 8h2.4L9 10.286L10.6 8H13z"/>'
+ },
+ 'file-pdf-2-line': {
+ body: '<path fill="currentColor" d="M5 4h10v4h4v12H5zM3.999 2A.995.995 0 0 0 3 2.992v18.016a1 1 0 0 0 .993.992h16.014A1 1 0 0 0 21 20.992V7l-5-5zm6.5 5.5c0 1.577-.455 3.437-1.224 5.153c-.772 1.723-1.814 3.197-2.9 4.066l1.18 1.613c2.927-1.952 6.168-3.29 9.304-2.842l.457-1.939C14.644 12.661 12.5 9.99 12.5 7.5zm.6 5.972c.268-.597.505-1.216.705-1.843a9.7 9.7 0 0 0 1.706 1.966c-.982.176-1.944.465-2.875.833q.248-.471.465-.956"/>'
+ },
+ 'fingerprint-line': {
+ body: '<path fill="currentColor" d="M17 13v1c0 2.77-.664 5.445-1.915 7.846l-.227.42l-1.746-.974a14.9 14.9 0 0 0 1.881-6.836L15 14v-1zm-6-3h2v4l-.005.379a12.94 12.94 0 0 1-2.691 7.549l-.231.29l-1.549-1.264a10.94 10.94 0 0 0 2.47-6.588L11 14zm1-4a5 5 0 0 1 5 5h-2a3 3 0 0 0-6 0v3c0 2.235-.82 4.344-2.27 5.977l-.212.23l-1.448-1.38a6.97 6.97 0 0 0 1.924-4.524L7 14v-3a5 5 0 0 1 5-5m0-4a9 9 0 0 1 9 9v3c0 1.698-.201 3.37-.596 4.99l-.14.539l-1.93-.526c.392-1.437.614-2.922.658-4.435L19 14v-3A7 7 0 0 0 7.808 5.394L6.383 3.968A8.96 8.96 0 0 1 12 2M4.968 5.383l1.426 1.425a6.97 6.97 0 0 0-1.39 3.951L5 11l.004 2c0 1.12-.264 2.203-.761 3.177l-.157.29l-1.736-.992c.379-.665.6-1.407.645-2.183L3.004 13v-2a8.94 8.94 0 0 1 1.964-5.617"/>'
+ },
+ 'fire-line': {
+ body: '<path fill="currentColor" d="M12 23a7.5 7.5 0 0 0 7.5-7.5c0-.866-.23-1.697-.5-2.47q-2.5 2.47-3.8 2.47c3.995-7 1.8-10-4.2-14c.5 5-2.796 7.274-4.138 8.537A7.5 7.5 0 0 0 12 23m.71-17.765c3.241 2.75 3.257 4.887.753 9.274c-.761 1.333.202 2.991 1.737 2.991c.688 0 1.384-.2 2.119-.595a5.5 5.5 0 1 1-9.087-5.412c.126-.118.765-.685.793-.71c.424-.38.773-.717 1.118-1.086c1.23-1.318 2.114-2.78 2.566-4.462"/>'
+ },
+ 'flag-2-line': {
+ body: '<path fill="currentColor" d="M21.138 3a.5.5 0 0 1 .434.748L18 10l3.573 6.252a.5.5 0 0 1-.435.748H4v5H2V3zm-2.584 2H4v10h14.554l-2.857-5z"/>'
+ },
+ 'function-line': {
+ body: '<path fill="currentColor" d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2-9v4h4V5zm0 10v4h4v-4zM5 5v4h4V5zm0 10v4h4v-4z"/>'
+ },
+ 'game-line': {
+ body: '<path fill="currentColor" d="M12 2a9.98 9.98 0 0 1 7.743 3.671L13.414 12l6.329 6.329A9.98 9.98 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 4.697 14.477l.208-.157l-6.32-6.32l6.32-6.321l-.208-.156a7.97 7.97 0 0 0-4.394-1.517zm0 1a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3"/>'
+ },
+ 'gamepad-line': {
+ body: '<path fill="currentColor" d="M17 4a6 6 0 0 1 6 6v4a6 6 0 0 1-6 6H7a6 6 0 0 1-6-6v-4a6 6 0 0 1 6-6zm0 2H7a4 4 0 0 0-3.995 3.8L3 10v4a4 4 0 0 0 3.8 3.995L7 18h10a4 4 0 0 0 3.995-3.8L21 14v-4a4 4 0 0 0-3.8-3.995zm-7 3v2h2v2H9.999L10 15H8l-.001-2H6v-2h2V9zm8 4v2h-2v-2zm-2-4v2h-2V9z"/>'
+ },
+ 'gift-2-line': {
+ body: '<path fill="currentColor" d="M14.505 2.003a3.5 3.5 0 0 1 3.163 5h3.337a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-1v8a1 1 0 0 1-1 1h-14a1 1 0 0 1-1-1v-8h-1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h3.337a3.5 3.5 0 0 1 5.664-3.95a3.48 3.48 0 0 1 2.499-1.05m3.5 11h-12v7h12zm2-4h-16v2h16zm-10.5-5a1.5 1.5 0 0 0-.145 2.993l.145.007h1.5v-1.5A1.5 1.5 0 0 0 9.649 4.01zm5 0l-.145.007a1.5 1.5 0 0 0-1.348 1.348l-.007.145v1.5h1.5l.144-.007a1.5 1.5 0 0 0 0-2.986z"/>'
+ },
+ 'github-fill': {
+ body: '<path fill="currentColor" d="M12.001 2c-5.525 0-10 4.475-10 10a9.99 9.99 0 0 0 6.837 9.488c.5.087.688-.213.688-.476c0-.237-.013-1.024-.013-1.862c-2.512.463-3.162-.612-3.362-1.175c-.113-.288-.6-1.175-1.025-1.413c-.35-.187-.85-.65-.013-.662c.788-.013 1.35.725 1.538 1.025c.9 1.512 2.337 1.087 2.912.825c.088-.65.35-1.087.638-1.337c-2.225-.25-4.55-1.113-4.55-4.938c0-1.088.387-1.987 1.025-2.687c-.1-.25-.45-1.275.1-2.65c0 0 .837-.263 2.75 1.024a9.3 9.3 0 0 1 2.5-.337c.85 0 1.7.112 2.5.337c1.913-1.3 2.75-1.024 2.75-1.024c.55 1.375.2 2.4.1 2.65c.637.7 1.025 1.587 1.025 2.687c0 3.838-2.337 4.688-4.562 4.938c.362.312.675.912.675 1.85c0 1.337-.013 2.412-.013 2.75c0 .262.188.574.688.474A10.02 10.02 0 0 0 22 12c0-5.525-4.475-10-10-10"/>'
+ },
+ 'github-line': {
+ body: '<path fill="currentColor" d="M5.884 18.653c-.3-.2-.558-.455-.86-.816a51 51 0 0 1-.466-.579c-.463-.575-.755-.841-1.056-.95a1 1 0 1 1 .675-1.882c.752.27 1.261.735 1.947 1.588c-.094-.117.34.427.433.539c.19.227.33.365.44.438c.204.137.588.196 1.15.14c.024-.382.094-.753.202-1.095c-2.968-.726-4.648-2.64-4.648-6.396c0-1.24.37-2.356 1.058-3.292c-.218-.894-.185-1.975.302-3.192a1 1 0 0 1 .63-.582c.081-.024.127-.035.208-.047c.803-.124 1.937.17 3.415 1.096a11.7 11.7 0 0 1 2.687-.308c.912 0 1.819.104 2.684.308c1.477-.933 2.614-1.227 3.422-1.096q.128.02.218.05a1 1 0 0 1 .616.58c.487 1.216.52 2.296.302 3.19c.691.936 1.058 2.045 1.058 3.293c0 3.757-1.674 5.665-4.642 6.392c.125.415.19.878.19 1.38c0 .665-.002 1.299-.007 2.01c0 .19-.002.394-.005.706a1 1 0 0 1-.018 1.958c-1.14.227-1.984-.532-1.984-1.525l.002-.447l.005-.705c.005-.707.008-1.337.008-1.997c0-.697-.184-1.152-.426-1.361c-.661-.57-.326-1.654.541-1.751c2.966-.333 4.336-1.482 4.336-4.66c0-.955-.312-1.744-.913-2.404A1 1 0 0 1 17.2 6.19c.166-.414.236-.957.095-1.614l-.01.003c-.491.139-1.11.44-1.858.949a1 1 0 0 1-.833.135a9.6 9.6 0 0 0-2.592-.349c-.89 0-1.772.118-2.592.35a1 1 0 0 1-.829-.134c-.753-.507-1.374-.807-1.87-.947c-.143.653-.072 1.194.093 1.607a1 1 0 0 1-.189 1.045c-.597.655-.913 1.458-.913 2.404c0 3.172 1.371 4.328 4.322 4.66c.865.097 1.202 1.177.545 1.748c-.193.168-.43.732-.43 1.364v3.15c0 .985-.834 1.725-1.96 1.528a1 1 0 0 1-.04-1.962v-.99c-.91.061-1.661-.088-2.254-.485"/>'
+ },
+ 'group-line': {
+ body: '<path fill="currentColor" d="M2 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m8.284 3.703A8 8 0 0 1 23 22h-2a6 6 0 0 0-3.537-5.473zm-.688-11.29A5.5 5.5 0 0 1 21 8.5a5.5 5.5 0 0 1-5 5.478v-2.013a3.5 3.5 0 0 0 1.041-6.609z"/>'
+ },
+ 'hard-drive-3-line': {
+ body: '<path fill="currentColor" d="M4.508 2.876A1 1 0 0 1 5.5 2h13a1 1 0 0 1 .992.876l1.5 12Q21 14.938 21 15v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 .008-.124zM6.383 4l-1.25 10h13.734l-1.25-10zM19 16H5v4h14zm-4 1h2v2h-2zm-2 0h-2v2h2z"/>'
+ },
+ 'heart-3-line': {
+ body: '<path fill="currentColor" d="M16.5 3C19.538 3 22 5.5 22 9c0 7-7.5 11-10 12.5C9.5 20 2 16 2 9c0-3.5 2.5-6 5.5-6C9.36 3 11 4 12 5c1-1 2.64-2 4.5-2m-3.566 15.604a27 27 0 0 0 2.42-1.701C18.335 14.533 20 11.943 20 9c0-2.36-1.537-4-3.5-4c-1.076 0-2.24.57-3.086 1.414L12 7.828l-1.414-1.414C9.74 5.57 8.576 5 7.5 5C5.56 5 4 6.657 4 9c0 2.944 1.666 5.533 4.645 7.903c.745.593 1.54 1.146 2.421 1.7c.299.189.595.37.934.572c.339-.202.635-.383.934-.571"/>'
+ },
+ 'heart-fill': {
+ body: '<path fill="currentColor" d="M12.001 4.529a6 6 0 0 1 8.242.228a6 6 0 0 1 .236 8.236l-8.48 8.492l-8.478-8.492a6 6 0 0 1 8.48-8.464"/>'
+ },
+ 'heart-line': {
+ body: '<path fill="currentColor" d="M12.001 4.529a6 6 0 0 1 8.242.228a6 6 0 0 1 .236 8.236l-8.48 8.492l-8.478-8.492a6 6 0 0 1 8.48-8.464m6.826 1.641a4 4 0 0 0-5.49-.153l-1.335 1.198l-1.336-1.197a4 4 0 0 0-5.686 5.605L12 18.654l7.02-7.03a4 4 0 0 0-.193-5.454"/>'
+ },
+ 'home-line': {
+ body: '<path fill="currentColor" d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.223a1 1 0 0 1 1.228 0l8 6.223a1 1 0 0 1 .386.79zm-2-1V9.978l-7-5.444l-7 5.444V19z"/>'
+ },
+ 'home-smile-2-line': {
+ body: '<path fill="currentColor" d="M19 19V9.799l-7-5.522l-7 5.522V19zm2 1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.314a1 1 0 0 1 .38-.785l8-6.311a1 1 0 0 1 1.24 0l8 6.31a1 1 0 0 1 .38.786zM7 12h2a3 3 0 1 0 6 0h2a5 5 0 0 1-10 0"/>'
+ },
+ 'image-line': {
+ body: '<path fill="currentColor" d="M2.992 21A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993zM20 15V5H4v14L14 9zm0 2.828l-6-6L6.828 19H20zM8 11a2 2 0 1 1 0-4a2 2 0 0 1 0 4"/>'
+ },
+ 'input-method-line': {
+ body: '<path fill="currentColor" d="M5 5v14h14V5zM4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m5.869 12l-.82 2H6.833L11 7h2l4.167 10H14.95l-.82-2zm.82-2h2.623L12 9.8z"/>'
+ },
+ 'layout-2-line': {
+ body: '<path fill="currentColor" d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1zM11 5H5v14h6zm8 8h-6v6h6zm0-8h-6v6h6z"/>'
+ },
+ 'layout-grid-line': {
+ body: '<path fill="currentColor" d="M21 3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM11 13H4v6h7zm9 0h-7v6h7zm-9-8H4v6h7zm9 0h-7v6h7z"/>'
+ },
+ 'loader-line': {
+ body: '<path fill="currentColor" d="M12 2a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1m0 15a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1m8.66-10a1 1 0 0 1-.366 1.366l-2.598 1.5a1 1 0 1 1-1-1.732l2.598-1.5A1 1 0 0 1 20.66 7M7.67 14.5a1 1 0 0 1-.367 1.366l-2.598 1.5a1 1 0 1 1-1-1.732l2.598-1.5a1 1 0 0 1 1.366.366M20.66 17a1 1 0 0 1-1.366.366l-2.598-1.5a1 1 0 0 1 1-1.732l2.598 1.5A1 1 0 0 1 20.66 17M7.67 9.5a1 1 0 0 1-1.367.366l-2.598-1.5a1 1 0 1 1 1-1.732l2.598 1.5A1 1 0 0 1 7.67 9.5"/>'
+ },
+ 'lock-line': {
+ body: '<path fill="currentColor" d="M19 10h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1h1V9a7 7 0 0 1 14 0zM5 12v8h14v-8zm6 2h2v4h-2zm6-4V9A5 5 0 0 0 7 9v1z"/>'
+ },
+ 'magic-line': {
+ body: '<path fill="currentColor" d="M15.199 9.944a2.6 2.6 0 0 1-.79-1.55l-.403-3.083l-2.731 1.486a2.6 2.6 0 0 1-1.719.272L6.5 6.5l.57 3.056a2.6 2.6 0 0 1-.273 1.72l-1.486 2.73l3.083.403a2.6 2.6 0 0 1 1.55.79l2.138 2.257l1.336-2.807a2.6 2.6 0 0 1 1.23-1.231l2.808-1.336zm.025 5.564l-2.213 4.65a.6.6 0 0 1-.977.155l-3.542-3.739a.6.6 0 0 0-.358-.182l-5.106-.668a.6.6 0 0 1-.45-.881l2.462-4.524a.6.6 0 0 0 .063-.396L4.16 4.86a.6.6 0 0 1 .7-.7l5.062.943a.6.6 0 0 0 .397-.063l4.523-2.46a.6.6 0 0 1 .882.448l.668 5.107a.6.6 0 0 0 .182.357l3.739 3.542a.6.6 0 0 1-.155.977l-4.65 2.213a.6.6 0 0 0-.284.284m.797 1.927l1.414-1.414l4.243 4.242l-1.415 1.415z"/>'
+ },
+ 'mail-line': {
+ body: '<path fill="currentColor" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m17 4.238l-7.928 7.1L4 7.216V19h16zM4.511 5l7.55 6.662L19.502 5z"/>'
+ },
+ 'map-pin-line': {
+ body: '<path fill="currentColor" d="m12 20.9l4.95-4.95a7 7 0 1 0-9.9 0zm0 2.828l-6.364-6.364a9 9 0 1 1 12.728 0zM12 13a2 2 0 1 0 0-4a2 2 0 0 0 0 4m0 2a4 4 0 1 1 0-8a4 4 0 0 1 0 8"/>'
+ },
+ 'menu-2-fill': {
+ body: '<path fill="currentColor" d="M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z"/>'
+ },
+ 'menu-2-line': {
+ body: '<path fill="currentColor" d="M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z"/>'
+ },
+ 'menu-line': {
+ body: '<path fill="currentColor" d="M3 4h18v2H3zm0 7h18v2H3zm0 7h18v2H3z"/>'
+ },
+ 'menu-unfold-3-line': {
+ body: '<path fill="currentColor" d="M17 4H3v2h14zm-4 7H3v2h10zm4 7H3v2h14zm-1.01-9.186L17.404 7.4L22 11.996l-4.596 4.596l-1.414-1.414l3.182-3.182z"/>'
+ },
+ 'message-3-line': {
+ body: '<path fill="currentColor" d="M2 8.994A5.99 5.99 0 0 1 8 3h8c3.313 0 6 2.695 6 5.994V21H8c-3.313 0-6-2.695-6-5.994zM20 19V8.994A4.004 4.004 0 0 0 16 5H8a3.99 3.99 0 0 0-4 3.994v6.012A4.004 4.004 0 0 0 8 19zm-6-8h2v2h-2zm-6 0h2v2H8z"/>'
+ },
+ 'money-cny-box-line': {
+ body: '<path fill="currentColor" d="M3.005 3.003h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-18a1 1 0 0 1-1-1v-16a1 1 0 0 1 1-1m1 2v14h16v-14zm9 8h3v2h-3v2h-2v-2h-3v-2h3v-1h-3v-2h2.586L8.469 7.88l1.415-1.414l2.12 2.122l2.122-2.122L15.54 7.88l-2.12 2.122h2.585v2h-3z"/>'
+ },
+ 'money-cny-circle-line': {
+ body: '<path fill="currentColor" d="M12.005 22.003c-5.523 0-10-4.477-10-10s4.477-10 10-10s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m1-7h3v2h-3v2h-2v-2h-3v-2h3v-1h-3v-2h2.586L8.469 7.88l1.415-1.414l2.12 2.122l2.122-2.122L15.54 7.88l-2.12 2.122h2.585v2h-3z"/>'
+ },
+ 'money-dollar-circle-line': {
+ body: '<path fill="currentColor" d="M12.005 22.003c-5.523 0-10-4.477-10-10s4.477-10 10-10s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-3.5-6h5.5a.5.5 0 1 0 0-1h-4a2.5 2.5 0 1 1 0-5h1v-2h2v2h2.5v2h-5.5a.5.5 0 0 0 0 1h4a2.5 2.5 0 0 1 0 5h-1v2h-2v-2h-2.5z"/>'
+ },
+ 'more-2-fill': {
+ body: '<path fill="currentColor" d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0 14c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0-7c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2"/>'
+ },
+ 'mouse-line': {
+ body: '<path fill="currentColor" d="M11.141 4c-1.582 0-2.387.169-3.128.565a3.45 3.45 0 0 0-1.448 1.448C6.169 6.753 6 7.559 6 9.14v5.718c0 1.582.169 2.387.565 3.128q.504.944 1.448 1.448c.74.396 1.546.565 3.128.565h1.718c1.582 0 2.387-.169 3.128-.565a3.45 3.45 0 0 0 1.448-1.448c.396-.74.565-1.546.565-3.128V9.14c0-1.582-.169-2.387-.565-3.128a3.45 3.45 0 0 0-1.448-1.448C15.247 4.169 14.441 4 12.86 4zm0-2h1.718c2.014 0 3.094.278 4.071.801A5.45 5.45 0 0 1 19.2 5.07c.522.978.801 2.058.801 4.072v5.718c0 2.014-.279 3.094-.801 4.071a5.45 5.45 0 0 1-2.27 2.269c-.977.522-2.057.801-4.071.801H11.14c-2.014 0-3.094-.279-4.072-.801A5.45 5.45 0 0 1 4.8 18.931c-.522-.977-.8-2.057-.8-4.071V9.14c0-2.014.278-3.094.801-4.072A5.45 5.45 0 0 1 7.07 2.801C8.047 2.278 9.127 2 11.141 2M11 6h2v5h-2z"/>'
+ },
+ 'notification-2-line': {
+ body: '<path fill="currentColor" d="M22 20H2v-2h1v-6.969C3 6.043 7.03 2 12 2s9 4.043 9 9.031V18h1zM5 18h14v-6.969C19 7.148 15.866 4 12 4s-7 3.148-7 7.031zm4.5 3h5a2.5 2.5 0 0 1-5 0"/>'
+ },
+ 'notification-3-line': {
+ body: '<path fill="currentColor" d="M20 17h2v2H2v-2h2v-7a8 8 0 1 1 16 0zm-2 0v-7a6 6 0 0 0-12 0v7zm-9 4h6v2H9z"/>'
+ },
+ 'palette-line': {
+ body: '<path fill="currentColor" d="M12 2c5.522 0 10 3.978 10 8.889a5.56 5.56 0 0 1-5.556 5.555h-1.966c-.922 0-1.667.745-1.667 1.667c0 .422.167.811.422 1.1c.267.3.434.689.434 1.122C13.667 21.256 12.9 22 12 22C6.478 22 2 17.522 2 12S6.478 2 12 2m-1.189 16.111a3.664 3.664 0 0 1 3.667-3.667h1.966A3.56 3.56 0 0 0 20 10.89C20 7.139 16.468 4 12 4a8 8 0 0 0-.676 15.972a3.65 3.65 0 0 1-.513-1.86M7.5 12a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m9 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3M12 9a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>'
+ },
+ 'pencil-line': {
+ body: '<path fill="currentColor" d="m15.728 9.576l-1.414-1.414L5 17.476v1.414h1.414zm1.414-1.414l1.414-1.414l-1.414-1.414l-1.414 1.414zm-9.9 12.728H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414z"/>'
+ },
+ 'phone-line': {
+ body: '<path fill="currentColor" d="M9.366 10.682a10.56 10.56 0 0 0 3.952 3.952l.884-1.238a1 1 0 0 1 1.294-.296a11.4 11.4 0 0 0 4.583 1.364a1 1 0 0 1 .921.997v4.462a1 1 0 0 1-.898.995Q19.307 21 18.5 21C9.94 21 3 14.06 3 5.5q0-.807.082-1.602A1 1 0 0 1 4.077 3h4.462a1 1 0 0 1 .997.921A11.4 11.4 0 0 0 10.9 8.504a1 1 0 0 1-.296 1.294zm-2.522-.657l1.9-1.357A13.4 13.4 0 0 1 7.647 5H5.01Q5 5.25 5 5.5C5 12.956 11.044 19 18.5 19q.25 0 .5-.01v-2.637a13.4 13.4 0 0 1-3.668-1.097l-1.357 1.9a12.5 12.5 0 0 1-1.588-.75l-.058-.033a12.56 12.56 0 0 1-4.702-4.702l-.033-.058a12.4 12.4 0 0 1-.75-1.588"/>'
+ },
+ 'pie-chart-line': {
+ body: '<path fill="currentColor" d="M9 2.458v2.124A8.003 8.003 0 0 0 12 20a8 8 0 0 0 7.419-5h2.123c-1.274 4.057-5.064 7-9.542 7c-5.523 0-10-4.477-10-10c0-4.478 2.943-8.268 7-9.542M12 2c5.523 0 10 4.477 10 10q0 .507-.05 1H11V2.05Q11.493 2 12 2m1 2.062V11h6.938A8.004 8.004 0 0 0 13 4.062"/>'
+ },
+ 'planet-line': {
+ body: '<path fill="currentColor" d="M3.918 8.037A9 9 0 0 0 15.966 20.08c.873.373 1.719.618 2.49.681c.902.074 1.844-.095 2.526-.777c.752-.752.88-1.816.746-2.812c-.123-.91-.48-1.92-1.002-2.961A9 9 0 0 0 9.791 3.274c-1.044-.524-2.055-.882-2.965-1.006c-.997-.135-2.062-.007-2.815.746c-.682.683-.851 1.626-.777 2.528c.064.773.31 1.62.684 2.495m1.404-2.071a4 4 0 0 1-.095-.587c-.048-.586.09-.842.198-.95c.12-.12.423-.275 1.132-.179q.298.04.643.136a9 9 0 0 0-1.878 1.58m14.29 10.837a5 5 0 0 1 .134.637c.096.709-.06 1.012-.178 1.13c-.109.109-.364.247-.95.199a4 4 0 0 1-.581-.094a9 9 0 0 0 1.575-1.872m-3.73 1.023c-1.677-.878-3.625-2.323-5.507-4.205c-1.88-1.88-3.324-3.825-4.203-5.5A7.02 7.02 0 0 1 9.97 5.298a7 7 0 0 1 5.912 12.528m-2.277.99a7 7 0 0 1-8.42-8.419c.964 1.516 2.25 3.112 3.776 4.638c1.528 1.528 3.126 2.815 4.644 3.78"/>'
+ },
+ 'price-tag-line': {
+ body: '<path fill="currentColor" d="m3.005 7l8.445-5.63a1 1 0 0 1 1.11 0L21.005 7v14a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1zm2 1.07V20h14V8.07l-7-4.667zm7 2.93a2 2 0 1 1 0-4a2 2 0 0 1 0 4"/>'
+ },
+ 'profile-line': {
+ body: '<path fill="currentColor" d="M21.008 3c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3zM20 5H4v14h16zm-2 10v2H6v-2zm-6-8v6H6V7zm6 4v2h-4v-2zm-8-2H8v2h2zm8-2v2h-4V7z"/>'
+ },
+ 'progress-2-line': {
+ body: '<path fill="currentColor" d="M2 12c0 5.523 4.477 10 10 10s10-4.477 10-10S17.523 2 12 2S2 6.477 2 12m18 0a8 8 0 1 1-16 0a8 8 0 0 1 16 0m-8 0V6a6 6 0 0 1 6 6z"/>'
+ },
+ 'pushpin-2-line': {
+ body: '<path fill="currentColor" d="M18 3v2h-1v6l2 3v2h-6v7h-2v-7H5v-2l2-3V5H6V3zM9 5v6.606L7.404 14h9.192L15 11.606V5z"/>'
+ },
+ 'qr-code-line': {
+ body: '<path fill="currentColor" d="M16 17v-1h-3v-3h3v2h2v2h-1v2h-2v2h-2v-3h2v-1zm5 4h-4v-2h2v-2h2zM3 3h8v8H3zm2 2v4h4V5zm8-2h8v8h-8zm2 2v4h4V5zM3 13h8v8H3zm2 2v4h4v-4zm13-2h3v2h-3zM6 6h2v2H6zm0 10h2v2H6zM16 6h2v2h-2z"/>'
+ },
+ 'rectangle-line': {
+ body: '<path fill="currentColor" d="M3 4h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1m1 2v12h16V6z"/>'
+ },
+ 'refresh-line': {
+ body: '<path fill="currentColor" d="M5.463 4.433A9.96 9.96 0 0 1 12 2c5.523 0 10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3A8 8 0 0 0 6.46 6.228zm13.074 15.134A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12c0-2.136.67-4.116 1.81-5.74L7 12H4a8 8 0 0 0 13.54 5.772z"/>'
+ },
+ 'screenshot-line': {
+ body: '<path fill="currentColor" d="m11.993 14.407l-1.552 1.552a4 4 0 1 1-1.418-1.41l1.555-1.556l-4.185-4.185l1.415-1.415l4.185 4.185l4.189-4.189l1.414 1.414l-4.19 4.19l1.562 1.56a4 4 0 1 1-1.414 1.414zM7 20a2 2 0 1 0 0-4a2 2 0 0 0 0 4m10 0a2 2 0 1 0 0-4a2 2 0 0 0 0 4m2-7V5H5v8H3V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v9z"/>'
+ },
+ 'search-line': {
+ body: '<path fill="currentColor" d="m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z"/>'
+ },
+ 'settings-line': {
+ body: '<path fill="currentColor" d="m12 1l9.5 5.5v11L12 23l-9.5-5.5v-11zm0 2.311L4.5 7.653v8.694l7.5 4.342l7.5-4.342V7.653zM12 16a4 4 0 1 1 0-8a4 4 0 0 1 0 8m0-2a2 2 0 1 0 0-4a2 2 0 0 0 0 4"/>'
+ },
+ 'shake-hands-line': {
+ body: '<path fill="currentColor" d="M11.861 2.39a3 3 0 0 1 3.275-.034L19.29 5H21a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-1.52a2.65 2.65 0 0 1-1.285 2.449l-5.093 3.056a2 2 0 0 1-2.07-.008a2 2 0 0 1-2.561.073l-5.14-4.039a2 2 0 0 1-.565-2.446A2 2 0 0 1 2 13.51V6a1 1 0 0 1 1-1h4.947zM4.173 13.646l.692-.605a3 3 0 0 1 4.195.24l2.702 2.972a3 3 0 0 1 .396 3.487l5.009-3.005a.66.66 0 0 0 .278-.79l-4.427-6.198a1 1 0 0 0-1.101-.377l-2.486.745a3 3 0 0 1-2.983-.752l-.293-.292A2 2 0 0 1 5.68 7H4v6.51zm9.89-9.602a1 1 0 0 0-1.093.012l-5.4 3.6l.292.293a1 1 0 0 0 .995.25l2.485-.745a3 3 0 0 1 3.303 1.13L18.515 14H20V7h-.709a2 2 0 0 1-1.074-.313zM6.181 14.545l-1.616 1.414l5.14 4.039l.705-1.232a1 1 0 0 0-.129-1.169L7.58 14.625a1 1 0 0 0-1.398-.08"/>'
+ },
+ 'share-forward-line': {
+ body: '<path fill="currentColor" d="M13 14h-2a9 9 0 0 0-7.968 4.81A10 10 0 0 1 3 18C3 12.477 7.477 8 13 8V2.5L23.5 11L13 19.5zm-2-2h4v3.308L20.321 11L15 6.692V10h-2a7.98 7.98 0 0 0-6.057 2.774A11 11 0 0 1 11 12"/>'
+ },
+ 'shield-check-line': {
+ body: '<path fill="currentColor" d="m12 1l8.217 1.826a1 1 0 0 1 .783.976v9.987a6 6 0 0 1-2.672 4.992L12 23l-6.328-4.219A6 6 0 0 1 3 13.79V3.802a1 1 0 0 1 .783-.976zm0 2.049L5 4.604v9.185a4 4 0 0 0 1.781 3.328L12 20.597l5.219-3.48A4 4 0 0 0 19 13.79V4.604zm4.452 5.173l1.415 1.414L11.503 16L7.26 11.757l1.414-1.414l2.828 2.828z"/>'
+ },
+ 'shopping-bag-3-line': {
+ body: '<path fill="currentColor" d="M6.505 2h11a1 1 0 0 1 .8.4l2.7 3.6v15a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1V6l2.7-3.6a1 1 0 0 1 .8-.4m12.5 6h-14v12h14zm-.5-2l-1.5-2h-10l-1.5 2zm-9.5 4v2a3 3 0 1 0 6 0v-2h2v2a5 5 0 0 1-10 0v-2z"/>'
+ },
+ 'shopping-bag-4-line': {
+ body: '<path fill="currentColor" d="M9 6h6a3 3 0 1 0-6 0M7 6a5 5 0 0 1 10 0h3a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zM5 8v12h14V8zm4 2a3 3 0 1 0 6 0h2a5 5 0 0 1-10 0z"/>'
+ },
+ 'shopping-bag-line': {
+ body: '<path fill="currentColor" d="M7.005 8V6a5 5 0 0 1 10 0v2h3a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1zm0 2h-2v10h14V10h-2v2h-2v-2h-6v2h-2zm2-2h6V6a3 3 0 0 0-6 0z"/>'
+ },
+ 'sparkling-line': {
+ body: '<path fill="currentColor" d="M14 4.438A2.437 2.437 0 0 0 16.438 2h1.125A2.437 2.437 0 0 0 20 4.438v1.125A2.437 2.437 0 0 0 17.563 8h-1.125A2.437 2.437 0 0 0 14 5.563zM1 11a6 6 0 0 0 6-6h2a6 6 0 0 0 6 6v2a6 6 0 0 0-6 6H7a6 6 0 0 0-6-6zm3.876 1A8.04 8.04 0 0 1 8 15.124A8.04 8.04 0 0 1 11.124 12A8.04 8.04 0 0 1 8 8.876A8.04 8.04 0 0 1 4.876 12m12.374 2A3.25 3.25 0 0 1 14 17.25v1.5A3.25 3.25 0 0 1 17.25 22h1.5A3.25 3.25 0 0 1 22 18.75v-1.5A3.25 3.25 0 0 1 18.75 14z"/>'
+ },
+ 'star-fill': {
+ body: '<path fill="currentColor" d="m12 18.26l-7.053 3.948l1.575-7.928L.588 8.792l8.027-.952L12 .5l3.385 7.34l8.027.952l-5.934 5.488l1.575 7.928z"/>'
+ },
+ 'subway-line': {
+ body: '<path fill="currentColor" d="m17.2 20l1.8 1.5v.5H5v-.5L6.8 20H5a2 2 0 0 1-2-2V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v11a2 2 0 0 1-2 2zM13 5v6h6V7a2 2 0 0 0-2-2zm-2 0H7a2 2 0 0 0-2 2v4h6zm8 8H5v5h14zM7.5 17a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m9 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>'
+ },
+ 't-box-line': {
+ body: '<path fill="currentColor" d="M5 5v14h14V5zM4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m9 7v7h-2v-7H7V8h10v2z"/>'
+ },
+ 'table-3': {
+ body: '<path fill="currentColor" d="M3 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm8 2v3H4V5zm-7 9v-4h7v4zm0 2h7v3H4zm9 0h7v3h-7zm7-2h-7v-4h7zm0-9v3h-7V5z"/>'
+ },
+ 'table-line': {
+ body: '<path fill="currentColor" d="M4 8h16V5H4zm10 11v-9h-4v9zm2 0h4v-9h-4zm-8 0v-9H4v9zM3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1"/>'
+ },
+ 'table-view': {
+ body: '<path fill="currentColor" d="M3 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm5 2v3H4V5zm-4 9v-4h4v4zm0 2h4v3H4zm6 0h10v3H10zm10-2H10v-4h10zm0-9v3H10V5z"/>'
+ },
+ 'telegram-2-line': {
+ body: '<path fill="currentColor" d="M17.094 7.146c.593-.215.888-.292 1.05-.32q.002.08-.002.122c-.232 2.444-1.251 8.457-1.775 11.255c-.122.655-.216.967-.85.595c-.416-.245-.792-.553-1.196-.817c-1.325-.869-3.221-2.162-3.065-2.084c-1.304-.86-.758-1.386-.03-2.088c.117-.113.24-.231.36-.356c.054-.056.317-.3.687-.645c1.188-1.104 3.484-3.239 3.542-3.486c.01-.04.018-.192-.071-.271c-.09-.08-.223-.053-.318-.031q-.203.046-6.474 4.279q-.918.63-1.664.614l.005.003c-.655-.231-1.308-.43-1.964-.63a66 66 0 0 1-1.3-.405l-.308-.098c4.527-1.972 7.542-3.27 9.053-3.899c2.194-.913 3.496-1.438 4.32-1.738m2.423-1.928a1.8 1.8 0 0 0-.726-.346c-.2-.048-.39-.063-.533-.06c-.477.008-.988.143-1.846.454c-.875.318-2.219.862-4.406 1.771Q9.691 8 2.804 11.001c-.404.161-.773.344-1.065.56c-.27.201-.647.56-.716 1.11c-.052.416.069.8.315 1.103c.214.263.488.423.697.524c.31.15.728.281 1.095.396c.573.18 1.144.363 1.719.539c1.778.544 3.242.992 4.852 2.054c1.181.778 2.34 1.59 3.523 2.366c.432.283.835.608 1.28.87c.488.285 1.106.546 1.86.477c1.138-.105 1.73-1.152 1.97-2.43c.521-2.79 1.557-8.886 1.8-11.432a3.8 3.8 0 0 0-.037-.885a1.66 1.66 0 0 0-.58-1.035"/>'
+ },
+ 'thumb-up-line': {
+ body: '<path fill="currentColor" d="M14.6 8H21a2 2 0 0 1 2 2v2.105c0 .26-.051.52-.15.761l-3.095 7.515a1 1 0 0 1-.925.62H2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1h3.482a1 1 0 0 0 .817-.424L11.752.851a.5.5 0 0 1 .632-.159l1.814.908a2.5 2.5 0 0 1 1.305 2.852zM7 10.588V19h11.16L21 12.105V10h-6.4a2 2 0 0 1-1.938-2.493l.903-3.548a.5.5 0 0 0-.261-.57l-.661-.331l-4.71 6.672c-.25.354-.57.645-.933.858M5 11H3v8h2z"/>'
+ },
+ 'time-line': {
+ body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m1-8h4v2h-6V7h2z"/>'
+ },
+ 'translate-2': {
+ body: '<path fill="currentColor" d="m18.5 10l4.4 11h-2.155l-1.201-3h-4.09l-1.199 3h-2.154L16.5 10zM10 2v2h6v2h-1.968a18.2 18.2 0 0 1-3.62 6.301a15 15 0 0 0 2.335 1.707l-.75 1.878A17 17 0 0 1 9 13.725a16.7 16.7 0 0 1-6.201 3.548l-.536-1.929a14.7 14.7 0 0 0 5.327-3.042A18 18 0 0 1 4.767 8h2.24A16 16 0 0 0 9 10.877a16.2 16.2 0 0 0 2.91-4.876L2 6V4h6V2zm7.5 10.885L16.253 16h2.492z"/>'
+ },
+ 'twitch-line': {
+ body: '<path fill="currentColor" d="M4.301 3h16.7v11.7l-4.7 4.7h-3.9l-2.5 2.4h-2.9v-2.4h-4V6.2zm.7 14.4h4v2.4h.095l2.5-2.4h3.877L19 13.872V5H5zm10-9.4h2v4.7h-2zm0 0h2v4.7h-2zm-5 0h2v4.7h-2z"/>'
+ },
+ 'user-3-line': {
+ body: '<path fill="currentColor" d="M20 22h-2v-2a3 3 0 0 0-3-3H9a3 3 0 0 0-3 3v2H4v-2a5 5 0 0 1 5-5h6a5 5 0 0 1 5 5zm-8-9a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8"/>'
+ },
+ 'user-add-line': {
+ body: '<path fill="currentColor" d="M14 14.252v2.09A6 6 0 0 0 6 22H4a8 8 0 0 1 10-7.749M12 13c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m6 6v-3h2v3h3v2h-3v3h-2v-3h-3v-2z"/>'
+ },
+ 'user-line': {
+ body: '<path fill="currentColor" d="M4 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4"/>'
+ },
+ 'user-location-line': {
+ body: '<path fill="currentColor" d="M12 14v2a6 6 0 0 0-6 6H4a8 8 0 0 1 8-8m0-1c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m8.828 10.071L18 24l-2.828-2.929c-1.562-1.618-1.562-4.24 0-5.858a3.904 3.904 0 0 1 5.656 0c1.563 1.618 1.563 4.24 0 5.858m-1.438-1.39c.813-.842.813-2.236 0-3.079a1.904 1.904 0 0 0-2.78 0c-.813.843-.813 2.237 0 3.08L18 21.12z"/>'
+ },
+ 'user-settings-line': {
+ body: '<path fill="currentColor" d="M12 14v2a6 6 0 0 0-6 6H4a8 8 0 0 1 8-8m0-1c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m2.595 7.811a3.5 3.5 0 0 1 0-1.622l-.992-.573l1-1.732l.992.573A3.5 3.5 0 0 1 17 14.645V13.5h2v1.145c.532.158 1.012.44 1.405.812l.992-.573l1 1.732l-.991.573a3.5 3.5 0 0 1 0 1.622l.991.573l-1 1.732l-.992-.573a3.5 3.5 0 0 1-1.405.812V22.5h-2v-1.145a3.5 3.5 0 0 1-1.405-.812l-.992.573l-1-1.732zM18 19.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"/>'
+ },
+ 'video-on-line': {
+ body: '<path fill="currentColor" d="m17 9.2l5.213-3.65a.5.5 0 0 1 .787.41v12.08a.5.5 0 0 1-.787.41L17 14.8V19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1zm0 3.159l4 2.8V8.84l-4 2.8zM3 6v12h12V6z"/>'
+ },
+ 'vidicon-line': {
+ body: '<path fill="currentColor" d="m17 9.2l5.213-3.65a.5.5 0 0 1 .787.41v12.08a.5.5 0 0 1-.787.41L17 14.8V19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1zm0 3.159l4 2.8V8.84l-4 2.8zM3 6v12h12V6zm2 2h2v2H5z"/>'
+ },
+ 'volume-down-line': {
+ body: '<path fill="currentColor" d="M13 7.22L9.603 10H6v4h3.603L13 16.78zM8.889 16H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3.889l5.294-4.332a.5.5 0 0 1 .817.387v15.89a.5.5 0 0 1-.817.387zm9.974.591l-1.422-1.422A4 4 0 0 0 19 12c0-1.43-.75-2.685-1.88-3.392l1.439-1.439A5.99 5.99 0 0 1 21 12c0 1.842-.83 3.49-2.137 4.591"/>'
+ },
+ 'wallet-line': {
+ body: '<path fill="currentColor" d="M18.005 7h3a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h15zm-14 2v10h16V9zm0-4v2h12V5zm11 8h3v2h-3z"/>'
+ },
+ 'water-flash-line': {
+ body: '<path fill="currentColor" d="m12.005 3.103l-4.95 4.95a7 7 0 1 0 9.9 0zm0-2.828l6.364 6.364A9 9 0 1 1 5.64 19.367a9 9 0 0 1 0-12.728zm1 10.728h2.5l-4.5 6.5v-4.5h-2.5l4.5-6.5z"/>'
+ },
+ 'windows-line': {
+ body: '<path fill="currentColor" d="M21.001 2.5v19l-18-2v-15zm-2 10.499l-7 .001v5.487l7 .779zm-14 4.71l5 .556V13l-5-.001zm14-6.71V4.735l-7 .777V11zm-9-5.265l-5 .556V11l5 .001z"/>'
+ }
+ },
+ prefix: 'ri',
+ width: 24
+ },
+ vaadin: {
+ icons: {
+ 'ctrl-a': {
+ body: '<path fill="currentColor" d="M9 7V6H8V5H7v1h-.5v1H7v3.56a1.823 1.823 0 0 0 2.009 1.439L9 11a.9.9 0 0 1-.998-.495L8 7zm5-4h1v9h-1zm-1 3l-.085-.001c-.773 0-1.462.358-1.911.917L11 6.001h-1v6h1v-3a1.88 1.88 0 0 1 2.006-2l-.006-1zm-8.81 6C2.16 12 1 10.54 1 8s1.16-4 3.19-4h.029c.539 0 1.052.114 1.515.32l-.424.901a2.7 2.7 0 0 0-1.08-.22h-.042C2.38 5.001 2 6.631 2 8.001s.38 3 2.19 3c.497-.013.96-.145 1.366-.368l.444.898a3.94 3.94 0 0 1-1.806.47z"/>'
+ }
+ },
+ prefix: 'vaadin'
+ }
+})
diff --git a/rsf-design/src/plugins/iconify.js b/rsf-design/src/plugins/iconify.js
new file mode 100644
index 0000000..5158b16
--- /dev/null
+++ b/rsf-design/src/plugins/iconify.js
@@ -0,0 +1,18 @@
+import { addCollection } from '@iconify/vue/offline'
+import { LOCAL_ICON_COLLECTIONS } from './iconify.collections.js'
+
+let iconCollectionsRegistered = false
+
+export { LOCAL_ICON_COLLECTIONS }
+
+export function registerLocalIconCollections() {
+ if (iconCollectionsRegistered) {
+ return
+ }
+
+ Object.values(LOCAL_ICON_COLLECTIONS).forEach((collection) => {
+ addCollection(collection)
+ })
+
+ iconCollectionsRegistered = true
+}
diff --git a/rsf-design/src/plugins/index.js b/rsf-design/src/plugins/index.js
new file mode 100644
index 0000000..e1b2166
--- /dev/null
+++ b/rsf-design/src/plugins/index.js
@@ -0,0 +1 @@
+export * from './echarts'
diff --git a/rsf-design/src/router/core/ComponentLoader.js b/rsf-design/src/router/core/ComponentLoader.js
new file mode 100644
index 0000000..eaf91b0
--- /dev/null
+++ b/rsf-design/src/router/core/ComponentLoader.js
@@ -0,0 +1,59 @@
+import { h } from 'vue'
+class ComponentLoader {
+ constructor() {
+ this.modules = import.meta.glob('../../views/**/*.vue')
+ }
+ /**
+ * 鍔犺浇缁勪欢
+ */
+ load(componentPath) {
+ if (!componentPath) {
+ return this.createEmptyComponent()
+ }
+ const fullPath = `../../views${componentPath}.vue`
+ const fullPathWithIndex = `../../views${componentPath}/index.vue`
+ const module = this.modules[fullPath] || this.modules[fullPathWithIndex]
+ if (!module) {
+ console.error(
+ `[ComponentLoader] 鏈壘鍒扮粍浠�: ${componentPath}锛屽皾璇曡繃鐨勮矾寰�: ${fullPath} 鍜� ${fullPathWithIndex}`
+ )
+ return this.createErrorComponent(componentPath)
+ }
+ return module
+ }
+ /**
+ * 鍔犺浇甯冨眬缁勪欢
+ */
+ loadLayout() {
+ return () => import('@/views/index/index.vue')
+ }
+ /**
+ * 鍔犺浇 iframe 缁勪欢
+ */
+ loadIframe() {
+ return () => import('@/views/outside/Iframe.vue')
+ }
+ /**
+ * 鍒涘缓绌虹粍浠�
+ */
+ createEmptyComponent() {
+ return () =>
+ Promise.resolve({
+ render() {
+ return h('div', {})
+ }
+ })
+ }
+ /**
+ * 鍒涘缓閿欒鎻愮ず缁勪欢
+ */
+ createErrorComponent(componentPath) {
+ return () =>
+ Promise.resolve({
+ render() {
+ return h('div', { class: 'route-error' }, `缁勪欢鏈壘鍒�: ${componentPath}`)
+ }
+ })
+ }
+}
+export { ComponentLoader }
diff --git a/rsf-design/src/router/core/IframeRouteManager.js b/rsf-design/src/router/core/IframeRouteManager.js
new file mode 100644
index 0000000..7ece34f
--- /dev/null
+++ b/rsf-design/src/router/core/IframeRouteManager.js
@@ -0,0 +1,63 @@
+class IframeRouteManager {
+ constructor() {
+ this.iframeRoutes = []
+ }
+ static getInstance() {
+ if (!IframeRouteManager.instance) {
+ IframeRouteManager.instance = new IframeRouteManager()
+ }
+ return IframeRouteManager.instance
+ }
+ /**
+ * 娣诲姞 iframe 璺敱
+ */
+ add(route) {
+ if (!this.iframeRoutes.find((r) => r.path === route.path)) {
+ this.iframeRoutes.push(route)
+ }
+ }
+ /**
+ * 鑾峰彇鎵�鏈� iframe 璺敱
+ */
+ getAll() {
+ return this.iframeRoutes
+ }
+ /**
+ * 鏍规嵁璺緞鏌ユ壘 iframe 璺敱
+ */
+ findByPath(path) {
+ return this.iframeRoutes.find((route) => route.path === path)
+ }
+ /**
+ * 娓呯┖鎵�鏈� iframe 璺敱
+ */
+ clear() {
+ this.iframeRoutes = []
+ }
+ /**
+ * 淇濆瓨鍒� sessionStorage
+ */
+ save() {
+ if (this.iframeRoutes.length > 0) {
+ sessionStorage.setItem('iframeRoutes', JSON.stringify(this.iframeRoutes))
+ }
+ }
+ /**
+ * 浠� sessionStorage 鍔犺浇
+ */
+ load() {
+ try {
+ const data = sessionStorage.getItem('iframeRoutes')
+ if (data) {
+ this.iframeRoutes = JSON.parse(data)
+ }
+ } catch (error) {
+ console.error('[IframeRouteManager] 鍔犺浇 iframe 璺敱澶辫触:', error)
+ this.iframeRoutes = []
+ }
+ }
+}
+
+IframeRouteManager.instance = null
+
+export { IframeRouteManager }
diff --git a/rsf-design/src/router/core/MenuProcessor.js b/rsf-design/src/router/core/MenuProcessor.js
new file mode 100644
index 0000000..13b37e9
--- /dev/null
+++ b/rsf-design/src/router/core/MenuProcessor.js
@@ -0,0 +1,208 @@
+import { useUserStore } from '@/store/modules/user'
+import { useAppMode } from '@/hooks/core/useAppMode'
+import { fetchGetMenuList } from '@/api/system-manage'
+import { asyncRoutes } from '../routes/asyncRoutes'
+import { RoutesAlias } from '../routesAlias'
+import { formatMenuTitle } from '@/utils'
+class MenuProcessor {
+ /**
+ * 鑾峰彇鑿滃崟鏁版嵁
+ */
+ async getMenuList() {
+ const { isFrontendMode } = useAppMode()
+ let menuList
+ if (isFrontendMode.value) {
+ menuList = await this.processFrontendMenu()
+ } else {
+ menuList = await this.processBackendMenu()
+ }
+ this.validateMenuPaths(menuList)
+ return this.normalizeMenuPaths(menuList)
+ }
+ /**
+ * 澶勭悊鍓嶇鎺у埗妯″紡鐨勮彍鍗�
+ */
+ async processFrontendMenu() {
+ 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)
+ }
+ /**
+ * 澶勭悊鍚庣鎺у埗妯″紡鐨勮彍鍗�
+ */
+ async processBackendMenu() {
+ const list = await fetchGetMenuList()
+ return this.filterEmptyMenus(list)
+ }
+ /**
+ * 鏍规嵁瑙掕壊杩囨护鑿滃崟
+ */
+ filterMenuByRoles(menu, roles) {
+ return menu.reduce((acc, 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
+ }, [])
+ }
+ /**
+ * 閫掑綊杩囨护绌鸿彍鍗曢」
+ */
+ filterEmptyMenus(menuList) {
+ 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) => {
+ if ('children' in item) {
+ return true
+ }
+ if (item.meta?.isIframe === true || item.meta?.link) {
+ return true
+ }
+ if (item.component && item.component !== '' && item.component !== RoutesAlias.Layout) {
+ return true
+ }
+ return false
+ })
+ }
+ /**
+ * 楠岃瘉鑿滃崟鍒楄〃鏄惁鏈夋晥
+ */
+ validateMenuList(menuList) {
+ return Array.isArray(menuList) && menuList.length > 0
+ }
+ /**
+ * 瑙勮寖鍖栬彍鍗曡矾寰�
+ * 灏嗙浉瀵硅矾寰勮浆鎹负瀹屾暣璺緞锛岀‘淇濊彍鍗曡烦杞纭�
+ */
+ normalizeMenuPaths(menuList, parentPath = '') {
+ return menuList.map((item) => {
+ const fullPath = this.buildFullPath(item.path || '', parentPath)
+ const children = item.children?.length
+ ? this.normalizeMenuPaths(item.children, fullPath)
+ : item.children
+ const redirect = item.redirect || this.resolveDefaultRedirect(children)
+ return {
+ ...item,
+ path: fullPath,
+ redirect,
+ children
+ }
+ })
+ }
+ /**
+ * 涓虹洰褰曞瀷鑿滃崟鎺ㄥ榛樿璺宠浆鍦板潃
+ */
+ resolveDefaultRedirect(children) {
+ if (!children?.length) {
+ return void 0
+ }
+ for (const child of children) {
+ if (this.isNavigableRoute(child)) {
+ return child.path
+ }
+ const nestedRedirect = this.resolveDefaultRedirect(child.children)
+ if (nestedRedirect) {
+ return nestedRedirect
+ }
+ }
+ return void 0
+ }
+ /**
+ * 鍒ゆ柇瀛愯矾鐢辨槸鍚﹀彲浠ヤ綔涓洪粯璁よ惤鐐�
+ */
+ isNavigableRoute(route) {
+ return Boolean(
+ route.path &&
+ route.path !== '/' &&
+ !route.meta?.link &&
+ route.meta?.isIframe !== true &&
+ route.component &&
+ route.component !== ''
+ )
+ }
+ /**
+ * 楠岃瘉鑿滃崟璺緞閰嶇疆
+ * 妫�娴嬮潪涓�绾ц彍鍗曟槸鍚﹂敊璇娇鐢ㄤ簡 / 寮�澶寸殑璺緞
+ */
+ /**
+ * 楠岃瘉鑿滃崟璺緞閰嶇疆
+ * 妫�娴嬮潪涓�绾ц彍鍗曟槸鍚﹂敊璇娇鐢ㄤ簡 / 寮�澶寸殑璺緞
+ */
+ validateMenuPaths(menuList, level = 1) {
+ menuList.forEach((route) => {
+ if (!route.children?.length) return
+ const parentName = String(route.name || route.path || '鏈煡璺敱')
+ route.children.forEach((child) => {
+ const childPath = child.path || ''
+ if (this.isValidAbsolutePath(childPath)) return
+ if (childPath.startsWith('/')) {
+ this.logPathError(child, childPath, parentName, level)
+ }
+ })
+ this.validateMenuPaths(route.children, level + 1)
+ })
+ }
+ /**
+ * 鍒ゆ柇鏄惁涓哄悎娉曠殑缁濆璺緞
+ */
+ isValidAbsolutePath(path) {
+ return (
+ path.startsWith('http://') ||
+ path.startsWith('https://') ||
+ path.startsWith('/outside/iframe/')
+ )
+ }
+ /**
+ * 杈撳嚭璺緞閰嶇疆閿欒鏃ュ織
+ */
+ logPathError(route, path, parentName, level) {
+ 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}) 閰嶇疆閿欒
+ 浣嶇疆: ${parentName} > ${routeName}
+ 闂: ${level + 1}绾ц彍鍗曠殑 path 涓嶈兘浠� / 寮�澶�
+ 褰撳墠閰嶇疆: path: '${path}'
+ 搴旇鏀逛负: path: '${suggestedPath}'`
+ )
+ }
+ /**
+ * 鏋勫缓瀹屾暣璺緞
+ */
+ buildFullPath(path, parentPath) {
+ 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}`
+ }
+}
+export { MenuProcessor }
diff --git a/rsf-design/src/router/core/RoutePermissionValidator.js b/rsf-design/src/router/core/RoutePermissionValidator.js
new file mode 100644
index 0000000..c0c4fbd
--- /dev/null
+++ b/rsf-design/src/router/core/RoutePermissionValidator.js
@@ -0,0 +1,105 @@
+class RoutePermissionValidator {
+ /**
+ * 楠岃瘉璺緞鏄惁鍦ㄧ敤鎴疯彍鍗曟潈闄愪腑
+ * @param targetPath 鐩爣璺緞
+ * @param menuList 鑿滃崟鍒楄〃
+ * @returns 鏄惁鏈夋潈闄愯闂�
+ */
+ static hasPermission(targetPath, menuList) {
+ if (targetPath === '/') {
+ return true
+ }
+ return this.matchRoute(targetPath, menuList)
+ }
+ /**
+ * 鏋勫缓鑿滃崟璺緞闆嗗悎锛堟墎骞冲寲澶勭悊锛�
+ * @param menuList 鑿滃崟鍒楄〃
+ * @param pathSet 璺緞闆嗗悎
+ * @returns 璺緞闆嗗悎
+ */
+ static buildMenuPathSet(menuList, pathSet = /* @__PURE__ */ new Set()) {
+ if (!Array.isArray(menuList) || menuList.length === 0) {
+ return pathSet
+ }
+ for (const menuItem of menuList) {
+ if (!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, pathSet) {
+ for (const menuPath of pathSet) {
+ if (targetPath.startsWith(`${menuPath}/`)) {
+ return true
+ }
+ }
+ return false
+ }
+ /**
+ * 閫掑綊鍖归厤璺敱閰嶇疆锛屾敮鎸侀殣钘忚矾鐢卞拰鍔ㄦ�佸弬鏁拌矾鐢�
+ */
+ static matchRoute(targetPath, routes) {
+ if (!Array.isArray(routes) || routes.length === 0) {
+ return false
+ }
+ for (const route of routes) {
+ if (!route.path) {
+ continue
+ }
+ const routePath = route.path.startsWith('/') ? route.path : `/${route.path}`
+ if (
+ routePath === targetPath ||
+ this.isDynamicRouteMatch(targetPath, routePath) ||
+ targetPath.startsWith(`${routePath}/`)
+ ) {
+ return true
+ }
+ if (route.children?.length && this.matchRoute(targetPath, route.children)) {
+ return true
+ }
+ }
+ return false
+ }
+ /**
+ * 妫�鏌ョ洰鏍囪矾寰勬槸鍚﹀尮閰嶅姩鎬佸弬鏁拌矾鐢憋紝濡� /demo/123 鍖归厤 /demo/:id
+ */
+ static isDynamicRouteMatch(targetPath, routePath) {
+ if (!routePath.includes(':')) {
+ return false
+ }
+ const pattern = routePath
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ .replace(/:([^/]+)/g, '[^/]+')
+ .replace(/\\\*/g, '.*')
+ return new RegExp(`^${pattern}$`).test(targetPath)
+ }
+ /**
+ * 楠岃瘉骞惰繑鍥炴湁鏁堢殑璺緞
+ * 濡傛灉鐩爣璺緞鏃犳潈闄愶紝杩斿洖棣栭〉璺緞
+ * @param targetPath 鐩爣璺緞
+ * @param menuList 鑿滃崟鍒楄〃
+ * @param homePath 棣栭〉璺緞
+ * @returns 楠岃瘉鍚庣殑璺緞
+ */
+ static validatePath(targetPath, menuList, homePath = '/') {
+ const hasPermission = this.hasPermission(targetPath, menuList)
+ if (hasPermission) {
+ return { path: targetPath, hasPermission: true }
+ }
+ return { path: homePath, hasPermission: false }
+ }
+}
+export { RoutePermissionValidator }
diff --git a/rsf-design/src/router/core/RouteRegistry.js b/rsf-design/src/router/core/RouteRegistry.js
new file mode 100644
index 0000000..bccc392
--- /dev/null
+++ b/rsf-design/src/router/core/RouteRegistry.js
@@ -0,0 +1,63 @@
+import { ComponentLoader } from './ComponentLoader'
+import { RouteValidator } from './RouteValidator'
+import { RouteTransformer } from './RouteTransformer'
+class RouteRegistry {
+ constructor(router) {
+ this.router = router
+ this.componentLoader = new ComponentLoader()
+ this.validator = new RouteValidator()
+ this.transformer = new RouteTransformer(this.componentLoader)
+ this.removeRouteFns = []
+ this.registered = false
+ }
+ /**
+ * 娉ㄥ唽鍔ㄦ�佽矾鐢�
+ */
+ register(menuList) {
+ if (this.registered) {
+ console.warn('[RouteRegistry] 璺敱宸叉敞鍐岋紝璺宠繃閲嶅娉ㄥ唽')
+ return
+ }
+ const validationResult = this.validator.validate(menuList)
+ if (!validationResult.valid) {
+ throw new Error(`璺敱閰嶇疆楠岃瘉澶辫触: ${validationResult.errors.join(', ')}`)
+ }
+ const removeRouteFns = []
+ menuList.forEach((route) => {
+ if (route.name && !this.router.hasRoute(route.name)) {
+ const routeConfig = this.transformer.transform(route)
+ const removeRouteFn = this.router.addRoute(routeConfig)
+ removeRouteFns.push(removeRouteFn)
+ }
+ })
+ this.removeRouteFns = removeRouteFns
+ this.registered = true
+ }
+ /**
+ * 绉婚櫎鎵�鏈夊姩鎬佽矾鐢�
+ */
+ unregister() {
+ this.removeRouteFns.forEach((fn) => fn())
+ this.removeRouteFns = []
+ this.registered = false
+ }
+ /**
+ * 妫�鏌ユ槸鍚﹀凡娉ㄥ唽
+ */
+ isRegistered() {
+ return this.registered
+ }
+ /**
+ * 鑾峰彇绉婚櫎鍑芥暟鍒楄〃锛堢敤浜� store 绠$悊锛�
+ */
+ getRemoveRouteFns() {
+ return this.removeRouteFns
+ }
+ /**
+ * 鏍囪涓哄凡娉ㄥ唽锛堢敤浜庨敊璇鐞嗗満鏅紝閬垮厤閲嶅璇锋眰锛�
+ */
+ markAsRegistered() {
+ this.registered = true
+ }
+}
+export { RouteRegistry }
diff --git a/rsf-design/src/router/core/RouteTransformer.js b/rsf-design/src/router/core/RouteTransformer.js
new file mode 100644
index 0000000..37456f8
--- /dev/null
+++ b/rsf-design/src/router/core/RouteTransformer.js
@@ -0,0 +1,84 @@
+import { IframeRouteManager } from './IframeRouteManager'
+class RouteTransformer {
+ constructor(componentLoader) {
+ this.componentLoader = componentLoader
+ this.iframeManager = IframeRouteManager.getInstance()
+ }
+ /**
+ * 杞崲璺敱閰嶇疆
+ */
+ transform(route, depth = 0) {
+ const { component, children, ...routeConfig } = route
+ const converted = {
+ ...routeConfig,
+ component: void 0
+ }
+ 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 鍖呰9锛�
+ */
+ isFirstLevelRoute(route, depth) {
+ return depth === 0 && (!route.children || route.children.length === 0)
+ }
+ /**
+ * 澶勭悊 iframe 绫诲瀷璺敱
+ */
+ handleIframeRoute(targetRoute, sourceRoute, depth) {
+ if (depth === 0) {
+ targetRoute.component = this.componentLoader.loadLayout()
+ targetRoute.path = this.extractFirstSegment(sourceRoute.path || '')
+ targetRoute.name = ''
+ targetRoute.children = [
+ {
+ ...sourceRoute,
+ component: this.componentLoader.loadIframe()
+ }
+ ]
+ } else {
+ targetRoute.component = this.componentLoader.loadIframe()
+ }
+ this.iframeManager.add(sourceRoute)
+ }
+ /**
+ * 澶勭悊涓�绾ц彍鍗曡矾鐢�
+ */
+ handleFirstLevelRoute(converted, route, component) {
+ converted.component = this.componentLoader.loadLayout()
+ converted.path = this.extractFirstSegment(route.path || '')
+ converted.name = ''
+ route.meta.isFirstLevel = true
+ converted.children = [
+ {
+ ...route,
+ component: component ? this.componentLoader.load(component) : void 0
+ }
+ ]
+ }
+ /**
+ * 澶勭悊鏅�氳矾鐢�
+ */
+ handleNormalRoute(converted, component) {
+ if (component) {
+ converted.component = this.componentLoader.load(component)
+ }
+ }
+ /**
+ * 鎻愬彇璺緞鐨勭涓�娈�
+ */
+ extractFirstSegment(path) {
+ const segments = path.split('/').filter(Boolean)
+ return segments.length > 0 ? `/${segments[0]}` : '/'
+ }
+}
+export { RouteTransformer }
diff --git a/rsf-design/src/router/core/RouteValidator.js b/rsf-design/src/router/core/RouteValidator.js
new file mode 100644
index 0000000..1be4918
--- /dev/null
+++ b/rsf-design/src/router/core/RouteValidator.js
@@ -0,0 +1,125 @@
+import { RoutesAlias } from '../routesAlias'
+class RouteValidator {
+ constructor() {
+ this.warnedRoutes = new Set()
+ }
+ /**
+ * 楠岃瘉璺敱閰嶇疆
+ */
+ validate(routes) {
+ const errors = []
+ const warnings = []
+ this.checkDuplicates(routes, errors, warnings)
+ this.checkComponents(routes, errors, warnings)
+ this.checkNestedIndexComponent(routes)
+ return {
+ valid: errors.length === 0,
+ errors,
+ warnings
+ }
+ }
+ /**
+ * 妫�娴嬮噸澶嶈矾鐢�
+ */
+ checkDuplicates(routes, errors, warnings, parentPath = '') {
+ const routeNameMap = /* @__PURE__ */ new Map()
+ const componentPathMap = /* @__PURE__ */ new Map()
+ const checkRoutes = (routes2, parentPath2 = '') => {
+ routes2.forEach((route) => {
+ const currentPath = route.path || ''
+ const fullPath = this.resolvePath(parentPath2, 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 = `${parentPath2}:${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)
+ }
+ /**
+ * 妫�娴嬬粍浠堕厤缃�
+ */
+ checkComponents(routes, errors, warnings, parentPath = '') {
+ 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
+ if (route.component) {
+ if (route.children?.length) {
+ const fullPath = this.resolvePath(parentPath, route.path || '')
+ this.checkComponents(route.children, errors, warnings, fullPath)
+ }
+ return
+ }
+ if (parentPath === '' && !hasExternalLink && !isIframe) {
+ errors.push(`涓�绾ц彍鍗�(${routePath}) 缂哄皯 component锛屽繀椤绘寚鍚� ${RoutesAlias.Layout}`)
+ return
+ }
+ 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锛屼簩绾у強浠ヤ笅鑿滃崟涓嶈兘浣跨敤
+ */
+ checkNestedIndexComponent(routes, level = 1) {
+ routes.forEach((route) => {
+ if (level > 1 && route.component === RoutesAlias.Layout) {
+ this.logLayoutError(route, level)
+ }
+ if (route.children?.length) {
+ this.checkNestedIndexComponent(route.children, level + 1)
+ }
+ })
+ }
+ /**
+ * 杈撳嚭 Layout 缁勪欢閰嶇疆閿欒鏃ュ織
+ */
+ logLayoutError(route, level) {
+ 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}) 閰嶇疆閿欒
+ 闂: ${level}绾ц彍鍗曚笉鑳戒娇鐢� ${RoutesAlias.Layout} 浣滀负 component
+ 璇存槑: 鍙湁涓�绾ц彍鍗曟墠鑳戒娇鐢� ${RoutesAlias.Layout}锛屼簩绾у強浠ヤ笅鑿滃崟搴旇鎸囧悜鍏蜂綋鐨勭粍浠惰矾寰�
+ 褰撳墠閰嶇疆: component: '${RoutesAlias.Layout}'
+ 搴旇鏀逛负: component: '/your/component/path' 鎴栫暀绌� ''锛堝鏋滄槸鐩綍鑿滃崟锛塦
+ )
+ }
+ /**
+ * 璺緞瑙f瀽
+ */
+ resolvePath(parent, child) {
+ return [parent.replace(/\/$/, ''), child.replace(/^\//, '')].filter(Boolean).join('/')
+ }
+}
+export { RouteValidator }
diff --git a/rsf-design/src/router/core/index.js b/rsf-design/src/router/core/index.js
new file mode 100644
index 0000000..8662ee8
--- /dev/null
+++ b/rsf-design/src/router/core/index.js
@@ -0,0 +1,16 @@
+import { RouteRegistry } from './RouteRegistry'
+import { ComponentLoader } from './ComponentLoader'
+import { RouteValidator } from './RouteValidator'
+import { RouteTransformer } from './RouteTransformer'
+import { IframeRouteManager } from './IframeRouteManager'
+import { MenuProcessor } from './MenuProcessor'
+import { RoutePermissionValidator } from './RoutePermissionValidator'
+export {
+ ComponentLoader,
+ IframeRouteManager,
+ MenuProcessor,
+ RoutePermissionValidator,
+ RouteRegistry,
+ RouteTransformer,
+ RouteValidator
+}
diff --git a/rsf-design/src/router/guards/afterEach.js b/rsf-design/src/router/guards/afterEach.js
new file mode 100644
index 0000000..f736bdc
--- /dev/null
+++ b/rsf-design/src/router/guards/afterEach.js
@@ -0,0 +1,26 @@
+import { nextTick } from 'vue'
+import { useSettingStore } from '@/store/modules/setting'
+import NProgress from 'nprogress'
+import { useCommon } from '@/hooks/core/useCommon'
+import { loadingService } from '@/utils/ui'
+import { getPendingLoading, resetPendingLoading } from './beforeEach'
+function setupAfterEachGuard(router) {
+ const { scrollToTop } = useCommon()
+ router.afterEach(() => {
+ scrollToTop()
+ const settingStore = useSettingStore()
+ if (settingStore.showNprogress) {
+ NProgress.done()
+ setTimeout(() => {
+ NProgress.remove()
+ }, 600)
+ }
+ if (getPendingLoading()) {
+ nextTick(() => {
+ loadingService.hideLoading()
+ resetPendingLoading()
+ })
+ }
+ })
+}
+export { setupAfterEachGuard }
diff --git a/rsf-design/src/router/guards/beforeEach.js b/rsf-design/src/router/guards/beforeEach.js
new file mode 100644
index 0000000..f3e37d4
--- /dev/null
+++ b/rsf-design/src/router/guards/beforeEach.js
@@ -0,0 +1,223 @@
+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 = null
+const menuProcessor = new MenuProcessor()
+let pendingLoading = false
+let routeInitFailed = false
+let routeInitInProgress = false
+function getPendingLoading() {
+ return pendingLoading
+}
+function resetPendingLoading() {
+ pendingLoading = false
+}
+function getRouteInitFailed() {
+ return routeInitFailed
+}
+function resetRouteInitState() {
+ routeInitFailed = false
+ routeInitInProgress = false
+}
+function setupBeforeEachGuard(router) {
+ routeRegistry = new RouteRegistry(router)
+ router.beforeEach(async (to, from, next) => {
+ try {
+ await handleRouteGuard(to, from, next, router)
+ } catch (error) {
+ console.error('[RouteGuard] 璺敱瀹堝崼澶勭悊澶辫触:', error)
+ closeLoading()
+ next({ name: 'Exception500' })
+ }
+ })
+}
+function closeLoading() {
+ if (pendingLoading) {
+ nextTick(() => {
+ loadingService.hideLoading()
+ pendingLoading = false
+ })
+ }
+}
+async function handleRouteGuard(to, from, next, router) {
+ const settingStore = useSettingStore()
+ const userStore = useUserStore()
+ if (settingStore.showNprogress) {
+ NProgress.start()
+ }
+ if (!handleLoginStatus(to, userStore, next)) {
+ return
+ }
+ if (routeInitFailed) {
+ if (to.matched.length > 0) {
+ next()
+ } else {
+ next({ name: 'Exception500', replace: true })
+ }
+ return
+ }
+ if (!routeRegistry?.isRegistered() && userStore.isLogin) {
+ if (routeInitInProgress) {
+ next(false)
+ return
+ }
+ await handleDynamicRoutes(to, next, router)
+ return
+ }
+ if (handleRootPathRedirect(to, next)) {
+ return
+ }
+ if (to.matched.length > 0) {
+ setWorktab(to)
+ setPageTitle(to)
+ next()
+ return
+ }
+ next({ name: 'Exception404' })
+}
+function handleLoginStatus(to, userStore, next) {
+ if (userStore.isLogin || to.path === RoutesAlias.Login || isStaticRoute(to.path)) {
+ return true
+ }
+ userStore.logOut()
+ next({
+ name: 'Login',
+ query: { redirect: to.fullPath }
+ })
+ return false
+}
+function isStaticRoute(path) {
+ const checkRoute = (routes, targetPath) => {
+ return routes.some((route) => {
+ if (route.name === 'Exception404') {
+ return false
+ }
+ 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, next, router) {
+ routeInitInProgress = true
+ pendingLoading = true
+ loadingService.showLoading()
+ try {
+ await fetchUserInfo()
+ const menuList = await menuProcessor.getMenuList()
+ if (!menuProcessor.validateMenuList(menuList)) {
+ throw new Error('鑾峰彇鑿滃崟鍒楄〃澶辫触锛岃閲嶆柊鐧诲綍')
+ }
+ routeRegistry?.register(menuList)
+ const menuStore = useMenuStore()
+ menuStore.setMenuList(menuList)
+ menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || [])
+ IframeRouteManager.getInstance().save()
+ useWorktabStore().validateWorktabs(router)
+ if (isStaticRoute(to.path)) {
+ routeInitInProgress = false
+ next({
+ path: to.path,
+ query: to.query,
+ hash: to.hash,
+ replace: true
+ })
+ return
+ }
+ const { homePath } = useCommon()
+ const { path: validatedPath, hasPermission } = RoutePermissionValidator.validatePath(
+ to.path,
+ menuList,
+ homePath.value || '/'
+ )
+ routeInitInProgress = false
+ 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)
+ closeLoading()
+ if (isUnauthorizedError(error)) {
+ routeInitInProgress = false
+ next(false)
+ return
+ }
+ routeInitFailed = true
+ routeInitInProgress = false
+ if (isHttpError(error)) {
+ console.error(`[RouteGuard] 閿欒鐮�: ${error.code}, 娑堟伅: ${error.message}`)
+ }
+ next({ name: 'Exception500', replace: true })
+ }
+}
+async function fetchUserInfo() {
+ const userStore = useUserStore()
+ const data = await fetchGetUserInfo()
+ userStore.setUserInfo(data)
+ userStore.checkAndClearWorktabs()
+}
+function resetRouterState(delay) {
+ setTimeout(() => {
+ routeRegistry?.unregister()
+ IframeRouteManager.getInstance().clear()
+ const menuStore = useMenuStore()
+ menuStore.removeAllDynamicRoutes()
+ menuStore.setMenuList([])
+ resetRouteInitState()
+ }, delay)
+}
+function handleRootPathRedirect(to, next) {
+ if (to.path !== '/') {
+ return false
+ }
+ const { homePath } = useCommon()
+ if (homePath.value && homePath.value !== '/') {
+ next({ path: homePath.value, replace: true })
+ return true
+ }
+ return false
+}
+function isUnauthorizedError(error) {
+ return isHttpError(error) && error.code === ApiStatus.unauthorized
+}
+export {
+ getPendingLoading,
+ getRouteInitFailed,
+ resetPendingLoading,
+ resetRouteInitState,
+ resetRouterState,
+ setupBeforeEachGuard
+}
diff --git a/rsf-design/src/router/index.js b/rsf-design/src/router/index.js
new file mode 100644
index 0000000..5e2da45
--- /dev/null
+++ b/rsf-design/src/router/index.js
@@ -0,0 +1,18 @@
+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'
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes: staticRoutes
+ // 闈欐�佽矾鐢�
+})
+function initRouter(app) {
+ configureNProgress()
+ setupBeforeEachGuard(router)
+ setupAfterEachGuard(router)
+ app.use(router)
+}
+const HOME_PAGE_PATH = ''
+export { HOME_PAGE_PATH, initRouter, router }
diff --git a/rsf-design/src/router/modules/dashboard.js b/rsf-design/src/router/modules/dashboard.js
new file mode 100644
index 0000000..e3c6be8
--- /dev/null
+++ b/rsf-design/src/router/modules/dashboard.js
@@ -0,0 +1,25 @@
+const dashboardRoutes = {
+ 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',
+ icon: 'ri:home-smile-2-line',
+ keepAlive: false,
+ fixedTab: true
+ }
+ }
+ ]
+}
+
+export { dashboardRoutes }
diff --git a/rsf-design/src/router/modules/exception.js b/rsf-design/src/router/modules/exception.js
new file mode 100644
index 0000000..39d1ad6
--- /dev/null
+++ b/rsf-design/src/router/modules/exception.js
@@ -0,0 +1,45 @@
+const exceptionRoutes = {
+ 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
+ }
+ }
+ ]
+}
+export { exceptionRoutes }
diff --git a/rsf-design/src/router/modules/index.js b/rsf-design/src/router/modules/index.js
new file mode 100644
index 0000000..906f9ab
--- /dev/null
+++ b/rsf-design/src/router/modules/index.js
@@ -0,0 +1,13 @@
+import { dashboardRoutes } from './dashboard'
+import { systemRoutes } from './system'
+import { resultRoutes } from './result'
+import { exceptionRoutes } from './exception'
+
+const routeModules = [
+ dashboardRoutes,
+ systemRoutes,
+ resultRoutes,
+ exceptionRoutes
+]
+
+export { routeModules }
diff --git a/rsf-design/src/router/modules/result.js b/rsf-design/src/router/modules/result.js
new file mode 100644
index 0000000..b902ad7
--- /dev/null
+++ b/rsf-design/src/router/modules/result.js
@@ -0,0 +1,32 @@
+const resultRoutes = {
+ 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
+ }
+ }
+ ]
+}
+export { resultRoutes }
diff --git a/rsf-design/src/router/modules/system.js b/rsf-design/src/router/modules/system.js
new file mode 100644
index 0000000..982b466
--- /dev/null
+++ b/rsf-design/src/router/modules/system.js
@@ -0,0 +1,64 @@
+const systemRoutes = {
+ 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',
+ icon: 'ri:user-line',
+ keepAlive: true,
+ roles: ['R_SUPER', 'R_ADMIN']
+ }
+ },
+ {
+ path: 'role',
+ name: 'Role',
+ component: '/system/role',
+ meta: {
+ title: 'menus.system.role',
+ icon: 'ri:user-settings-line',
+ keepAlive: true,
+ roles: ['R_SUPER']
+ }
+ },
+ {
+ path: 'user-center',
+ name: 'UserCenter',
+ component: '/system/user-center',
+ meta: {
+ title: 'menus.system.userCenter',
+ icon: 'ri:user-line',
+ isHide: true,
+ keepAlive: true,
+ isHideTab: true
+ }
+ },
+ {
+ path: 'menu',
+ name: 'Menus',
+ component: '/system/menu',
+ meta: {
+ title: 'menus.system.menu',
+ icon: 'ri:menu-line',
+ keepAlive: true,
+ roles: ['R_SUPER'],
+ authList: [
+ { title: '鏂板', authMark: 'add' },
+ { title: '缂栬緫', authMark: 'edit' },
+ { title: '鍒犻櫎', authMark: 'delete' }
+ ]
+ }
+ }
+ ]
+}
+
+export { systemRoutes }
diff --git a/rsf-design/src/router/routes/asyncRoutes.js b/rsf-design/src/router/routes/asyncRoutes.js
new file mode 100644
index 0000000..9373cf7
--- /dev/null
+++ b/rsf-design/src/router/routes/asyncRoutes.js
@@ -0,0 +1,3 @@
+import { routeModules } from '../modules'
+const asyncRoutes = routeModules
+export { asyncRoutes }
diff --git a/rsf-design/src/router/routes/staticRoutes.js b/rsf-design/src/router/routes/staticRoutes.js
new file mode 100644
index 0000000..9dc9903
--- /dev/null
+++ b/rsf-design/src/router/routes/staticRoutes.js
@@ -0,0 +1,61 @@
+const staticRoutes = [
+ // 涓嶉渶瑕佺櫥褰曞氨鑳借闂殑璺敱绀轰緥
+ // {
+ // 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: '/auth/forget-password',
+ name: 'ForgetPassword',
+ component: () => import('@views/auth/forget-password/index.vue'),
+ meta: { title: 'menus.forgetPassword.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' }
+ }
+ ]
+ }
+]
+export { staticRoutes }
diff --git a/rsf-design/src/router/routesAlias.js b/rsf-design/src/router/routesAlias.js
new file mode 100644
index 0000000..2abac0a
--- /dev/null
+++ b/rsf-design/src/router/routesAlias.js
@@ -0,0 +1,6 @@
+const RoutesAlias = Object.freeze({
+ Layout: '/index/index',
+ Login: '/auth/login'
+})
+
+export { RoutesAlias }
diff --git a/rsf-design/src/store/index.js b/rsf-design/src/store/index.js
new file mode 100644
index 0000000..b437a53
--- /dev/null
+++ b/rsf-design/src/store/index.js
@@ -0,0 +1,19 @@
+import { createPinia } from 'pinia'
+import { createPersistedState } from 'pinia-plugin-persistedstate'
+import { StorageKeyManager } from '@/utils/storage/storage-key-manager'
+const store = createPinia()
+const storageKeyManager = new StorageKeyManager()
+store.use(
+ createPersistedState({
+ key: (storeId) => storageKeyManager.getStorageKey(storeId),
+ storage: localStorage,
+ serializer: {
+ serialize: JSON.stringify,
+ deserialize: JSON.parse
+ }
+ })
+)
+function initStore(app) {
+ app.use(store)
+}
+export { initStore, store }
diff --git a/rsf-design/src/store/modules/menu.js b/rsf-design/src/store/modules/menu.js
new file mode 100644
index 0000000..db546d2
--- /dev/null
+++ b/rsf-design/src/store/modules/menu.js
@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { getFirstMenuPath } from '@/utils'
+import { HOME_PAGE_PATH } from '@/router'
+const useMenuStore = defineStore('menuStore', () => {
+ const homePath = ref(HOME_PAGE_PATH)
+ const menuList = ref([])
+ const menuWidth = ref('')
+ const removeRouteFns = ref([])
+ const setMenuList = (list) => {
+ menuList.value = list
+ setHomePath(HOME_PAGE_PATH || getFirstMenuPath(list))
+ }
+ const getHomePath = () => homePath.value
+ const setHomePath = (path) => {
+ homePath.value = path
+ }
+ const addRemoveRouteFns = (fns) => {
+ 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
+ }
+})
+export { useMenuStore }
diff --git a/rsf-design/src/store/modules/setting.js b/rsf-design/src/store/modules/setting.js
new file mode 100644
index 0000000..b737b40
--- /dev/null
+++ b/rsf-design/src/store/modules/setting.js
@@ -0,0 +1,225 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import AppConfig from '@/config'
+import { SystemThemeEnum } 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'
+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(() => {
+ const list = AppConfig.themeList.filter((item) => item.theme === menuThemeType.value)
+ if (isDark.value) {
+ return AppConfig.darkMenuStyles[0]
+ } else {
+ return list[0]
+ }
+ })
+ const isDark = computed(() => {
+ return systemThemeType.value === SystemThemeEnum.DARK
+ })
+ const getMenuOpenWidth = computed(() => {
+ return menuOpenWidth.value + 'px' || SETTING_DEFAULT_CONFIG.menuOpenWidth + 'px'
+ })
+ const getCustomRadius = computed(() => {
+ return customRadius.value + 'rem' || SETTING_DEFAULT_CONFIG.customRadius + 'rem'
+ })
+ const isShowFireworks = computed(() => {
+ return festivalDate.value === useCeremony().currentFestivalData.value?.date ? false : true
+ })
+ const switchMenuLayouts = (type) => {
+ menuType.value = type
+ }
+ const setMenuOpenWidth = (width) => {
+ menuOpenWidth.value = width
+ }
+ const setGlopTheme = (theme, themeMode) => {
+ systemThemeType.value = theme
+ systemThemeMode.value = themeMode
+ localStorage.setItem(StorageConfig.THEME_KEY, theme)
+ }
+ const switchMenuStyles = (theme) => {
+ menuThemeType.value = theme
+ }
+ const setElementTheme = (theme) => {
+ systemThemeColor.value = theme
+ setElementThemeColor(theme)
+ }
+ const setBorderMode = () => {
+ boxBorderMode.value = !boxBorderMode.value
+ }
+ const setContainerWidth = (width) => {
+ 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
+ }
+ const setWorkTab = (show) => {
+ 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
+ }
+ const setPageTransition = (transition) => {
+ pageTransition.value = transition
+ }
+ const setTabStyle = (style) => {
+ tabStyle.value = style
+ }
+ const setMenuOpen = (open) => {
+ menuOpen.value = open
+ }
+ const reload = () => {
+ refresh.value = !refresh.value
+ }
+ const setWatermarkVisible = (visible) => {
+ watermarkVisible.value = visible
+ }
+ const setCustomRadius = (radius) => {
+ customRadius.value = radius
+ document.documentElement.style.setProperty('--custom-radius', `${radius}rem`)
+ }
+ const setholidayFireworksLoaded = (isLoad) => {
+ holidayFireworksLoaded.value = isLoad
+ }
+ const setShowFestivalText = (show) => {
+ showFestivalText.value = show
+ }
+ const setFestivalDate = (date) => {
+ festivalDate.value = date
+ }
+ const setDualMenuShowText = (show) => {
+ 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
+ }
+ }
+)
+export { useSettingStore }
diff --git a/rsf-design/src/store/modules/table.js b/rsf-design/src/store/modules/table.js
new file mode 100644
index 0000000..31d6a2c
--- /dev/null
+++ b/rsf-design/src/store/modules/table.js
@@ -0,0 +1,37 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { TableSizeEnum } from '@/enums/formEnum'
+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)
+ const setTableSize = (size) => (tableSize.value = size)
+ const setIsZebra = (value) => (isZebra.value = value)
+ const setIsBorder = (value) => (isBorder.value = value)
+ const setIsHeaderBackground = (value) => (isHeaderBackground.value = value)
+ const setIsFullScreen = (value) => (isFullScreen.value = value)
+ return {
+ tableSize,
+ isZebra,
+ isBorder,
+ isHeaderBackground,
+ setTableSize,
+ setIsZebra,
+ setIsBorder,
+ setIsHeaderBackground,
+ isFullScreen,
+ setIsFullScreen
+ }
+ },
+ {
+ persist: {
+ key: 'table',
+ storage: localStorage
+ }
+ }
+)
+export { useTableStore }
diff --git a/rsf-design/src/store/modules/user.js b/rsf-design/src/store/modules/user.js
new file mode 100644
index 0000000..19839ae
--- /dev/null
+++ b/rsf-design/src/store/modules/user.js
@@ -0,0 +1,115 @@
+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 { setPageTitle } from '@/utils/router'
+import { resetRouterState } from '@/router/guards/beforeEach'
+import { useMenuStore } from './menu'
+import { StorageConfig } from '@/utils/storage/storage-config'
+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 getUserInfo = computed(() => info.value)
+ const getSettingState = computed(() => useSettingStore().$state)
+ const getWorktabState = computed(() => useWorktabStore().$state)
+ const setUserInfo = (newInfo) => {
+ info.value = newInfo
+ }
+ const setLoginStatus = (status) => {
+ isLogin.value = status
+ }
+ const setLanguage = (lang) => {
+ setPageTitle(router.currentRoute.value)
+ language.value = lang
+ }
+ const setSearchHistory = (list) => {
+ searchHistory.value = list
+ }
+ const setLockStatus = (status) => {
+ isLock.value = status
+ }
+ const setLockPassword = (password) => {
+ lockPassword.value = password
+ }
+ const setToken = (newAccessToken, newRefreshToken) => {
+ accessToken.value = newAccessToken
+ if (newRefreshToken) {
+ refreshToken.value = newRefreshToken
+ }
+ }
+ const logOut = () => {
+ 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 = ''
+ sessionStorage.removeItem('iframeRoutes')
+ useMenuStore().setHomePath('')
+ resetRouterState(500)
+ const currentRoute = router.currentRoute.value
+ const redirect = currentRoute.path !== '/login' ? currentRoute.fullPath : void 0
+ router.push({
+ name: 'Login',
+ query: redirect ? { redirect } : void 0
+ })
+ }
+ const checkAndClearWorktabs = () => {
+ const lastUserId = localStorage.getItem(StorageConfig.LAST_USER_ID_KEY)
+ const currentUserId = info.value.userId
+ 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,
+ getUserInfo,
+ getSettingState,
+ getWorktabState,
+ setUserInfo,
+ setLoginStatus,
+ setLanguage,
+ setSearchHistory,
+ setLockStatus,
+ setLockPassword,
+ setToken,
+ logOut,
+ checkAndClearWorktabs
+ }
+ },
+ {
+ persist: {
+ key: 'user',
+ storage: localStorage
+ }
+ }
+)
+export { useUserStore }
diff --git a/rsf-design/src/store/modules/worktab.js b/rsf-design/src/store/modules/worktab.js
new file mode 100644
index 0000000..301c807
--- /dev/null
+++ b/rsf-design/src/store/modules/worktab.js
@@ -0,0 +1,343 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { router } from '@/router'
+import { useCommon } from '@/hooks/core/useCommon'
+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) => {
+ return opened.value.findIndex((tab) => tab.path === path)
+ }
+ const getTab = (path) => {
+ return opened.value.find((tab) => tab.path === path)
+ }
+ const isTabClosable = (tab) => {
+ return !tab.fixedTab
+ }
+ const safeRouterPush = (tab) => {
+ if (!tab.path) {
+ console.warn('灏濊瘯璺宠浆鍒版棤鏁堣矾寰勭殑鏍囩椤�')
+ return
+ }
+ try {
+ router.push({
+ path: tab.path,
+ query: tab.query
+ })
+ } catch (error) {
+ console.error('璺敱璺宠浆澶辫触:', error)
+ }
+ }
+ const openTab = (tab) => {
+ if (!tab.path) {
+ console.warn('灏濊瘯鎵撳紑鏃犳晥鐨勬爣绛鹃〉')
+ return
+ }
+ 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 = () => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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) => {
+ 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 = () => {
+ 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)
+ }
+ const addKeepAliveExclude = (tab) => {
+ if (!tab.keepAlive || !tab.name) return
+ if (!keepAliveExclude.value.includes(tab.name)) {
+ keepAliveExclude.value.push(tab.name)
+ }
+ }
+ const removeKeepAliveExclude = (name) => {
+ if (!name) return
+ keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name)
+ }
+ const markTabsToRemove = (tabs) => {
+ tabs.forEach((tab) => {
+ if (tab.name) {
+ addKeepAliveExclude(tab)
+ }
+ })
+ }
+ const toggleFixedTab = (path) => {
+ 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) => {
+ try {
+ const isTabRouteValid = (tab) => {
+ 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 || void 0
+ })
+ 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 = () => {
+ current.value = {}
+ opened.value = []
+ keepAliveExclude.value = []
+ }
+ const getStateSnapshot = () => {
+ return {
+ current: { ...current.value },
+ opened: [...opened.value],
+ keepAliveExclude: [...keepAliveExclude.value]
+ }
+ }
+ const getTabTitle = (path) => {
+ const tab = getTab(path)
+ return tab
+ }
+ const updateTabTitle = (path, title) => {
+ const tab = getTab(path)
+ if (tab) {
+ tab.customTitle = title
+ }
+ }
+ const resetTabTitle = (path) => {
+ 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
+ }
+ }
+)
+export { useWorktabStore }
diff --git a/rsf-design/src/utils/constants/index.js b/rsf-design/src/utils/constants/index.js
new file mode 100644
index 0000000..83238e0
--- /dev/null
+++ b/rsf-design/src/utils/constants/index.js
@@ -0,0 +1 @@
+export * from './links'
diff --git a/rsf-design/src/utils/constants/links.js b/rsf-design/src/utils/constants/links.js
new file mode 100644
index 0000000..b9f81f0
--- /dev/null
+++ b/rsf-design/src/utils/constants/links.js
@@ -0,0 +1,21 @@
+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'
+}
+export { WEB_LINKS }
diff --git a/rsf-design/src/utils/form/index.js b/rsf-design/src/utils/form/index.js
new file mode 100644
index 0000000..bec50e5
--- /dev/null
+++ b/rsf-design/src/utils/form/index.js
@@ -0,0 +1,2 @@
+export * from './validator'
+export * from './responsive'
diff --git a/rsf-design/src/utils/form/responsive.js b/rsf-design/src/utils/form/responsive.js
new file mode 100644
index 0000000..e18dc47
--- /dev/null
+++ b/rsf-design/src/utils/form/responsive.js
@@ -0,0 +1,26 @@
+const BREAKPOINT_CONFIG = {
+ xs: { threshold: 12, fallback: 24 },
+ // 鎵嬫満锛氬皬浜� 12 鏃朵娇鐢ㄦ弧瀹�
+ sm: { threshold: 12, fallback: 12 },
+ // 骞虫澘锛氬皬浜� 12 鏃朵娇鐢ㄥ崐瀹�
+ md: { threshold: 8, fallback: 8 },
+ // 涓瓑灞忓箷锛氬皬浜� 8 鏃朵娇鐢ㄤ笁鍒嗕箣涓�瀹�
+ lg: null,
+ // 澶у睆骞曪細鐩存帴浣跨敤璁剧疆鐨� span
+ xl: null
+ // 瓒呭ぇ灞忓箷锛氱洿鎺ヤ娇鐢ㄨ缃殑 span
+}
+function calculateResponsiveSpan(itemSpan, defaultSpan, breakpoint) {
+ const finalSpan = itemSpan ?? defaultSpan
+ const config = BREAKPOINT_CONFIG[breakpoint]
+ if (!config) {
+ return finalSpan
+ }
+ return finalSpan >= config.threshold ? finalSpan : config.fallback
+}
+function createResponsiveSpanCalculator(defaultSpan) {
+ return (itemSpan, breakpoint) => {
+ return calculateResponsiveSpan(itemSpan, defaultSpan, breakpoint)
+ }
+}
+export { calculateResponsiveSpan, createResponsiveSpanCalculator }
diff --git a/rsf-design/src/utils/form/validator.js b/rsf-design/src/utils/form/validator.js
new file mode 100644
index 0000000..ad0798a
--- /dev/null
+++ b/rsf-design/src/utils/form/validator.js
@@ -0,0 +1,172 @@
+const PasswordStrength = Object.freeze({
+ WEAK: '寮�',
+ MEDIUM: '涓�',
+ STRONG: '寮�'
+})
+
+function trimSpaces(value) {
+ if (typeof value !== 'string') {
+ return ''
+ }
+ return value.trim()
+}
+function validatePhone(value) {
+ if (!value || typeof value !== 'string') {
+ return false
+ }
+ const phoneRegex = /^1[3-9]\d{9}$/
+ return phoneRegex.test(value.trim())
+}
+function validateTelPhone(value) {
+ if (!value || typeof value !== 'string') {
+ return false
+ }
+ const telRegex = /^0\d{2,3}-?\d{7,8}$/
+ return telRegex.test(value.trim().replace(/\s+/g, ''))
+}
+function validateAccount(value) {
+ if (!value || typeof value !== 'string') {
+ return false
+ }
+ const accountRegex = /^[a-zA-Z][a-zA-Z0-9_]{4,19}$/
+ return accountRegex.test(value.trim())
+}
+function validatePassword(value) {
+ 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
+}
+function validateStrongPassword(value) {
+ 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
+}
+function getPasswordStrength(value) {
+ if (!value || typeof value !== 'string') {
+ return '寮�'
+ }
+ const trimmedValue = value.trim()
+ if (trimmedValue.length < 6) {
+ return '寮�'
+ }
+ 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 '寮�'
+ } else if (typeCount >= 2) {
+ return '涓�'
+ } else {
+ return '寮�'
+ }
+}
+function validateIPv4Address(value) {
+ 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
+ })
+}
+function validateEmail(value) {
+ if (!value || typeof value !== 'string') {
+ return false
+ }
+ const trimmedValue = value.trim()
+ 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
+}
+function validateURL(value) {
+ if (!value || typeof value !== 'string') {
+ return false
+ }
+ try {
+ new URL(value.trim())
+ return true
+ } catch {
+ return false
+ }
+}
+function validateChineseIDCard(value) {
+ if (!value || typeof value !== 'string') {
+ return false
+ }
+ const trimmedValue = value.trim()
+ 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
+}
+function validateBankCard(value) {
+ if (!value || typeof value !== 'string') {
+ return false
+ }
+ const trimmedValue = value.trim().replace(/\s+/g, '')
+ if (!/^\d{13,19}$/.test(trimmedValue)) {
+ return false
+ }
+ 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
+}
+export {
+ PasswordStrength,
+ getPasswordStrength,
+ trimSpaces,
+ validateAccount,
+ validateBankCard,
+ validateChineseIDCard,
+ validateEmail,
+ validateIPv4Address,
+ validatePassword,
+ validatePhone,
+ validateStrongPassword,
+ validateTelPhone,
+ validateURL
+}
diff --git a/rsf-design/src/utils/http/error.js b/rsf-design/src/utils/http/error.js
new file mode 100644
index 0000000..5220c17
--- /dev/null
+++ b/rsf-design/src/utils/http/error.js
@@ -0,0 +1,76 @@
+import { ApiStatus } from './status'
+import { $t } from '@/locales'
+class HttpError extends Error {
+ constructor(message, code, options) {
+ super(message)
+ this.name = 'HttpError'
+ this.code = code
+ this.data = options?.data
+ this.timestamp = /* @__PURE__ */ new Date().toISOString()
+ this.url = options?.url
+ this.method = options?.method
+ }
+ toLogData() {
+ return {
+ code: this.code,
+ message: this.message,
+ data: this.data,
+ timestamp: this.timestamp,
+ url: this.url,
+ method: this.method,
+ stack: this.stack
+ }
+ }
+}
+const getErrorMessage = (status) => {
+ const errorMap = {
+ [ApiStatus.unauthorized]: 'httpMsg.unauthorized',
+ [ApiStatus.forbidden]: 'httpMsg.forbidden',
+ [ApiStatus.notFound]: 'httpMsg.notFound',
+ [ApiStatus.methodNotAllowed]: 'httpMsg.methodNotAllowed',
+ [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')
+}
+function handleError(error) {
+ 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 errorMessage = error.response?.data?.msg || error.message
+ const requestConfig = error.config
+ if (!error.response) {
+ throw new HttpError($t('httpMsg.networkError'), ApiStatus.error, {
+ url: requestConfig?.url,
+ method: requestConfig?.method?.toUpperCase()
+ })
+ }
+ const message = statusCode
+ ? getErrorMessage(statusCode)
+ : errorMessage || $t('httpMsg.requestFailed')
+ throw new HttpError(message, statusCode || ApiStatus.error, {
+ data: error.response.data,
+ url: requestConfig?.url,
+ method: requestConfig?.method?.toUpperCase()
+ })
+}
+function showError(error, showMessage = true) {
+ if (showMessage) {
+ ElMessage.error(error.message)
+ }
+ console.error('[HTTP Error]', error.toLogData())
+}
+function showSuccess(message, showMessage = true) {
+ if (showMessage) {
+ ElMessage.success(message)
+ }
+}
+const isHttpError = (error) => {
+ return error instanceof HttpError
+}
+export { HttpError, handleError, isHttpError, showError, showSuccess }
diff --git a/rsf-design/src/utils/http/index.js b/rsf-design/src/utils/http/index.js
new file mode 100644
index 0000000..b9205e5
--- /dev/null
+++ b/rsf-design/src/utils/http/index.js
@@ -0,0 +1,152 @@
+import axios from 'axios'
+import { useUserStore } from '@/store/modules/user'
+import { ApiStatus } from './status'
+import { HttpError, handleError, showError, showSuccess } from './error'
+import { $t } from '@/locales'
+const REQUEST_TIMEOUT = 15e3
+const LOGOUT_DELAY = 500
+const MAX_RETRIES = 0
+const RETRY_DELAY = 1e3
+const UNAUTHORIZED_DEBOUNCE_TIME = 3e3
+let isUnauthorizedErrorShown = false
+let unauthorizedTimer = null
+const { VITE_API_URL, VITE_WITH_CREDENTIALS } = import.meta.env
+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 {
+ return JSON.parse(data)
+ } catch {
+ return data
+ }
+ }
+ return data
+ }
+ ]
+})
+axiosInstance.interceptors.request.use(
+ (request2) => {
+ const { accessToken } = useUserStore()
+ if (accessToken) request2.headers.set('Authorization', accessToken)
+ if (
+ request2.data &&
+ !(request2.data instanceof FormData) &&
+ !request2.headers['Content-Type']
+ ) {
+ request2.headers.set('Content-Type', 'application/json')
+ request2.data = JSON.stringify(request2.data)
+ }
+ return request2
+ },
+ (error) => {
+ showError(createHttpError($t('httpMsg.requestConfigError'), ApiStatus.error))
+ return Promise.reject(error)
+ }
+)
+axiosInstance.interceptors.response.use(
+ (response) => {
+ const { code, msg } = response.data
+ if (code === ApiStatus.success) return response
+ if (code === ApiStatus.unauthorized) handleUnauthorizedError(msg)
+ throw createHttpError(msg || $t('httpMsg.requestFailed'), code)
+ },
+ (error) => {
+ if (error.response?.status === ApiStatus.unauthorized) handleUnauthorizedError()
+ return Promise.reject(handleError(error))
+ }
+)
+function createHttpError(message, code) {
+ return new HttpError(message, code)
+}
+function handleUnauthorizedError(message) {
+ const error = createHttpError(message || $t('httpMsg.unauthorized'), ApiStatus.unauthorized)
+ if (!isUnauthorizedErrorShown) {
+ isUnauthorizedErrorShown = true
+ logOut()
+ unauthorizedTimer = setTimeout(resetUnauthorizedError, UNAUTHORIZED_DEBOUNCE_TIME)
+ showError(error, true)
+ throw error
+ }
+ throw error
+}
+function resetUnauthorizedError() {
+ isUnauthorizedErrorShown = false
+ if (unauthorizedTimer) clearTimeout(unauthorizedTimer)
+ unauthorizedTimer = null
+}
+function logOut() {
+ setTimeout(() => {
+ useUserStore().logOut()
+ }, LOGOUT_DELAY)
+}
+function shouldRetry(statusCode) {
+ return [
+ ApiStatus.requestTimeout,
+ ApiStatus.internalServerError,
+ ApiStatus.badGateway,
+ ApiStatus.serviceUnavailable,
+ ApiStatus.gatewayTimeout
+ ].includes(statusCode)
+}
+async function retryRequest(config, retries = MAX_RETRIES) {
+ 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) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+async function request(config) {
+ if (
+ ['POST', 'PUT'].includes(config.method?.toUpperCase() || '') &&
+ config.params &&
+ !config.data
+ ) {
+ config.data = config.params
+ config.params = void 0
+ }
+ try {
+ const res = await axiosInstance.request(config)
+ if (config.showSuccessMessage && res.data.msg) {
+ showSuccess(res.data.msg)
+ }
+ return res.data.data
+ } catch (error) {
+ if (error instanceof HttpError && error.code !== ApiStatus.unauthorized) {
+ const showMsg = config.showErrorMessage !== false
+ showError(error, showMsg)
+ }
+ return Promise.reject(error)
+ }
+}
+const api = {
+ get(config) {
+ return retryRequest({ ...config, method: 'GET' })
+ },
+ post(config) {
+ return retryRequest({ ...config, method: 'POST' })
+ },
+ put(config) {
+ return retryRequest({ ...config, method: 'PUT' })
+ },
+ del(config) {
+ return retryRequest({ ...config, method: 'DELETE' })
+ },
+ request(config) {
+ return retryRequest(config)
+ }
+}
+
+export default api
diff --git a/rsf-design/src/utils/http/status.js b/rsf-design/src/utils/http/status.js
new file mode 100644
index 0000000..b69ea07
--- /dev/null
+++ b/rsf-design/src/utils/http/status.js
@@ -0,0 +1,17 @@
+const ApiStatus = Object.freeze({
+ success: 200,
+ error: 400,
+ unauthorized: 401,
+ forbidden: 403,
+ notFound: 404,
+ methodNotAllowed: 405,
+ requestTimeout: 408,
+ internalServerError: 500,
+ notImplemented: 501,
+ badGateway: 502,
+ serviceUnavailable: 503,
+ gatewayTimeout: 504,
+ httpVersionNotSupported: 505
+})
+
+export { ApiStatus }
diff --git a/rsf-design/src/utils/index.js b/rsf-design/src/utils/index.js
new file mode 100644
index 0000000..aaf5bef
--- /dev/null
+++ b/rsf-design/src/utils/index.js
@@ -0,0 +1,9 @@
+export * from './ui'
+export * from './router'
+export * from './navigation'
+export * from './sys'
+export * from './constants'
+export * from './storage'
+export * from './http'
+export * from './form'
+export * from './socket'
diff --git a/rsf-design/src/utils/navigation/index.js b/rsf-design/src/utils/navigation/index.js
new file mode 100644
index 0000000..a3c3618
--- /dev/null
+++ b/rsf-design/src/utils/navigation/index.js
@@ -0,0 +1,3 @@
+export * from './jump'
+export * from './worktab'
+export * from './route'
diff --git a/rsf-design/src/utils/navigation/jump.js b/rsf-design/src/utils/navigation/jump.js
new file mode 100644
index 0000000..1bb0b79
--- /dev/null
+++ b/rsf-design/src/utils/navigation/jump.js
@@ -0,0 +1,31 @@
+import { router } from '@/router'
+import { isNavigableMenuItem } from './route'
+const openExternalLink = (link) => {
+ window.open(link, '_blank')
+}
+const handleMenuJump = (item, jumpToFirst = false) => {
+ const { link, isIframe } = item.meta
+ if (link && !isIframe) {
+ return openExternalLink(link)
+ }
+ if (!jumpToFirst || !item.children?.length) {
+ return router.push(item.path)
+ }
+ const findFirstLeafMenu = (items) => {
+ for (const child of items) {
+ if (isNavigableMenuItem(child)) {
+ return child.children?.length ? findFirstLeafMenu(child.children) || child : child
+ }
+ }
+ return void 0
+ }
+ const firstChild = findFirstLeafMenu(item.children)
+ if (!firstChild) {
+ return router.push(item.path)
+ }
+ if (firstChild.meta?.link) {
+ return openExternalLink(firstChild.meta.link)
+ }
+ router.push(firstChild.path)
+}
+export { handleMenuJump, openExternalLink }
diff --git a/rsf-design/src/utils/navigation/route.js b/rsf-design/src/utils/navigation/route.js
new file mode 100644
index 0000000..822ab32
--- /dev/null
+++ b/rsf-design/src/utils/navigation/route.js
@@ -0,0 +1,34 @@
+function isIframe(url) {
+ return url.startsWith('/outside/iframe/')
+}
+const isNavigableMenuItem = (menuItem) => {
+ if (!menuItem.path || !menuItem.path.trim()) {
+ return false
+ }
+ if (!menuItem.meta?.isHide) {
+ return true
+ }
+ return menuItem.meta?.isFullPage === true
+}
+const normalizePath = (path) => {
+ return path.startsWith('/') ? path : `/${path}`
+}
+const getFirstMenuPath = (menuList) => {
+ if (!Array.isArray(menuList) || menuList.length === 0) {
+ return ''
+ }
+ for (const menuItem of menuList) {
+ if (!isNavigableMenuItem(menuItem)) {
+ continue
+ }
+ if (menuItem.children?.length) {
+ const childPath = getFirstMenuPath(menuItem.children)
+ if (childPath) {
+ return childPath
+ }
+ }
+ return normalizePath(menuItem.path)
+ }
+ return ''
+}
+export { getFirstMenuPath, isIframe, isNavigableMenuItem }
diff --git a/rsf-design/src/utils/navigation/worktab.js b/rsf-design/src/utils/navigation/worktab.js
new file mode 100644
index 0000000..f49f680
--- /dev/null
+++ b/rsf-design/src/utils/navigation/worktab.js
@@ -0,0 +1,37 @@
+import { useWorktabStore } from '@/store/modules/worktab'
+import { isIframe } from './route'
+import { useSettingStore } from '@/store/modules/setting'
+import { IframeRouteManager } from '@/router/core'
+import { useCommon } from '@/hooks/core/useCommon'
+const setWorktab = (to) => {
+ const worktabStore = useWorktabStore()
+ const { meta, path, name, params, query } = to
+ if (!meta.isHideTab) {
+ if (isIframe(path)) {
+ const iframeRoute = IframeRouteManager.getInstance().findByPath(to.path)
+ if (iframeRoute?.meta) {
+ worktabStore.openTab({
+ title: iframeRoute.meta.title,
+ icon: meta.icon,
+ path,
+ name,
+ keepAlive: meta.keepAlive,
+ params,
+ query
+ })
+ }
+ } else if (useSettingStore().showWorkTab || path === useCommon().homePath.value) {
+ worktabStore.openTab({
+ title: meta.title,
+ icon: meta.icon,
+ path,
+ name,
+ keepAlive: meta.keepAlive,
+ params,
+ query,
+ fixedTab: meta.fixedTab
+ })
+ }
+ }
+}
+export { setWorktab }
diff --git a/rsf-design/src/utils/router.js b/rsf-design/src/utils/router.js
new file mode 100644
index 0000000..9f8b47e
--- /dev/null
+++ b/rsf-design/src/utils/router.js
@@ -0,0 +1,34 @@
+import AppConfig from '@/config'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import i18n, { $t } from '@/locales'
+const configureNProgress = () => {
+ NProgress.configure({
+ easing: 'ease',
+ speed: 600,
+ showSpinner: false,
+ parent: 'body'
+ })
+}
+const setPageTitle = (to) => {
+ const { title } = to.meta
+ if (title) {
+ setTimeout(() => {
+ document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}`
+ }, 150)
+ }
+}
+const formatMenuTitle = (title) => {
+ if (title) {
+ if (title.startsWith('menus.')) {
+ if (i18n.global.te(title)) {
+ return $t(title)
+ } else {
+ return title.split('.').pop() || title
+ }
+ }
+ return title
+ }
+ return ''
+}
+export { configureNProgress, formatMenuTitle, setPageTitle }
diff --git a/rsf-design/src/utils/socket/index.js b/rsf-design/src/utils/socket/index.js
new file mode 100644
index 0000000..db1e3a7
--- /dev/null
+++ b/rsf-design/src/utils/socket/index.js
@@ -0,0 +1,331 @@
+const DEFAULT_LOGIN_WEBSOCKET = import.meta.env.VITE_LOGIN_WEBSOCKET || 'ws://localhost:8080/ws'
+
+class WebSocketClient {
+ constructor(options) {
+ this.ws = null
+ this.url = options.url || DEFAULT_LOGIN_WEBSOCKET
+ this.messageHandler = options.messageHandler || (() => {})
+ this.reconnectInterval = options.reconnectInterval ?? 20 * 1e3
+ this.heartbeatInterval = options.heartbeatInterval ?? 5 * 1e3
+ this.pingInterval = options.pingInterval ?? 10 * 1e3
+ this.reconnectTimeout = options.reconnectTimeout ?? 30 * 1e3
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10
+ this.connectionTimeout = options.connectionTimeout ?? 10 * 1e3
+ this.reconnectAttempts = 0
+ this.messageQueue = []
+ this.detectionTimer = null
+ this.timeoutTimer = null
+ this.reconnectTimer = null
+ this.pingTimer = null
+ this.connectionTimer = null
+ this.isConnected = false
+ this.isConnecting = false
+ this.stopReconnect = false
+ this.isReconnecting = false
+ }
+ // 鍗曚緥妯″紡鑾峰彇瀹炰緥
+ static getInstance(options) {
+ if (!WebSocketClient.instance) {
+ WebSocketClient.instance = new WebSocketClient(options)
+ } else {
+ WebSocketClient.instance.messageHandler = options.messageHandler || (() => {})
+ if (options.url && WebSocketClient.instance.url !== options.url) {
+ WebSocketClient.instance.url = options.url
+ WebSocketClient.instance.reconnectAttempts = 0
+ WebSocketClient.instance.init()
+ }
+ }
+ return WebSocketClient.instance
+ }
+ // 鍒濆鍖栬繛鎺�
+ init() {
+ this.connect(true)
+ }
+ connect(resetReconnectAttempts = false) {
+ if (this.isConnecting) {
+ console.log('姝e湪寤虹珛WebSocket杩炴帴涓�...')
+ return
+ }
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ console.warn('WebSocket杩炴帴宸插瓨鍦�')
+ this.flushMessageQueue()
+ return
+ }
+ try {
+ this.isConnecting = true
+ this.stopReconnect = false
+ if (resetReconnectAttempts) {
+ this.reconnectAttempts = 0
+ this.isReconnecting = false
+ this.clearTimer('reconnectTimer')
+ }
+ 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()
+ }
+ }
+ // 澶勭悊杩炴帴瓒呮椂
+ handleConnectionTimeout() {
+ if (this.ws?.readyState !== WebSocket.OPEN) {
+ console.error('WebSocket杩炴帴瓒呮椂锛屽己鍒跺叧闂繛鎺�')
+ this.ws?.close(1e3, 'Connection timeout')
+ this.isConnecting = false
+ this.reconnect()
+ }
+ }
+ // 鍏抽棴杩炴帴
+ close(force) {
+ this.clearAllTimers()
+ this.stopReconnect = true
+ this.isReconnecting = false
+ this.isConnecting = false
+ if (this.ws) {
+ this.ws.close(force ? 1001 : 1e3, force ? 'Force closed' : 'Normal close')
+ this.ws = null
+ }
+ this.isConnected = false
+ }
+ // 鍙戦�佹秷鎭� - 澧炲姞娑堟伅闃熷垪
+ send(data, immediate = false) {
+ 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()
+ }
+ }
+ // 鍙戦�侀槦鍒椾腑鐨勬秷鎭�
+ flushMessageQueue() {
+ 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
+ }
+ }
+ }
+ }
+ }
+ // 澶勭悊杩炴帴鎵撳紑
+ handleOpen(event) {
+ console.log('WebSocket杩炴帴鎴愬姛', event)
+ this.clearTimer('connectionTimer')
+ this.isConnected = true
+ this.isConnecting = false
+ this.isReconnecting = false
+ this.stopReconnect = false
+ this.reconnectAttempts = 0
+ this.startHeartbeat()
+ this.startPing()
+ this.flushMessageQueue()
+ }
+ // 澶勭悊鏀跺埌鐨勬秷鎭�
+ handleMessage(event) {
+ console.log('鏀跺埌WebSocket娑堟伅:', event)
+ this.resetHeartbeat()
+ this.messageHandler(event)
+ }
+ // 澶勭悊杩炴帴鍏抽棴
+ handleClose(event) {
+ console.log(
+ `WebSocket鏂紑: 浠g爜=${event.code}, 鍘熷洜=${event.reason}, 骞插噣鍏抽棴=${event.wasClean}`
+ )
+ const isNormalClose = event.code === 1e3
+ this.isConnected = false
+ this.isConnecting = false
+ this.clearConnectionTimers()
+ this.ws = null
+ if (!this.stopReconnect && !isNormalClose) {
+ this.reconnect()
+ }
+ }
+ // 澶勭悊閿欒 - 澧炲姞璇︾粏閿欒淇℃伅
+ handleError(event) {
+ 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()
+ }
+ }
+ closeCurrentSocketForReconnect() {
+ this.clearConnectionTimers()
+ this.isConnected = false
+ this.isConnecting = false
+ if (this.ws) {
+ this.ws.onopen = null
+ this.ws.onmessage = null
+ this.ws.onclose = null
+ this.ws.onerror = null
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
+ this.ws.close(1001, 'Reconnect')
+ }
+ this.ws = null
+ }
+ }
+ // 杞崲杩炴帴鐘舵�佷负鏂囨湰鎻忚堪
+ getReadyStateText(state) {
+ switch (state) {
+ case WebSocket.CONNECTING:
+ return 'CONNECTING (0) - 姝e湪杩炴帴'
+ case WebSocket.OPEN:
+ return 'OPEN (1) - 宸茶繛鎺�'
+ case WebSocket.CLOSING:
+ return 'CLOSING (2) - 姝e湪鍏抽棴'
+ case WebSocket.CLOSED:
+ return 'CLOSED (3) - 宸插叧闂�'
+ default:
+ return `鏈煡鐘舵�� (${state})`
+ }
+ }
+ // 寮�濮嬪績璺虫娴�
+ startHeartbeat() {
+ 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)
+ }
+ // 閲嶇疆蹇冭烦妫�娴�
+ resetHeartbeat() {
+ this.clearTimer('detectionTimer')
+ this.clearTimer('timeoutTimer')
+ this.startHeartbeat()
+ }
+ // 寮�濮嬪彂閫乸ing娑堟伅
+ startPing() {
+ this.clearTimer('pingTimer')
+ this.pingTimer = setInterval(() => {
+ if (this.ws?.readyState !== WebSocket.OPEN) {
+ console.warn('WebSocket鏈繛鎺ワ紝鍋滄鍙戦�乸ing')
+ this.clearTimer('pingTimer')
+ this.reconnect()
+ return
+ }
+ try {
+ this.ws.send('ping')
+ console.log('鍙戦�乸ing娑堟伅')
+ } catch (error) {
+ console.error('鍙戦�乸ing娑堟伅澶辫触:', error)
+ this.clearTimer('pingTimer')
+ this.reconnect()
+ }
+ }, this.pingInterval)
+ }
+ // 閲嶈繛 - 澧炲姞閲嶈繛娆℃暟闄愬埗
+ reconnect() {
+ if (this.stopReconnect || this.isConnecting || this.reconnectInterval <= 0) {
+ return
+ }
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ console.error(`宸茶揪鍒版渶澶ч噸杩炴鏁�(${this.maxReconnectAttempts})锛屽仠姝㈤噸杩瀈)
+ this.close(true)
+ return
+ }
+ this.reconnectAttempts++
+ this.isReconnecting = true
+ this.closeCurrentSocketForReconnect()
+ const delay = this.calculateReconnectDelay()
+ console.log(
+ `灏嗗湪${delay / 1e3}绉掑悗灏濊瘯閲嶆柊杩炴帴锛堢${this.reconnectAttempts}/${this.maxReconnectAttempts}娆★級`
+ )
+ this.clearTimer('reconnectTimer')
+ this.reconnectTimer = setTimeout(() => {
+ console.log(`灏濊瘯閲嶆柊杩炴帴WebSocket锛堢${this.reconnectAttempts}娆★級`)
+ this.connect(false)
+ }, delay)
+ }
+ // 璁$畻閲嶈繛寤惰繜 - 鎸囨暟閫�閬跨瓥鐣�
+ calculateReconnectDelay() {
+ const jitter = Math.random() * 1e3
+ const baseDelay = Math.min(
+ this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
+ this.reconnectInterval * 5
+ )
+ return baseDelay + jitter
+ }
+ // 娓呴櫎鎸囧畾瀹氭椂鍣�
+ clearTimer(timerName) {
+ if (this[timerName]) {
+ clearTimeout(this[timerName])
+ this[timerName] = null
+ }
+ }
+ // 娓呴櫎鎵�鏈夊畾鏃跺櫒
+ clearAllTimers() {
+ this.clearConnectionTimers()
+ this.clearTimer('reconnectTimer')
+ }
+ clearConnectionTimers() {
+ this.clearTimer('detectionTimer')
+ this.clearTimer('timeoutTimer')
+ this.clearTimer('pingTimer')
+ this.clearTimer('connectionTimer')
+ }
+ // 鑾峰彇褰撳墠杩炴帴鐘舵��
+ get isWebSocketConnected() {
+ return this.isConnected
+ }
+ // 鑾峰彇褰撳墠杩炴帴鐘舵�佹枃鏈�
+ get connectionStatusText() {
+ if (this.isConnecting) return '姝e湪杩炴帴'
+ if (this.isConnected) return '宸茶繛鎺�'
+ if (this.isReconnecting && this.reconnectAttempts > 0)
+ return `閲嶈繛涓紙${this.reconnectAttempts}/${this.maxReconnectAttempts}锛塦
+ return '宸叉柇寮�'
+ }
+ // 閿�姣佸疄渚�
+ static destroyInstance() {
+ if (WebSocketClient.instance) {
+ WebSocketClient.instance.close()
+ WebSocketClient.instance = null
+ }
+ }
+}
+
+WebSocketClient.instance = null
+
+export default WebSocketClient
diff --git a/rsf-design/src/utils/storage/index.js b/rsf-design/src/utils/storage/index.js
new file mode 100644
index 0000000..c0c90a9
--- /dev/null
+++ b/rsf-design/src/utils/storage/index.js
@@ -0,0 +1,3 @@
+export * from './storage'
+export * from './storage-config'
+export * from './storage-key-manager'
diff --git a/rsf-design/src/utils/storage/storage-config.js b/rsf-design/src/utils/storage/storage-config.js
new file mode 100644
index 0000000..5509cfb
--- /dev/null
+++ b/rsf-design/src/utils/storage/storage-config.js
@@ -0,0 +1,74 @@
+class StorageConfig {
+ /**
+ * 鐢熸垚鐗堟湰鍖栫殑瀛樺偍閿悕
+ * @param storeId 瀛樺偍ID
+ * @param version 鐗堟湰鍙凤紝榛樿浣跨敤褰撳墠鐗堟湰
+ */
+ static generateStorageKey(storeId, version = this.CURRENT_VERSION) {
+ return `${this.STORAGE_PREFIX}${version}-${storeId}`
+ }
+ /**
+ * 鐢熸垚鏃х増鏈殑瀛樺偍閿悕锛堜笉甯﹀垎闅旂锛�
+ * @param version 鐗堟湰鍙凤紝榛樿浣跨敤褰撳墠鐗堟湰
+ */
+ static generateLegacyKey(version = this.CURRENT_VERSION) {
+ return `${this.STORAGE_PREFIX}${version}`
+ }
+ /**
+ * 鍒涘缓瀛樺偍閿尮閰嶇殑姝e垯琛ㄨ揪寮�
+ * @param storeId 瀛樺偍ID
+ */
+ static createKeyPattern(storeId) {
+ return new RegExp(`^${this.STORAGE_PREFIX}[^-]+-${storeId}$`)
+ }
+ /**
+ * 鍒涘缓褰撳墠鐗堟湰瀛樺偍閿尮閰嶇殑姝e垯琛ㄨ揪寮�
+ */
+ static createCurrentVersionPattern() {
+ return new RegExp(`^${this.STORAGE_PREFIX}${this.CURRENT_VERSION}-`)
+ }
+ /**
+ * 鍒涘缓浠绘剰鐗堟湰瀛樺偍閿尮閰嶇殑姝e垯琛ㄨ揪寮�
+ */
+ static createVersionPattern() {
+ return new RegExp(`^${this.STORAGE_PREFIX}`)
+ }
+ /**
+ * 妫�鏌ユ槸鍚︿负褰撳墠鐗堟湰鐨勯敭
+ */
+ static isCurrentVersionKey(key) {
+ return key.startsWith(`${this.STORAGE_PREFIX}${this.CURRENT_VERSION}`)
+ }
+ /**
+ * 妫�鏌ユ槸鍚︿负鐗堟湰鍖栫殑閿�
+ */
+ static isVersionedKey(key) {
+ return key.startsWith(this.STORAGE_PREFIX)
+ }
+ /**
+ * 浠庡瓨鍌ㄩ敭涓彁鍙栫増鏈彿
+ */
+ static extractVersionFromKey(key) {
+ const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}([^-]+)`))
+ return match ? match[1] : null
+ }
+ /**
+ * 浠庡瓨鍌ㄩ敭涓彁鍙栧瓨鍌↖D
+ */
+ static extractStoreIdFromKey(key) {
+ const match = key.match(new RegExp(`^${this.STORAGE_PREFIX}[^-]+-(.+)$`))
+ return match ? match[1] : null
+ }
+}
+
+StorageConfig.CURRENT_VERSION = __APP_VERSION__
+StorageConfig.STORAGE_PREFIX = 'sys-v'
+StorageConfig.VERSION_KEY = 'sys-version'
+StorageConfig.THEME_KEY = 'sys-theme'
+StorageConfig.LAST_USER_ID_KEY = 'sys-last-user-id'
+StorageConfig.RESPONSIVE_MENU_TYPE_KEY = 'sys-responsive-menu-type'
+StorageConfig.SKIP_UPGRADE_VERSION = '1.0.0'
+StorageConfig.UPGRADE_DELAY = 1e3
+StorageConfig.LOGOUT_DELAY = 1e3
+
+export { StorageConfig }
diff --git a/rsf-design/src/utils/storage/storage-key-manager.js b/rsf-design/src/utils/storage/storage-key-manager.js
new file mode 100644
index 0000000..975989d
--- /dev/null
+++ b/rsf-design/src/utils/storage/storage-key-manager.js
@@ -0,0 +1,52 @@
+import { StorageConfig } from '@/utils/storage'
+class StorageKeyManager {
+ /**
+ * 鑾峰彇褰撳墠鐗堟湰鐨勫瓨鍌ㄩ敭鍚�
+ */
+ getCurrentVersionKey(storeId) {
+ return StorageConfig.generateStorageKey(storeId)
+ }
+ /**
+ * 妫�鏌ュ綋鍓嶇増鏈殑鏁版嵁鏄惁瀛樺湪
+ */
+ hasCurrentVersionData(key) {
+ return localStorage.getItem(key) !== null
+ }
+ /**
+ * 鏌ユ壘鍏朵粬鐗堟湰鐨勫悓鍚嶅瓨鍌ㄩ敭
+ */
+ findExistingKey(storeId) {
+ const storageKeys = Object.keys(localStorage)
+ const pattern = StorageConfig.createKeyPattern(storeId)
+ return storageKeys.find((key) => pattern.test(key) && localStorage.getItem(key)) || null
+ }
+ /**
+ * 灏嗘暟鎹粠鏃х増鏈縼绉诲埌褰撳墠鐗堟湰
+ */
+ migrateData(fromKey, toKey) {
+ 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) {
+ const currentKey = this.getCurrentVersionKey(storeId)
+ if (this.hasCurrentVersionData(currentKey)) {
+ return currentKey
+ }
+ const existingKey = this.findExistingKey(storeId)
+ if (existingKey) {
+ this.migrateData(existingKey, currentKey)
+ }
+ return currentKey
+ }
+}
+export { StorageKeyManager }
diff --git a/rsf-design/src/utils/storage/storage.js b/rsf-design/src/utils/storage/storage.js
new file mode 100644
index 0000000..ae2224a
--- /dev/null
+++ b/rsf-design/src/utils/storage/storage.js
@@ -0,0 +1,160 @@
+import { router } from '@/router'
+import { useUserStore } from '@/store/modules/user'
+import { StorageConfig } from '@/utils/storage/storage-config'
+class StorageCompatibilityManager {
+ /**
+ * 鑾峰彇绯荤粺鐗堟湰鍙�
+ */
+ getSystemVersion() {
+ return localStorage.getItem(StorageConfig.VERSION_KEY)
+ }
+ /**
+ * 鑾峰彇绯荤粺瀛樺偍鏁版嵁锛堝吋瀹规棫鏍煎紡锛�
+ */
+ getSystemStorage() {
+ const version = this.getSystemVersion() || StorageConfig.CURRENT_VERSION
+ const legacyKey = StorageConfig.generateLegacyKey(version)
+ const data = localStorage.getItem(legacyKey)
+ return data ? JSON.parse(data) : null
+ }
+ /**
+ * 妫�鏌ュ綋鍓嶇増鏈槸鍚︽湁瀛樺偍鏁版嵁
+ */
+ hasCurrentVersionStorage() {
+ const storageKeys = Object.keys(localStorage)
+ const currentVersionPattern = StorageConfig.createCurrentVersionPattern()
+ return storageKeys.some(
+ (key) => currentVersionPattern.test(key) && localStorage.getItem(key) !== null
+ )
+ }
+ /**
+ * 妫�鏌ユ槸鍚﹀瓨鍦ㄤ换浣曠増鏈殑瀛樺偍鏁版嵁
+ */
+ hasAnyVersionStorage() {
+ const storageKeys = Object.keys(localStorage)
+ const versionPattern = StorageConfig.createVersionPattern()
+ return storageKeys.some((key) => versionPattern.test(key) && localStorage.getItem(key) !== null)
+ }
+ /**
+ * 鑾峰彇鏃ф牸寮忕殑鏈湴瀛樺偍鏁版嵁
+ */
+ getLegacyStorageData() {
+ try {
+ const systemStorage = this.getSystemStorage()
+ return systemStorage || {}
+ } catch (error) {
+ console.warn('[Storage] 瑙f瀽鏃ф牸寮忓瓨鍌ㄦ暟鎹け璐�:', error)
+ return {}
+ }
+ }
+ /**
+ * 鏄剧ず瀛樺偍閿欒娑堟伅
+ */
+ showStorageError() {
+ ElMessage({
+ type: 'error',
+ offset: 40,
+ duration: 5e3,
+ message: '绯荤粺妫�娴嬪埌鏈湴鏁版嵁寮傚父锛岃閲嶆柊鐧诲綍绯荤粺鎭㈠浣跨敤锛�'
+ })
+ }
+ /**
+ * 鎵ц绯荤粺鐧诲嚭
+ */
+ performSystemLogout() {
+ setTimeout(() => {
+ try {
+ localStorage.clear()
+ useUserStore().logOut()
+ router.push({ name: 'Login' })
+ console.info('[Storage] 宸叉墽琛岀郴缁熺櫥鍑�')
+ } catch (error) {
+ console.error('[Storage] 绯荤粺鐧诲嚭澶辫触:', error)
+ }
+ }, StorageConfig.LOGOUT_DELAY)
+ }
+ /**
+ * 澶勭悊瀛樺偍寮傚父
+ */
+ handleStorageError() {
+ this.showStorageError()
+ this.performSystemLogout()
+ }
+ /**
+ * 楠岃瘉瀛樺偍鏁版嵁瀹屾暣鎬�
+ * @param requireAuth 鏄惁闇�瑕侀獙璇佺櫥褰曠姸鎬侊紙榛樿 false锛�
+ */
+ validateStorageData(requireAuth = false) {
+ try {
+ if (this.hasCurrentVersionStorage()) {
+ return true
+ }
+ if (this.hasAnyVersionStorage()) {
+ return true
+ }
+ const legacyData = this.getLegacyStorageData()
+ if (Object.keys(legacyData).length === 0) {
+ if (requireAuth) {
+ console.warn('[Storage] 鏈彂鐜颁换浣曞瓨鍌ㄦ暟鎹紝闇�瑕侀噸鏂扮櫥褰�')
+ this.performSystemLogout()
+ return false
+ }
+ return true
+ }
+ console.debug('[Storage] 鍙戠幇鏃х増鏈瓨鍌ㄦ暟鎹�')
+ return true
+ } catch (error) {
+ console.error('[Storage] 瀛樺偍鏁版嵁楠岃瘉澶辫触:', error)
+ if (requireAuth) {
+ this.handleStorageError()
+ return false
+ }
+ return true
+ }
+ }
+ /**
+ * 妫�鏌ュ瓨鍌ㄦ槸鍚︿负绌�
+ */
+ isStorageEmpty() {
+ 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 = false) {
+ try {
+ const isValid = this.validateStorageData(requireAuth)
+ const isEmpty = this.isStorageEmpty()
+ if (isValid || isEmpty) {
+ return true
+ }
+ console.warn('[Storage] 瀛樺偍鍏煎鎬ф鏌ュけ璐�')
+ return false
+ } catch (error) {
+ console.error('[Storage] 鍏煎鎬ф鏌ュ紓甯�:', error)
+ return false
+ }
+ }
+}
+const storageManager = new StorageCompatibilityManager()
+function getSystemStorage() {
+ return storageManager.getSystemStorage()
+}
+function getSysVersion() {
+ return storageManager.getSystemVersion()
+}
+function validateStorageData(requireAuth = false) {
+ return storageManager.validateStorageData(requireAuth)
+}
+function checkStorageCompatibility(requireAuth = false) {
+ return storageManager.checkCompatibility(requireAuth)
+}
+export { checkStorageCompatibility, getSysVersion, getSystemStorage, validateStorageData }
diff --git a/rsf-design/src/utils/sys/console.js b/rsf-design/src/utils/sys/console.js
new file mode 100644
index 0000000..7ea5e98
--- /dev/null
+++ b/rsf-design/src/utils/sys/console.js
@@ -0,0 +1,11 @@
+const asciiArt = `
+\x1B[32m娆㈣繋浣跨敤 Art Design Pro锛�
+\x1B[0m
+\x1B[36m鍝囷紒浣犲眳鐒跺湪鐢ㄦ垜鐨勯」鐩綖 濂界敤鐨勮瘽鍒繕浜嗗幓 GitHub 鐐逛釜 鈽匰tar 鍛�锛屼綘鐨勬敮鎸佸氨鏄垜鏇存柊鐨勮秴寮哄姩鍔涳紒绁濅娇鐢ㄤ綋楠屾弧鍒嗮煉�
+\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/rsf-design/src/utils/sys/error-handle.js b/rsf-design/src/utils/sys/error-handle.js
new file mode 100644
index 0000000..8c8c0bc
--- /dev/null
+++ b/rsf-design/src/utils/sys/error-handle.js
@@ -0,0 +1,73 @@
+const IGNORABLE_SCRIPT_ERRORS = [
+ 'ResizeObserver loop completed with undelivered notifications.',
+ 'ResizeObserver loop limit exceeded'
+]
+function normalizeErrorMessage(message) {
+ if (typeof message === 'string') {
+ return message
+ }
+ if ('message' in message && typeof message.message === 'string') {
+ return message.message
+ }
+ return ''
+}
+function isIgnorableScriptError(message, source) {
+ const normalizedMessage = normalizeErrorMessage(message)
+ if (!normalizedMessage) {
+ return false
+ }
+ if (IGNORABLE_SCRIPT_ERRORS.some((item) => normalizedMessage.includes(item))) {
+ return true
+ }
+ if (normalizedMessage === 'Script error.' && source === '') {
+ return true
+ }
+ return false
+}
+function vueErrorHandler(err, instance, info) {
+ console.error('[VueError]', err, info, instance)
+}
+function scriptErrorHandler(message, source, lineno, colno, error) {
+ if (isIgnorableScriptError(message, source)) {
+ return true
+ }
+ console.error('[ScriptError]', { message, source, lineno, colno, error })
+ return true
+}
+function registerPromiseErrorHandler() {
+ window.addEventListener('unhandledrejection', (event) => {
+ console.error('[PromiseError]', event.reason)
+ })
+}
+function registerResourceErrorHandler() {
+ window.addEventListener(
+ 'error',
+ (event) => {
+ const target = event.target
+ if (
+ target &&
+ (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')
+ ) {
+ console.error('[ResourceError]', {
+ tagName: target.tagName,
+ src: target.src || target.src || target.href
+ })
+ }
+ },
+ true
+ // 鎹曡幏闃舵鎵嶈兘鐩戝惉鍒拌祫婧愰敊璇�
+ )
+}
+function setupErrorHandle(app) {
+ app.config.errorHandler = vueErrorHandler
+ window.onerror = scriptErrorHandler
+ registerPromiseErrorHandler()
+ registerResourceErrorHandler()
+}
+export {
+ registerPromiseErrorHandler,
+ registerResourceErrorHandler,
+ scriptErrorHandler,
+ setupErrorHandle,
+ vueErrorHandler
+}
diff --git a/rsf-design/src/utils/sys/index.js b/rsf-design/src/utils/sys/index.js
new file mode 100644
index 0000000..01b82cc
--- /dev/null
+++ b/rsf-design/src/utils/sys/index.js
@@ -0,0 +1,3 @@
+export * from './upgrade'
+import { default as default2 } from './mittBus'
+export { default2 as mittBus }
diff --git a/rsf-design/src/utils/sys/mittBus.js b/rsf-design/src/utils/sys/mittBus.js
new file mode 100644
index 0000000..8d29026
--- /dev/null
+++ b/rsf-design/src/utils/sys/mittBus.js
@@ -0,0 +1,4 @@
+import mitt from 'mitt'
+const mittBus = mitt()
+
+export default mittBus
diff --git a/rsf-design/src/utils/sys/upgrade.js b/rsf-design/src/utils/sys/upgrade.js
new file mode 100644
index 0000000..2f6631f
--- /dev/null
+++ b/rsf-design/src/utils/sys/upgrade.js
@@ -0,0 +1,181 @@
+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'
+ */
+ normalizeVersion(version) {
+ return version.replace(/^v/, '')
+ }
+ /**
+ * 鑾峰彇瀛樺偍鐨勭増鏈彿
+ */
+ getStoredVersion() {
+ return localStorage.getItem(StorageConfig.VERSION_KEY)
+ }
+ /**
+ * 璁剧疆鐗堟湰鍙峰埌瀛樺偍
+ */
+ setStoredVersion(version) {
+ localStorage.setItem(StorageConfig.VERSION_KEY, version)
+ }
+ /**
+ * 妫�鏌ユ槸鍚﹀簲璇ヨ烦杩囧崌绾у鐞�
+ */
+ shouldSkipUpgrade() {
+ return StorageConfig.CURRENT_VERSION === StorageConfig.SKIP_UPGRADE_VERSION
+ }
+ /**
+ * 妫�鏌ユ槸鍚︿负棣栨璁块棶
+ */
+ isFirstVisit(storedVersion) {
+ return !storedVersion
+ }
+ /**
+ * 妫�鏌ョ増鏈槸鍚︾浉鍚�
+ */
+ isSameVersion(storedVersion) {
+ return storedVersion === StorageConfig.CURRENT_VERSION
+ }
+ /**
+ * 鏌ユ壘鏃х殑瀛樺偍缁撴瀯
+ */
+ findLegacyStorage() {
+ 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 }
+ }
+ /**
+ * 妫�鏌ユ槸鍚﹂渶瑕侀噸鏂扮櫥褰�
+ */
+ shouldRequireReLogin(storedVersion) {
+ 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
+ )
+ })
+ }
+ /**
+ * 鏋勫缓鍗囩骇閫氱煡娑堟伅
+ */
+ buildUpgradeMessage(requireReLogin) {
+ const { title: content } = upgradeLogList.value[0]
+ const messageParts = [
+ `<p style="color: var(--art-gray-800) !important; padding-bottom: 5px;">`,
+ `绯荤粺宸插崌绾у埌 ${StorageConfig.CURRENT_VERSION} 鐗堟湰锛屾娆℃洿鏂板甫鏉ヤ簡浠ヤ笅鏀硅繘锛歚,
+ `</p>`,
+ content
+ ]
+ if (requireReLogin) {
+ messageParts.push(
+ `<p style="color: var(--theme-color); padding-top: 5px;">鍗囩骇瀹屾垚锛岃閲嶆柊鐧诲綍鍚庣户缁娇鐢ㄣ��</p>`
+ )
+ }
+ return messageParts.join('')
+ }
+ /**
+ * 鏄剧ず鍗囩骇閫氱煡
+ */
+ showUpgradeNotification(message) {
+ ElNotification({
+ title: '绯荤粺鍗囩骇鍏憡',
+ message,
+ duration: 0,
+ type: 'success',
+ dangerouslyUseHTMLString: true
+ })
+ }
+ /**
+ * 娓呯悊鏃х増鏈暟鎹�
+ */
+ cleanupLegacyData(oldSysKey, oldVersionKeys) {
+ if (oldSysKey) {
+ localStorage.removeItem(oldSysKey)
+ console.info(`[Upgrade] 宸叉竻鐞嗘棫瀛樺偍: ${oldSysKey}`)
+ }
+ oldVersionKeys.forEach((key) => {
+ localStorage.removeItem(key)
+ console.info(`[Upgrade] 宸叉竻鐞嗘棫瀛樺偍: ${key}`)
+ })
+ }
+ /**
+ * 鎵ц鍗囩骇鍚庣殑鐧诲嚭鎿嶄綔
+ */
+ performLogout() {
+ try {
+ useUserStore().logOut()
+ console.info('[Upgrade] 宸叉墽琛屽崌绾у悗鐧诲嚭')
+ } catch (error) {
+ console.error('[Upgrade] 鍗囩骇鍚庣櫥鍑哄け璐�:', error)
+ }
+ }
+ /**
+ * 鎵ц鍗囩骇娴佺▼
+ */
+ async executeUpgrade(storedVersion, legacyStorage) {
+ 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() {
+ if (this.shouldSkipUpgrade()) {
+ console.debug('[Upgrade] 璺宠繃鐗堟湰鍗囩骇妫�鏌�')
+ return
+ }
+ const storedVersion = this.getStoredVersion()
+ if (this.isFirstVisit(storedVersion)) {
+ this.setStoredVersion(StorageConfig.CURRENT_VERSION)
+ return
+ }
+ if (this.isSameVersion(storedVersion)) {
+ 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()
+async function systemUpgrade() {
+ await versionManager.processUpgrade()
+}
+export { systemUpgrade }
diff --git a/rsf-design/src/utils/table/tableCache.js b/rsf-design/src/utils/table/tableCache.js
new file mode 100644
index 0000000..1ff1e37
--- /dev/null
+++ b/rsf-design/src/utils/table/tableCache.js
@@ -0,0 +1,154 @@
+import { hash } from 'ohash'
+const CacheInvalidationStrategy = Object.freeze({
+ CLEAR_ALL: 'clear_all',
+ CLEAR_CURRENT: 'clear_current',
+ CLEAR_PAGINATION: 'clear_pagination',
+ KEEP_ALL: 'keep_all'
+})
+
+class TableCache {
+ constructor(cacheTime = 5 * 60 * 1e3, maxSize = 50, enableLog = false) {
+ this.cache = new Map()
+ this.cacheTime = cacheTime
+ this.maxSize = maxSize
+ this.enableLog = enableLog
+ }
+ // 鍐呴儴鏃ュ織宸ュ叿
+ log(message, ...args) {
+ if (this.enableLog) {
+ console.log(`[TableCache] ${message}`, ...args)
+ }
+ }
+ // 鐢熸垚绋冲畾鐨勭紦瀛橀敭
+ generateKey(params) {
+ return hash(params)
+ }
+ // 馃敡 浼樺寲锛氬寮虹被鍨嬪畨鍏ㄦ��
+ generateTags(params) {
+ const tags = /* @__PURE__ */ new Set()
+ const searchKeys = Object.keys(params).filter(
+ (key) =>
+ !['current', 'size', 'total'].includes(key) &&
+ params[key] !== void 0 &&
+ 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
+ }
+ // 馃敡 浼樺寲锛歀RU 缂撳瓨娓呯悊
+ evictLRU() {
+ 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, data, response) {
+ const key = this.generateKey(params)
+ const tags = this.generateTags(params)
+ const now = Date.now()
+ this.evictLRU()
+ this.cache.set(key, {
+ data,
+ response,
+ timestamp: now,
+ params: key,
+ tags,
+ accessCount: 1,
+ lastAccessTime: now
+ })
+ }
+ // 鑾峰彇缂撳瓨
+ get(params) {
+ 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) {
+ 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) {
+ const key = this.generateKey(params)
+ const deleted = this.cache.delete(key)
+ return deleted ? 1 : 0
+ }
+ // 娓呴櫎鍒嗛〉缂撳瓨
+ clearPagination() {
+ return this.clearByTags(['pagination'])
+ }
+ // 娓呯┖鎵�鏈夌紦瀛�
+ clear() {
+ this.cache.clear()
+ }
+ // 鑾峰彇缂撳瓨缁熻淇℃伅
+ getStats() {
+ const total = this.cache.size
+ let totalSize = 0
+ let totalAccess = 0
+ for (const item of this.cache.values()) {
+ 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() {
+ 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
+ }
+}
+export { CacheInvalidationStrategy, TableCache }
diff --git a/rsf-design/src/utils/table/tableConfig.js b/rsf-design/src/utils/table/tableConfig.js
new file mode 100644
index 0000000..404942a
--- /dev/null
+++ b/rsf-design/src/utils/table/tableConfig.js
@@ -0,0 +1,20 @@
+const tableConfig = {
+ // 鍝嶅簲鏁版嵁瀛楁鏄犲皠閰嶇疆锛岀郴缁熶細浠庢帴鍙h繑鍥炴暟鎹腑鎸夐『搴忔煡鎵捐繖浜涘瓧娈�
+ // 鍒楄〃鏁版嵁
+ recordFields: ['list', 'data', 'records', 'items', 'result', 'rows'],
+ // 鎬绘潯鏁�
+ totalFields: ['total', 'count'],
+ // 褰撳墠椤电爜
+ currentFields: ['current', 'page', 'pageNum'],
+ // 姣忛〉澶у皬
+ sizeFields: ['size', 'pageSize', 'limit'],
+ // 璇锋眰鍙傛暟鏄犲皠閰嶇疆锛屽墠绔彂閫佽姹傛椂浣跨敤鐨勫垎椤靛弬鏁板悕
+ // useTable 缁勫悎寮忓嚱鏁颁紶閫掑垎椤靛弬鏁扮殑鏃跺�� 鐢� current 璺� size
+ paginationKey: {
+ // 褰撳墠椤电爜
+ current: 'current',
+ // 姣忛〉澶у皬
+ size: 'size'
+ }
+}
+export { tableConfig }
diff --git a/rsf-design/src/utils/table/tableUtils.js b/rsf-design/src/utils/table/tableUtils.js
new file mode 100644
index 0000000..70efc13
--- /dev/null
+++ b/rsf-design/src/utils/table/tableUtils.js
@@ -0,0 +1,188 @@
+import { tableConfig } from './tableConfig'
+function extractRecords(obj, fields) {
+ for (const field of fields) {
+ if (field in obj && Array.isArray(obj[field])) {
+ return obj[field]
+ }
+ }
+ return []
+}
+function extractTotal(obj, records, fields) {
+ for (const field of fields) {
+ if (field in obj && typeof obj[field] === 'number') {
+ return obj[field]
+ }
+ }
+ return records.length
+}
+function extractPagination(obj, data) {
+ const result = {}
+ 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]
+ break
+ }
+ }
+ if (result.current !== void 0) 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]
+ break
+ }
+ }
+ if (result.size !== void 0) break
+ }
+ if (result.current === void 0 && result.size === void 0) return void 0
+ return result
+}
+const defaultResponseAdapter = (response) => {
+ 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('/') +
+ '瀛楁鐨勫璞°�佸祵濂梔ata瀵硅薄銆傚綋鍓嶆牸寮�:',
+ response
+ )
+ return { records: [], total: 0 }
+ }
+ const res = response
+ let records = []
+ let total = 0
+ let pagination
+ records = extractRecords(res, recordFields)
+ total = extractTotal(res, records, tableConfig.totalFields)
+ pagination = extractPagination(res)
+ if (records.length === 0 && 'data' in res && typeof res.data === 'object') {
+ const data = res.data
+ records = extractRecords(data, ['list', 'records', 'items'])
+ total = extractTotal(data, records, tableConfig.totalFields)
+ pagination = extractPagination(res, data)
+ if (Array.isArray(res.data)) {
+ records = res.data
+ 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 = { records, total }
+ if (pagination) {
+ Object.assign(result, pagination)
+ }
+ return result
+}
+const extractTableData = (response) => {
+ const data = response.records || response.data || []
+ return Array.isArray(data) ? data : []
+}
+const updatePaginationFromResponse = (pagination, response) => {
+ pagination.total = response.total ?? pagination.total ?? 0
+ if (response.current !== void 0) {
+ pagination.current = response.current
+ }
+ const maxPage = Math.max(1, Math.ceil(pagination.total / (pagination.size || 1)))
+ if (pagination.current > maxPage) {
+ pagination.current = maxPage
+ }
+}
+const createSmartDebounce = (fn, delay) => {
+ let timeoutId = null
+ let lastArgs = null
+ let lastResolve = null
+ let lastReject = null
+ const debouncedFn = (...args) => {
+ 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
+}
+const createErrorHandler = (onError, enableLog = false) => {
+ const logger = {
+ error: (message, ...args) => {
+ if (enableLog) console.error(`[useTable] ${message}`, ...args)
+ }
+ }
+ return (err, context) => {
+ const 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
+ }
+}
+export {
+ createErrorHandler,
+ createSmartDebounce,
+ defaultResponseAdapter,
+ extractTableData,
+ updatePaginationFromResponse
+}
diff --git a/rsf-design/src/utils/ui/animation.js b/rsf-design/src/utils/ui/animation.js
new file mode 100644
index 0000000..c743086
--- /dev/null
+++ b/rsf-design/src/utils/ui/animation.js
@@ -0,0 +1,33 @@
+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
+const themeAnimation = (e) => {
+ const x = e.clientX
+ const y = e.clientY
+ const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
+ 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()
+}
+const toggleTransition = (enable) => {
+ const body = document.body
+ if (enable) {
+ body.classList.add('theme-change')
+ } else {
+ setTimeout(() => {
+ body.classList.remove('theme-change')
+ }, 300)
+ }
+}
+export { themeAnimation, toggleTransition }
diff --git a/rsf-design/src/utils/ui/colors.js b/rsf-design/src/utils/ui/colors.js
new file mode 100644
index 0000000..681647a
--- /dev/null
+++ b/rsf-design/src/utils/ui/colors.js
@@ -0,0 +1,125 @@
+import { useSettingStore } from '@/store/modules/setting'
+function getCssVar(name) {
+ return getComputedStyle(document.documentElement).getPropertyValue(name)
+}
+function isValidHexColor(hex) {
+ const cleanHex = hex.trim().replace(/^#/, '')
+ return /^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex)
+}
+function isValidRgbValue(r, g, b) {
+ const isValid = (value) => Number.isInteger(value) && value >= 0 && value <= 255
+ return isValid(r) && isValid(g) && isValid(b)
+}
+function hexToRgba(hex, opacity) {
+ if (!isValidHexColor(hex)) {
+ throw new Error('Invalid hex color format')
+ }
+ let cleanHex = hex.trim().replace(/^#/, '').toUpperCase()
+ if (cleanHex.length === 3) {
+ cleanHex = cleanHex
+ .split('')
+ .map((char) => char.repeat(2))
+ .join('')
+ }
+ const [red, green, blue] = cleanHex.match(/\w\w/g).map((x) => parseInt(x, 16))
+ const validOpacity = Math.max(0, Math.min(1, opacity))
+ const rgba = `rgba(${red}, ${green}, ${blue}, ${validOpacity.toFixed(2)})`
+ return { red, green, blue, rgba }
+}
+function hexToRgb(hexColor) {
+ if (!isValidHexColor(hexColor)) {
+ ElMessage.warning('杈撳叆閿欒鐨刪ex棰滆壊鍊�')
+ 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))
+}
+function rgbToHex(r, g, b) {
+ if (!isValidRgbValue(r, g, b)) {
+ ElMessage.warning('杈撳叆閿欒鐨凴GB棰滆壊鍊�')
+ throw new Error('Invalid RGB color values')
+ }
+ const toHex = (value) => {
+ const hex = value.toString(16)
+ return hex.length === 1 ? `0${hex}` : hex
+ }
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
+}
+function colourBlend(color1, color2, ratio) {
+ 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])
+}
+function getLightColor(color, level, isDark = false) {
+ if (!isValidHexColor(color)) {
+ ElMessage.warning('杈撳叆閿欒鐨刪ex棰滆壊鍊�')
+ 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])
+}
+function getDarkColor(color, level) {
+ if (!isValidHexColor(color)) {
+ ElMessage.warning('杈撳叆閿欒鐨刪ex棰滆壊鍊�')
+ 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])
+}
+function handleElementThemeColor(theme, isDark = false) {
+ 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)
+ )
+ }
+}
+function setElementThemeColor(color) {
+ 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)
+ }
+}
+export {
+ colourBlend,
+ getCssVar,
+ getDarkColor,
+ getLightColor,
+ handleElementThemeColor,
+ hexToRgb,
+ hexToRgba,
+ rgbToHex,
+ setElementThemeColor
+}
diff --git a/rsf-design/src/utils/ui/emojo.js b/rsf-design/src/utils/ui/emojo.js
new file mode 100644
index 0000000..e6e0391
--- /dev/null
+++ b/rsf-design/src/utils/ui/emojo.js
@@ -0,0 +1,12 @@
+const EmojiText = {
+ 0: 'O_O',
+ // 绌�
+ 200: '^_^',
+ // 鎴愬姛
+ 400: 'T_T',
+ // 閿欒璇锋眰
+ 500: 'X_X'
+ // 鏈嶅姟鍣ㄥ唴閮ㄩ敊璇紝鏃犳硶瀹屾垚璇锋眰
+}
+
+export default EmojiText
diff --git a/rsf-design/src/utils/ui/iconify-loader.js b/rsf-design/src/utils/ui/iconify-loader.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/rsf-design/src/utils/ui/iconify-loader.js
diff --git a/rsf-design/src/utils/ui/index.js b/rsf-design/src/utils/ui/index.js
new file mode 100644
index 0000000..b1f2810
--- /dev/null
+++ b/rsf-design/src/utils/ui/index.js
@@ -0,0 +1,4 @@
+export * from './colors'
+export * from './loading'
+export * from './tabs'
+export * from './emojo'
diff --git a/rsf-design/src/utils/ui/loading.js b/rsf-design/src/utils/ui/loading.js
new file mode 100644
index 0000000..85932db
--- /dev/null
+++ b/rsf-design/src/utils/ui/loading.js
@@ -0,0 +1,41 @@
+import { fourDotsSpinnerSvg } from '@/assets/svg/loading'
+const getLoadingBackground = () => {
+ 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'
+}
+let loadingInstance = null
+const loadingService = {
+ /**
+ * 鏄剧ず loading
+ * @returns 鍏抽棴 loading 鐨勫嚱鏁�
+ */
+ showLoading() {
+ if (!loadingInstance) {
+ const config = {
+ ...DEFAULT_LOADING_CONFIG,
+ background: getLoadingBackground()
+ }
+ loadingInstance = ElLoading.service(config)
+ }
+ return () => this.hideLoading()
+ },
+ /**
+ * 闅愯棌 loading
+ */
+ hideLoading() {
+ if (loadingInstance) {
+ loadingInstance.close()
+ loadingInstance = null
+ }
+ }
+}
+export { loadingService }
diff --git a/rsf-design/src/utils/ui/tabs.js b/rsf-design/src/utils/ui/tabs.js
new file mode 100644
index 0000000..7f8741e
--- /dev/null
+++ b/rsf-design/src/utils/ui/tabs.js
@@ -0,0 +1,24 @@
+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
+ }
+}
+const getTabConfig = (style) => {
+ return TAB_CONFIG[style] || TAB_CONFIG['tab-card']
+}
+export { TAB_CONFIG, getTabConfig }
diff --git a/rsf-design/src/views/auth/forget-password/index.vue b/rsf-design/src/views/auth/forget-password/index.vue
new file mode 100644
index 0000000..6ebf118
--- /dev/null
+++ b/rsf-design/src/views/auth/forget-password/index.vue
@@ -0,0 +1,58 @@
+<template>
+ <div class="flex w-full h-screen">
+ <LoginLeftView />
+
+ <div class="relative flex-1">
+ <AuthTopBar />
+
+ <div class="auth-right-wrap">
+ <div class="form">
+ <h3 class="title">{{ $t('forgetPassword.title') }}</h3>
+ <p class="sub-title">{{ $t('forgetPassword.subTitle') }}</p>
+ <div class="mt-5">
+ <span class="input-label" v-if="showInputLabel">璐﹀彿</span>
+ <ElInput
+ class="custom-height"
+ :placeholder="$t('forgetPassword.placeholder')"
+ v-model.trim="username"
+ />
+ </div>
+
+ <div style="margin-top: 15px">
+ <ElButton
+ class="w-full custom-height"
+ type="primary"
+ @click="register"
+ :loading="loading"
+ v-ripple
+ >
+ {{ $t('forgetPassword.submitBtnText') }}
+ </ElButton>
+ </div>
+
+ <div style="margin-top: 15px">
+ <ElButton class="w-full custom-height" plain @click="toLogin">
+ {{ $t('forgetPassword.backBtnText') }}
+ </ElButton>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ForgetPassword' })
+ const router = useRouter()
+ const showInputLabel = ref(false)
+ const username = ref('')
+ const loading = ref(false)
+ const register = async () => {}
+ const toLogin = () => {
+ router.push({ name: 'Login' })
+ }
+</script>
+
+<style scoped>
+ @import '../login/style.css';
+</style>
diff --git a/rsf-design/src/views/auth/login/index.vue b/rsf-design/src/views/auth/login/index.vue
new file mode 100644
index 0000000..f3e4a2f
--- /dev/null
+++ b/rsf-design/src/views/auth/login/index.vue
@@ -0,0 +1,233 @@
+<!-- 鐧诲綍椤甸潰 -->
+<template>
+ <div class="flex w-full h-screen">
+ <LoginLeftView />
+
+ <div class="relative flex-1">
+ <AuthTopBar />
+
+ <div class="auth-right-wrap">
+ <div class="form">
+ <h3 class="title">{{ $t('login.title') }}</h3>
+ <p class="sub-title">{{ $t('login.subTitle') }}</p>
+ <ElForm
+ ref="formRef"
+ :model="formData"
+ :rules="rules"
+ :key="formKey"
+ @keyup.enter="handleSubmit"
+ style="margin-top: 25px"
+ >
+ <ElFormItem prop="account">
+ <ElSelect v-model="formData.account" @change="setupAccount">
+ <ElOption
+ v-for="account in accounts"
+ :key="account.key"
+ :label="account.label"
+ :value="account.key"
+ >
+ <span>{{ account.label }}</span>
+ </ElOption>
+ </ElSelect>
+ </ElFormItem>
+ <ElFormItem prop="username">
+ <ElInput
+ class="custom-height"
+ :placeholder="$t('login.placeholder.username')"
+ v-model.trim="formData.username"
+ />
+ </ElFormItem>
+ <ElFormItem prop="password">
+ <ElInput
+ class="custom-height"
+ :placeholder="$t('login.placeholder.password')"
+ v-model.trim="formData.password"
+ type="password"
+ autocomplete="off"
+ show-password
+ />
+ </ElFormItem>
+
+ <!-- 鎺ㄦ嫿楠岃瘉 -->
+ <div class="relative pb-5 mt-6">
+ <div
+ class="relative z-[2] overflow-hidden select-none rounded-lg border border-transparent tad-300"
+ :class="{ '!border-[#FF4E4F]': !isPassing && isClickPass }"
+ >
+ <ArtDragVerify
+ ref="dragVerify"
+ v-model:value="isPassing"
+ :text="$t('login.sliderText')"
+ textColor="var(--art-gray-700)"
+ :successText="$t('login.sliderSuccessText')"
+ progressBarBg="var(--main-color)"
+ :background="isDark ? '#26272F' : '#F1F1F4'"
+ handlerBg="var(--default-box-color)"
+ />
+ </div>
+ <p
+ class="absolute top-0 z-[1] px-px mt-2 text-xs text-[#f56c6c] tad-300"
+ :class="{ 'translate-y-10': !isPassing && isClickPass }"
+ >
+ {{ $t('login.placeholder.slider') }}
+ </p>
+ </div>
+
+ <div class="flex-cb mt-2 text-sm">
+ <ElCheckbox v-model="formData.rememberPassword">{{
+ $t('login.rememberPwd')
+ }}</ElCheckbox>
+ <RouterLink class="text-theme" :to="{ name: 'ForgetPassword' }">{{
+ $t('login.forgetPwd')
+ }}</RouterLink>
+ </div>
+
+ <div style="margin-top: 30px">
+ <ElButton
+ class="w-full custom-height"
+ type="primary"
+ @click="handleSubmit"
+ :loading="loading"
+ v-ripple
+ >
+ {{ $t('login.btnText') }}
+ </ElButton>
+ </div>
+
+ <div class="mt-5 text-sm text-gray-600">
+ <span>{{ $t('login.noAccount') }}</span>
+ <RouterLink class="text-theme" :to="{ name: 'Register' }">{{
+ $t('login.register')
+ }}</RouterLink>
+ </div>
+ </ElForm>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import AppConfig from '@/config'
+ import { useUserStore } from '@/store/modules/user'
+ import { useI18n } from 'vue-i18n'
+ import { HttpError } from '@/utils/http/error'
+ import { fetchLogin } from '@/api/auth'
+ import { ElNotification } from 'element-plus'
+ import { useSettingStore } from '@/store/modules/setting'
+ defineOptions({ name: 'Login' })
+ const settingStore = useSettingStore()
+ const { isDark } = storeToRefs(settingStore)
+ const { t, locale } = useI18n()
+ const formKey = ref(0)
+ watch(locale, () => {
+ formKey.value++
+ })
+ const accounts = computed(() => [
+ {
+ key: 'super',
+ label: t('login.roles.super'),
+ userName: 'Super',
+ password: '123456',
+ roles: ['R_SUPER']
+ },
+ {
+ key: 'admin',
+ label: t('login.roles.admin'),
+ userName: 'Admin',
+ password: '123456',
+ roles: ['R_ADMIN']
+ },
+ {
+ key: 'user',
+ label: t('login.roles.user'),
+ userName: 'User',
+ password: '123456',
+ roles: ['R_USER']
+ }
+ ])
+ const dragVerify = ref()
+ const userStore = useUserStore()
+ const router = useRouter()
+ const route = useRoute()
+ const isPassing = ref(false)
+ const isClickPass = ref(false)
+ const systemName = AppConfig.systemInfo.name
+ const formRef = ref()
+ const formData = reactive({
+ account: '',
+ username: '',
+ password: '',
+ rememberPassword: true
+ })
+ const rules = computed(() => ({
+ username: [{ required: true, message: t('login.placeholder.username'), trigger: 'blur' }],
+ password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }]
+ }))
+ const loading = ref(false)
+ onMounted(() => {
+ setupAccount('super')
+ })
+ const setupAccount = (key) => {
+ const selectedAccount = accounts.value.find((account) => account.key === key)
+ formData.account = key
+ formData.username = selectedAccount?.userName ?? ''
+ formData.password = selectedAccount?.password ?? ''
+ }
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ if (!isPassing.value) {
+ isClickPass.value = true
+ return
+ }
+ loading.value = true
+ const { username, password } = formData
+ const { token, refreshToken } = await fetchLogin({
+ userName: username,
+ password
+ })
+ if (!token) {
+ throw new Error('Login failed - no token received')
+ }
+ userStore.setToken(token, refreshToken)
+ userStore.setLoginStatus(true)
+ showLoginSuccessNotice()
+ const redirect = route.query.redirect
+ router.push(redirect || '/')
+ } catch (error) {
+ if (!(error instanceof HttpError)) {
+ console.error('[Login] Unexpected error:', error)
+ }
+ } finally {
+ loading.value = false
+ resetDragVerify()
+ }
+ }
+ const resetDragVerify = () => {
+ dragVerify.value.reset()
+ }
+ const showLoginSuccessNotice = () => {
+ setTimeout(() => {
+ ElNotification({
+ title: t('login.success.title'),
+ type: 'success',
+ duration: 2500,
+ zIndex: 1e4,
+ message: `${t('login.success.message')}, ${systemName}!`
+ })
+ }, 1e3)
+ }
+</script>
+
+<style scoped>
+ @import './style.css';
+</style>
+
+<style lang="scss" scoped>
+ :deep(.el-select__wrapper) {
+ height: 40px !important;
+ }
+</style>
diff --git a/rsf-design/src/views/auth/login/style.css b/rsf-design/src/views/auth/login/style.css
new file mode 100644
index 0000000..bd8c3a9
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/views/auth/register/index.vue b/rsf-design/src/views/auth/register/index.vue
new file mode 100644
index 0000000..cd9208f
--- /dev/null
+++ b/rsf-design/src/views/auth/register/index.vue
@@ -0,0 +1,178 @@
+<!-- 娉ㄥ唽椤甸潰 -->
+<template>
+ <div class="flex w-full h-screen">
+ <LoginLeftView />
+
+ <div class="relative flex-1">
+ <AuthTopBar />
+
+ <div class="auth-right-wrap">
+ <div class="form">
+ <h3 class="title">{{ $t('register.title') }}</h3>
+ <p class="sub-title">{{ $t('register.subTitle') }}</p>
+ <ElForm
+ class="mt-7.5"
+ ref="formRef"
+ :model="formData"
+ :rules="rules"
+ label-position="top"
+ :key="formKey"
+ >
+ <ElFormItem prop="username">
+ <ElInput
+ class="custom-height"
+ v-model.trim="formData.username"
+ :placeholder="$t('register.placeholder.username')"
+ />
+ </ElFormItem>
+
+ <ElFormItem prop="password">
+ <ElInput
+ class="custom-height"
+ v-model.trim="formData.password"
+ :placeholder="$t('register.placeholder.password')"
+ type="password"
+ autocomplete="off"
+ show-password
+ />
+ </ElFormItem>
+
+ <ElFormItem prop="confirmPassword">
+ <ElInput
+ class="custom-height"
+ v-model.trim="formData.confirmPassword"
+ :placeholder="$t('register.placeholder.confirmPassword')"
+ type="password"
+ autocomplete="off"
+ @keyup.enter="register"
+ show-password
+ />
+ </ElFormItem>
+
+ <ElFormItem prop="agreement">
+ <ElCheckbox v-model="formData.agreement">
+ {{ $t('register.agreeText') }}
+ <RouterLink
+ style="color: var(--theme-color); text-decoration: none"
+ to="/privacy-policy"
+ >{{ $t('register.privacyPolicy') }}</RouterLink
+ >
+ </ElCheckbox>
+ </ElFormItem>
+
+ <div style="margin-top: 15px">
+ <ElButton
+ class="w-full custom-height"
+ type="primary"
+ @click="register"
+ :loading="loading"
+ v-ripple
+ >
+ {{ $t('register.submitBtnText') }}
+ </ElButton>
+ </div>
+
+ <div class="mt-5 text-sm text-g-600">
+ <span>{{ $t('register.hasAccount') }}</span>
+ <RouterLink class="text-theme" :to="{ name: 'Login' }">{{
+ $t('register.toLogin')
+ }}</RouterLink>
+ </div>
+ </ElForm>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { useI18n } from 'vue-i18n'
+ defineOptions({ name: 'Register' })
+ const USERNAME_MIN_LENGTH = 3
+ const USERNAME_MAX_LENGTH = 20
+ const PASSWORD_MIN_LENGTH = 6
+ const REDIRECT_DELAY = 1e3
+ const { t, locale } = useI18n()
+ const router = useRouter()
+ const formRef = ref()
+ const loading = ref(false)
+ const formKey = ref(0)
+ watch(locale, () => {
+ formKey.value++
+ })
+ const formData = reactive({
+ username: '',
+ password: '',
+ confirmPassword: '',
+ agreement: false
+ })
+ const validatePassword = (_rule, value, callback) => {
+ if (!value) {
+ callback(new Error(t('register.placeholder.password')))
+ return
+ }
+ if (formData.confirmPassword) {
+ formRef.value?.validateField('confirmPassword')
+ }
+ callback()
+ }
+ const validateConfirmPassword = (_rule, value, callback) => {
+ if (!value) {
+ callback(new Error(t('register.rule.confirmPasswordRequired')))
+ return
+ }
+ if (value !== formData.password) {
+ callback(new Error(t('register.rule.passwordMismatch')))
+ return
+ }
+ callback()
+ }
+ const validateAgreement = (_rule, value, callback) => {
+ if (!value) {
+ callback(new Error(t('register.rule.agreementRequired')))
+ return
+ }
+ callback()
+ }
+ const rules = computed(() => ({
+ username: [
+ { required: true, message: t('register.placeholder.username'), trigger: 'blur' },
+ {
+ min: USERNAME_MIN_LENGTH,
+ max: USERNAME_MAX_LENGTH,
+ message: t('register.rule.usernameLength'),
+ trigger: 'blur'
+ }
+ ],
+ password: [
+ { required: true, validator: validatePassword, trigger: 'blur' },
+ { min: PASSWORD_MIN_LENGTH, message: t('register.rule.passwordLength'), trigger: 'blur' }
+ ],
+ confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
+ agreement: [{ validator: validateAgreement, trigger: 'change' }]
+ }))
+ const register = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ loading.value = true
+ setTimeout(() => {
+ loading.value = false
+ ElMessage.success('娉ㄥ唽鎴愬姛')
+ toLogin()
+ }, REDIRECT_DELAY)
+ } catch (error) {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', error)
+ loading.value = false
+ }
+ }
+ const toLogin = () => {
+ setTimeout(() => {
+ router.push({ name: 'Login' })
+ }, REDIRECT_DELAY)
+ }
+</script>
+
+<style scoped>
+ @import '../login/style.css';
+</style>
diff --git a/rsf-design/src/views/dashboard/console/index.vue b/rsf-design/src/views/dashboard/console/index.vue
new file mode 100644
index 0000000..39fa3be
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/index.vue
@@ -0,0 +1,44 @@
+<!-- 宸ヤ綔鍙伴〉闈� -->
+<template>
+ <div>
+ <CardList></CardList>
+
+ <ElRow :gutter="20">
+ <ElCol :sm="24" :md="12" :lg="10">
+ <ActiveUser />
+ </ElCol>
+ <ElCol :sm="24" :md="12" :lg="14">
+ <SalesOverview />
+ </ElCol>
+ </ElRow>
+
+ <ElRow :gutter="20">
+ <ElCol :sm="24" :md="24" :lg="12">
+ <NewUser />
+ </ElCol>
+ <ElCol :sm="24" :md="12" :lg="6">
+ <Dynamic />
+ </ElCol>
+ <ElCol :sm="24" :md="12" :lg="6">
+ <TodoList />
+ </ElCol>
+ </ElRow>
+
+ <AboutProject />
+ </div>
+</template>
+
+<script setup>
+ import CardList from './modules/card-list.vue'
+ import ActiveUser from './modules/active-user.vue'
+
+ import SalesOverview from './modules/sales-overview.vue'
+ import NewUser from './modules/new-user.vue'
+
+ import Dynamic from './modules/dynamic-stats.vue'
+ import TodoList from './modules/todo-list.vue'
+
+ import AboutProject from './modules/about-project.vue'
+
+ defineOptions({ name: 'Console' })
+</script>
diff --git a/rsf-design/src/views/dashboard/console/modules/about-project.vue b/rsf-design/src/views/dashboard/console/modules/about-project.vue
new file mode 100644
index 0000000..c4cd690
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/modules/about-project.vue
@@ -0,0 +1,37 @@
+<template>
+ <div class="art-card p-5 flex-b mb-5 max-sm:mb-4">
+ <div>
+ <h2 class="text-2xl font-medium">鍏充簬椤圭洰</h2>
+ <p class="text-g-700 mt-1">{{ systemName }} 鏄竴娆惧吋鍏疯璁$編瀛︿笌楂樻晥寮�鍙戠殑鍚庡彴绯荤粺</p>
+ <p class="text-g-700 mt-1">浣跨敤浜� Vue3銆丣avaScript銆乂ite銆丒lement Plus 绛夊墠娌挎妧鏈�</p>
+
+ <div class="flex flex-wrap gap-3.5 max-w-150 mt-9">
+ <div
+ class="w-60 flex-cb h-12.5 px-3.5 border border-g-300 c-p rounded-lg text-sm bg-g-100 duration-300 hover:-translate-y-1 max-sm:w-full"
+ v-for="link in linkList"
+ :key="link.label"
+ @click="goPage(link.url)"
+ >
+ <span class="text-g-700">{{ link.label }}</span>
+ <ArtSvgIcon icon="ri:arrow-right-s-line" class="text-lg text-g-600" />
+ </div>
+ </div>
+ </div>
+ <img class="w-75 max-md:!hidden" src="@imgs/draw/draw1.png" alt="draw1" />
+ </div>
+</template>
+
+<script setup>
+ import AppConfig from '@/config'
+ import { WEB_LINKS } from '@/utils/constants'
+ const systemName = AppConfig.systemInfo.name
+ const linkList = [
+ { label: '椤圭洰瀹樼綉', url: WEB_LINKS.DOCS },
+ { label: '鏂囨。', url: WEB_LINKS.INTRODUCE },
+ { label: 'Github', url: WEB_LINKS.GITHUB_HOME },
+ { label: '鍝斿摡鍝斿摡', url: WEB_LINKS.BILIBILI }
+ ]
+ const goPage = (url) => {
+ window.open(url, '_blank', 'noopener,noreferrer')
+ }
+</script>
diff --git a/rsf-design/src/views/dashboard/console/modules/active-user.vue b/rsf-design/src/views/dashboard/console/modules/active-user.vue
new file mode 100644
index 0000000..6fb3731
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/modules/active-user.vue
@@ -0,0 +1,34 @@
+<template>
+ <div class="art-card h-105 p-4 box-border mb-5 max-sm:mb-4">
+ <ArtBarChart
+ class="box-border p-2"
+ barWidth="50%"
+ height="13.7rem"
+ :showAxisLine="false"
+ :data="chartData"
+ :xAxisData="xAxisLabels"
+ />
+ <div class="ml-1">
+ <h3 class="mt-5 text-lg font-medium">鐢ㄦ埛姒傝堪</h3>
+ <p class="mt-1 text-sm">姣斾笂鍛� <span class="text-success font-medium">+23%</span></p>
+ <p class="mt-1 text-sm">鎴戜滑涓烘偍鍒涘缓浜嗗涓�夐」锛屽彲灏嗗畠浠粍鍚堝湪涓�璧峰苟瀹氬埗涓哄儚绱犲畬缇庣殑椤甸潰</p>
+ </div>
+ <div class="flex-b mt-2">
+ <div class="flex-1" v-for="(item, index) in list" :key="index">
+ <p class="text-2xl text-g-900">{{ item.num }}</p>
+ <p class="text-xs text-g-500">{{ item.name }}</p>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ const xAxisLabels = ['1鏈�', '2鏈�', '3鏈�', '4鏈�', '5鏈�', '6鏈�', '7鏈�', '8鏈�', '9鏈�']
+ const chartData = [160, 100, 150, 80, 190, 100, 175, 120, 160]
+ const list = [
+ { name: '鎬荤敤鎴烽噺', num: '32k' },
+ { name: '鎬昏闂噺', num: '128k' },
+ { name: '鏃ヨ闂噺', num: '1.2k' },
+ { name: '鍛ㄥ悓姣�', num: '+5%' }
+ ]
+</script>
diff --git a/rsf-design/src/views/dashboard/console/modules/card-list.vue b/rsf-design/src/views/dashboard/console/modules/card-list.vue
new file mode 100644
index 0000000..327e139
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/modules/card-list.vue
@@ -0,0 +1,61 @@
+<template>
+ <ElRow :gutter="20" class="flex">
+ <ElCol v-for="(item, index) in dataList" :key="index" :sm="12" :md="6" :lg="6">
+ <div class="art-card relative flex flex-col justify-center h-35 px-5 mb-5 max-sm:mb-4">
+ <span class="text-g-700 text-sm">{{ item.des }}</span>
+ <ArtCountTo class="text-[26px] font-medium mt-2" :target="item.num" :duration="1300" />
+ <div class="flex-c mt-1">
+ <span class="text-xs text-g-600">杈冧笂鍛�</span>
+ <span
+ class="ml-1 text-xs font-semibold"
+ :class="[item.change.indexOf('+') === -1 ? 'text-danger' : 'text-success']"
+ >
+ {{ item.change }}
+ </span>
+ </div>
+ <div
+ class="absolute top-0 bottom-0 right-5 m-auto size-12.5 rounded-xl flex-cc bg-theme/10"
+ >
+ <ArtSvgIcon :icon="item.icon" class="text-xl text-theme" />
+ </div>
+ </div>
+ </ElCol>
+ </ElRow>
+</template>
+
+<script setup>
+ const dataList = reactive([
+ {
+ des: '鎬昏闂鏁�',
+ icon: 'ri:pie-chart-line',
+ startVal: 0,
+ duration: 1e3,
+ num: 9120,
+ change: '+20%'
+ },
+ {
+ des: '鍦ㄧ嚎璁垮鏁�',
+ icon: 'ri:group-line',
+ startVal: 0,
+ duration: 1e3,
+ num: 182,
+ change: '+10%'
+ },
+ {
+ des: '鐐瑰嚮閲�',
+ icon: 'ri:fire-line',
+ startVal: 0,
+ duration: 1e3,
+ num: 9520,
+ change: '-12%'
+ },
+ {
+ des: '鏂扮敤鎴�',
+ icon: 'ri:progress-2-line',
+ startVal: 0,
+ duration: 1e3,
+ num: 156,
+ change: '+30%'
+ }
+ ])
+</script>
diff --git a/rsf-design/src/views/dashboard/console/modules/dynamic-stats.vue b/rsf-design/src/views/dashboard/console/modules/dynamic-stats.vue
new file mode 100644
index 0000000..da0eca4
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/modules/dynamic-stats.vue
@@ -0,0 +1,69 @@
+<template>
+ <div class="art-card h-128 p-5 mb-5 max-sm:mb-4">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>鍔ㄦ��</h4>
+ <p>鏂板<span class="text-success">+6</span></p>
+ </div>
+ </div>
+
+ <div class="h-9/10 mt-2 overflow-hidden">
+ <ElScrollbar>
+ <div
+ class="h-17.5 leading-17.5 border-b border-g-300 text-sm overflow-hidden last:border-b-0"
+ v-for="(item, index) in list"
+ :key="index"
+ >
+ <span class="text-g-800 font-medium">{{ item.username }}</span>
+ <span class="mx-2 text-g-600">{{ item.type }}</span>
+ <span class="text-theme">{{ item.target }}</span>
+ </div>
+ </ElScrollbar>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ const list = reactive([
+ {
+ username: '涓皬楸�',
+ type: '鍏虫敞浜�',
+ target: '瑾惰娣�'
+ },
+ {
+ username: '浣曞皬鑽�',
+ type: '鍙戣〃鏂囩珷',
+ target: 'Vue3 + JavaScript + Vite 椤圭洰瀹炴垬绗旇'
+ },
+ {
+ username: '涓皬楸�',
+ type: '鍏虫敞浜�',
+ target: '瑾惰娣�'
+ },
+ {
+ username: '浣曞皬鑽�',
+ type: '鍙戣〃鏂囩珷',
+ target: 'Vue3 + JavaScript + Vite 椤圭洰瀹炴垬绗旇'
+ },
+ {
+ username: '瑾惰娣�',
+ type: '鎻愬嚭闂',
+ target: '涓婚鍙互閰嶇疆鍚�'
+ },
+ {
+ username: '鍙戝憜鑽�',
+ type: '鍏戞崲浜嗙墿鍝�',
+ target: '銆婂鐗圭殑涓�鐢熴��'
+ },
+ {
+ username: '鐢滅瓛',
+ type: '鍏抽棴浜嗛棶棰�',
+ target: '鍙戝憜鑽�'
+ },
+ {
+ username: '鍐锋湀鍛嗗憜',
+ type: '鍏戞崲浜嗙墿鍝�',
+ target: '銆婇珮鏁堜汉澹殑涓冧釜涔犳儻銆�'
+ }
+ ])
+</script>
diff --git a/rsf-design/src/views/dashboard/console/modules/new-user.vue b/rsf-design/src/views/dashboard/console/modules/new-user.vue
new file mode 100644
index 0000000..41c2c88
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/modules/new-user.vue
@@ -0,0 +1,145 @@
+<template>
+ <div class="art-card p-5 h-128 overflow-hidden mb-5 max-sm:mb-4">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>鏂扮敤鎴�</h4>
+ <p>杩欎釜鏈堝闀�<span class="text-success">+20%</span></p>
+ </div>
+ <ElRadioGroup v-model="radio2">
+ <ElRadioButton value="鏈湀" label="鏈湀"></ElRadioButton>
+ <ElRadioButton value="涓婃湀" label="涓婃湀"></ElRadioButton>
+ <ElRadioButton value="浠婂勾" label="浠婂勾"></ElRadioButton>
+ </ElRadioGroup>
+ </div>
+ <ArtTable
+ class="w-full"
+ :data="tableData"
+ style="width: 100%"
+ size="large"
+ :border="false"
+ :stripe="false"
+ :header-cell-style="{ background: 'transparent' }"
+ >
+ <template #default>
+ <ElTableColumn label="澶村儚" prop="avatar" width="150px">
+ <template #default="scope">
+ <div style="display: flex; align-items: center">
+ <img class="size-9 rounded-lg" :src="scope.row.avatar" alt="avatar" />
+ <span class="ml-2">{{ scope.row.username }}</span>
+ </div>
+ </template>
+ </ElTableColumn>
+ <ElTableColumn label="鍦板尯" prop="province" />
+ <ElTableColumn label="鎬у埆" prop="avatar">
+ <template #default="scope">
+ <div style="display: flex; align-items: center">
+ <span style="margin-left: 10px">{{ scope.row.sex === 1 ? '鐢�' : '濂�' }}</span>
+ </div>
+ </template>
+ </ElTableColumn>
+ <ElTableColumn label="杩涘害" width="240">
+ <template #default="scope">
+ <ElProgress
+ :percentage="scope.row.pro"
+ :color="scope.row.color"
+ :stroke-width="4"
+ :aria-label="`${scope.row.username}鐨勫畬鎴愯繘搴�: ${scope.row.pro}%`"
+ />
+ </template>
+ </ElTableColumn>
+ </template>
+ </ArtTable>
+ </div>
+</template>
+
+<script setup>
+ 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'
+ const ANIMATION_DELAY = 100
+ const radio2 = ref('鏈湀')
+ const tableData = reactive([
+ {
+ username: '涓皬楸�',
+ province: '鍖椾含',
+ sex: 0,
+ age: 22,
+ percentage: 60,
+ pro: 0,
+ color: 'var(--art-primary)',
+ avatar: avatar1
+ },
+ {
+ username: '浣曞皬鑽�',
+ province: '娣卞湷',
+ sex: 1,
+ age: 21,
+ percentage: 20,
+ pro: 0,
+ color: 'var(--art-secondary)',
+ avatar: avatar2
+ },
+ {
+ username: '瑾惰娣�',
+ province: '涓婃捣',
+ sex: 1,
+ age: 23,
+ percentage: 60,
+ pro: 0,
+ color: 'var(--art-warning)',
+ avatar: avatar3
+ },
+ {
+ username: '鍙戝憜鑽�',
+ province: '闀挎矙',
+ sex: 0,
+ age: 28,
+ percentage: 50,
+ pro: 0,
+ color: 'var(--art-info)',
+ avatar: avatar4
+ },
+ {
+ username: '鐢滅瓛',
+ province: '娴欐睙',
+ sex: 1,
+ age: 26,
+ percentage: 70,
+ pro: 0,
+ color: 'var(--art-error)',
+ avatar: avatar5
+ },
+ {
+ username: '鍐锋湀鍛嗗憜',
+ province: '婀栧寳',
+ sex: 1,
+ age: 25,
+ percentage: 90,
+ pro: 0,
+ color: 'var(--art-success)',
+ avatar: avatar6
+ }
+ ])
+ const addAnimation = () => {
+ setTimeout(() => {
+ tableData.forEach((item) => {
+ item.pro = item.percentage
+ })
+ }, ANIMATION_DELAY)
+ }
+ onMounted(() => {
+ addAnimation()
+ })
+</script>
+
+<style lang="scss" scoped>
+ .art-card {
+ :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ color: var(--el-color-primary) !important;
+ background: transparent !important;
+ }
+ }
+</style>
diff --git a/rsf-design/src/views/dashboard/console/modules/sales-overview.vue b/rsf-design/src/views/dashboard/console/modules/sales-overview.vue
new file mode 100644
index 0000000..7f7b0a6
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/modules/sales-overview.vue
@@ -0,0 +1,35 @@
+<template>
+ <div class="art-card h-105 p-5 mb-5 max-sm:mb-4">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>璁块棶閲�</h4>
+ <p>浠婂勾澧為暱<span class="text-success">+15%</span></p>
+ </div>
+ </div>
+ <ArtLineChart
+ height="calc(100% - 56px)"
+ :data="data"
+ :xAxisData="xAxisData"
+ :showAreaColor="true"
+ :showAxisLine="false"
+ />
+ </div>
+</template>
+
+<script setup>
+ const data = [50, 25, 40, 20, 70, 35, 65, 30, 35, 20, 40, 44]
+ const xAxisData = [
+ '1鏈�',
+ '2鏈�',
+ '3鏈�',
+ '4鏈�',
+ '5鏈�',
+ '6鏈�',
+ '7鏈�',
+ '8鏈�',
+ '9鏈�',
+ '10鏈�',
+ '11鏈�',
+ '12鏈�'
+ ]
+</script>
diff --git a/rsf-design/src/views/dashboard/console/modules/todo-list.vue b/rsf-design/src/views/dashboard/console/modules/todo-list.vue
new file mode 100644
index 0000000..2cf67cf
--- /dev/null
+++ b/rsf-design/src/views/dashboard/console/modules/todo-list.vue
@@ -0,0 +1,61 @@
+<template>
+ <div class="art-card h-128 p-5 mb-5 max-sm:mb-4">
+ <div class="art-card-header">
+ <div class="title">
+ <h4>浠e姙浜嬮」</h4>
+ <p>寰呭鐞�<span class="text-danger">3</span></p>
+ </div>
+ </div>
+
+ <div class="h-[calc(100%-40px)] overflow-auto">
+ <ElScrollbar>
+ <div
+ class="flex-cb h-17.5 border-b border-g-300 text-sm last:border-b-0"
+ v-for="(item, index) in list"
+ :key="index"
+ >
+ <div>
+ <p class="text-sm">{{ item.username }}</p>
+ <p class="text-g-500 mt-1">{{ item.date }}</p>
+ </div>
+ <ElCheckbox v-model="item.complate" />
+ </div>
+ </ElScrollbar>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ const list = reactive([
+ {
+ username: '鏌ョ湅浠婂ぉ宸ヤ綔鍐呭',
+ date: '涓婂崍 09:30',
+ complate: true
+ },
+ {
+ username: '鍥炲閭欢',
+ date: '涓婂崍 10:30',
+ complate: true
+ },
+ {
+ username: '宸ヤ綔姹囨姤鏁寸悊',
+ date: '涓婂崍 11:00',
+ complate: true
+ },
+ {
+ username: '浜у搧闇�姹備細璁�',
+ date: '涓嬪崍 02:00',
+ complate: false
+ },
+ {
+ username: '鏁寸悊浼氳鍐呭',
+ date: '涓嬪崍 03:30',
+ complate: false
+ },
+ {
+ username: '鏄庡ぉ宸ヤ綔璁″垝',
+ date: '涓嬪崍 06:30',
+ complate: false
+ }
+ ])
+</script>
diff --git a/rsf-design/src/views/exception/403/index.vue b/rsf-design/src/views/exception/403/index.vue
new file mode 100644
index 0000000..d18fbac
--- /dev/null
+++ b/rsf-design/src/views/exception/403/index.vue
@@ -0,0 +1,17 @@
+<!-- 403椤甸潰 -->
+<template>
+ <ArtException
+ :data="{
+ title: '403',
+ desc: $t('exceptionPage.403'),
+ btnText: $t('exceptionPage.gohome'),
+ imgUrl
+ }"
+ />
+</template>
+
+<script setup>
+ import imgUrl from '@imgs/svg/403.svg'
+
+ defineOptions({ name: 'Exception403' })
+</script>
diff --git a/rsf-design/src/views/exception/404/index.vue b/rsf-design/src/views/exception/404/index.vue
new file mode 100644
index 0000000..daacef5
--- /dev/null
+++ b/rsf-design/src/views/exception/404/index.vue
@@ -0,0 +1,17 @@
+<!-- 404椤甸潰 -->
+<template>
+ <ArtException
+ :data="{
+ title: '404',
+ desc: $t('exceptionPage.404'),
+ btnText: $t('exceptionPage.gohome'),
+ imgUrl
+ }"
+ />
+</template>
+
+<script setup>
+ import imgUrl from '@imgs/svg/404.svg'
+
+ defineOptions({ name: 'Exception404' })
+</script>
diff --git a/rsf-design/src/views/exception/500/index.vue b/rsf-design/src/views/exception/500/index.vue
new file mode 100644
index 0000000..aa5490c
--- /dev/null
+++ b/rsf-design/src/views/exception/500/index.vue
@@ -0,0 +1,17 @@
+<!-- 500椤甸潰 -->
+<template>
+ <ArtException
+ :data="{
+ title: '500',
+ desc: $t('exceptionPage.500'),
+ btnText: $t('exceptionPage.gohome'),
+ imgUrl
+ }"
+ />
+</template>
+
+<script setup>
+ import imgUrl from '@imgs/svg/500.svg'
+
+ defineOptions({ name: 'Exception500' })
+</script>
diff --git a/rsf-design/src/views/index/index.vue b/rsf-design/src/views/index/index.vue
new file mode 100644
index 0000000..649178e
--- /dev/null
+++ b/rsf-design/src/views/index/index.vue
@@ -0,0 +1,29 @@
+<!-- 甯冨眬瀹瑰櫒 -->
+<template>
+ <div class="app-layout">
+ <aside id="app-sidebar">
+ <ArtSidebarMenu />
+ </aside>
+
+ <main id="app-main">
+ <div id="app-header">
+ <ArtHeaderBar />
+ </div>
+ <div id="app-content">
+ <ArtPageContent />
+ </div>
+ </main>
+
+ <div id="app-global">
+ <ArtGlobalComponent />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ defineOptions({ name: 'AppLayout' })
+</script>
+
+<style lang="scss" scoped>
+ @use './style';
+</style>
diff --git a/rsf-design/src/views/index/style.scss b/rsf-design/src/views/index/style.scss
new file mode 100644
index 0000000..c89f354
--- /dev/null
+++ b/rsf-design/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/rsf-design/src/views/outside/Iframe.vue b/rsf-design/src/views/outside/Iframe.vue
new file mode 100644
index 0000000..ed484f2
--- /dev/null
+++ b/rsf-design/src/views/outside/Iframe.vue
@@ -0,0 +1,29 @@
+<template>
+ <div class="box-border w-full h-full" v-loading="isLoading">
+ <iframe
+ ref="iframeRef"
+ :src="iframeUrl"
+ frameborder="0"
+ class="w-full h-full min-h-[calc(100vh-120px)] border-none"
+ @load="handleIframeLoad"
+ ></iframe>
+ </div>
+</template>
+
+<script setup>
+ import { IframeRouteManager } from '@/router/core'
+ defineOptions({ name: 'IframeView' })
+ const route = useRoute()
+ const isLoading = ref(true)
+ const iframeUrl = ref('')
+ const iframeRef = ref(null)
+ onMounted(() => {
+ const iframeRoute = IframeRouteManager.getInstance().findByPath(route.path)
+ if (iframeRoute?.meta) {
+ iframeUrl.value = iframeRoute.meta.link || ''
+ }
+ })
+ const handleIframeLoad = () => {
+ isLoading.value = false
+ }
+</script>
diff --git a/rsf-design/src/views/result/fail/index.vue b/rsf-design/src/views/result/fail/index.vue
new file mode 100644
index 0000000..cc76126
--- /dev/null
+++ b/rsf-design/src/views/result/fail/index.vue
@@ -0,0 +1,28 @@
+<template>
+ <ArtResultPage
+ type="fail"
+ title="鎻愪氦澶辫触"
+ message="璇锋牳瀵瑰苟淇敼浠ヤ笅淇℃伅鍚庯紝鍐嶉噸鏂版彁浜ゃ��"
+ iconCode="ri:close-fill"
+ >
+ <template #content>
+ <p>鎮ㄦ彁浜ょ殑鍐呭鏈夊涓嬮敊璇細</p>
+ <p>
+ <ArtSvgIcon icon="ri:close-circle-line" class="text-red-500 mr-1" />
+ <span>鎮ㄧ殑璐︽埛宸茶鍐荤粨</span>
+ </p>
+ <p>
+ <ArtSvgIcon icon="ri:close-circle-line" class="text-red-500 mr-1" />
+ <span>鎮ㄧ殑璐︽埛杩樹笉鍏峰鐢宠璧勬牸</span>
+ </p>
+ </template>
+ <template #buttons>
+ <ElButton type="primary" v-ripple>杩斿洖淇敼</ElButton>
+ <ElButton v-ripple>鏌ョ湅</ElButton>
+ </template>
+ </ArtResultPage>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ResultFail' })
+</script>
diff --git a/rsf-design/src/views/result/success/index.vue b/rsf-design/src/views/result/success/index.vue
new file mode 100644
index 0000000..df738b5
--- /dev/null
+++ b/rsf-design/src/views/result/success/index.vue
@@ -0,0 +1,21 @@
+<template>
+ <ArtResultPage
+ type="success"
+ title="鎻愪氦鎴愬姛"
+ message="鎻愪氦缁撴灉椤电敤浜庡弽棣堜竴绯诲垪鎿嶄綔浠诲姟鐨勫鐞嗙粨鏋滐紝濡傛灉浠呮槸绠�鍗曟搷浣滐紝浣跨敤 Message 鍏ㄥ眬鎻愮ず鍙嶉鍗冲彲銆傜伆鑹插尯鍩熷彲浠ユ樉绀轰竴浜涜ˉ鍏呯殑淇℃伅銆�"
+ iconCode="ri:check-fill"
+ >
+ <template #content>
+ <p>宸叉彁浜ょ敵璇凤紝绛夊緟閮ㄩ棬瀹℃牳銆�</p>
+ </template>
+ <template #buttons>
+ <ElButton type="primary" v-ripple>杩斿洖淇敼</ElButton>
+ <ElButton v-ripple>鏌ョ湅</ElButton>
+ <ElButton v-ripple>鎵撳嵃</ElButton>
+ </template>
+ </ArtResultPage>
+</template>
+
+<script setup>
+ defineOptions({ name: 'ResultSuccess' })
+</script>
diff --git a/rsf-design/src/views/system/menu/index.vue b/rsf-design/src/views/system/menu/index.vue
new file mode 100644
index 0000000..d28afa8
--- /dev/null
+++ b/rsf-design/src/views/system/menu/index.vue
@@ -0,0 +1,352 @@
+<!-- 鑿滃崟绠$悊椤甸潰 -->
+<template>
+ <div class="menu-page art-full-height">
+ <!-- 鎼滅储鏍� -->
+ <ArtSearchBar
+ v-model="formFilters"
+ :items="formItems"
+ :showExpand="false"
+ @reset="handleReset"
+ @search="handleSearch"
+ />
+
+ <ElCard class="art-table-card">
+ <!-- 琛ㄦ牸澶撮儴 -->
+ <ArtTableHeader
+ :showZebra="false"
+ :loading="loading"
+ v-model:columns="columnChecks"
+ @refresh="handleRefresh"
+ >
+ <template #left>
+ <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple> 娣诲姞鑿滃崟 </ElButton>
+ <ElButton @click="toggleExpand" v-ripple>
+ {{ isExpanded ? '鏀惰捣' : '灞曞紑' }}
+ </ElButton>
+ </template>
+ </ArtTableHeader>
+
+ <ArtTable
+ ref="tableRef"
+ rowKey="path"
+ :loading="loading"
+ :columns="columns"
+ :data="filteredTableData"
+ :stripe="false"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ :default-expand-all="false"
+ />
+
+ <!-- 鑿滃崟寮圭獥 -->
+ <MenuDialog
+ v-model:visible="dialogVisible"
+ :type="dialogType"
+ :editData="editData"
+ :lockType="lockMenuType"
+ @submit="handleSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import MenuDialog from './modules/menu-dialog.vue'
+
+ import { formatMenuTitle } from '@/utils/router'
+ import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+ import { useTableColumns } from '@/hooks/core/useTableColumns'
+ import { fetchGetMenuList } from '@/api/system-manage'
+ import { ElTag, ElMessageBox } from 'element-plus'
+ defineOptions({ name: 'Menus' })
+ const loading = ref(false)
+ const isExpanded = ref(false)
+ const tableRef = ref()
+ const dialogVisible = ref(false)
+ const dialogType = ref('menu')
+ const editData = ref(null)
+ const lockMenuType = ref(false)
+ const initialSearchState = {
+ name: '',
+ route: ''
+ }
+ const formFilters = reactive({ ...initialSearchState })
+ const appliedFilters = reactive({ ...initialSearchState })
+ const formItems = computed(() => [
+ {
+ label: '鑿滃崟鍚嶇О',
+ key: 'name',
+ type: 'input',
+ props: { clearable: true }
+ },
+ {
+ label: '璺敱鍦板潃',
+ key: 'route',
+ type: 'input',
+ props: { clearable: true }
+ }
+ ])
+ onMounted(() => {
+ getMenuList()
+ })
+ const getMenuList = async () => {
+ loading.value = true
+ try {
+ const list = await fetchGetMenuList()
+ tableData.value = list
+ } catch (error) {
+ throw error instanceof Error ? error : new Error('鑾峰彇鑿滃崟澶辫触')
+ } finally {
+ loading.value = false
+ }
+ }
+ const getMenuTypeTag = (row) => {
+ if (row.meta?.isAuthButton) return 'danger'
+ if (row.children?.length) return 'info'
+ if (row.meta?.link && row.meta?.isIframe) return 'success'
+ if (row.path) return 'primary'
+ if (row.meta?.link) return 'warning'
+ return 'info'
+ }
+ const getMenuTypeText = (row) => {
+ if (row.meta?.isAuthButton) return '鎸夐挳'
+ if (row.children?.length) return '鐩綍'
+ if (row.meta?.link && row.meta?.isIframe) return '鍐呭祵'
+ if (row.path) return '鑿滃崟'
+ if (row.meta?.link) return '澶栭摼'
+ return '鏈煡'
+ }
+ const { columnChecks, columns } = useTableColumns(() => [
+ {
+ prop: 'meta.title',
+ label: '鑿滃崟鍚嶇О',
+ minWidth: 120,
+ formatter: (row) => formatMenuTitle(row.meta?.title)
+ },
+ {
+ prop: 'type',
+ label: '鑿滃崟绫诲瀷',
+ formatter: (row) => {
+ return h(ElTag, { type: getMenuTypeTag(row) }, () => getMenuTypeText(row))
+ }
+ },
+ {
+ prop: 'path',
+ label: '璺敱',
+ formatter: (row) => {
+ if (row.meta?.isAuthButton) return ''
+ return row.meta?.link || row.path || ''
+ }
+ },
+ {
+ prop: 'meta.authList',
+ label: '鏉冮檺鏍囪瘑',
+ formatter: (row) => {
+ if (row.meta?.isAuthButton) {
+ return row.meta?.authMark || ''
+ }
+ if (!row.meta?.authList?.length) return ''
+ return `${row.meta.authList.length} 涓潈闄愭爣璇哷
+ }
+ },
+ {
+ prop: 'date',
+ label: '缂栬緫鏃堕棿',
+ formatter: () => '2022-3-12 12:00:00'
+ },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ formatter: () => h(ElTag, { type: 'success' }, () => '鍚敤')
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 180,
+ align: 'right',
+ formatter: (row) => {
+ const buttonStyle = { style: 'text-align: right' }
+ if (row.meta?.isAuthButton) {
+ return h('div', buttonStyle, [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEditAuth(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDeleteAuth()
+ })
+ ])
+ }
+ return h('div', buttonStyle, [
+ h(ArtButtonTable, {
+ type: 'add',
+ onClick: () => handleAddAuth(),
+ title: '鏂板鏉冮檺'
+ }),
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => handleEditMenu(row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => handleDeleteMenu()
+ })
+ ])
+ }
+ }
+ ])
+ const tableData = ref([])
+ const handleReset = () => {
+ Object.assign(formFilters, { ...initialSearchState })
+ Object.assign(appliedFilters, { ...initialSearchState })
+ getMenuList()
+ }
+ const handleSearch = () => {
+ Object.assign(appliedFilters, { ...formFilters })
+ getMenuList()
+ }
+ const handleRefresh = () => {
+ getMenuList()
+ }
+ const deepClone = (obj) => {
+ if (obj === null || typeof obj !== 'object') return obj
+ if (obj instanceof Date) return new Date(obj)
+ if (Array.isArray(obj)) return obj.map((item) => deepClone(item))
+ const cloned = {}
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ cloned[key] = deepClone(obj[key])
+ }
+ }
+ return cloned
+ }
+ const convertAuthListToChildren = (items) => {
+ return items.map((item) => {
+ const clonedItem = deepClone(item)
+ if (clonedItem.children?.length) {
+ clonedItem.children = convertAuthListToChildren(clonedItem.children)
+ }
+ if (item.meta?.authList?.length) {
+ const authChildren = item.meta.authList.map((auth) => ({
+ path: `${item.path}_auth_${auth.authMark}`,
+ name: `${String(item.name)}_auth_${auth.authMark}`,
+ meta: {
+ title: auth.title,
+ authMark: auth.authMark,
+ isAuthButton: true,
+ parentPath: item.path
+ }
+ }))
+ clonedItem.children = clonedItem.children?.length
+ ? [...clonedItem.children, ...authChildren]
+ : authChildren
+ }
+ return clonedItem
+ })
+ }
+ const searchMenu = (items) => {
+ const results = []
+ for (const item of items) {
+ const searchName = appliedFilters.name?.toLowerCase().trim() || ''
+ const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
+ const menuTitle = formatMenuTitle(item.meta?.title || '').toLowerCase()
+ const menuPath = (item.path || '').toLowerCase()
+ const nameMatch = !searchName || menuTitle.includes(searchName)
+ const routeMatch = !searchRoute || menuPath.includes(searchRoute)
+ if (item.children?.length) {
+ const matchedChildren = searchMenu(item.children)
+ if (matchedChildren.length > 0) {
+ const clonedItem = deepClone(item)
+ clonedItem.children = matchedChildren
+ results.push(clonedItem)
+ continue
+ }
+ }
+ if (nameMatch && routeMatch) {
+ results.push(deepClone(item))
+ }
+ }
+ return results
+ }
+ const filteredTableData = computed(() => {
+ const searchedData = searchMenu(tableData.value)
+ return convertAuthListToChildren(searchedData)
+ })
+ const handleAddMenu = () => {
+ dialogType.value = 'menu'
+ editData.value = null
+ lockMenuType.value = true
+ dialogVisible.value = true
+ }
+ const handleAddAuth = () => {
+ dialogType.value = 'menu'
+ editData.value = null
+ lockMenuType.value = false
+ dialogVisible.value = true
+ }
+ const handleEditMenu = (row) => {
+ dialogType.value = 'menu'
+ editData.value = row
+ lockMenuType.value = true
+ dialogVisible.value = true
+ }
+ const handleEditAuth = (row) => {
+ dialogType.value = 'button'
+ editData.value = {
+ title: row.meta?.title,
+ authMark: row.meta?.authMark
+ }
+ lockMenuType.value = false
+ dialogVisible.value = true
+ }
+ const handleSubmit = (formData) => {
+ console.log('鎻愪氦鏁版嵁:', formData)
+ getMenuList()
+ }
+ const handleDeleteMenu = async () => {
+ try {
+ await ElMessageBox.confirm('纭畾瑕佸垹闄よ鑿滃崟鍚楋紵鍒犻櫎鍚庢棤娉曟仮澶�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getMenuList()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error('鍒犻櫎澶辫触')
+ }
+ }
+ }
+ const handleDeleteAuth = async () => {
+ try {
+ await ElMessageBox.confirm('纭畾瑕佸垹闄よ鏉冮檺鍚楋紵鍒犻櫎鍚庢棤娉曟仮澶�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getMenuList()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error('鍒犻櫎澶辫触')
+ }
+ }
+ }
+ const toggleExpand = () => {
+ isExpanded.value = !isExpanded.value
+ nextTick(() => {
+ if (tableRef.value?.elTableRef && filteredTableData.value) {
+ const processRows = (rows) => {
+ rows.forEach((row) => {
+ if (row.children?.length) {
+ tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
+ processRows(row.children)
+ }
+ })
+ }
+ processRows(filteredTableData.value)
+ }
+ })
+ }
+</script>
diff --git a/rsf-design/src/views/system/menu/modules/menu-dialog.vue b/rsf-design/src/views/system/menu/modules/menu-dialog.vue
new file mode 100644
index 0000000..7c928de
--- /dev/null
+++ b/rsf-design/src/views/system/menu/modules/menu-dialog.vue
@@ -0,0 +1,287 @@
+<template>
+ <ElDialog
+ :title="dialogTitle"
+ :model-value="visible"
+ @update:model-value="handleCancel"
+ width="860px"
+ align-center
+ class="menu-dialog"
+ @closed="handleClosed"
+ >
+ <ArtForm
+ ref="formRef"
+ v-model="form"
+ :items="formItems"
+ :rules="rules"
+ :span="width > 640 ? 12 : 24"
+ :gutter="20"
+ label-width="100px"
+ :show-reset="false"
+ :show-submit="false"
+ >
+ <template #menuType>
+ <ElRadioGroup v-model="form.menuType" :disabled="disableMenuType">
+ <ElRadioButton value="menu" label="menu">鑿滃崟</ElRadioButton>
+ <ElRadioButton value="button" label="button">鎸夐挳</ElRadioButton>
+ </ElRadioGroup>
+ </template>
+ </ArtForm>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <ElButton @click="handleCancel">鍙� 娑�</ElButton>
+ <ElButton type="primary" @click="handleSubmit">纭� 瀹�</ElButton>
+ </span>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import ArtForm from '@/components/core/forms/art-form/index.vue'
+
+ import { ElIcon, ElTooltip } from 'element-plus'
+ import { QuestionFilled } from '@element-plus/icons-vue'
+ import { formatMenuTitle } from '@/utils/router'
+ import { useWindowSize } from '@vueuse/core'
+ const { width } = useWindowSize()
+ const createLabelTooltip = (label, tooltip) => {
+ return () =>
+ h('span', { class: 'flex items-center' }, [
+ h('span', label),
+ h(
+ ElTooltip,
+ {
+ content: tooltip,
+ placement: 'top'
+ },
+ () => h(ElIcon, { class: 'ml-0.5 cursor-help' }, () => h(QuestionFilled))
+ )
+ ])
+ }
+ const props = defineProps({
+ visible: { required: false, default: false },
+ type: { required: false, default: 'menu' },
+ lockType: { required: false, default: false }
+ })
+ const emit = defineEmits(['update:visible', 'submit'])
+ const formRef = ref()
+ const isEdit = ref(false)
+ const form = reactive({
+ menuType: 'menu',
+ id: 0,
+ name: '',
+ path: '',
+ label: '',
+ component: '',
+ icon: '',
+ isEnable: true,
+ sort: 1,
+ isMenu: true,
+ keepAlive: true,
+ isHide: false,
+ isHideTab: false,
+ link: '',
+ isIframe: false,
+ showBadge: false,
+ showTextBadge: '',
+ fixedTab: false,
+ activePath: '',
+ roles: [],
+ isFullPage: false,
+ authName: '',
+ authLabel: '',
+ authIcon: '',
+ authSort: 1
+ })
+ const rules = reactive({
+ name: [
+ { required: true, message: '璇疯緭鍏ヨ彍鍗曞悕绉�', trigger: 'blur' },
+ { min: 2, max: 20, message: '闀垮害鍦� 2 鍒� 20 涓瓧绗�', trigger: 'blur' }
+ ],
+ path: [{ required: true, message: '璇疯緭鍏ヨ矾鐢卞湴鍧�', trigger: 'blur' }],
+ label: [{ required: true, message: '杈撳叆鏉冮檺鏍囪瘑', trigger: 'blur' }],
+ authName: [{ required: true, message: '璇疯緭鍏ユ潈闄愬悕绉�', trigger: 'blur' }],
+ authLabel: [{ required: true, message: '璇疯緭鍏ユ潈闄愭爣璇�', trigger: 'blur' }]
+ })
+ const formItems = computed(() => {
+ const baseItems = [{ label: '鑿滃崟绫诲瀷', key: 'menuType', span: 24 }]
+ const switchSpan = width.value < 640 ? 12 : 6
+ if (form.menuType === 'menu') {
+ return [
+ ...baseItems,
+ { label: '鑿滃崟鍚嶇О', key: 'name', type: 'input', props: { placeholder: '鑿滃崟鍚嶇О' } },
+ {
+ label: createLabelTooltip(
+ '璺敱鍦板潃',
+ '涓�绾ц彍鍗曪細浠� / 寮�澶寸殑缁濆璺緞锛堝 /dashboard锛塡n浜岀骇鍙婁互涓嬶細鐩稿璺緞锛堝 console銆乽ser锛�'
+ ),
+ key: 'path',
+ type: 'input',
+ props: { placeholder: '濡傦細/dashboard 鎴� console' }
+ },
+ { label: '鏉冮檺鏍囪瘑', key: 'label', type: 'input', props: { placeholder: '濡傦細User' } },
+ {
+ label: createLabelTooltip(
+ '缁勪欢璺緞',
+ '涓�绾х埗绾ц彍鍗曪細濉啓 /index/index\n鍏蜂綋椤甸潰锛氬~鍐欑粍浠惰矾寰勶紙濡� /system/user锛塡n鐩綍鑿滃崟锛氱暀绌�'
+ ),
+ key: 'component',
+ type: 'input',
+ props: { placeholder: '濡傦細/system/user 鎴栫暀绌�' }
+ },
+ { label: '鍥炬爣', key: 'icon', type: 'input', props: { placeholder: '濡傦細ri:user-line' } },
+ {
+ label: createLabelTooltip(
+ '瑙掕壊鏉冮檺',
+ '浠呯敤浜庡墠绔潈闄愭ā寮忥細閰嶇疆瑙掕壊鏍囪瘑锛堝 R_SUPER銆丷_ADMIN锛塡n鍚庣鏉冮檺妯″紡锛氭棤闇�閰嶇疆'
+ ),
+ key: 'roles',
+ type: 'inputtag',
+ props: { placeholder: '杈撳叆瑙掕壊鏍囪瘑鍚庢寜鍥炶溅锛屽锛歊_SUPER' }
+ },
+ {
+ label: '鑿滃崟鎺掑簭',
+ key: 'sort',
+ type: 'number',
+ props: { min: 1, controlsPosition: 'right', style: { width: '100%' } }
+ },
+ {
+ label: '澶栭儴閾炬帴',
+ key: 'link',
+ type: 'input',
+ props: { placeholder: '濡傦細https://www.example.com' }
+ },
+ {
+ label: '鏂囨湰寰界珷',
+ key: 'showTextBadge',
+ type: 'input',
+ props: { placeholder: '濡傦細New銆丠ot' }
+ },
+ {
+ label: createLabelTooltip(
+ '婵�娲昏矾寰�',
+ '鐢ㄤ簬璇︽儏椤电瓑闅愯棌鑿滃崟锛屾寚瀹氶珮浜樉绀虹殑鐖剁骇鑿滃崟璺緞\n渚嬪锛氱敤鎴疯鎯呴〉楂樹寒鏄剧ず"鐢ㄦ埛绠$悊"鑿滃崟'
+ ),
+ key: 'activePath',
+ type: 'input',
+ props: { placeholder: '濡傦細/system/user' }
+ },
+ { label: '鏄惁鍚敤', key: 'isEnable', type: 'switch', span: switchSpan },
+ { label: '椤甸潰缂撳瓨', key: 'keepAlive', type: 'switch', span: switchSpan },
+ { label: '闅愯棌鑿滃崟', key: 'isHide', type: 'switch', span: switchSpan },
+ { label: '鏄惁鍐呭祵', key: 'isIframe', type: 'switch', span: switchSpan },
+ { label: '鏄剧ず寰界珷', key: 'showBadge', type: 'switch', span: switchSpan },
+ { label: '鍥哄畾鏍囩', key: 'fixedTab', type: 'switch', span: switchSpan },
+ { label: '鏍囩闅愯棌', key: 'isHideTab', type: 'switch', span: switchSpan },
+ { label: '鍏ㄥ睆椤甸潰', key: 'isFullPage', type: 'switch', span: switchSpan }
+ ]
+ } else {
+ return [
+ ...baseItems,
+ {
+ label: '鏉冮檺鍚嶇О',
+ key: 'authName',
+ type: 'input',
+ props: { placeholder: '濡傦細鏂板銆佺紪杈戙�佸垹闄�' }
+ },
+ {
+ label: '鏉冮檺鏍囪瘑',
+ key: 'authLabel',
+ type: 'input',
+ props: { placeholder: '濡傦細add銆乪dit銆乨elete' }
+ },
+ {
+ label: '鏉冮檺鎺掑簭',
+ key: 'authSort',
+ type: 'number',
+ props: { min: 1, controlsPosition: 'right', style: { width: '100%' } }
+ }
+ ]
+ }
+ })
+ const dialogTitle = computed(() => {
+ const type = form.menuType === 'menu' ? '鑿滃崟' : '鎸夐挳'
+ return isEdit.value ? `缂栬緫${type}` : `鏂板缓${type}`
+ })
+ const disableMenuType = computed(() => {
+ if (isEdit.value) return true
+ if (!isEdit.value && form.menuType === 'menu' && props.lockType) return true
+ return false
+ })
+ const resetForm = () => {
+ formRef.value?.reset()
+ form.menuType = 'menu'
+ }
+ const loadFormData = () => {
+ if (!props.editData) return
+ isEdit.value = true
+ if (form.menuType === 'menu') {
+ const row = props.editData
+ form.id = row.id || 0
+ form.name = formatMenuTitle(row.meta?.title || '')
+ form.path = row.path || ''
+ form.label = row.name || ''
+ form.component = row.component || ''
+ form.icon = row.meta?.icon || ''
+ form.sort = row.meta?.sort || 1
+ form.isMenu = row.meta?.isMenu ?? true
+ form.keepAlive = row.meta?.keepAlive ?? false
+ form.isHide = row.meta?.isHide ?? false
+ form.isHideTab = row.meta?.isHideTab ?? false
+ form.isEnable = row.meta?.isEnable ?? true
+ form.link = row.meta?.link || ''
+ form.isIframe = row.meta?.isIframe ?? false
+ form.showBadge = row.meta?.showBadge ?? false
+ form.showTextBadge = row.meta?.showTextBadge || ''
+ form.fixedTab = row.meta?.fixedTab ?? false
+ form.activePath = row.meta?.activePath || ''
+ form.roles = row.meta?.roles || []
+ form.isFullPage = row.meta?.isFullPage ?? false
+ } else {
+ const row = props.editData
+ form.authName = row.title || ''
+ form.authLabel = row.authMark || ''
+ form.authIcon = row.icon || ''
+ form.authSort = row.sort || 1
+ }
+ }
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ emit('submit', { ...form })
+ ElMessage.success(`${isEdit.value ? '缂栬緫' : '鏂板'}鎴愬姛`)
+ handleCancel()
+ } catch {
+ ElMessage.error('琛ㄥ崟鏍¢獙澶辫触锛岃妫�鏌ヨ緭鍏�')
+ }
+ }
+ const handleCancel = () => {
+ emit('update:visible', false)
+ }
+ const handleClosed = () => {
+ resetForm()
+ isEdit.value = false
+ }
+ watch(
+ () => props.visible,
+ (newVal) => {
+ if (newVal) {
+ form.menuType = props.type
+ nextTick(() => {
+ if (props.editData) {
+ loadFormData()
+ }
+ })
+ }
+ }
+ )
+ watch(
+ () => props.type,
+ (newType) => {
+ if (props.visible) {
+ form.menuType = newType
+ }
+ }
+ )
+</script>
diff --git a/rsf-design/src/views/system/role/index.vue b/rsf-design/src/views/system/role/index.vue
new file mode 100644
index 0000000..d6a11ca
--- /dev/null
+++ b/rsf-design/src/views/system/role/index.vue
@@ -0,0 +1,213 @@
+<!-- 瑙掕壊绠$悊椤甸潰 -->
+<template>
+ <div class="art-full-height">
+ <RoleSearch
+ v-show="showSearchBar"
+ v-model="searchForm"
+ @search="handleSearch"
+ @reset="resetSearchParams"
+ ></RoleSearch>
+
+ <ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
+ <ArtTableHeader
+ v-model:columns="columnChecks"
+ v-model:showSearchBar="showSearchBar"
+ :loading="loading"
+ @refresh="refreshData"
+ >
+ <template #left>
+ <ElSpace wrap>
+ <ElButton @click="showDialog('add')" v-ripple>鏂板瑙掕壊</ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <!-- 琛ㄦ牸 -->
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ </ArtTable>
+ </ElCard>
+
+ <!-- 瑙掕壊缂栬緫寮圭獥 -->
+ <RoleEditDialog
+ v-model="dialogVisible"
+ :dialog-type="dialogType"
+ :role-data="currentRoleData"
+ @success="refreshData"
+ />
+
+ <!-- 鑿滃崟鏉冮檺寮圭獥 -->
+ <RolePermissionDialog
+ v-model="permissionDialog"
+ :role-data="currentRoleData"
+ @success="refreshData"
+ />
+ </div>
+</template>
+
+<script setup>
+ import RoleSearch from './modules/role-search.vue'
+ import RoleEditDialog from './modules/role-edit-dialog.vue'
+
+ import RolePermissionDialog from './modules/role-permission-dialog.vue'
+
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchGetRoleList } from '@/api/system-manage'
+ import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+ import { ElTag, ElMessageBox } from 'element-plus'
+ defineOptions({ name: 'Role' })
+ const searchForm = ref({
+ roleName: void 0,
+ roleCode: void 0,
+ description: void 0,
+ enabled: void 0,
+ daterange: void 0
+ })
+ const showSearchBar = ref(false)
+ const dialogVisible = ref(false)
+ const permissionDialog = ref(false)
+ const currentRoleData = ref(void 0)
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ // 鏍稿績閰嶇疆
+ core: {
+ apiFn: fetchGetRoleList,
+ apiParams: {
+ current: 1,
+ size: 20
+ },
+ // 鎺掗櫎 apiParams 涓殑灞炴��
+ excludeParams: ['daterange'],
+ columnsFactory: () => [
+ {
+ prop: 'roleId',
+ label: '瑙掕壊ID',
+ width: 100
+ },
+ {
+ prop: 'roleName',
+ label: '瑙掕壊鍚嶇О',
+ minWidth: 120
+ },
+ {
+ prop: 'roleCode',
+ label: '瑙掕壊缂栫爜',
+ minWidth: 120
+ },
+ {
+ prop: 'description',
+ label: '瑙掕壊鎻忚堪',
+ minWidth: 150,
+ showOverflowTooltip: true
+ },
+ {
+ prop: 'enabled',
+ label: '瑙掕壊鐘舵��',
+ width: 100,
+ formatter: (row) => {
+ const statusConfig = row.enabled
+ ? { type: 'success', text: '鍚敤' }
+ : { type: 'warning', text: '绂佺敤' }
+ return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
+ }
+ },
+ {
+ prop: 'createTime',
+ label: '鍒涘缓鏃ユ湡',
+ width: 180,
+ sortable: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 80,
+ fixed: 'right',
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonMore, {
+ list: [
+ {
+ key: 'permission',
+ label: '鑿滃崟鏉冮檺',
+ icon: 'ri:user-3-line'
+ },
+ {
+ key: 'edit',
+ label: '缂栬緫瑙掕壊',
+ icon: 'ri:edit-2-line'
+ },
+ {
+ key: 'delete',
+ label: '鍒犻櫎瑙掕壊',
+ icon: 'ri:delete-bin-4-line',
+ color: '#f56c6c'
+ }
+ ],
+ onClick: (item) => buttonMoreClick(item, row)
+ })
+ ])
+ }
+ ]
+ }
+ })
+ const dialogType = ref('add')
+ const showDialog = (type, row) => {
+ dialogVisible.value = true
+ dialogType.value = type
+ currentRoleData.value = row
+ }
+ const handleSearch = (params) => {
+ const { daterange, ...filtersParams } = params
+ const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null]
+ replaceSearchParams({ ...filtersParams, startTime, endTime })
+ getData()
+ }
+ const buttonMoreClick = (item, row) => {
+ switch (item.key) {
+ case 'permission':
+ showPermissionDialog(row)
+ break
+ case 'edit':
+ showDialog('edit', row)
+ break
+ case 'delete':
+ deleteRole(row)
+ break
+ }
+ }
+ const showPermissionDialog = (row) => {
+ permissionDialog.value = true
+ currentRoleData.value = row
+ }
+ const deleteRole = (row) => {
+ ElMessageBox.confirm(`纭畾鍒犻櫎瑙掕壊"${row.roleName}"鍚楋紵姝ゆ搷浣滀笉鍙仮澶嶏紒`, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ .then(() => {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ refreshData()
+ })
+ .catch(() => {
+ ElMessage.info('宸插彇娑堝垹闄�')
+ })
+ }
+</script>
diff --git a/rsf-design/src/views/system/role/modules/role-edit-dialog.vue b/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
new file mode 100644
index 0000000..08d3e41
--- /dev/null
+++ b/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
@@ -0,0 +1,109 @@
+<template>
+ <ElDialog
+ v-model="visible"
+ :title="dialogType === 'add' ? '鏂板瑙掕壊' : '缂栬緫瑙掕壊'"
+ width="30%"
+ align-center
+ @close="handleClose"
+ >
+ <ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
+ <ElFormItem label="瑙掕壊鍚嶇О" prop="roleName">
+ <ElInput v-model="form.roleName" placeholder="璇疯緭鍏ヨ鑹插悕绉�" />
+ </ElFormItem>
+ <ElFormItem label="瑙掕壊缂栫爜" prop="roleCode">
+ <ElInput v-model="form.roleCode" placeholder="璇疯緭鍏ヨ鑹茬紪鐮�" />
+ </ElFormItem>
+ <ElFormItem label="鎻忚堪" prop="description">
+ <ElInput
+ v-model="form.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヨ鑹叉弿杩�"
+ />
+ </ElFormItem>
+ <ElFormItem label="鍚敤">
+ <ElSwitch v-model="form.enabled" />
+ </ElFormItem>
+ </ElForm>
+ <template #footer>
+ <ElButton @click="handleClose">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">鎻愪氦</ElButton>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ const props = defineProps({
+ modelValue: { required: false, default: false },
+ dialogType: { required: false, default: 'add' },
+ roleData: { required: false, default: void 0 }
+ })
+ const emit = defineEmits(['update:modelValue', 'success'])
+ const formRef = ref()
+ const visible = computed({
+ get: () => props.modelValue,
+ set: (value) => emit('update:modelValue', value)
+ })
+ const rules = reactive({
+ roleName: [
+ { required: true, message: '璇疯緭鍏ヨ鑹插悕绉�', trigger: 'blur' },
+ { min: 2, max: 20, message: '闀垮害鍦� 2 鍒� 20 涓瓧绗�', trigger: 'blur' }
+ ],
+ roleCode: [
+ { required: true, message: '璇疯緭鍏ヨ鑹茬紪鐮�', trigger: 'blur' },
+ { min: 2, max: 50, message: '闀垮害鍦� 2 鍒� 50 涓瓧绗�', trigger: 'blur' }
+ ],
+ description: [{ required: true, message: '璇疯緭鍏ヨ鑹叉弿杩�', trigger: 'blur' }]
+ })
+ const form = reactive({
+ roleId: 0,
+ roleName: '',
+ roleCode: '',
+ description: '',
+ createTime: '',
+ enabled: true
+ })
+ watch(
+ () => props.modelValue,
+ (newVal) => {
+ if (newVal) initForm()
+ }
+ )
+ watch(
+ () => props.roleData,
+ (newData) => {
+ if (newData && props.modelValue) initForm()
+ },
+ { deep: true }
+ )
+ const initForm = () => {
+ if (props.dialogType === 'edit' && props.roleData) {
+ Object.assign(form, props.roleData)
+ } else {
+ Object.assign(form, {
+ roleId: 0,
+ roleName: '',
+ roleCode: '',
+ description: '',
+ createTime: '',
+ enabled: true
+ })
+ }
+ }
+ const handleClose = () => {
+ visible.value = false
+ formRef.value?.resetFields()
+ }
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ try {
+ await formRef.value.validate()
+ const message = props.dialogType === 'add' ? '鏂板鎴愬姛' : '淇敼鎴愬姛'
+ ElMessage.success(message)
+ emit('success')
+ handleClose()
+ } catch (error) {
+ console.log('琛ㄥ崟楠岃瘉澶辫触:', error)
+ }
+ }
+</script>
diff --git a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
new file mode 100644
index 0000000..0940fab
--- /dev/null
+++ b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -0,0 +1,153 @@
+<template>
+ <ElDialog
+ v-model="visible"
+ title="鑿滃崟鏉冮檺"
+ width="520px"
+ align-center
+ class="el-dialog-border"
+ @close="handleClose"
+ >
+ <ElScrollbar height="70vh">
+ <ElTree
+ ref="treeRef"
+ :data="processedMenuList"
+ show-checkbox
+ node-key="name"
+ :default-expand-all="isExpandAll"
+ :default-checked-keys="[1, 2, 3]"
+ :props="defaultProps"
+ @check="handleTreeCheck"
+ >
+ <template #default="{ data }">
+ <div style="display: flex; align-items: center">
+ <span v-if="data.isAuth">
+ {{ data.label }}
+ </span>
+ <span v-else>{{ defaultProps.label(data) }}</span>
+ </div>
+ </template>
+ </ElTree>
+ </ElScrollbar>
+ <template #footer>
+ <ElButton @click="outputSelectedData" style="margin-left: 8px">鑾峰彇閫変腑鏁版嵁</ElButton>
+
+ <ElButton @click="toggleExpandAll">{{ isExpandAll ? '鍏ㄩ儴鏀惰捣' : '鍏ㄩ儴灞曞紑' }}</ElButton>
+ <ElButton @click="toggleSelectAll" style="margin-left: 8px">{{
+ isSelectAll ? '鍙栨秷鍏ㄩ��' : '鍏ㄩ儴閫夋嫨'
+ }}</ElButton>
+ <ElButton type="primary" @click="savePermission">淇濆瓨</ElButton>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { useMenuStore } from '@/store/modules/menu'
+ import { formatMenuTitle } from '@/utils/router'
+ const props = defineProps({
+ modelValue: { required: false, default: false },
+ roleData: { required: false, default: void 0 }
+ })
+ const emit = defineEmits(['update:modelValue', 'success'])
+ const { menuList } = storeToRefs(useMenuStore())
+ const treeRef = ref()
+ const isExpandAll = ref(true)
+ const isSelectAll = ref(false)
+ const visible = computed({
+ get: () => props.modelValue,
+ set: (value) => emit('update:modelValue', value)
+ })
+ const processedMenuList = computed(() => {
+ const processNode = (node) => {
+ const processed = { ...node }
+ if (node.meta?.authList?.length) {
+ const authNodes = node.meta.authList.map((auth) => ({
+ id: `${node.id}_${auth.authMark}`,
+ name: `${node.name}_${auth.authMark}`,
+ label: auth.title,
+ authMark: auth.authMark,
+ isAuth: true,
+ checked: auth.checked || false
+ }))
+ processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
+ }
+ if (processed.children) {
+ processed.children = processed.children.map(processNode)
+ }
+ return processed
+ }
+ return menuList.value.map(processNode)
+ })
+ const defaultProps = {
+ children: 'children',
+ label: (data) => formatMenuTitle(data.meta?.title) || data.label || ''
+ }
+ watch(
+ () => props.modelValue,
+ (newVal) => {
+ if (newVal && props.roleData) {
+ console.log('璁剧疆鏉冮檺:', props.roleData)
+ }
+ }
+ )
+ const handleClose = () => {
+ visible.value = false
+ treeRef.value?.setCheckedKeys([])
+ }
+ const savePermission = () => {
+ ElMessage.success('鏉冮檺淇濆瓨鎴愬姛')
+ emit('success')
+ handleClose()
+ }
+ const toggleExpandAll = () => {
+ const tree = treeRef.value
+ if (!tree) return
+ const nodes = tree.store.nodesMap
+ Object.values(nodes).forEach((node) => {
+ node.expanded = !isExpandAll.value
+ })
+ isExpandAll.value = !isExpandAll.value
+ }
+ const toggleSelectAll = () => {
+ const tree = treeRef.value
+ if (!tree) return
+ if (!isSelectAll.value) {
+ const allKeys = getAllNodeKeys(processedMenuList.value)
+ tree.setCheckedKeys(allKeys)
+ } else {
+ tree.setCheckedKeys([])
+ }
+ isSelectAll.value = !isSelectAll.value
+ }
+ const getAllNodeKeys = (nodes) => {
+ const keys = []
+ const traverse = (nodeList) => {
+ nodeList.forEach((node) => {
+ if (node.name) keys.push(node.name)
+ if (node.children?.length) traverse(node.children)
+ })
+ }
+ traverse(nodes)
+ return keys
+ }
+ const handleTreeCheck = () => {
+ const tree = treeRef.value
+ if (!tree) return
+ const checkedKeys = tree.getCheckedKeys()
+ const allKeys = getAllNodeKeys(processedMenuList.value)
+ isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
+ }
+ const outputSelectedData = () => {
+ const tree = treeRef.value
+ if (!tree) return
+ const selectedData = {
+ checkedKeys: tree.getCheckedKeys(),
+ halfCheckedKeys: tree.getHalfCheckedKeys(),
+ checkedNodes: tree.getCheckedNodes(),
+ halfCheckedNodes: tree.getHalfCheckedNodes(),
+ totalChecked: tree.getCheckedKeys().length,
+ totalHalfChecked: tree.getHalfCheckedKeys().length
+ }
+ console.log('=== 閫変腑鐨勬潈闄愭暟鎹� ===', selectedData)
+ ElMessage.success(`宸茶緭鍑洪�変腑鏁版嵁鍒版帶鍒跺彴锛屽叡閫変腑 ${selectedData.totalChecked} 涓妭鐐筦)
+ }
+</script>
diff --git a/rsf-design/src/views/system/role/modules/role-search.vue b/rsf-design/src/views/system/role/modules/role-search.vue
new file mode 100644
index 0000000..afcb688
--- /dev/null
+++ b/rsf-design/src/views/system/role/modules/role-search.vue
@@ -0,0 +1,87 @@
+<template>
+ <ArtSearchBar
+ ref="searchBarRef"
+ v-model="formData"
+ :items="formItems"
+ :rules="rules"
+ @reset="handleReset"
+ @search="handleSearch"
+ >
+ </ArtSearchBar>
+</template>
+
+<script setup>
+ const props = defineProps({
+ modelValue: { required: true }
+ })
+ const emit = defineEmits(['update:modelValue', 'search', 'reset'])
+ const searchBarRef = ref()
+ const formData = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+ })
+ const rules = {}
+ const statusOptions = ref([
+ { label: '鍚敤', value: true },
+ { label: '绂佺敤', value: false }
+ ])
+ const formItems = computed(() => [
+ {
+ label: '瑙掕壊鍚嶇О',
+ key: 'roleName',
+ type: 'input',
+ placeholder: '璇疯緭鍏ヨ鑹插悕绉�',
+ clearable: true
+ },
+ {
+ label: '瑙掕壊缂栫爜',
+ key: 'roleCode',
+ type: 'input',
+ placeholder: '璇疯緭鍏ヨ鑹茬紪鐮�',
+ clearable: true
+ },
+ {
+ label: '瑙掕壊鎻忚堪',
+ key: 'description',
+ type: 'input',
+ placeholder: '璇疯緭鍏ヨ鑹叉弿杩�',
+ clearable: true
+ },
+ {
+ label: '瑙掕壊鐘舵��',
+ key: 'enabled',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: statusOptions.value,
+ clearable: true
+ }
+ },
+ {
+ label: '鍒涘缓鏃ユ湡',
+ key: 'daterange',
+ type: 'datetime',
+ props: {
+ style: { width: '100%' },
+ placeholder: '璇烽�夋嫨鏃ユ湡鑼冨洿',
+ type: 'daterange',
+ rangeSeparator: '鑷�',
+ startPlaceholder: '寮�濮嬫棩鏈�',
+ endPlaceholder: '缁撴潫鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ shortcuts: [
+ { text: '浠婃棩', value: [/* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()] },
+ { text: '鏈�杩戜竴鍛�', value: [new Date(Date.now() - 6048e5), /* @__PURE__ */ new Date()] },
+ { text: '鏈�杩戜竴涓湀', value: [new Date(Date.now() - 2592e6), /* @__PURE__ */ new Date()] }
+ ]
+ }
+ }
+ ])
+ const handleReset = () => {
+ emit('reset')
+ }
+ const handleSearch = async (params) => {
+ await searchBarRef.value.validate()
+ emit('search', params)
+ }
+</script>
diff --git a/rsf-design/src/views/system/user-center/index.vue b/rsf-design/src/views/system/user-center/index.vue
new file mode 100644
index 0000000..bb6b3ca
--- /dev/null
+++ b/rsf-design/src/views/system/user-center/index.vue
@@ -0,0 +1,209 @@
+<!-- 涓汉涓績椤甸潰 -->
+<template>
+ <div class="w-full h-full p-0 bg-transparent border-none shadow-none">
+ <div class="relative flex-b mt-2.5 max-md:block max-md:mt-1">
+ <div class="w-112 mr-5 max-md:w-full max-md:mr-0">
+ <div class="art-card-sm relative p-9 pb-6 overflow-hidden text-center">
+ <img class="absolute top-0 left-0 w-full h-50 object-cover" src="@imgs/user/bg.webp" />
+ <img
+ class="relative z-10 w-20 h-20 mt-30 mx-auto object-cover border-2 border-white rounded-full"
+ src="@imgs/user/avatar.webp"
+ />
+ <h2 class="mt-5 text-xl font-normal">{{ userInfo.userName }}</h2>
+ <p class="mt-5 text-sm">涓撴敞浜庣敤鎴蜂綋楠岃窡瑙嗚璁捐</p>
+
+ <div class="w-75 mx-auto mt-7.5 text-left">
+ <div class="mt-2.5">
+ <ArtSvgIcon icon="ri:mail-line" class="text-g-700" />
+ <span class="ml-2 text-sm">jdkjjfnndf@mall.com</span>
+ </div>
+ <div class="mt-2.5">
+ <ArtSvgIcon icon="ri:user-3-line" class="text-g-700" />
+ <span class="ml-2 text-sm">浜や簰涓撳</span>
+ </div>
+ <div class="mt-2.5">
+ <ArtSvgIcon icon="ri:map-pin-line" class="text-g-700" />
+ <span class="ml-2 text-sm">骞夸笢鐪佹繁鍦冲競</span>
+ </div>
+ <div class="mt-2.5">
+ <ArtSvgIcon icon="ri:dribbble-fill" class="text-g-700" />
+ <span class="ml-2 text-sm">瀛楄妭璺冲姩锛嶆煇鏌愬钩鍙伴儴锛峌ED</span>
+ </div>
+ </div>
+
+ <div class="mt-10">
+ <h3 class="text-sm font-medium">鏍囩</h3>
+ <div class="flex flex-wrap justify-center mt-3.5">
+ <div
+ v-for="item in lableList"
+ :key="item"
+ class="py-1 px-1.5 mr-2.5 mb-2.5 text-xs border border-g-300 rounded"
+ >
+ {{ item }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="flex-1 overflow-hidden max-md:w-full max-md:mt-3.5">
+ <div class="art-card-sm">
+ <h1 class="p-4 text-xl font-normal border-b border-g-300">鍩烘湰璁剧疆</h1>
+
+ <ElForm
+ :model="form"
+ class="box-border p-5 [&>.el-row_.el-form-item]:w-[calc(50%-10px)] [&>.el-row_.el-input]:w-full [&>.el-row_.el-select]:w-full"
+ ref="ruleFormRef"
+ :rules="rules"
+ label-width="86px"
+ label-position="top"
+ >
+ <ElRow>
+ <ElFormItem label="濮撳悕" prop="realName">
+ <ElInput v-model="form.realName" :disabled="!isEdit" />
+ </ElFormItem>
+ <ElFormItem label="鎬у埆" prop="sex" class="ml-5">
+ <ElSelect v-model="form.sex" placeholder="Select" :disabled="!isEdit">
+ <ElOption
+ v-for="item in options"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </ElSelect>
+ </ElFormItem>
+ </ElRow>
+
+ <ElRow>
+ <ElFormItem label="鏄电О" prop="nikeName">
+ <ElInput v-model="form.nikeName" :disabled="!isEdit" />
+ </ElFormItem>
+ <ElFormItem label="閭" prop="email" class="ml-5">
+ <ElInput v-model="form.email" :disabled="!isEdit" />
+ </ElFormItem>
+ </ElRow>
+
+ <ElRow>
+ <ElFormItem label="鎵嬫満" prop="mobile">
+ <ElInput v-model="form.mobile" :disabled="!isEdit" />
+ </ElFormItem>
+ <ElFormItem label="鍦板潃" prop="address" class="ml-5">
+ <ElInput v-model="form.address" :disabled="!isEdit" />
+ </ElFormItem>
+ </ElRow>
+
+ <ElFormItem label="涓汉浠嬬粛" prop="des" class="h-32">
+ <ElInput type="textarea" :rows="4" v-model="form.des" :disabled="!isEdit" />
+ </ElFormItem>
+
+ <div class="flex-c justify-end [&_.el-button]:!w-27.5">
+ <ElButton type="primary" class="w-22.5" v-ripple @click="edit">
+ {{ isEdit ? '淇濆瓨' : '缂栬緫' }}
+ </ElButton>
+ </div>
+ </ElForm>
+ </div>
+
+ <div class="art-card-sm my-5">
+ <h1 class="p-4 text-xl font-normal border-b border-g-300">鏇存敼瀵嗙爜</h1>
+
+ <ElForm :model="pwdForm" class="box-border p-5" label-width="86px" label-position="top">
+ <ElFormItem label="褰撳墠瀵嗙爜" prop="password">
+ <ElInput
+ v-model="pwdForm.password"
+ type="password"
+ :disabled="!isEditPwd"
+ show-password
+ />
+ </ElFormItem>
+
+ <ElFormItem label="鏂板瘑鐮�" prop="newPassword">
+ <ElInput
+ v-model="pwdForm.newPassword"
+ type="password"
+ :disabled="!isEditPwd"
+ show-password
+ />
+ </ElFormItem>
+
+ <ElFormItem label="纭鏂板瘑鐮�" prop="confirmPassword">
+ <ElInput
+ v-model="pwdForm.confirmPassword"
+ type="password"
+ :disabled="!isEditPwd"
+ show-password
+ />
+ </ElFormItem>
+
+ <div class="flex-c justify-end [&_.el-button]:!w-27.5">
+ <ElButton type="primary" class="w-22.5" v-ripple @click="editPwd">
+ {{ isEditPwd ? '淇濆瓨' : '缂栬緫' }}
+ </ElButton>
+ </div>
+ </ElForm>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { useUserStore } from '@/store/modules/user'
+ defineOptions({ name: 'UserCenter' })
+ const userStore = useUserStore()
+ const userInfo = computed(() => userStore.getUserInfo)
+ const isEdit = ref(false)
+ const isEditPwd = ref(false)
+ const date = ref('')
+ const ruleFormRef = ref()
+ const form = reactive({
+ realName: 'John Snow',
+ nikeName: '鐨崱涓�',
+ email: '59301283@mall.com',
+ mobile: '18888888888',
+ address: '骞夸笢鐪佹繁鍦冲競瀹濆畨鍖鸿タ涔¤閬�101鏍�201',
+ sex: '2',
+ des: 'Art Design Pro 鏄竴娆惧吋鍏疯璁$編瀛︿笌楂樻晥寮�鍙戠殑鍚庡彴绯荤粺.'
+ })
+ const pwdForm = reactive({
+ password: '123456',
+ newPassword: '123456',
+ confirmPassword: '123456'
+ })
+ const rules = reactive({
+ realName: [
+ { required: true, message: '璇疯緭鍏ュ鍚�', trigger: 'blur' },
+ { min: 2, max: 50, message: '闀垮害鍦� 2 鍒� 50 涓瓧绗�', trigger: 'blur' }
+ ],
+ nikeName: [
+ { required: true, message: '璇疯緭鍏ユ樀绉�', trigger: 'blur' },
+ { min: 2, max: 50, message: '闀垮害鍦� 2 鍒� 50 涓瓧绗�', trigger: 'blur' }
+ ],
+ email: [{ required: true, message: '璇疯緭鍏ラ偖绠�', trigger: 'blur' }],
+ mobile: [{ required: true, message: '璇疯緭鍏ユ墜鏈哄彿鐮�', trigger: 'blur' }],
+ address: [{ required: true, message: '璇疯緭鍏ュ湴鍧�', trigger: 'blur' }],
+ sex: [{ required: true, message: '璇烽�夋嫨鎬у埆', trigger: 'blur' }]
+ })
+ const options = [
+ { value: '1', label: '鐢�' },
+ { value: '2', label: '濂�' }
+ ]
+ const lableList = ['涓撴敞璁捐', '寰堟湁鎯虫硶', '杈', '澶ч暱鑵�', '宸濆瀛�', '娴风撼鐧惧窛']
+ onMounted(() => {
+ getDate()
+ })
+ const getDate = () => {
+ const h = /* @__PURE__ */ new Date().getHours()
+ if (h >= 6 && h < 9) date.value = '鏃╀笂濂�'
+ else if (h >= 9 && h < 11) date.value = '涓婂崍濂�'
+ else if (h >= 11 && h < 13) date.value = '涓崍濂�'
+ else if (h >= 13 && h < 18) date.value = '涓嬪崍濂�'
+ else if (h >= 18 && h < 24) date.value = '鏅氫笂濂�'
+ else date.value = '寰堟櫄浜嗭紝鏃╃偣鐫�'
+ }
+ const edit = () => {
+ isEdit.value = !isEdit.value
+ }
+ const editPwd = () => {
+ isEditPwd.value = !isEditPwd.value
+ }
+</script>
diff --git a/rsf-design/src/views/system/user/index.vue b/rsf-design/src/views/system/user/index.vue
new file mode 100644
index 0000000..bf169fd
--- /dev/null
+++ b/rsf-design/src/views/system/user/index.vue
@@ -0,0 +1,222 @@
+<!-- 鐢ㄦ埛绠$悊椤甸潰 -->
+<!-- art-full-height 鑷姩璁$畻鍑洪〉闈㈠墿浣欓珮搴� -->
+<!-- art-table-card 涓�涓鍚堢郴缁熸牱寮忕殑 class锛屽悓鏃惰嚜鍔ㄦ拺婊″墿浣欓珮搴� -->
+<!-- 鏇村 useTable 浣跨敤绀轰緥璇风Щ姝ヨ嚦 鍔熻兘绀轰緥 涓嬮潰鐨勯珮绾ц〃鏍肩ず渚嬫垨鑰呮煡鐪嬪畼鏂规枃妗� -->
+<!-- useTable 鏂囨。锛歨ttps://www.artd.pro/docs/zh/guide/hooks/use-table.html -->
+<template>
+ <div class="user-page art-full-height">
+ <!-- 鎼滅储鏍� -->
+ <UserSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></UserSearch>
+
+ <ElCard class="art-table-card">
+ <!-- 琛ㄦ牸澶撮儴 -->
+ <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
+ <template #left>
+ <ElSpace wrap>
+ <ElButton @click="showDialog('add')" v-ripple>鏂板鐢ㄦ埛</ElButton>
+ </ElSpace>
+ </template>
+ </ArtTableHeader>
+
+ <!-- 琛ㄦ牸 -->
+ <ArtTable
+ :loading="loading"
+ :data="data"
+ :columns="columns"
+ :pagination="pagination"
+ @selection-change="handleSelectionChange"
+ @pagination:size-change="handleSizeChange"
+ @pagination:current-change="handleCurrentChange"
+ >
+ </ArtTable>
+
+ <!-- 鐢ㄦ埛寮圭獥 -->
+ <UserDialog
+ v-model:visible="dialogVisible"
+ :type="dialogType"
+ :user-data="currentUserData"
+ @submit="handleDialogSubmit"
+ />
+ </ElCard>
+ </div>
+</template>
+
+<script setup>
+ import UserSearch from './modules/user-search.vue'
+ import UserDialog from './modules/user-dialog.vue'
+
+ import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+ import { ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
+ import { useTable } from '@/hooks/core/useTable'
+ import { fetchGetUserList } from '@/api/system-manage'
+ import { ElTag, ElMessageBox, ElImage } from 'element-plus'
+ defineOptions({ name: 'User' })
+ const dialogType = ref('add')
+ const dialogVisible = ref(false)
+ const currentUserData = ref({})
+ const selectedRows = ref([])
+ const searchForm = ref({
+ userName: void 0,
+ userGender: void 0,
+ userPhone: void 0,
+ userEmail: void 0,
+ status: '1'
+ })
+ const USER_STATUS_CONFIG = {
+ 1: { type: 'success', text: '鍦ㄧ嚎' },
+ 2: { type: 'info', text: '绂荤嚎' },
+ 3: { type: 'warning', text: '寮傚父' },
+ 4: { type: 'danger', text: '娉ㄩ攢' }
+ }
+ const getUserStatusConfig = (status) => {
+ return (
+ USER_STATUS_CONFIG[status] || {
+ type: 'info',
+ text: '鏈煡'
+ }
+ )
+ }
+ const {
+ columns,
+ columnChecks,
+ data,
+ loading,
+ pagination,
+ getData,
+ replaceSearchParams,
+ resetSearchParams,
+ handleSizeChange,
+ handleCurrentChange,
+ refreshData
+ } = useTable({
+ // 鏍稿績閰嶇疆
+ core: {
+ apiFn: fetchGetUserList,
+ apiParams: {
+ current: 1,
+ size: 20,
+ ...searchForm.value
+ },
+ // 鑷畾涔夊垎椤靛瓧娈垫槧灏勶紝鏈缃椂灏嗕娇鐢ㄥ叏灞�閰嶇疆 tableConfig.ts 涓殑 paginationKey
+ // paginationKey: {
+ // current: 'pageNum',
+ // size: 'pageSize'
+ // },
+ columnsFactory: () => [
+ { type: 'selection' },
+ // 鍕鹃�夊垪
+ { type: 'index', width: 60, label: '搴忓彿' },
+ // 搴忓彿
+ {
+ prop: 'userInfo',
+ label: '鐢ㄦ埛鍚�',
+ width: 280,
+ // visible: false, // 榛樿鏄惁鏄剧ず鍒�
+ formatter: (row) => {
+ return h('div', { class: 'user flex-c' }, [
+ h(ElImage, {
+ class: 'size-9.5 rounded-md',
+ src: row.avatar,
+ previewSrcList: [row.avatar],
+ // 鍥剧墖棰勮鏄惁鎻掑叆鑷� body 鍏冪礌涓婏紝鐢ㄤ簬瑙e喅琛ㄦ牸鍐呴儴鍥剧墖棰勮鏍峰紡寮傚父
+ previewTeleported: true
+ }),
+ h('div', { class: 'ml-2' }, [
+ h('p', { class: 'user-name' }, row.userName),
+ h('p', { class: 'email' }, row.userEmail)
+ ])
+ ])
+ }
+ },
+ {
+ prop: 'userGender',
+ label: '鎬у埆',
+ sortable: true,
+ formatter: (row) => row.userGender
+ },
+ { prop: 'userPhone', label: '鎵嬫満鍙�' },
+ {
+ prop: 'status',
+ label: '鐘舵��',
+ formatter: (row) => {
+ const statusConfig = getUserStatusConfig(row.status)
+ return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
+ }
+ },
+ {
+ prop: 'createTime',
+ label: '鍒涘缓鏃ユ湡',
+ sortable: true
+ },
+ {
+ prop: 'operation',
+ label: '鎿嶄綔',
+ width: 120,
+ fixed: 'right',
+ // 鍥哄畾鍒�
+ formatter: (row) =>
+ h('div', [
+ h(ArtButtonTable, {
+ type: 'edit',
+ onClick: () => showDialog('edit', row)
+ }),
+ h(ArtButtonTable, {
+ type: 'delete',
+ onClick: () => deleteUser(row)
+ })
+ ])
+ }
+ ]
+ },
+ // 鏁版嵁澶勭悊
+ transform: {
+ // 鏁版嵁杞崲鍣� - 鏇挎崲澶村儚
+ dataTransformer: (records) => {
+ if (!Array.isArray(records)) {
+ console.warn('鏁版嵁杞崲鍣�: 鏈熸湜鏁扮粍绫诲瀷锛屽疄闄呮敹鍒�:', typeof records)
+ return []
+ }
+ return records.map((item, index) => {
+ return {
+ ...item,
+ avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar
+ }
+ })
+ }
+ }
+ })
+ const handleSearch = (params) => {
+ replaceSearchParams(params)
+ getData()
+ }
+ const showDialog = (type, row) => {
+ console.log('鎵撳紑寮圭獥:', { type, row })
+ dialogType.value = type
+ currentUserData.value = row || {}
+ nextTick(() => {
+ dialogVisible.value = true
+ })
+ }
+ const deleteUser = (row) => {
+ console.log('鍒犻櫎鐢ㄦ埛:', row)
+ ElMessageBox.confirm(`纭畾瑕佹敞閿�璇ョ敤鎴峰悧锛焋, '娉ㄩ攢鐢ㄦ埛', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'error'
+ }).then(() => {
+ ElMessage.success('娉ㄩ攢鎴愬姛')
+ })
+ }
+ const handleDialogSubmit = async () => {
+ try {
+ dialogVisible.value = false
+ currentUserData.value = {}
+ } catch (error) {
+ console.error('鎻愪氦澶辫触:', error)
+ }
+ }
+ const handleSelectionChange = (selection) => {
+ selectedRows.value = selection
+ console.log('閫変腑琛屾暟鎹�:', selectedRows.value)
+ }
+</script>
diff --git a/rsf-design/src/views/system/user/modules/user-dialog.vue b/rsf-design/src/views/system/user/modules/user-dialog.vue
new file mode 100644
index 0000000..706c5c3
--- /dev/null
+++ b/rsf-design/src/views/system/user/modules/user-dialog.vue
@@ -0,0 +1,106 @@
+<template>
+ <ElDialog
+ v-model="dialogVisible"
+ :title="dialogType === 'add' ? '娣诲姞鐢ㄦ埛' : '缂栬緫鐢ㄦ埛'"
+ width="30%"
+ align-center
+ >
+ <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
+ <ElFormItem label="鐢ㄦ埛鍚�" prop="username">
+ <ElInput v-model="formData.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
+ </ElFormItem>
+ <ElFormItem label="鎵嬫満鍙�" prop="phone">
+ <ElInput v-model="formData.phone" placeholder="璇疯緭鍏ユ墜鏈哄彿" />
+ </ElFormItem>
+ <ElFormItem label="鎬у埆" prop="gender">
+ <ElSelect v-model="formData.gender">
+ <ElOption label="鐢�" value="鐢�" />
+ <ElOption label="濂�" value="濂�" />
+ </ElSelect>
+ </ElFormItem>
+ <ElFormItem label="瑙掕壊" prop="role">
+ <ElSelect v-model="formData.role" multiple>
+ <ElOption
+ v-for="role in roleList"
+ :key="role.roleCode"
+ :value="role.roleCode"
+ :label="role.roleName"
+ />
+ </ElSelect>
+ </ElFormItem>
+ </ElForm>
+ <template #footer>
+ <div class="dialog-footer">
+ <ElButton @click="dialogVisible = false">鍙栨秷</ElButton>
+ <ElButton type="primary" @click="handleSubmit">鎻愪氦</ElButton>
+ </div>
+ </template>
+ </ElDialog>
+</template>
+
+<script setup>
+ import { ROLE_LIST_DATA } from '@/mock/temp/formData'
+ const props = defineProps({
+ visible: { required: true },
+ type: { required: true },
+ userData: { required: false }
+ })
+ const emit = defineEmits(['update:visible', 'submit'])
+ const roleList = ref(ROLE_LIST_DATA)
+ const dialogVisible = computed({
+ get: () => props.visible,
+ set: (value) => emit('update:visible', value)
+ })
+ const dialogType = computed(() => props.type)
+ const formRef = ref()
+ const formData = reactive({
+ username: '',
+ phone: '',
+ gender: '鐢�',
+ role: []
+ })
+ const rules = {
+ username: [
+ { required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' },
+ { min: 2, max: 20, message: '闀垮害鍦� 2 鍒� 20 涓瓧绗�', trigger: 'blur' }
+ ],
+ phone: [
+ { required: true, message: '璇疯緭鍏ユ墜鏈哄彿', trigger: 'blur' },
+ { pattern: /^1[3-9]\d{9}$/, message: '璇疯緭鍏ユ纭殑鎵嬫満鍙锋牸寮�', trigger: 'blur' }
+ ],
+ gender: [{ required: true, message: '璇烽�夋嫨鎬у埆', trigger: 'blur' }],
+ role: [{ required: true, message: '璇烽�夋嫨瑙掕壊', trigger: 'blur' }]
+ }
+ const initFormData = () => {
+ const isEdit = props.type === 'edit' && props.userData
+ const row = props.userData
+ Object.assign(formData, {
+ username: isEdit && row ? row.userName || '' : '',
+ phone: isEdit && row ? row.userPhone || '' : '',
+ gender: isEdit && row ? row.userGender || '鐢�' : '鐢�',
+ role: isEdit && row ? (Array.isArray(row.userRoles) ? row.userRoles : []) : []
+ })
+ }
+ watch(
+ () => [props.visible, props.type, props.userData],
+ ([visible]) => {
+ if (visible) {
+ initFormData()
+ nextTick(() => {
+ formRef.value?.clearValidate()
+ })
+ }
+ },
+ { immediate: true }
+ )
+ const handleSubmit = async () => {
+ if (!formRef.value) return
+ await formRef.value.validate((valid) => {
+ if (valid) {
+ ElMessage.success(dialogType.value === 'add' ? '娣诲姞鎴愬姛' : '鏇存柊鎴愬姛')
+ dialogVisible.value = false
+ emit('submit')
+ }
+ })
+ }
+</script>
diff --git a/rsf-design/src/views/system/user/modules/user-search.vue b/rsf-design/src/views/system/user/modules/user-search.vue
new file mode 100644
index 0000000..071a0f9
--- /dev/null
+++ b/rsf-design/src/views/system/user/modules/user-search.vue
@@ -0,0 +1,92 @@
+<template>
+ <ArtSearchBar
+ ref="searchBarRef"
+ v-model="formData"
+ :items="formItems"
+ :rules="rules"
+ @reset="handleReset"
+ @search="handleSearch"
+ >
+ </ArtSearchBar>
+</template>
+
+<script setup>
+ const props = defineProps({
+ modelValue: { required: true }
+ })
+ const emit = defineEmits(['update:modelValue', 'search', 'reset'])
+ const searchBarRef = ref()
+ const formData = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+ })
+ const rules = {
+ // userName: [{ required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }]
+ }
+ const statusOptions = ref([])
+ function fetchStatusOptions() {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve([
+ { label: '鍦ㄧ嚎', value: '1' },
+ { label: '绂荤嚎', value: '2' },
+ { label: '寮傚父', value: '3' },
+ { label: '娉ㄩ攢', value: '4' }
+ ])
+ }, 1e3)
+ })
+ }
+ onMounted(async () => {
+ statusOptions.value = await fetchStatusOptions()
+ })
+ const formItems = computed(() => [
+ {
+ label: '鐢ㄦ埛鍚�',
+ key: 'userName',
+ type: 'input',
+ placeholder: '璇疯緭鍏ョ敤鎴峰悕',
+ clearable: true
+ },
+ {
+ label: '鎵嬫満鍙�',
+ key: 'userPhone',
+ type: 'input',
+ props: { placeholder: '璇疯緭鍏ユ墜鏈哄彿', maxlength: '11' }
+ },
+ {
+ label: '閭',
+ key: 'userEmail',
+ type: 'input',
+ props: { placeholder: '璇疯緭鍏ラ偖绠�' }
+ },
+ {
+ label: '鐘舵��',
+ key: 'status',
+ type: 'select',
+ props: {
+ placeholder: '璇烽�夋嫨鐘舵��',
+ options: statusOptions.value
+ }
+ },
+ {
+ label: '鎬у埆',
+ key: 'userGender',
+ type: 'radiogroup',
+ props: {
+ options: [
+ { label: '鐢�', value: '1' },
+ { label: '濂�', value: '2' }
+ ]
+ }
+ }
+ ])
+ function handleReset() {
+ console.log('閲嶇疆琛ㄥ崟')
+ emit('reset')
+ }
+ async function handleSearch(params) {
+ await searchBarRef.value.validate()
+ emit('search', params)
+ console.log('琛ㄥ崟鏁版嵁', params)
+ }
+</script>
diff --git a/rsf-design/tests/clean-dev-helpers.test.mjs b/rsf-design/tests/clean-dev-helpers.test.mjs
new file mode 100644
index 0000000..1c536f7
--- /dev/null
+++ b/rsf-design/tests/clean-dev-helpers.test.mjs
@@ -0,0 +1,112 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import {
+ CLEAN_DEV_TARGETS,
+ buildMinimalRouteModuleFiles,
+ createMinimalChangeLogContent,
+ createMinimalFastEnterContent,
+ pruneLocaleMessages,
+ rewriteMenuApiContent
+} from '../scripts/clean-dev.helpers.mjs'
+
+test('clean:dev targets align with current JS workspace structure', () => {
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/template'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/widgets'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/examples'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/article'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/safeguard'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/change'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/dashboard/analysis'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/views/dashboard/ecommerce'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/components/core/charts/art-map-chart'))
+ assert.ok(CLEAN_DEV_TARGETS.includes('src/components/business/comment-widget'))
+})
+
+test('minimal route module files keep only dashboard/system/result/exception', () => {
+ const routeFiles = buildMinimalRouteModuleFiles()
+
+ assert.match(routeFiles.dashboard, /path: 'console'/)
+ assert.doesNotMatch(routeFiles.dashboard, /analysis/)
+ assert.doesNotMatch(routeFiles.dashboard, /ecommerce/)
+ assert.doesNotMatch(routeFiles.system, /nested/i)
+ assert.match(routeFiles.index, /dashboardRoutes/)
+ assert.match(routeFiles.index, /systemRoutes/)
+ assert.match(routeFiles.index, /resultRoutes/)
+ assert.match(routeFiles.index, /exceptionRoutes/)
+ assert.doesNotMatch(
+ routeFiles.index,
+ /templateRoutes|widgetsRoutes|examplesRoutes|articleRoutes|helpRoutes/
+ )
+})
+
+test('locale pruning removes demo menu entries and extra dashboard/system branches', () => {
+ const locale = {
+ menus: {
+ dashboard: {
+ title: 'Dashboard',
+ console: 'Console',
+ analysis: 'Analysis',
+ ecommerce: 'Ecommerce'
+ },
+ widgets: { title: 'Widgets' },
+ template: { title: 'Template' },
+ article: { title: 'Article' },
+ examples: { title: 'Examples' },
+ safeguard: { title: 'Safeguard' },
+ plan: { title: 'Plan' },
+ help: { title: 'Help' },
+ system: {
+ title: 'System',
+ user: 'User',
+ role: 'Role',
+ userCenter: 'User Center',
+ menu: 'Menu',
+ nested: 'Nested',
+ menu1: 'Menu1',
+ menu2: 'Menu2',
+ menu21: 'Menu21',
+ menu3: 'Menu3',
+ menu31: 'Menu31',
+ menu32: 'Menu32',
+ menu321: 'Menu321'
+ }
+ }
+ }
+
+ const result = pruneLocaleMessages(locale)
+
+ assert.deepEqual(Object.keys(result.menus).sort(), ['dashboard', 'system'])
+ assert.deepEqual(Object.keys(result.menus.dashboard).sort(), ['console', 'title'])
+ assert.deepEqual(Object.keys(result.menus.system).sort(), [
+ 'menu',
+ 'role',
+ 'title',
+ 'user',
+ 'userCenter'
+ ])
+})
+
+test('minimal fast enter content removes demo quick entries', () => {
+ const content = createMinimalFastEnterContent()
+
+ assert.match(content, /routeName: 'Console'/)
+ assert.match(content, /routeName: 'Login'/)
+ assert.match(content, /routeName: 'Register'/)
+ assert.match(content, /routeName: 'ForgetPassword'/)
+ assert.match(content, /routeName: 'UserCenter'/)
+ assert.doesNotMatch(content, /Analysis|Fireworks|Chat|ChangeLog|Pricing|ArticleComment/)
+})
+
+test('menu api rewrite switches to simple endpoint', () => {
+ const content = "url: '/api/v3/system/menus'"
+
+ assert.equal(rewriteMenuApiContent(content), "url: '/api/v3/system/menus/simple'")
+})
+
+test('minimal change log content clears upgrade history', () => {
+ assert.equal(
+ createMinimalChangeLogContent().trim(),
+ "import { ref } from 'vue'\n\nexport const upgradeLogList = ref([])"
+ )
+})
diff --git a/rsf-design/tests/iconify-local-minimal.test.mjs b/rsf-design/tests/iconify-local-minimal.test.mjs
new file mode 100644
index 0000000..93d9975
--- /dev/null
+++ b/rsf-design/tests/iconify-local-minimal.test.mjs
@@ -0,0 +1,97 @@
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+import test from 'node:test'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const projectRoot = path.resolve(__dirname, '..')
+const srcRoot = path.join(projectRoot, 'src')
+const iconRegistryModule = pathToFileURL(path.join(srcRoot, 'plugins', 'iconify.js')).href
+
+function collectSourceFiles(dir) {
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
+ return entries.flatMap((entry) => {
+ const fullPath = path.join(dir, entry.name)
+
+ if (entry.isDirectory()) {
+ return collectSourceFiles(fullPath)
+ }
+
+ return /\.(vue|js)$/.test(entry.name) ? [fullPath] : []
+ })
+}
+
+function collectUsedIconsByPrefix() {
+ const iconPattern = /icon\s*[:=]\s*["']([a-z0-9-]+):([a-z0-9-]+)["']/g
+ const usedIconsByPrefix = new Map()
+
+ for (const filePath of collectSourceFiles(srcRoot)) {
+ const content = fs.readFileSync(filePath, 'utf8')
+
+ for (const [, prefix, name] of content.matchAll(iconPattern)) {
+ const names = usedIconsByPrefix.get(prefix) || new Set()
+ names.add(name)
+ usedIconsByPrefix.set(prefix, names)
+ }
+ }
+
+ return usedIconsByPrefix
+}
+
+function collectRequiredEntries(collection, usedNames) {
+ const requiredIcons = new Set()
+ const requiredAliases = new Set()
+ const aliases = collection.aliases || {}
+
+ function addDependencies(name) {
+ if (collection.icons[name]) {
+ requiredIcons.add(name)
+ return
+ }
+
+ const alias = aliases[name]
+ assert.ok(alias, `Icon "${collection.prefix}:${name}" is missing from local collection`)
+
+ if (requiredAliases.has(name)) {
+ return
+ }
+
+ requiredAliases.add(name)
+ addDependencies(alias.parent)
+ }
+
+ usedNames.forEach((name) => addDependencies(name))
+
+ return {
+ icons: [...requiredIcons].sort(),
+ aliases: [...requiredAliases].sort()
+ }
+}
+
+test('local Iconify collections only contain icons that are actually used by the app', async () => {
+ const { LOCAL_ICON_COLLECTIONS } = await import(iconRegistryModule)
+ const usedIconsByPrefix = collectUsedIconsByPrefix()
+
+ for (const [prefix, usedNames] of usedIconsByPrefix.entries()) {
+ const collection = LOCAL_ICON_COLLECTIONS[prefix]
+
+ assert.ok(collection, `Missing local collection for prefix "${prefix}"`)
+
+ const requiredEntries = collectRequiredEntries(collection, usedNames)
+ const actualIcons = Object.keys(collection.icons || {}).sort()
+ const actualAliases = Object.keys(collection.aliases || {}).sort()
+
+ assert.deepEqual(
+ actualIcons,
+ requiredEntries.icons,
+ `Local collection "${prefix}" includes unused icon entries`
+ )
+
+ assert.deepEqual(
+ actualAliases,
+ requiredEntries.aliases,
+ `Local collection "${prefix}" includes unused alias entries`
+ )
+ }
+})
diff --git a/rsf-design/tests/iconify-local-prefixes.test.mjs b/rsf-design/tests/iconify-local-prefixes.test.mjs
new file mode 100644
index 0000000..83b7163
--- /dev/null
+++ b/rsf-design/tests/iconify-local-prefixes.test.mjs
@@ -0,0 +1,50 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const projectRoot = path.resolve(__dirname, '..')
+const srcRoot = path.join(projectRoot, 'src')
+const iconRegistryModule = pathToFileURL(path.join(srcRoot, 'plugins', 'iconify.js')).href
+
+function collectSourceFiles(dir) {
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
+ return entries.flatMap((entry) => {
+ const fullPath = path.join(dir, entry.name)
+
+ if (entry.isDirectory()) {
+ return collectSourceFiles(fullPath)
+ }
+
+ return /\.(vue|js)$/.test(entry.name) ? [fullPath] : []
+ })
+}
+
+function collectIconPrefixes() {
+ const iconPattern = /icon\s*[:=]\s*["']([a-z0-9-]+):/g
+ const prefixes = new Set()
+
+ for (const filePath of collectSourceFiles(srcRoot)) {
+ const content = fs.readFileSync(filePath, 'utf8')
+
+ for (const match of content.matchAll(iconPattern)) {
+ prefixes.add(match[1])
+ }
+ }
+
+ return [...prefixes].sort()
+}
+
+test('all used Iconify prefixes are mapped to local collections', async () => {
+ const { LOCAL_ICON_COLLECTIONS } = await import(iconRegistryModule)
+ const actualPrefixes = collectIconPrefixes()
+ const registeredPrefixes = Object.keys(LOCAL_ICON_COLLECTIONS).sort()
+
+ assert.deepEqual(
+ registeredPrefixes,
+ actualPrefixes,
+ `Missing local Iconify collections for: ${actualPrefixes.filter((prefix) => !registeredPrefixes.includes(prefix)).join(', ')}`
+ )
+})
diff --git a/rsf-design/tests/manual-chunks.test.mjs b/rsf-design/tests/manual-chunks.test.mjs
new file mode 100644
index 0000000..a4f2fd1
--- /dev/null
+++ b/rsf-design/tests/manual-chunks.test.mjs
@@ -0,0 +1,37 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import { createManualChunks } from '../build/manualChunks.js'
+
+test('createManualChunks groups heavy dependencies into stable vendor chunks', () => {
+ assert.equal(createManualChunks('/repo/node_modules/echarts/core.js'), 'vendor-echarts')
+ assert.equal(
+ createManualChunks('/repo/node_modules/@wangeditor/editor-for-vue/dist/index.js'),
+ 'vendor-editor'
+ )
+ assert.equal(createManualChunks('/repo/node_modules/highlight.js/lib/index.js'), 'vendor-editor')
+ assert.equal(createManualChunks('/repo/node_modules/xlsx/xlsx.mjs'), 'vendor-xlsx')
+ assert.equal(createManualChunks('/repo/node_modules/xgplayer/dist/index.min.js'), 'vendor-media')
+ assert.equal(
+ createManualChunks('/repo/node_modules/element-plus/es/index.mjs'),
+ 'vendor-element-plus'
+ )
+ assert.equal(
+ createManualChunks('/repo/node_modules/@element-plus/icons-vue/dist/index.mjs'),
+ 'vendor-element-plus'
+ )
+ assert.equal(createManualChunks('/repo/node_modules/vue-router/dist/index.mjs'), 'vendor-vue')
+ assert.equal(createManualChunks('/repo/node_modules/pinia/dist/pinia.mjs'), 'vendor-vue')
+ assert.equal(createManualChunks('/repo/node_modules/@vueuse/core/index.mjs'), 'vendor-vue')
+ assert.equal(createManualChunks('/repo/node_modules/@iconify/vue/dist/index.mjs'), 'vendor-utils')
+ assert.equal(
+ createManualChunks('/repo/node_modules/file-saver/dist/FileSaver.min.js'),
+ 'vendor-utils'
+ )
+ assert.equal(createManualChunks('/repo/node_modules/axios/index.js'), 'vendor-utils')
+})
+
+test('createManualChunks leaves application modules untouched', () => {
+ assert.equal(createManualChunks('/repo/src/views/dashboard/index.vue'), undefined)
+ assert.equal(createManualChunks('\u0000plugin-vue:export-helper'), undefined)
+})
diff --git a/rsf-design/tests/repo-hygiene.test.mjs b/rsf-design/tests/repo-hygiene.test.mjs
new file mode 100644
index 0000000..c7ac397
--- /dev/null
+++ b/rsf-design/tests/repo-hygiene.test.mjs
@@ -0,0 +1,37 @@
+import assert from 'node:assert/strict'
+import fs from 'node:fs'
+import path from 'node:path'
+import test from 'node:test'
+
+const projectRoot = path.resolve(import.meta.dirname, '..')
+
+const migrationArtifacts = [
+ 'docs/superpowers/specs/2026-03-27-art-design-pro-js-migration-design.md',
+ 'docs/superpowers/plans/2026-03-27-art-design-pro-js-migration-plan.md',
+ 'scripts/migrate-to-js.mjs',
+ 'scripts/restore-vue-template-imports.mjs',
+ 'tests/js-migration-sanity.test.mjs',
+ 'src/types'
+]
+
+test('migration-only artifacts have been removed from the working tree', () => {
+ for (const relativePath of migrationArtifacts) {
+ const fullPath = path.join(projectRoot, relativePath)
+ assert.equal(
+ fs.existsSync(fullPath),
+ false,
+ `Expected migration-only artifact to be removed: ${relativePath}`
+ )
+ }
+})
+
+test('package scripts no longer expose one-off migration entry points', () => {
+ const packageJsonPath = path.join(projectRoot, 'package.json')
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
+
+ assert.equal(
+ Reflect.has(packageJson.scripts || {}, 'migrate:js'),
+ false,
+ 'Expected package.json scripts.migrate:js to be removed'
+ )
+})
diff --git a/rsf-design/vite.config.js b/rsf-design/vite.config.js
new file mode 100644
index 0000000..9963fb9
--- /dev/null
+++ b/rsf-design/vite.config.js
@@ -0,0 +1,152 @@
+import path from 'path'
+import { fileURLToPath } from 'url'
+import { defineConfig, loadEnv } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import tailwindcss from '@tailwindcss/vite'
+import AutoImport from 'unplugin-auto-import/vite'
+import ElementPlus from 'unplugin-element-plus/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+import viteCompression from 'vite-plugin-compression'
+import vueDevTools from 'vite-plugin-vue-devtools'
+import { createManualChunks } from './build/manualChunks.js'
+// import { visualizer } from 'rollup-plugin-visualizer'
+
+export default ({ mode }) => {
+ 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,
+ rollupOptions: {
+ output: {
+ manualChunks: createManualChunks
+ }
+ },
+ minify: 'terser',
+ terserOptions: {
+ compress: {
+ drop_console: true,
+ drop_debugger: true
+ }
+ },
+ dynamicImportVarsOptions: {
+ warnOnError: true,
+ exclude: [],
+ include: ['src/views/**/*.vue']
+ }
+ },
+ plugins: [
+ vue(),
+ tailwindcss(),
+ AutoImport({
+ imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
+ dts: false,
+ resolvers: [ElementPlusResolver()],
+ eslintrc: {
+ enabled: true,
+ filepath: './.auto-import.json',
+ globalsPropValue: true
+ }
+ }),
+ Components({
+ dts: false,
+ resolvers: [ElementPlusResolver()]
+ }),
+ ElementPlus({
+ useSource: true
+ }),
+ viteCompression({
+ verbose: false,
+ disable: false,
+ algorithm: 'gzip',
+ ext: '.gz',
+ threshold: 10240,
+ 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: {
+ 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(targetPath) {
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), targetPath)
+}
--
Gitblit v1.9.1